SRS提供了一系列http回调,根据客户端连接服务器的不同状态触发该状态下用户指定的http请求,用户自定义服务接受SRS服务传递信息做一系列操作,比如客户端连接服务器时根据回调数据判断是否允许客户端连接服务器。
SRS服务提供了以下回调:
SRS服务版本:4.0.166
on_connect:当客户端连接到指定的vhost和app时触发
{
"action": "on_connect",
"client_id": 1985,
"ip": "192.168.1.10",
"vhost": "video.test.com",
"app": "live",
"tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a",
"pageUrl": "http://www.test.com/live.html",
"server_id": "vid-werty"
}
on_close:当客户端关闭连接,或者SRS主动关闭连接时触发
{
"action": "on_close",
"client_id": 1985,
"ip": "192.168.1.10",
"vhost": "video.test.com",
"app": "live",
"send_bytes": 10240,
"recv_bytes": 10240,
"server_id": "vid-werty"
}
on_publish:当客户端发布流时,譬如flash/FMLE方式推流到服务器触发
{
"action": "on_publish",
"client_id": 1985,
"ip": "192.168.1.10",
"vhost": "video.test.com",
"app": "live",
"stream": "livestream",
"param":"?token=xxx&salt=yyy",
"server_id": "vid-werty"
}
on_unpublish:当客户端停止发布流时触发
{
"action": "on_unpublish",
"client_id": 1985,
"ip": "192.168.1.10",
"vhost": "video.test.com",
"app": "live",
"stream": "livestream",
"param":"?token=xxx&salt=yyy",
"server_id": "vid-werty"
}
on_play:当客户端开始播放流时触发
{
"action": "on_play",
"client_id": 1985,
"ip": "192.168.1.10",
"vhost": "video.test.com",
"app": "live",
"stream": "livestream",
"param":"?token=xxx&salt=yyy",
"pageUrl": "http://www.test.com/live.html",
"server_id": "vid-werty"
}
on_stop:当客户端停止播放时触发。备注:停止播放可能不会关闭连接,还能再继续播放。
{
"action": "on_stop",
"client_id": 1985,
"ip": "192.168.1.10",
"vhost": "video.test.com",
"app": "live",
"stream": "livestream",
"param":"?token=xxx&salt=yyy",
"server_id": "vid-werty"
}
on_dvr:当DVR录制关闭一个flv文件时触发
{
"action": "on_dvr",
"client_id": 1985,
"ip": "192.168.1.10",
"vhost": "video.test.com",
"app": "live",
"stream": "livestream",
"param":"?token=xxx&salt=yyy",
"cwd": "/usr/local/srs",
"file": "./objs/nginx/html/live/livestream.1420254068776.flv",
"server_id": "vid-werty"
}
on_hls:当SRS获取HLS的ts文件时触发
{
"action": "on_hls",
"client_id": 1985,
"ip": "192.168.1.10",
"vhost": "video.test.com",
"app": "live",
"stream": "livestream",
"param":"?token=xxx&salt=yyy",
"duration": 9.36, // in seconds
"cwd": "/usr/local/srs",
"file": "./objs/nginx/html/live/livestream/2015-04-23/01/476584165.ts",
"url": "live/livestream/2015-04-23/01/476584165.ts",
"m3u8": "./objs/nginx/html/live/livestream/live.m3u8",
"m3u8_url": "live/livestream/live.m3u8",
"seq_no": 100, "server_id": "vid-werty"
}
on_hls_notify:当SRS获取HLS的ts文件时触发,用于将文件推送到CDN网络,通过从CDN网络获取ts文件。该请求是一个GET请求,请求地址为格式:
http://127.0.0.1:8085/api/v1/hls/[server_id]/[app]/[stream]/[ts_url][param];
[server_id]服务器ID替换
[app]应用名称替换
[stream]流名称替换
[ts_url]url替换
[param]参数替换
以上回调传递信息由官方GitHub里的http_hooks回调请求信息提供,与实际获取到的信息里部分信息可能有区别,可在自定义服务接口里获取打印查看,开启回调并指定请求URL参考SRS回调请求设置
回调请求格式,多个接口用空格隔开,分号结尾:
on_connect http(https)://ip:port/api http(https)://ip:port/api;
测试demo
服务器系统:CentOS7.4
VM15Pro虚拟机网络模式:桥接
服务器IP地址:192.168.5.105
服务器端口和服务:
本机IP地址:192.168.5.104
一、开启SRS服务,进入安装目录
1.1、查看配置文件,设置回调函数
# main config for srs.
# @see full.conf for detail config.
listen 1935; //RTMP默认监听1935 TCP端口
max_connections 1000; //最大连接数
#srs_log_tank file;
#srs_log_file ./objs/srs.log;
daemon on; //开启后台模式
http_api {
enabled on; //支持外部程序管理SRS服务器,支持跨域请求,请求和响应数据只支持JSON
listen 1985; //
}
http_server {
enabled on; //开启SRS服务可视化界面查看系统信息和播放器推流器
listen 8080;
dir ./objs/nginx/html;
}
rtc_server {
enabled on; //开启RTC服务器
listen 8000; //RTC默认监听8000端口为UDP端口
candidate $CANDIDATE; //服务器提供服务的IP地址。开启RTC服务,这里一定不能错,云服务器这里可能读取不到可访问的外网IP或者错误IP地址,需要手动指定
}
vhost __defaultVhost__ {
hls {
enabled on;
}
http_remux {
enabled on; //RTMP流转封装为http flv流分发
mount [vhost]/[app]/[stream].flv;
}
rtc {
enabled on;
#rtmp_to_rtc off; //RTMP推流转RTC拉流
#rtc_to_rtmp off; //RTC推流转RTMP拉流
}
http_hooks {
enabled on;
on_connect https://192.168.5.102:400/connect;
on_close https://192.168.5.102:400/close;
on_publish https://192.168.5.102:400/publish;
on_unpublish https://192.168.5.102:400/unpublish;
on_play https://192.168.5.102:400/play;
on_stop https://192.168.5.102:400/stop;
}
}
SRS4.0.166无法设置rtmp_to_rtc,否则启动服务报错非法,但可以RTMP推流RTC拉流。后续版本设置此项无报错,SRS4.0.X版本目前属于开发版本
1.2、开启服务指定srs.conf配置启动
cd /usr/local/srs/SRS4.0/trunk //进入安装目录
./objs/srs -c conf/srs.conf //启动服务指定配置文件为srs.conf
./etc/init.d/srs status //查看运行状态
./objs/srs -v //查看安装版本号
二、创建springboot项目,编写接口
配置类
package com.example.live.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/*
* 配置类
* @author jiang
* @date 2021.10.17
* */
@Configuration
public class WebConfig implements WebMvcConfigurer {
//解决 No mapping for GET 访问静态资源
public void addResourceHandlers(ResourceHandlerRegistry registry){
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/");
}
//http(https)://ip:port/访问本地址
public void addViewControllers(ViewControllerRegistry registry){
registry.addViewController("/").setViewName("client");
}
}
token生成与校验类
package com.example.live.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.live.constant.Constant;
import java.io.UnsupportedEncodingException;
import java.util.Date;
/*
* token生成、验证工具类
* @author jiang
* @date 2021.10.17
* */
public class TokenUtils {
//有效期 1000为1秒
private static final long EXPIRE_TIME = 60*60*1000;
//密钥
private static final String SECRET = Constant.SECRET;
/*
* 校验token是否正确
*/
public static boolean verify(String token, String username) {
try {
//根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
//效验token
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}
/*
* 获得token中的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/*
* 生成签名
*/
public static String sign(String username) throws UnsupportedEncodingException {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
Date date1 = new Date();
return JWT.create()
.withIssuer("auth0")//签发人
.withClaim("username", username)//载体数据
.withIssuedAt(date1)//签发时间
.withExpiresAt(date)//过期时间
.sign(algorithm);//生成新的JWT
}
}
常量类
package com.example.live.constant;
/*
* 常量累
* @author jiang
* @date 2021.10.17
* */
public class Constant {
//token加密密钥
public static final String SECRET="live";
}
SRS回调数据封装类
package com.example.live.callbackbean;
/*
* SRS回调数据
* @author jiang
* @date 2021.10.17
* */
public class CallBackDataOnConnect {
String action;
String client_id;
String ip;
String vhost;
String app;
String stream;
String tcUrl;
String pageUrl;
String param;
public String getStream() {
return stream;
}
public void setStream(String stream) {
this.stream = stream;
}
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
public void setAction(String action) {
this.action = action;
}
public void setClient_id(String client_id) {
this.client_id = client_id;
}
public void setIp(String ip) {
this.ip = ip;
}
public void setVhost(String vhost) {
this.vhost = vhost;
}
public void setApp(String app) {
this.app = app;
}
public void setTcUrl(String tcUrl) {
this.tcUrl = tcUrl;
}
public void setPageUrl(String pageUrl) {
this.pageUrl = pageUrl;
}
public String getAction() {
return action;
}
public String getClient_id() {
return client_id;
}
public String getIp() {
return ip;
}
public String getVhost() {
return vhost;
}
public String getApp() {
return app;
}
public String getTcUrl() {
return tcUrl;
}
public String getPageUrl() {
return pageUrl;
}
}
package com.example.live.callbackbean;
/*
* SRS回调数据
* @author jiang
* @date 2021.10.17
* */
public class CallBackDataOnPublish {
String server_id;
String action ;
String client_id ;
String ip ;
String vhost ;
String app ;
String tcUrl ;
String stream ;
String param ;
public String getServer_id() {
return server_id;
}
public void setServer_id(String server_id) {
this.server_id = server_id;
}
public String getTcUrl() {
return tcUrl;
}
public void setTcUrl(String tcUrl) {
this.tcUrl = tcUrl;
}
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
public String getAction() {
return action;
}
public String getClient_id() {
return client_id;
}
public String getIp() {
return ip;
}
public String getVhost() {
return vhost;
}
public String getApp() {
return app;
}
public String getStream() {
return stream;
}
public void setAction(String action) {
this.action = action;
}
public void setClient_id(String client_id) {
this.client_id = client_id;
}
public void setIp(String ip) {
this.ip = ip;
}
public void setVhost(String vhost) {
this.vhost = vhost;
}
public void setApp(String app) {
this.app = app;
}
public void setStream(String stream) {
this.stream = stream;
}
}
package com.example.live.callbackbean;
/**
* SRS回调数据
* @author jiang
* @date 2021/10/21 21:26
*/
public class CallBackDataOnPlay {
String server_id;
String action ;
String client_id ;
String ip ;
String vhost ;
String app ;
String pageUrl ;
String stream ;
String param ;
public String getServer_id() {
return server_id;
}
public void setServer_id(String server_id) {
this.server_id = server_id;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getClient_id() {
return client_id;
}
public void setClient_id(String client_id) {
this.client_id = client_id;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getVhost() {
return vhost;
}
public void setVhost(String vhost) {
this.vhost = vhost;
}
public String getApp() {
return app;
}
public void setApp(String app) {
this.app = app;
}
public String getPageUrl() {
return pageUrl;
}
public void setPageUrl(String pageUrl) {
this.pageUrl = pageUrl;
}
public String getStream() {
return stream;
}
public void setStream(String stream) {
this.stream = stream;
}
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
}
服务类接口及实现
package com.example.live.service;
public interface StreamService {
public String generateURL();
}
package com.example.live.service.serviceImpl;
import com.example.live.service.StreamService;
import com.example.live.utils.TokenUtils;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class StreamServiceImpl implements StreamService {
@Override
public String generateURL() {
String token = null;
String stream = null;
try{
token = TokenUtils.sign("jx");
stream= UUID.randomUUID().toString();
}catch (Exception e){
e.getStackTrace();
}
String url="rtmp://192.168.5.105/live/"+stream+"?token="+token;
return url;
}
}
处理前端页面获取推流地址请求接口
package com.example.live.controllers;
import com.example.live.service.StreamService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/*
*
* @author jiang
* @date 2021.10.17
* */
@Controller()
public class StreamController {
@Autowired
private StreamService streamService;
@ResponseBody
@RequestMapping("/getUrl")
public String getUrl(){
return streamService.generateURL();
}
}
SRS回调接口
package com.example.live.controllers;
import com.example.live.callbackbean.CallBackDataOnConnect;
import com.example.live.callbackbean.CallBackDataOnPlay;
import com.example.live.callbackbean.CallBackDataOnPublish;
import com.example.live.utils.TokenUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/*
* 操作SRS流媒体服务回调函数
* @author jiang
* @date 2021.10.17
*
* 该类所有方法返回值均为int类型,根据SRS回调返回值要求
* 含义:0成功,非0失败
* */
@Controller
public class SRSCallBackController {
private Logger log = LoggerFactory.getLogger(this.getClass());
@ResponseBody
@RequestMapping("/connect")
/*
* 客户端连接SRS服务器时回调函数请求此接口
*返回0表示校验成功,非0为失败
* */
public int on_connect(@RequestBody CallBackDataOnConnect data){
int code = 1;
if(data.getApp()!=""&&data.getApp()!=null&&!data.getApp().isEmpty()&&"live".equals(data.getApp())){
code = 0;
log.info("推流应用名称正确...客户端连接成功...");
return code;
}else {
log.info("推流应用名称错误...客户端连接失败...");
return code;
}
}
@ResponseBody
@RequestMapping("/close")
/*
* 客户端断开连接时触发
* 客户端断开包括客户端主动断开和客户端被动断开(服务器主动停止客户端推流断开)
* */
public int on_close(@RequestBody String data){
log.info("客户端断开连接"+data);
return 0;
}
@ResponseBody
@RequestMapping("/publish")
/*
* 用户推流(即直播)时触发
* 返回0允许推流,返回非0拒绝推流
* */
public int on_publish(@RequestBody CallBackDataOnPublish data){
int code = 1;
if(data.getApp()!=""&&data.getApp()!=null&&!data.getApp().isEmpty()&&"live".equals(data.getApp())){
//携带url验证格式为?token=....携带参数应为大于7个字符且问号在第一位
if (data.getParam().length()>7&&data.getParam().indexOf("?")==0){
Boolean flag = false;
//截取?后面5个字符判断是否为token
String str = data.getParam().substring(data.getParam().indexOf("?")+1,6);
if("token".equals(str)){
//截取=后面字符串校验token是否有效
String token = data.getParam().substring(data.getParam().indexOf("=")+1,data.getParam().length());
flag = TokenUtils.verify(token,"jx");
if(flag){
code = 0;
String username = TokenUtils.getUsername(token);
log.info("token验证成功...客户端开始推流...,用户名为:"+username);
return code;
}else {
log.info("token验证失败...禁止客户端推流...");
return code;
}
}else {
log.info("请求未携带token...禁止客户端推流");
return code;
}
}else {
log.info("请求未携带token...禁止客户端推流");
return code;
}
}else {
log.info("推流应用名称错误...禁止客户端推流");
return code;
}
}
@ResponseBody
@RequestMapping("/unpublish")
/*
* 客户端停止推流时触发
* 包括客户端主动停止推流和客户端被动停止推流(服务器主动停止客户端推流)
* */
public int on_unpublish(@RequestBody String data){
log.info("客户端停止推流"+data);
return 0;
}
@ResponseBody
@RequestMapping("/play")
/*
* 客户端拉流(播放)时触发
* 0表示允许拉流,非0表示拒绝拉流
*
* */
public int on_play(@RequestBody CallBackDataOnPlay data) {
int code = 1;
if (data.getApp() != "" && data.getApp() != null && !data.getApp().isEmpty() && "live".equals(data.getApp())) {
//携带url验证格式为?token=....携带参数应为大于7个字符且问号在第一位
if (data.getParam().length()>7&&data.getParam().indexOf("?")==0){
//截取?后面5个字符判断是否为token
String str = data.getParam().substring(data.getParam().indexOf("?") + 1, 6);
if ("token".equals(str)) {
Boolean flag = false;
//截取=后面字符串校验token是否有效
String token = data.getParam().substring(data.getParam().indexOf("=") + 1, data.getParam().length());
flag = TokenUtils.verify(token, "jx");
if (flag) {
code = 0;
String username = TokenUtils.getUsername(token);
log.info("token验证成功...客户端开始播放...,用户名为:" + username);
return code;
} else {
log.info("token验证失败...禁止客户端播放...");
return code;
}
} else {
log.info("请求未携带token...禁止客户端播放");
return code;
}
}else {
log.info("请求未携带token...禁止客户端播放");
return code;
}
} else {
log.info("推流应用名称错误...禁止客户端播放");
return code;
}
}
@ResponseBody
@RequestMapping("/stop")
/*
* 客户端停止播放时触发
* 官方介绍客户端停止播放时触发,可再次播放,实际通过PC端和移动端谷歌/火狐浏览器(webRTC协议拉流仅支持这俩浏览器)测试貌似仅服务器主动踢流时会触发此回调,
* 客户端端主动暂停后可再播放,不会触发此回调,客户端关闭窗口会触发此回调
* */
public int on_stop(@RequestBody String data){
log.info("客户端停止播放"+data);
return 0;
}
}
启动类
package com.example.live;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LiveApplication {
public static void main(String[] args) {
SpringApplication.run(LiveApplication.class, args);
}
}
前端页面
主播端
主播推流端
推流地址:
application.properties配置
# 应用名称
spring.application.name=live
# 应用服务 WEB 访问端口
#server.port=8080
#端口号HTTPS默认端口号为443,本机443端口被占用
server.port=400
#你生成的证书名字
server.ssl.key-store=tomcat.keystore
#密钥库密码
server.ssl.key-store-password=123456
server.ssl.keyStoreType=JKS
#别名
server.ssl.keyAlias=tomcat
pom.xml
4.0.0
com.example
live
0.0.1-SNAPSHOT
live
Demo project for Spring Boot
1.8
UTF-8
UTF-8
2.3.7.RELEASE
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
com.auth0
java-jwt
3.2.0
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
org.apache.maven.plugins
maven-compiler-plugin
3.8.1
1.8
1.8
UTF-8
org.springframework.boot
spring-boot-maven-plugin
2.3.7.RELEASE
com.example.live.LiveApplication
repackage
repackage
三、启动springboot
访问https://192.168.5.104:400获取推流直播
OBS工具推流
回调函数生效
拉流
RTMP推流 RTC拉流快很多。。。嗯。。跟服务器环境有关。。。