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