vue+flv.js+SpringBoot+websocket實(shí)現(xiàn)視頻監(jiān)控與回放功能
需求:
vue+springboot的項(xiàng)目,需要在頁(yè)面展示出??档挠脖P錄像機(jī)連接的攝像頭的實(shí)時(shí)監(jiān)控畫面以及回放功能.
- 之前項(xiàng)目里是純前端實(shí)現(xiàn)視頻監(jiān)控和回放功能.但是有局限性.就是ip地址必須固定.新的需求里設(shè)備ip不固定.所以必須換一種思路.
- 通過(guò)設(shè)備的主動(dòng)注冊(cè),讓設(shè)備去主動(dòng)連接服務(wù)器后端通過(guò)socket推流給前端實(shí)現(xiàn)實(shí)時(shí)監(jiān)控和回放功能;
思路:
1:初始化設(shè)備.后端項(xiàng)目啟動(dòng)時(shí)就調(diào)用初始化方法.
2:開啟socket連接.前端頁(yè)面加載時(shí)嘗試連接socket.
3:點(diǎn)擊播放,調(diào)用后端推流接口.并且前端使用flv.js實(shí)現(xiàn)播放.
準(zhǔn)備工作:
1:vue項(xiàng)目引入flv.js。
npm install --save flv.js
main.js里面引入
import flvjs from ‘flv.js’;
Vue.use(flvjs)
但是這里我遇見一個(gè)坑.開發(fā)模式?jīng)]有問(wèn)題.但是打包之后發(fā)現(xiàn)ie瀏覽器報(bào)語(yǔ)法錯(cuò)誤.不支持此引用.所以修改引用地址.
在webpack.base.conf.js的module.exports下添加
resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), 'flvjs':'flv.js/dist/flv.js' } },
plugins下添加
plugins: [ new webpack.ProvidePlugin({ flvjs:'flvjs', $: "jquery", jQuery: "jquery", "window.jQuery": "jquery" }) ],
最后頁(yè)面引入時(shí):
import flvjs from "flv.js/dist/flv.js";
2.準(zhǔn)備一個(gè)硬盤錄像機(jī),并添加一個(gè)攝像頭設(shè)備以做測(cè)試使用.
硬盤錄像機(jī)設(shè)置為主動(dòng)注冊(cè)模式.并配置好ip和端口以及子設(shè)備ID
在設(shè)置里的網(wǎng)絡(luò)設(shè)置里面
3.后端搭建好websocket工具類
包含通用的OnOpen,onClose,onError等方法.
實(shí)現(xiàn):
1.項(xiàng)目啟動(dòng)開啟設(shè)備服務(wù).這個(gè)SDKLIB里面都有就不介紹了.
2.頁(yè)面加載嘗試開啟socket連接.
//嘗試連接websocket startSocket(channelnum, device_value) { try { let videoWin = document.getElementById(this.currentSelect); if (flvjs.isSupported()) { let websocketName = "/device/monitor/videoConnection/" + channelnum + device_value; console.log("進(jìn)入連接websocket", this.ipurl + websocketName); const flvPlayer = flvjs.createPlayer( { type: "flv", //是否是實(shí)時(shí)流 isLive: true, //是否有音頻 hasAudio: false, url: this.ipurl + websocketName, enableStashBuffer: true, }, { enableStashBuffer: false, stashInitialSize: 128, } ); flvPlayer.on("error", (err) => { console.log("err", err); }); flvjs.getFeatureList(); flvPlayer.attachMediaElement(videoWin); flvPlayer.load(); flvPlayer.play(); return true; } } catch (error) { console.log("連接websocket異常", error); return false; } },
這里傳的參數(shù)是通道號(hào)和設(shè)備信息.無(wú)需在意.只要是唯一key就可以.
2.socket連接成功后.調(diào)用后端推流方法實(shí)現(xiàn)播放.
這里說(shuō)一下后端的推流方法.
調(diào)用SDK里的CLIENT_RealPlayByDataType方法
/** * 實(shí)時(shí)預(yù)覽拉流 * * @param loginHandler 登錄句柄 * @param channel 通道號(hào) * @param emDataType 回調(diào)拉出的碼流類型,{@link NetSDKLib.EM_REAL_DATA_TYPE} */ public long preview(long loginHandler, int channel, NetSDKLib.fRealDataCallBackEx realDataCallBackEx, fRealDataCallBackEx2 realPlayDataCallback, int emDataType, int rType, boolean saveFile, int emAudioType) { NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE inParam = new NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE(); NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE outParam = new NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE(); inParam.nChannelID = channel; inParam.rType = rType; if(realDataCallBackEx!=null){ inParam.cbRealData=realDataCallBackEx; } if(realPlayDataCallback!=null){ inParam.cbRealDataEx = realPlayDataCallback; } inParam.emDataType = emDataType; inParam.emAudioType=emAudioType; if (saveFile) { inParam.szSaveFileName = UUID.randomUUID().toString().replace(".", "").replace("-", "") + "." + EMRealDataType.getRealDataType(emDataType).getFileType(); } NetSDKLib.LLong realPlayHandler = netsdk.CLIENT_RealPlayByDataType(new NetSDKLib.LLong(loginHandler), inParam, outParam, 3000); if (realPlayHandler.longValue() != 0) { netsdk.CLIENT_MakeKeyFrame(new NetSDKLib.LLong(loginHandler),channel,0); RealPlayInfo info = new RealPlayInfo(loginHandler, emDataType, channel, rType); realPlayHandlers.put(realPlayHandler.longValue(), info); } else { log.error("realplay failed.error is " + ENUMERROR.getErrorMessage(), this); } return realPlayHandler.longValue(); }
注意:這里的碼流類型選擇flv.
回調(diào)函數(shù)里面:
// 回調(diào)建議寫成單例模式, 回調(diào)里處理數(shù)據(jù),需要另開線程 @Autowired private WebSocketServer server; private Log log = Log.get(WebSocketRealDataCallback.class); @Override public void invoke(NetSDKLib.LLong lRealHandle, int dwDataType, Pointer pBuffer, int dwBufSize, int param, Pointer dwUser) { RealPlayInfo info = DeviceApi.realPlayHandlers.get(lRealHandle.longValue()); if (info != null && info.getLoginHandler() != 0) { //過(guò)濾碼流 byte[] buffer = pBuffer.getByteArray(0, dwBufSize); if (info.getEmDataType() == 0 || info.getEmDataType() == 3) { //選擇私有碼流或mp4碼流,拉流出的碼流都是私有碼流 if (dwDataType == 0) { log.info(dwDataType + ",length:" + buffer.length + " " + Arrays.toString(buffer), WebSocketRealDataCallback.class); sendBuffer(buffer, lRealHandle.longValue()); } } else if ((dwDataType - 1000) == info.getEmDataType()) { log.info(dwDataType + ",length: " + buffer.length + Arrays.toString(buffer), WebSocketRealDataCallback.class); sendBuffer(pBuffer.getByteArray(0, dwBufSize), lRealHandle.longValue()); } } }
以及調(diào)用Websocket里面的sendMessageToOne發(fā)送給指定客戶端
/** * 發(fā)送數(shù)據(jù) * @param bytes * @param realPlayHandler */ private static void sendBuffer(byte[] bytes, long realPlayHandler) { /** * 發(fā)送流數(shù)據(jù) * 使用pBuffer.getByteBuffer(0,dwBufSize)得到的是一個(gè)指向native pointer的ByteBuffer對(duì)象,其數(shù)據(jù)存儲(chǔ)在native, * 而webSocket發(fā)送的數(shù)據(jù)需要存儲(chǔ)在ByteBuffer的成員變量hb,使用pBuffer的getByteBuffer得到的ByteBuffer其hb為null * 所以,需要先得到pBuffer的字節(jié)數(shù)組,手動(dòng)創(chuàng)建一個(gè)ByteBuffer */ ByteBuffer buffer = ByteBuffer.wrap(bytes); server.sendMessageToOne(realPlayHandler, buffer); }
這里傳的參數(shù)是設(shè)備初始化的時(shí)候得到的登錄句柄.以及流數(shù)據(jù).
/** * 發(fā)送binary消息給指定客戶端 * * @param realPlayHandler 預(yù)覽句柄 * @param buffer 碼流數(shù)據(jù) */ public void sendMessageToOne(long realPlayHandler, ByteBuffer buffer) { //登錄句柄無(wú)效 if (realPlayHandler == 0) { log.error("loginHandler is invalid.please check.", this); return; } RealPlayInfo realPlayInfo = AutoRegisterEventModule.findRealPlayInfo(realPlayHandler); if(realPlayInfo == null){ //連接已斷開 } String key = realPlayInfo.getChannel()+realPlayInfo.getSbbh(); Session session = sessions.get(key); if (session != null) { synchronized (session) { try { session.getBasicRemote().sendBinary(buffer); byte[] bytes=new byte[buffer.limit()]; buffer.get(bytes); } catch (IOException e) { e.printStackTrace(); } } } else { //log.error("session is null.please check.", this); } }
這樣就實(shí)現(xiàn)了視頻監(jiān)控.
效果:
分享一下websocket代碼:
package com.dahuatech.netsdk.webpreview.websocket; import cn.hutool.log.Log; import cn.hutool.log.LogFactory; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** * @description websocket實(shí)現(xiàn)類 */ @ServerEndpoint("/websocket/{realPlayHandler}") @Component public class WebSocketServer { private static Log log = LogFactory.get(WebSocketServer.class); private FileOutputStream outputStream; /** * 靜態(tài)變量,用來(lái)記錄當(dāng)前在線連接數(shù)。應(yīng)該把它設(shè)計(jì)成線程安全 */ private final AtomicInteger onlineCount = new AtomicInteger(0); /** * 存放每個(gè)客戶端對(duì)應(yīng)的WebSocket對(duì)象,根據(jù)設(shè)備realPlayHandler建立session */ public static ConcurrentHashMap<Long, Session> sessions = new ConcurrentHashMap<>(); /** * 存放客戶端的對(duì)象 *//* public static CopyOnWriteArrayList<Session> sessionList=new CopyOnWriteArrayList<>();*/ /** * 有websocket client連接 * * @param realPlayHandler 預(yù)覽句柄 * @param session */ @OnOpen public void OnOpen(@PathParam("realPlayHandler") long realPlayHandler, Session session) { if (sessions.containsKey(realPlayHandler)) { sessions.put(realPlayHandler, session); } else { sessions.put(realPlayHandler, session); addOnlineCount(); } log.info("websocket connect.session: " + session); } /** * 連接關(guān)閉調(diào)用的方法 * * @param realPlayHandler 預(yù)覽句柄 * @param session websocket連接對(duì)象 */ @OnClose public void onClose(@PathParam("realPlayHandler") Long realPlayHandler, Session session) { if (sessions.containsKey(realPlayHandler)) { sessions.remove(realPlayHandler); subOnlineCount(); } } /** * 發(fā)生錯(cuò)誤 * * @param throwable e */ @OnError public void onError(Throwable throwable) { throwable.printStackTrace(); } /** * 收到客戶端發(fā)來(lái)消息 * * @param message 消息對(duì)象 */ @OnMessage public void onMessage(ByteBuffer message) { log.info("服務(wù)端收到客戶端發(fā)來(lái)的消息: {}", message); } /** * 收到客戶端發(fā)來(lái)消息 * * @param message 字符串類型消息 */ @OnMessage public void onMessage(String message) { log.info("服務(wù)端收到客戶端發(fā)來(lái)的消息: {}", message); } /** * 發(fā)送消息 * * @param message 字符串類型的消息 */ public void sendAll(String message) { for (Map.Entry<Long, Session> session : sessions.entrySet()) { session.getValue().getAsyncRemote().sendText(message); } } /** * 發(fā)送binary消息 * * @param buffer */ public void sendMessage(ByteBuffer buffer) { for (Map.Entry<Long, Session> session : sessions.entrySet()) { session.getValue().getAsyncRemote().sendBinary(buffer); } } /** * 發(fā)送binary消息給指定客戶端 * * @param realPlayHandler 預(yù)覽句柄 * @param buffer 碼流數(shù)據(jù) */ public void sendMessageToOne(long realPlayHandler, ByteBuffer buffer) { //登錄句柄無(wú)效 if (realPlayHandler == 0) { log.error("loginHandler is invalid.please check.", this); return; } Session session = sessions.get(realPlayHandler); if (session != null) { synchronized (session) { try { session.getBasicRemote().sendBinary(buffer); byte[] bytes=new byte[buffer.limit()]; buffer.get(bytes); } catch (IOException e) { e.printStackTrace(); } } } else { //log.error("session is null.please check.", this); } } public void sendMessageToAll(ByteBuffer buffer) { for (Session session : sessions.values()) { synchronized (session) { try { /** * tomcat的原因,使用session.getAsyncRemote()會(huì)報(bào)Writing FULL WAITING error * 需要使用session.getBasicRemote() */ session.getBasicRemote().sendBinary(buffer); } catch (Exception e) { e.printStackTrace(); } } } } /** * 主動(dòng)關(guān)閉websocket連接 * * @param realPlayHandler 預(yù)覽句柄 */ public void closeSession(long realPlayHandler) { try { Session session = sessions.get(realPlayHandler); if (session != null) { session.close(); } } catch (IOException e) { e.printStackTrace(); } } /** * 獲取當(dāng)前連接數(shù) * * @return */ public int getOnlineCount() { return onlineCount.get(); } /** * 增加當(dāng)前連接數(shù) * * @return */ public int addOnlineCount() { return onlineCount.getAndIncrement(); } /** * 減少當(dāng)前連接數(shù) * * @return */ public int subOnlineCount() { return onlineCount.getAndDecrement(); } }
遇見的坑:
前端在播放的時(shí)候一開始始終不出畫面.流數(shù)據(jù)已經(jīng)拉過(guò)來(lái)了.后來(lái)才發(fā)現(xiàn)是因?yàn)閔asAudio參數(shù)
這里如果設(shè)置成了true.則你的電腦必須插入耳機(jī).不然會(huì)報(bào)錯(cuò);
總結(jié):
之前使用純前端實(shí)現(xiàn)視頻監(jiān)控和回放時(shí).瀏覽器時(shí)只支持IE.使用后端推流的方式實(shí)現(xiàn)視頻監(jiān)控和回放時(shí).瀏覽器支持谷歌火狐Edge等.但是又不支持IE了.很有意思.
flv的官方文檔解釋的是:
由于IO限制,flv.js可以支持HTTP上的FLV直播流Chrome 43+,F(xiàn)ireFox 42+,Edge 15.15048+和Safari 10.1+現(xiàn)在。
最后:
由于是后端不停的拉流.所以流量和服務(wù)器壓力比較大.可能同時(shí)打開多個(gè)監(jiān)控.會(huì)出現(xiàn)卡頓的情況.需要注意.
到此這篇關(guān)于vue+flv.js+SpringBoot+websocket實(shí)現(xiàn)視頻監(jiān)控與回放的文章就介紹到這了,更多相關(guān)vue SpringBoot websocket視頻監(jiān)控與回放內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Element-Plus?el-col、el-row快速布局及使用方法
這篇文章主要介紹了Element-Plus?el-col、el-row快速布局及使用方法,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12淺析Vue2和Vue3中雙向綁定機(jī)制的優(yōu)化與差異
Vue.js?核心特性之一就是雙向綁定,本文將深入探討?Vue2?和?Vue3?在雙向綁定上的區(qū)別,并分析這些改進(jìn)對(duì)性能和開發(fā)體驗(yàn)的影響,希望對(duì)大家有所幫助2024-11-11antd-DatePicker組件獲取時(shí)間值,及相關(guān)設(shè)置方式
這篇文章主要介紹了antd-DatePicker組件獲取時(shí)間值,及相關(guān)設(shè)置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10Vue:Bin Code Editor格式化JSON編輯器詳解
文章介紹了BinCodeEditor組件的安裝、注冊(cè)、使用方法以及注意事項(xiàng),組件可以通過(guò)npm或yarn安裝,支持全局或局部注冊(cè),使用時(shí),可以通過(guò)value屬性綁定JavaScript值,或使用v-model,組件事件與方法包括編輯區(qū)顯示問(wèn)題的解決2024-12-12vue2.0中g(shù)oods選購(gòu)欄滾動(dòng)算法的實(shí)現(xiàn)代碼
這篇文章主要介紹了vue2.0中g(shù)oods選購(gòu)欄滾動(dòng)算法的實(shí)現(xiàn)代碼,需要的朋友可以參考下2017-05-05監(jiān)聽element-ui table滾動(dòng)事件的方法
這篇文章主要介紹了監(jiān)聽element-ui table滾動(dòng)事件的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03