海康威視RTSP攝像頭轉(zhuǎn)RTMP推流前端方案(系統(tǒng)集成)

??前言:測試階段驗(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)行不了蓖康。


海康SDK

二. 代碼編寫
??后端代碼的整體執(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è)問題:

  1. 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í)候需要查看日志修改代碼的畴。
  2. Player “xxx“ is already initialised. Options will not be applied。這個(gè)上面說過就不過多贅述尝胆。
  3. 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)煎娇。
  4. 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)的解決方案(方案三)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蜗细,一起剝皮案震驚了整個(gè)濱河市裆操,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌炉媒,老刑警劉巖踪区,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異吊骤,居然都是意外死亡缎岗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門水援,熙熙樓的掌柜王于貴愁眉苦臉地迎上來密强,“玉大人茅郎,你說我怎么就攤上這事蜗元。” “怎么了系冗?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵奕扣,是天一觀的道長。 經(jīng)常有香客問我掌敬,道長惯豆,這世上最難降的妖魔是什么池磁? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮楷兽,結(jié)果婚禮上地熄,老公的妹妹穿的比我還像新娘。我一直安慰自己芯杀,他們只是感情好端考,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著揭厚,像睡著了一般却特。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上筛圆,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天裂明,我揣著相機(jī)與錄音,去河邊找鬼太援。 笑死闽晦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的提岔。 我是一名探鬼主播尼荆,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼唧垦!你這毒婦竟也來了捅儒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤振亮,失蹤者是張志新(化名)和其女友劉穎巧还,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坊秸,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡麸祷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了褒搔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阶牍。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖星瘾,靈堂內(nèi)的尸體忽然破棺而出走孽,到底是詐尸還是另有隱情,我是刑警寧澤琳状,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布磕瓷,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏困食。R本人自食惡果不足惜边翁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望硕盹。 院中可真熱鬧符匾,春花似錦、人聲如沸瘩例。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仰剿。三九已至创淡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間南吮,已是汗流浹背琳彩。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留部凑,地道東北人露乏。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像涂邀,于是被迫代替她去往敵國和親瘟仿。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容