??前言:測試階段驗(yàn)證了RTSP流轉(zhuǎn)RTMP流并讓前端拉流播放的可行性悦荒,但是用控制臺實(shí)現(xiàn)功能有很多弊端唯欣,所以需要將其集成到web系統(tǒng)中。
??因?yàn)閷σ曨l推流等相關(guān)技術(shù)不甚了解搬味,所以解決方案也是從相關(guān)博文中進(jìn)行提取和優(yōu)化境氢,接下來說明下嘗試的幾種技術(shù)方案:
方案一: 用后翱剑康威視的web3.0包。當(dāng)時(shí)測試時(shí)如大多數(shù)博客所說萍聊,只能在IE瀏覽器順利運(yùn)行问芬,且要下載相應(yīng)的插件,局限性實(shí)在太大寿桨,所以棄用此衅。
方案二: 使用海康威視SDK調(diào)用攝像頭的相關(guān)方法亭螟,并用FFmpegCommandHandler將RTSP流轉(zhuǎn)RTMP流推到nginx流媒體服務(wù)器(使用nginx-rtmp-module插件)上挡鞍,然后流媒體服務(wù)器通過前端使用videojs視頻通過設(shè)置源推流地址播放。這里主要借鑒了這兩篇博文中的方案和代碼https://blog.csdn.net/lee_decimal/article/details/111215248
https://blog.csdn.net/qq_36720088/article/details/82893924
方案三: nginx插件換成nginx-http-flv-module预烙,然后前端播放插件換成flvjs墨微,其他都跟方案二一樣。
??方案二過度到方案三的原因:H5的默認(rèn)播放器video支持的視頻格式有限扁掸,不能直接播放RTMP直播流翘县,而市面上大多數(shù)的前端視頻插件需要以flv格式播放RTMP流,且它們都需要flash插件的幫助才能播放谴分,最糾結(jié)的地方在于chrome等大眾瀏覽器都開始不再支持flash插件了锈麸。所以最好的替代方案就是flvjs(HTML5 Flash 視頻播放器,可自行百度)牺蹄,而flvjs播放直播流的必要條件就是必須是http協(xié)議的流忘伞,所以在nginx中安裝nginx-http-flv-module插件替換了原有插件。
??我先將博文中最普遍的方案二繼續(xù)了實(shí)現(xiàn)钞馁,后一篇中再說改進(jìn)方案虑省。
系統(tǒng)集成
一. 下載需要的軟件包和插件
??1. VLC media player匿刮。用于測試是否推流成功僧凰。
??2. FFmpeg。安裝完后需要配置環(huán)境變量熟丸,項(xiàng)目中的FFmpegCommandHandler也需要配置相應(yīng)的配置文件指定FFmpeg路徑训措。
??3. nginx和nginx-rtmp-module插件。安裝完后進(jìn)行conf文件配置光羞,測試階段有相應(yīng)配置代碼绩鸣。
??4. videojs。前端流媒體播放插件纱兑,下載5.xx.x版本(我用的是5.20.5)呀闻。
??5. 海康威視SDK潜慎,根據(jù)系統(tǒng)位數(shù)(32位和64位)下載捡多,不然運(yùn)行不了蓖康。
二. 代碼編寫
??后端代碼的整體執(zhí)行流程:首先調(diào)用豪菔郑康的sdk工具連接攝像頭設(shè)備蒜焊,進(jìn)行操作后獲取操作結(jié)果或者需要的數(shù)據(jù),再由后端條用FFmpegcommandHandler執(zhí)行推流(將直播流推到nginx服務(wù)器上)科贬;瀏覽器用戶瀏覽實(shí)時(shí)監(jiān)控時(shí)泳梆,插件會從直播地址拉流到瀏覽器中進(jìn)行播放。
??1. 工具類編寫榜掌。HCNetUtil是主要負(fù)責(zé)和SDK直接操作优妙,而MonitorUtil則負(fù)責(zé)直接調(diào)用HCNetUtil,并將返回結(jié)果和FFmpegcommandHandler操作一起封裝成通用行為憎账。其中HCNetUtil的主要行為是設(shè)備初始化鳞溉,設(shè)備注冊(其他接口可以查看SDK中的手冊)
/**
* 初始化資源配置
* @return
*/
public boolean initDevice() {
return hCNetSDK.NET_DVR_Init();
}
/**
* 注冊設(shè)備
* @param userName
* @param password
* @param ipAddress
* @param port
* @return
*/
public boolean registDevice(String userName, String password, String ipAddress, String port) {
if (lUserID.longValue() > -1) {
hCNetSDK.NET_DVR_Logout_V30(lUserID);
lUserID = new NativeLong(-1);
}
// 開始注冊
// 獲取設(shè)備參數(shù)數(shù)據(jù)結(jié)構(gòu)
deviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V30();
lUserID = hCNetSDK.NET_DVR_Login_V30(ipAddress, (short)Integer.parseInt(port), userName, password, deviceInfo);
long userID = lUserID.longValue();
System.out.println("lUserID:" + lUserID + "***********************");
if (-1 == userID) {
deviceIP = "";
return false;
}
deviceIP = ipAddress;
System.out.println("注冊設(shè)備成功***********************");
return true;
}
/**
* (注銷并)釋放SDK資源
* @return
*/
public boolean shutDownDev() {
// 如果已經(jīng)注冊,則注銷
if (lUserID.longValue() > -1) {
if (!hCNetSDK.NET_DVR_Logout_V30(lUserID)) {
return false;
}
}
// 釋放SDK資源
if (!hCNetSDK.NET_DVR_Cleanup()) {
return false;
}
return true;
}
??MinitorUtil中實(shí)現(xiàn)了推流鼠哥,停止推流熟菲,推流并獲取進(jìn)程ID等方法。
/**
* 開始推流并返回ffmpeg進(jìn)程ID
* @param monitor
* @param prsName
* @return
*/
public Map<String, String> startTranscodeAndGetTask(String prsName, String userName, String password, String ipAddress, String port) {
Map<String, String> result = new HashMap<>();
int chanNum = -1;
// 初始化設(shè)備并注冊
if(hCUtil.initDevice() && hCUtil.registDevice(userName, password, ipAddress, port)) {
chanNum = hCUtil.getChannelNum();
if(chanNum != -1) {
result = MonitorUtil.startTranscode(prsName, userName, password, ipAddress, port, chanNum);
}
}
result.put("errorMsg", hCUtil.getErrorMsg());
if(!hCUtil.shutDownDev()) {
System.err.println(hCUtil.getErrorMsg());
}
return result;
}
/**
* 開始推流
* @param prsName 進(jìn)程名稱
* @param userName 設(shè)備用戶名
* @param password 密碼
* @param ipAddress ip地址
* @param chanNum 通道號(針對這個(gè)ffmpeg工具jar包有點(diǎn)冗余朴恳,但還是加上這個(gè)參數(shù)抄罕,方便后期維護(hù))
* @return 成功返回進(jìn)程ID(與設(shè)置的進(jìn)程名稱相同)
*/
public static Map<String, String> startTranscode(String prsName, String userName, String password, String ipAddress, String port, int chanNum) {
String chanNumStr = "ch" + chanNum;
String taskID = "";
String RTMPUrl = "";
Map<String, String> map = new HashMap<>();
Map<String, String> res = new HashMap<>();
if(fManager == null) {
fManager = new FFmpegManagerImpl();
}
// 如果進(jìn)程存在,則直接返回
if(MonitorUtil.taskIsRun(prsName)) {
return res;
}
//進(jìn)程名
map.put("appName", prsName);
// map.put("input", "rtsp://"+ userName +":" + password + "@" + ipAddress + ":554/Streaming/channels/001 -c:v libx264");
map.put("input", "\"rtsp://" + userName + ":" + password + "@" + ipAddress + ":554" + "/h264/" + chanNumStr + "/main/av_stream\" -b:v 500k -bufsize 500k -maxrate 600k");
map.put("output", RTMP_OUTPUT_URL); //rtmp流.live為nginx-rtmp的配置
map.put("fps", "25");
map.put("fmt", "flv");
map.put("rtsp_transport", "tcp");
map.put("twoPart", "1"); //只推元碼流
// 成功返回進(jìn)程ID
taskID = fManager.start(map);
RTMPUrl = RTMP_OUTPUT_URL + taskID;
res.put("taskID", (StringUtils.isNotEmpty(taskID)) ? taskID : "");
res.put("RTMPUrl", RTMPUrl);
return res;
}
/**
* 關(guān)閉推流進(jìn)程
* @param taskID 進(jìn)程ID號
* @return
*/
public static boolean stopTranscode(String taskID) {
if(!taskIsRun(taskID)) {
return true;
}
return fManager.stop(taskID);
}
??2. 框架中對應(yīng)的接口實(shí)現(xiàn)于颖。中間的三層結(jié)構(gòu)代碼實(shí)現(xiàn)就不貼出來了呆贿,主要目的就是將rtmp推流地址傳到前端并讓前端動(dòng)態(tài)加載視頻。
??3. 前端顯示森渐。
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link rel="stylesheet" type="text/css" href="../../../videojs/video-js.css" />
<script type="text/javascript" src="videojs/video.js"></script>
<script type="text/javascript" src="videojs/videojs-ie8.js"></script>
<script type="text/javascript" src="js/monitor.js"></script>
</head>
<body>
<div id="monitorPreview"></div>
</body>
</html>
videojs.options.flash.swf = "videojs/video-js.swf"
monitorPreview("rtmp://localhost:1935/live/room")
function monitorPreview(targetPushAddress) {
if (!targetPushAddress) {
return
}
initMonitor(
'monitorV', {
controls: false, // 是否顯示控制條
poster: '', // 視頻封面圖地址
preload: 'auto',
width: 770,
height: 550,
language: 'zh-CN', // 設(shè)置語言
muted: true, // 是否靜音
inactivityTimeout: false,
sources: [ // 視頻來源路徑
{
src: targetPushAddress,
type: 'rtmp/flv'
}
]
},
function(player) {
player.play()
}
)
}
function initMonitor(id, options, callback) {
var mon = $('<video id="' + id + '" class="video-js" data-setup="{}"></video>')
$('#monitorPreview').append(mon)
// 掛到window上方便銷毀
this.monitor = videojs(id, options, function() {
callback(this)
})
}
??這里需要注意做入,如果直接在html頁面中寫對應(yīng)的video控件并指定了視頻流的源路徑,當(dāng)執(zhí)行到j(luò)s代碼時(shí)同衣,videojs控件就已經(jīng)進(jìn)行了初始化竟块,而與jquery不同的是,videojs不允許對視頻控件進(jìn)行二次渲染(會報(bào)錯(cuò)Player “xxx“ is already initialised. Options will not be applied)所以這時(shí)就需要?jiǎng)討B(tài)添加video控件了耐齐。
三. 問題和總結(jié)
??開發(fā)過程中主要碰到了以下幾個(gè)問題:
-
Unable to load library 'HCNetSDK': ???μ???¨浪秘。根本問題是找不到HCNetSDK.dll文件,分兩種情況:
- SDK版本和系統(tǒng)版本不對應(yīng)埠况,32位安裝32位SDK耸携,64對64。
- 路徑不對辕翰。一種場景是當(dāng)動(dòng)態(tài)獲取路徑時(shí)夺衍,因?yàn)槟承┫到y(tǒng)中通過getResource("/").getPath()獲取的路徑如果包含" "則會把空格轉(zhuǎn)成%20字符串,所以需要將對應(yīng)字符替換回來喜命;另一種場景就是粗心了沟沙,這時(shí)候需要查看日志修改代碼的畴。
- Player “xxx“ is already initialised. Options will not be applied。這個(gè)上面說過就不過多贅述尝胆。
- SDK報(bào)錯(cuò)碼29(NET_DVR_DVROPRATEFAILED)丧裁、186(NET_SDK_ERR_FUNCTION_INVALID)。出現(xiàn)這寫錯(cuò)誤就需要查看SDK的dll文件是否導(dǎo)全了含衔、SDK包和系統(tǒng)版本是否對應(yīng)煎娇。
-
FFmpegCommandHandler使用了已經(jīng)棄用的stop()方法。
線程方法stop()
拿到FFmpegCommandHandler源碼贪染,修改代碼并重新打jar包缓呛。
@SuppressWarnings("deprecation")
@Override
public boolean stop(Thread outHandler) {
try {
if (outHandler != null && outHandler.isAlive()) {
outHandler.sleep(200);
outHandler.interrupt();
outHandler.destroy();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
return true;
}
}
??系統(tǒng)集成階段總結(jié):此方案極度依賴flash插件,現(xiàn)在最新版chrome已經(jīng)不支持(連詢問后加載的余地都沒有)杭隙,雖然有相應(yīng)的解決方法哟绊,但是需要客戶在瀏覽器令行安裝插件,使用上并不友好痰憎。然后是安全問題票髓,因?yàn)榱髅襟w部署后,所有外網(wǎng)用戶都能進(jìn)行訪問铣耘,這時(shí)需要對某些監(jiān)控視頻進(jìn)行權(quán)限操作這個(gè)方案就不太行了洽沟,所以想出了對應(yīng)的解決方案(方案三)。