1.安装ffmpeg
yum安装即可,安装后检测版本是否安装成功,不详述
yum install -y ffmpeg
ffmpeg -version
2.安装nginx-rtmp
nginx本身不详述,这里已安装nginx的情况下增加编译rtmp模块,git上可下载rtmp模块nginx-rtmp-module存放至nginx安装目录下
./configure --prefix=/usr/local/nginx --add-module=/usr/local/nginx/nginx-rtmp-module
./configure --prefix=/你的安装目录 --add-module=/第三方模块目录
3.rtmp播放和推流监控
nginx-rtmp-module文件夹下有stat.xsl文件,如下配置 /stat可访问监控如图,有playing播放和publishing推流
location /stat{
rtmp_stat all;
rtmp_stat_stylesheet stat.xsl;
}
location /stat.xsl{
root nginx-rtmp-module-master/;
}
4.推流服务配置
rtmp {
server {
listen 1935;
chunk_size 4096; #默认区块大小
#chunk_size 1024;
application devlive1 { #rtmp推流请求路径
live on;
on_play http://127.0.0.1:8082/ffmpeg/on_play?device_token=ocxflddu;
on_play_done http://127.0.0.1:8082/ffmpeg/on_play_done?device_token=ocxflddu;
}
}
server {
listen 1936;
chunk_size 4096; #默认区块大小
#chunk_size 1024;
application devlive2 { #rtmp推流请求路径
live on;
on_play http://127.0.0.1:8082/ffmpeg/on_play?device_token=ocxflddu;
on_play_done http://127.0.0.1:8082//ffmpeg/on_play_done?device_token=ocxflddu;
}
}
}
配置了两个推流端口两个应用,一个端口一个应用
on_play:有用户播放,on_play_done:用户关闭播放
会调用配置的接口,request.getParameter("name");可获取到当前流名称,还有on_publish等其他配置项
5.ffmpeg推流
个人采用的是多线程推流,一个线程管理一个外部进程(ffmpeg)
关闭推流可通过线程名匹配到并执行interrupt终止操作
推流异常时销毁进程终止当前线程,延时10秒新启线程来推当前流(代码有点多就不粘出来了)
//java调第三方引用ffmpeg,RTSP转RTMP
String[] convert = new String[]{"ffmpeg", "-i", rtspUrl, "-r", "25", "-c:v", "copy", "-f", "flv", "-y", "-an", rtmpUrl};
processBuilder.command(convert);
processBuilder.redirectErrorStream(true);
process = processBuilder.start();
6.进程阻塞问题
当网络情况不稳定或直播流不存在或者没有数据的情况下,ffmpeg进程容易发生阻塞,这里就需要特殊处理,终止阻塞的进程。
进程输出过程中持续往redis写入更新心跳时间,另起定时任务检测该进程心跳时间,如心跳停止2分钟判定为阻塞,kill进程
7.在线人数监测,无人播放时关闭推流
on_lpay和on_play_done理论可实现在线人数监测,onplay人数+1,onlpaydone人数-1,在服务不掉线始终和nginx保持网络通畅的情况下可实现,否则。。。
上述提到过/stat可监测播放playing和推流publishing,通过解析XML格式,可获取每个视频playing数量,为了方便解析,nginx-rtmp服务配置,不建议application同名
附上解析代码
//检测RTMP流播放人数,超过expire时长播放人数为0,关闭推流
public void chkFfmpegPlayerZero(long expire) {
String sendGet = HttpKit.sendGet(rtmpstat, null);
if (ToolUtil.isNotEmpty(sendGet)){
logger.info("RTMPSTAT:{}",sendGet);
JSONObject xmlJSONObj = toJSONObject(sendGet);
JSONObject rtmp = xmlJSONObj.getJSONObject("rtmp");
JSONArray server = rtmp.getJSONArray("server");
for (int i = 0; i < server.length(); i++) {
JSONObject application = server.getJSONObject(i).getJSONObject("application");
String applicationName = application.getString("name");
JSONObject live = application.getJSONObject("live");
long nclients = live.getLong("nclients");
if (nclients>0){
//有数据
//此处不确定stream节点是object还是array
//JSONObject client = live.getJSONObject("stream").getJSONObject("client");
String simpleName = live.get("stream").getClass().getSimpleName();
if ("JSONObject".equals(simpleName)){
JSONObject stream = live.getJSONObject("stream");
chkFfmpegPlayerZeroByStream(stream,expire);
}else {
JSONArray stream = live.getJSONArray("stream");
for (int i1 = 0; i1 < stream.length(); i1++) {
JSONObject jsonObject = stream.getJSONObject(i1);
chkFfmpegPlayerZeroByStream(jsonObject,expire);
}
}
}else {
logger.info("{}下无推流信息",applicationName);
}
}
}
}
private void chkFfmpegPlayerZeroByStream(JSONObject stream,long expire){
String cameraId = stream.get("name").toString();
String simpleName = stream.get("client").getClass().getSimpleName();
int player = 0;
if ("JSONObject".equals(simpleName)){
JSONObject client = stream.getJSONObject("client");
if (!client.has("publishing")){
player++;
}
}else {
JSONArray clients = stream.getJSONArray("client");
for (int i = 0; i < clients.length(); i++) {
if (!clients.getJSONObject(i).has("publishing")){
player++;
}
}
}
logger.info("推流播放人数 devId:{},人数:{}",cameraId,player);
//持续2分钟播放人数为0关闭推流
String rediskeyPlaytime = RedisKey.CAMERA_INFO + cameraId + RedisKey.CAMERA_PLAYERTIME;
if (player==0){
String playtimeStr = redisTemplate.opsForValue().get(rediskeyPlaytime);
if (ToolUtil.isEmpty(playtimeStr)){
//首次检测到无人播放,赋值
logger.info("首次检测到无人播放,记录时间戳,devId:{}",cameraId);
redisTemplate.opsForValue().set(rediskeyPlaytime,System.currentTimeMillis()+"");
return;
}else {
//非首次监测到,判断多久无人播放
Long playtime = Long.valueOf(playtimeStr);
if ((System.currentTimeMillis() - playtime)>expire){
//关闭推流
logger.info("超过2分钟无人播放,关闭推流,devId:{}",cameraId);
redisTemplate.delete(rediskeyPlaytime);
this.closeFfmpeg(cameraId);
}else {
logger.info("无人播放未超时,暂不处理,devId:{}",cameraId);
}
}
}else {
//有用户播放清空数据
redisTemplate.delete(rediskeyPlaytime);
}
}