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/;
        }

Java 视频链接推流 java实现流媒体_rtmp

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);
        }
    }