vue+flv.js+SpringBoot+websocket實(shí)現(xiàn)視頻監(jiān)控與回放
需求:vue+springboot的項(xiàng)目,需要在頁面展示出??档挠脖P錄像機(jī)連接的攝像頭的實(shí)時(shí)監(jiān)控畫面以及回放功能.
- 之前項(xiàng)目里是純前端實(shí)現(xiàn)視頻監(jiān)控和回放功能.但是有局限性.就是ip地址必須固定.新的需求里設(shè)備ip不固定.所以必須換一種思路.
- 通過設(shè)備的主動(dòng)注冊,讓設(shè)備去主動(dòng)連接服務(wù)器后端通過socket推流給前端實(shí)現(xiàn)實(shí)時(shí)監(jiān)控和回放功能;
思路:
1:初始化設(shè)備.后端項(xiàng)目啟動(dòng)時(shí)就調(diào)用初始化方法.
2:開啟socket連接.前端頁面加載時(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)]有問題.但是打包之后發(fā)現(xiàn)ie瀏覽器報(bào)語法錯(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"
})
],
最后頁面引入時(shí):
import flvjs from "flv.js/dist/flv.js";
2.準(zhǔn)備一個(gè)硬盤錄像機(jī),并添加一個(gè)攝像頭設(shè)備以做測試使用.
硬盤錄像機(jī)設(shè)置為主動(dòng)注冊模式.并配置好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.頁面加載嘗試開啟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ù)是通道號和設(shè)備信息.無需在意.只要是唯一key就可以.
2.socket連接成功后.調(diào)用后端推流方法實(shí)現(xiàn)播放.
這里說一下后端的推流方法.
調(diào)用SDK里的CLIENT_RealPlayByDataType方法
/**
* 實(shí)時(shí)預(yù)覽拉流
*
* @param loginHandler 登錄句柄
* @param channel 通道號
* @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) {
//過濾碼流
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對象,其數(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) {
//登錄句柄無效
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)變量,用來記錄當(dāng)前在線連接數(shù)。應(yīng)該把它設(shè)計(jì)成線程安全
*/
private final AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 存放每個(gè)客戶端對應(yīng)的WebSocket對象,根據(jù)設(shè)備realPlayHandler建立session
*/
public static ConcurrentHashMap<Long, Session> sessions = new ConcurrentHashMap<>();
/**
* 存放客戶端的對象
*//*
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連接對象
*/
@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ā)來消息
*
* @param message 消息對象
*/
@OnMessage
public void onMessage(ByteBuffer message) {
log.info("服務(wù)端收到客戶端發(fā)來的消息: {}", message);
}
/**
* 收到客戶端發(fā)來消息
*
* @param message 字符串類型消息
*/
@OnMessage
public void onMessage(String message) {
log.info("服務(wù)端收到客戶端發(fā)來的消息: {}", 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) {
//登錄句柄無效
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)拉過來了.后來才發(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)在。文章來源:http://www.zghlxwxcb.cn/news/detail-432895.html
最后:
由于是后端不停的拉流.所以流量和服務(wù)器壓力比較大.可能同時(shí)打開多個(gè)監(jiān)控.會(huì)出現(xiàn)卡頓的情況.需要注意.文章來源地址http://www.zghlxwxcb.cn/news/detail-432895.html
到了這里,關(guān)于vue+flv.js+SpringBoot+websocket實(shí)現(xiàn)視頻監(jiān)控與回放的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!