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]参数替换
- 事件:发生该事件时,即回调指定的HTTP地址。
- HTTP地址:可以支持多个,以空格分隔,SRS会依次回调这些接口。
- 数据:SRS将数据POST到HTTP接口。最后一个回调on_hls_notify为GET请求,在URL里拼接请求参数。
- 返回值:SRS要求HTTP服务器返回HTTP200并且response内容为整数错误码(0表示成功),其他错误码会断开客户端连接。
-
以上回调传递信息由官方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;
@Configuration
public class WebConfig implements WebMvcConfigurer {
//解决 No mapping for GET 访问静态资源
public void addResourceHandlers(ResourceHandlerRegistry registry){
registry.addResourceHandler("
public class TokenUtils {
//有效期 1000为1秒
private static final long EXPIRE_TIME = 60*60*1000;
//密钥
private static final String SECRET = Constant.SECRET;
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;
}
}
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;
public class Constant {
//token加密密钥
public static final String SECRET="live";
}
SRS回调数据封装类
package com.example.live.callbackbean;
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;
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;
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;
@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;
@Controller
public class SRSCallBackController {
private Logger log = LoggerFactory.getLogger(this.getClass());
@ResponseBody
@RequestMapping("/connect")
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")
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")
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")
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);
}
}
前端页面
主播端
.stream{
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
background-color: #0e90d2;
}
.div_btn{
width: 100%;
height: 30px;
margin-top: 30px;
}
.btn{
background-color: #2f96b4;
width: 20%;
height: 100%;
text-align: center;
margin-left: 40%;;
}
.push_url{
width: 100%;
height: 30px;
margin-top: 50px;
}
span{
display: inline-block;
width: 20%;
height: 30px;
line-height: 30px;
font-size: 18px;
text-align: right;
margin-left: 15%;
margin-right: 0;
}
.input{
display: inline-block;
width: 50%;
height: 29px;
line-height: 29px;
padding: 0;
margin:0;
border-width: 1px;
}
.tips{
width: 100%;
height: 50px;
}
.tips>div{
width: 80%;
height: 100%;
margin: 0 10%;
}
.tips>div>span{
width: 100%;
height: 100%;
font-size: 18px;
line-height: 50px;
text-align: center;
margin-left: 0;
color: red;
}
主播推流端
推流地址:
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 live0.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-thymeleaforg.springframework.boot spring-boot-starter-webcom.auth0 java-jwt3.2.0 org.springframework.boot spring-boot-starter-testtest org.junit.vintage junit-vintage-engineorg.springframework.boot spring-boot-dependencies${spring-boot.version} pom import org.apache.maven.plugins maven-compiler-plugin3.8.1 1.8 1.8 UTF-8 org.springframework.boot spring-boot-maven-plugin2.3.7.RELEASE com.example.live.LiveApplication repackage repackage
三、启动springboot
访问https://192.168.5.104:400获取推流直播
OBS工具推流
回调函数生效
拉流
关闭播放器,OBS停止推流
修改错误token,OBS推流尝试鉴权
RTMP推流 HTTP_FLV拉流,延迟高的吓人
RTMP推流 RTC拉流快很多。。。嗯。。跟服务器环境有关。。。



