開發(fā)所需
1.大華NetSDK_JAVA; 這里使用的是 Linux64的架包
2.websocket 前端使用的vue框架? ?
3.flv.js的播放插件? ??
4.大華攝像頭提供的平臺(后面稱為官方平臺)
【實時預(yù)覽】流程分析
根據(jù)大華《NetSDK_JAVA編程指導(dǎo)手冊》的流程圖
根據(jù)圖可以得知關(guān)鍵流程為:
初始化sdk——>登錄設(shè)備——>打開實時預(yù)覽——>設(shè)置視頻流的回調(diào)函數(shù)——>發(fā)送視頻流到前端
因該需求為內(nèi)網(wǎng)開發(fā)所以需要外網(wǎng)開發(fā)實現(xiàn)的可以搜索添加相關(guān)主動注冊方法來實現(xiàn)連接外網(wǎng)。
【整體流程】
1.Java后端通過NetSDK得到IPC回調(diào)的FLV流;
2.后端與前端通過websocket進(jìn)行數(shù)據(jù)的傳輸;
3.前端通過后端轉(zhuǎn)發(fā)的FLV流,使用flv.js進(jìn)行解析并播放。
注:以下代碼僅為方便測試寫的極簡版只為對第一次進(jìn)行這類開發(fā)的可以進(jìn)行單視頻的監(jiān)控預(yù)覽來更好的理解進(jìn)行后續(xù)開發(fā)。
代碼實現(xiàn):后端
一.導(dǎo)入相關(guān)的依賴和資源
lib包與common包可以從大華的demo中直接復(fù)制過來第一次開發(fā)可以把整個demo也復(fù)制進(jìn)來部分流程直接調(diào)用demo中方法后期再剔除掉多余部分。
二.添加websocket
websocket的教程有很多就不過多陳述了。
1.導(dǎo)入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.配置工具類
package com.ruoyi.power.common;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
@Configuration
//@EnableWebSocket
public class WebsocketConfig {
@Bean
public ServerEndpointExporter serverEndpoint() {
return new ServerEndpointExporter();
}
/**
* WebSocket 配置信息
*
* @return servletServerContainerFactoryBean
*/
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean bean = new ServletServerContainerFactoryBean();
// 文本緩沖區(qū)大小
bean.setMaxTextMessageBufferSize(8192);
// 字節(jié)緩沖區(qū)大小
bean.setMaxBinaryMessageBufferSize(8192);
return bean;
}
}
3.編寫websocket類
package com.ruoyi.power.service.impl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.stereotype.Controller; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.ConcurrentHashMap; @ServerEndpoint("/ws/monitor/{device}/{channel}") @Controller public class Websocket { private static ConfigurableApplicationContext applicationContext; public static void setApplicationContext(ConfigurableApplicationContext context) { applicationContext = context; } /** * 與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù) */ public static Session session; private static Websocket instance; /** * 與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù) */ private static ConcurrentHashMap<Long, Session> sessions = new ConcurrentHashMap<>(); private static final Logger log = LoggerFactory.getLogger(Websocket.class); /** * 靜態(tài)變量,用來記錄當(dāng)前在線連接數(shù)。應(yīng)該把它設(shè)計成線程安全的。 */ private static int onlineCount = 0; /** * 連接成功 * * @param session */ @OnOpen public void onOpen(Session session) { this.session = session; // 保存客戶端連接的Session對象 } /** * 連接關(guān)閉 * * @param session */ @OnClose public void onClose(Session session) { } /** * 接收到消息 * * @param text */ @OnMessage public String onMsg(String text) throws IOException { System.out.println("連接成功"); return null; } /** * 實現(xiàn)服務(wù)器主動推送 * @param realPlayHandler 播放拉流的回調(diào)句柄 * @param buffer 發(fā)送的數(shù)據(jù) * @throws IOException */ public void sendMessageToOne(long realPlayHandler, ByteBuffer buffer) throws IOException { session.getBasicRemote().sendBinary(buffer); } public static void sendBuffer(byte[] bytes, long realPlayHandler) { Websocket wsServerEndpoint = new Websocket(); /** * 發(fā)送流數(shù)據(jù) * 使用pBuffer.getByteBuffer(0,dwBufSize)得到的是一個指向native pointer的ByteBuffer對象,其數(shù)據(jù)存儲在native, * 而webSocket發(fā)送的數(shù)據(jù)需要存儲在ByteBuffer的成員變量hb,使用pBuffer的getByteBuffer得到的ByteBuffer其hb為null * 所以,需要先得到pBuffer的字節(jié)數(shù)組,手動創(chuàng)建一個ByteBuffer */ ByteBuffer buffer = ByteBuffer.wrap(bytes); try { wsServerEndpoint.sendMessageToOne(realPlayHandler, buffer); } catch (IOException e) { throw new RuntimeException(e); } } }
websocket的主要作用是將拉流成功后的視頻流回調(diào)數(shù)據(jù)發(fā)送給前端由flv.js處理成視頻呈現(xiàn)。
4.websocket調(diào)用其他類
這里注意如果websocket類中想要調(diào)用項目其他類內(nèi)的方法需要再運行類中配置。
public static void main(String[] args) {
// System.setProperty("spring.devtools.restart.enabled", "false");
ConfigurableApplicationContext run = SpringApplication.run(RuoYiApplication.class, args);
Websocket.setApplicationContext(run);
System.out.println("(????)?? 項目啟動成功 ?(′?`?)? \n");
}
三.初始化sdk 并登錄用戶
按照大華示例,進(jìn)行先觀摩demo中LoginModule的編寫,可以復(fù)制或直接調(diào)用相關(guān)方法或者自己按需編寫。
1.初始化
/**
* 初始化
* @param disConnect 斷線時的回調(diào)函數(shù)
* @param haveReConnect 斷線時的回調(diào)用戶參數(shù)為null時不會回調(diào)給用戶
* @return
*/
public static boolean init(NetSDKLib.fDisConnect disConnect, NetSDKLib.fHaveReConnect haveReConnect) {
bInit = netsdk.CLIENT_Init(disConnect, null);
if(!bInit) {
System.out.println("Initialize SDK failed");
return false;
}
//打開日志,可選
NetSDKLib.LOG_SET_PRINT_INFO setLog = new NetSDKLib.LOG_SET_PRINT_INFO();
File path = new File("./sdklog/");
if (!path.exists()) {
path.mkdir();
}
String logPath = path.getAbsoluteFile().getParent() + "\\sdklog\\" + ToolKits.getDate() + ".log";
setLog.nPrintStrategy = 0;
setLog.bSetFilePath = 1;
System.arraycopy(logPath.getBytes(), 0, setLog.szLogFilePath, 0, logPath.getBytes().length);
System.out.println(logPath);
setLog.bSetPrintStrategy = 1;
bLogopen = netsdk.CLIENT_LogOpen(setLog);
if(!bLogopen ) {
System.err.println("Failed to open NetSDK log");
}
// 設(shè)置斷線重連回調(diào)接口,設(shè)置過斷線重連成功回調(diào)函數(shù)后,當(dāng)設(shè)備出現(xiàn)斷線情況,SDK內(nèi)部會自動進(jìn)行重連操作
// 此操作為可選操作,但建議用戶進(jìn)行設(shè)置
netsdk.CLIENT_SetAutoReconnect(haveReConnect, null);
//設(shè)置登錄超時時間和嘗試次數(shù),可選
int waitTime = 5000; //登錄請求響應(yīng)超時時間設(shè)置為5S
int tryTimes = 1; //登錄時嘗試建立鏈接1次
netsdk.CLIENT_SetConnectTime(waitTime, tryTimes);
// 設(shè)置更多網(wǎng)絡(luò)參數(shù),NET_PARAM的nWaittime,nConnectTryNum成員與CLIENT_SetConnectTime
// 接口設(shè)置的登錄設(shè)備超時時間和嘗試次數(shù)意義相同,可選
NetSDKLib.NET_PARAM netParam = new NetSDKLib.NET_PARAM();
netParam.nConnectTime = 10000; // 登錄時嘗試建立鏈接的超時時間
netParam.nGetConnInfoTime = 3000; // 設(shè)置子連接的超時時間
netParam.nGetDevInfoTime = 3000;//獲取設(shè)備信息超時時間,為0默認(rèn)1000ms
netsdk.CLIENT_SetNetworkParam(netParam);
return true;
}
綜上代碼可看出初始化關(guān)鍵部分為bInit = netsdk.CLIENT_Init(disConnect, null);這句代碼進(jìn)行初始話如無回調(diào)需求直接參數(shù)都為null即可。
2.設(shè)備登錄
/**
* 登錄設(shè)備
* @param m_strIp 地址ip如:192.168.2.100(這里為你攝像頭官方給的平臺ip)
* @param m_nPort 端口37777 (同為你攝像頭官方給的平臺的端口)
* @param m_strUser 你的用戶名
* @param m_strPassword 密碼
* @return
*/
public static boolean login(String m_strIp, int m_nPort, String m_strUser, String m_strPassword) {
//IntByReference nError = new IntByReference(0);
//入?yún)? NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY pstInParam=new NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY();
pstInParam.nPort=m_nPort;
pstInParam.szIP=m_strIp.getBytes();
pstInParam.szPassword=m_strPassword.getBytes();
pstInParam.szUserName=m_strUser.getBytes();
//出參
NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY pstOutParam=new NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY();
pstOutParam.stuDeviceInfo=m_stDeviceInfo;
//m_hLoginHandle = netsdk.CLIENT_LoginEx2(m_strIp, m_nPort, m_strUser, m_strPassword, 0, null, m_stDeviceInfo, nError);
m_hLoginHandle=netsdk.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam);
if(m_hLoginHandle.longValue() == 0) {
System.err.printf("Login Device[%s] Port[%d]Failed. %s\n", m_strIp, m_nPort, ToolKits.getErrorCodePrint());
} else {
System.out.println("Login Success [ " + m_strIp + " ]");
}
return m_hLoginHandle.longValue() == 0? false:true;
}
四.拉取實時預(yù)覽的視頻流
同樣先看demo中的Realpaly的啟動類通過斷點來了解其運行的順序和各功能下的實現(xiàn)方法后
找出此段對視頻進(jìn)行拉流的具體操作。
public void realplay(){
lRealHandle= netSdk.CLIENT_RealPlayEx(loginHandle, 0, null, 0);
if(lRealHandle.longValue()!=0){
System.out.println("realplay success");
netSdk.CLIENT_SetRealDataCallBackEx(lRealHandle, CbfRealDataCallBackEx.getInstance(),null, 31);
}
}
但是要注意的是?netSdk.CLIENT_RealPlayEx的拉流為默認(rèn)的視頻流格式或許純在不適用與flv.js情況所以根據(jù)需要用到另一種自定義設(shè)置回調(diào)流格式的預(yù)覽方法接口。
該方法位于官方所提供的sdk類中但并沒有在說明文檔中具體解釋下面是該接口的主觀使用方法
這里我們需要將:
回調(diào)的數(shù)據(jù)類型選擇為流式FLV;
通道號:可以在官方平臺下查看什么通道號;
而登錄的回調(diào)函數(shù)是之前登錄方法的?m_hLoginHandle=netsdk.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam);該接口的回調(diào)數(shù)據(jù)?m_hLoginHandle;
/**
* 開始實時預(yù)覽
* @param m_hLoginHandle 登錄句柄
* @param nChannelID 通道ID
* @param rType 碼流類型 ,參考 NET_RealPlayType
* 0 // 實時預(yù)覽
* 3 // 實時預(yù)覽-從碼流1
* 7 // 多畫面預(yù)覽-4畫面
* 9 // 多畫面預(yù)覽-9畫面
* 10 // 多畫面預(yù)覽-16畫面
* @param emDataType 回調(diào)的數(shù)據(jù)類型,詳見 EM_REAL_DATA_TYPE
* 0; // 私有碼流
* 1; // 國標(biāo)PS碼流
* 2; // TS碼流
* 3; // MP4文件
* 4; // 裸H264碼流
* 5; // 流式FLV`
* @return 預(yù)覽句柄
*/
public static NetSDKLib.LLong startRealPlay(NetSDKLib.LLong m_hLoginHandle,
int nChannelID, int rType, int emDataType) {
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 = nChannelID;
inParam.rType = rType;
inParam.emDataType = emDataType;
NetSDKLib.LLong lRealHandle = netsdk.CLIENT_RealPlayByDataType(m_hLoginHandle,
inParam, outParam, 3000);
}
當(dāng)然關(guān)于inParam 的所有參數(shù)使用還需要根據(jù)sdk中的這段代碼具體理解
// 開始實時預(yù)覽并指定回調(diào)數(shù)據(jù)格式入?yún)? public static class NET_IN_REALPLAY_BY_DATA_TYPE extends SdkStructure
{
public int dwSize; // 結(jié)構(gòu)體大小
public int nChannelID; // 通道編號
public Pointer hWnd; // 窗口句柄, HWND類型
public int rType; // 碼流類型 ,參考 NET_RealPlayType
public fRealDataCallBackEx cbRealData; // 數(shù)據(jù)回調(diào)函數(shù)
public int emDataType; // 回調(diào)的數(shù)據(jù)類型,參考 EM_REAL_DATA_TYPE
public Pointer dwUser; // 用戶數(shù)據(jù)
public String szSaveFileName; // 轉(zhuǎn)換后的文件名
public fRealDataCallBackEx2 cbRealDataEx; // 數(shù)據(jù)回調(diào)函數(shù)-擴(kuò)展
public int emAudioType; // 音頻格式,對應(yīng)枚舉EM_AUDIO_DATA_TYPE
public Callback cbRealDataEx2;// 數(shù)據(jù)回調(diào)(擴(kuò)展帶時間戳,幀類型),使用fDataCallBackEx
public NET_IN_REALPLAY_BY_DATA_TYPE() {
this.dwSize = this.size();
}
}
五.設(shè)置回調(diào)的函數(shù)接口
在拉取流判斷成功后我們需要去接受攝像機(jī)向我們發(fā)送過來的視頻流,而這需要一個對應(yīng)的接口而設(shè)置方法如下:
代碼實現(xiàn)為:
/**
* 開始實時預(yù)覽
* @param m_hLoginHandle 登錄句柄
* @param nChannelID 通道ID
* @param rType 碼流類型 ,參考 NET_RealPlayType
* 0 // 實時預(yù)覽
* 3 // 實時預(yù)覽-從碼流1
* 7 // 多畫面預(yù)覽-4畫面
* 9 // 多畫面預(yù)覽-9畫面
* 10 // 多畫面預(yù)覽-16畫面
* @param emDataType 回調(diào)的數(shù)據(jù)類型,詳見 EM_REAL_DATA_TYPE
* 0; // 私有碼流
* 1; // 國標(biāo)PS碼流
* 2; // TS碼流
* 3; // MP4文件
* 4; // 裸H264碼流
* 5; // 流式FLV`
* @return 預(yù)覽句柄
*/
public static NetSDKLib.LLong startRealPlay(NetSDKLib.LLong m_hLoginHandle,
int nChannelID, int rType, int emDataType) {
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 = nChannelID;
inParam.rType = rType;
inParam.emDataType = emDataType;
NetSDKLib.LLong lRealHandle = netsdk.CLIENT_RealPlayByDataType(m_hLoginHandle,
inParam, outParam, 3000);
if (lRealHandle.longValue() != 0) {
System.out.println("拉取預(yù)覽成功 success" + lRealHandle);
//設(shè)置回調(diào)的接收
//lRealHandle :拉流成功的句柄
//你的接收回調(diào)的類里的方法 注:這里使用的是官方demo中的RealplayEx類下的接口
netsdk.CLIENT_SetRealDataCallBackEx(lRealHandle, RealplayEx.CbfRealDataCallBackEx.getInstance(),
null, 31);
return lRealHandle;
} else {
return null;
}
}
六.接收視頻流并推送給前端
該方法也在RealplayEx.java內(nèi)有模板。
1.接收視頻流
/**
* 實時預(yù)覽數(shù)據(jù)回調(diào)函數(shù)--擴(kuò)展(pBuffer內(nèi)存由SDK內(nèi)部申請釋放)
*/
private static class CbfRealDataCallBackEx implements NetSDKLib.fRealDataCallBackEx {
private CbfRealDataCallBackEx() {
}
private static class CallBackHolder {
private static CbfRealDataCallBackEx instance = new CbfRealDataCallBackEx();
}
public static CbfRealDataCallBackEx getInstance() {
return CallBackHolder.instance;
}
@Override
public void invoke(LLong lRealHandle, int dwDataType, Pointer pBuffer,
int dwBufSize, int param, Pointer dwUser) {
int bInput=0;
if(0 != lRealHandle.longValue())
{
switch(dwDataType) {
case 0:
System.out.println("碼流大小為" + dwBufSize + "\n" + "碼流類型為原始音視頻混合數(shù)據(jù)");
break;
case 1:
//標(biāo)準(zhǔn)視頻數(shù)據(jù)
break;
case 2:
//yuv 數(shù)據(jù)
break;
case 3:
//pcm 音頻數(shù)據(jù)
break;
case 4:
//原始音頻數(shù)據(jù)
break;
default:
break;
}
}
}
}
在我們開啟實時預(yù)覽后攝像機(jī)會把視頻流發(fā)送給這個方法中其中獲得的參數(shù)有:
lRealHandle? 代表該條流是哪個拉流的回調(diào)? ;
dwDataType? 回調(diào)的視頻流的格式是什么前文設(shè)置為flv的視頻流;
pBuffer? 視頻流的具體數(shù)據(jù);
dwBufSize 視頻流的大小;
在知道了這些之后再來對他進(jìn)行改寫為我們需要的形式。
/**
* 實時預(yù)覽數(shù)據(jù)回調(diào)函數(shù)--擴(kuò)展(pBuffer內(nèi)存由SDK內(nèi)部申請釋放)
*/
public static class CbfRealDataCallBackEx implements NetSDKLib.fRealDataCallBackEx {
@Autowired
private WsServerEndpoint server;
private CbfRealDataCallBackEx() {
}
private static class CallBackHolder {
private static CbfRealDataCallBackEx instance = new CbfRealDataCallBackEx();
}
public static CbfRealDataCallBackEx getInstance() {
return CallBackHolder.instance;
}
@Override
public void invoke(LLong lRealHandle, int dwDataType, Pointer pBuffer,
int dwBufSize, int param, Pointer dwUser) {
//將內(nèi)容轉(zhuǎn)換為字節(jié)數(shù)組
byte[] buffer = pBuffer.getByteArray(0, dwBufSize);
if ((dwDataType - 1000) == 5) {//回調(diào)格式為flv的流
//通過websocket發(fā)送
server.sendBuffer(buffer, lRealHandle.longValue());
}
}
}
2.websocket向前端發(fā)送
public static void sendBuffer(byte[] bytes, long realPlayHandler) {
Websocket wsServerEndpoint = new Websocket();
/**
* 發(fā)送流數(shù)據(jù)
* 使用pBuffer.getByteBuffer(0,dwBufSize)得到的是一個指向native pointer的ByteBuffer對象,其數(shù)據(jù)存儲在native,
* 而webSocket發(fā)送的數(shù)據(jù)需要存儲在ByteBuffer的成員變量hb,使用pBuffer的getByteBuffer得到的ByteBuffer其hb為null
* 所以,需要先得到pBuffer的字節(jié)數(shù)組,手動創(chuàng)建一個ByteBuffer
*/
ByteBuffer buffer = ByteBuffer.wrap(bytes);
try {
wsServerEndpoint.sendMessageToOne(realPlayHandler, buffer);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 實現(xiàn)服務(wù)器主動推送
*/
public void sendMessageToOne(long realPlayHandler, ByteBuffer buffer) throws IOException {
if (realPlayHandler == 0) {
log.error("loginHandler is invalid.please check.", this);
return;
}
Session session = sessions.get(realPlayHandler);
if (session != null && session.isOpen()) { // 確保session不為null
synchronized (session) {
try {
System.out.println(buffer);
session.getBasicRemote().sendBinary(buffer);
} catch (Exception e) {
e.printStackTrace();
}
}
}else {
log.error("session is null.please check.", this);
}
}
至此后端拉流推流已經(jīng)完成,但是在斷開連接時也要記得停止實時預(yù)覽的拉流和釋放sdk詳情產(chǎn)靠官方文檔中有介紹
代碼實現(xiàn):前端
使用的是vue的框架結(jié)構(gòu)
vue項目引入flv.js。
npm install --save flv.js
main.js里面引入
import flvjs from ‘flv.js’;
Vue.use(flvjs)
1.js
(1):導(dǎo)入
import flvjs from "flv.js/dist/flv.js";
(2):使用flv.js實現(xiàn)播放flv格式流,獲取video節(jié)點
videoElement = this.$refs.videoElement
if (flvjs.isSupported()) {
flvPlayer = flvjs.createPlayer({
type: 'flv', //媒體類型
url: 'ws://127.0.0.1:6102/ws/monitor/1/0' //flv格式媒體URL
isLive: true, //數(shù)據(jù)源是否為直播流
hasAudio: false, //數(shù)據(jù)源是否包含有音頻
hasVideo: true, //數(shù)據(jù)源是否包含有視頻
enableStashBuffer: false //是否啟用緩存區(qū)
},{
enableWorker: false, //不啟用分離線程
enableStashBuffer: false, //關(guān)閉IO隱藏緩沖區(qū)
autoCleanupSourceBuffer: true //自動清除緩存
});
flvPlayer.attachMediaElement(videoElement); //將播放實例注冊到節(jié)點
flvPlayer.load(); //加載數(shù)據(jù)流
flvPlayer.play(); //播放數(shù)據(jù)流
}
(3):關(guān)閉視頻流
flvPlayer.pause(); //暫停播放數(shù)據(jù)流
flvPlayer.unload(); //取消數(shù)據(jù)流加載
flvPlayer.detachMediaElement(); //將播放實例從節(jié)點中取出
flvPlayer.destroy(); //銷毀播放實例
2.html
<div class="row" style=" height:500px;width: auto;background-color: #1c84c6">
<video ref="videoElement"
class="centeredVideo"
id="myPlayer"
preload="auto"
type="rtmp/flv"
controls
autoplay
muted
style="width: 100%;height: 100%"
@click="handleClick"
></video>
</div>
成果展示
功能補充
flv.js的接口介紹文檔
flv.js的追幀、斷流重連及實時更新的直播優(yōu)化方案
最后的最后如果你的視頻還是不能播放比如顯示格式不正確獲得的流不是標(biāo)準(zhǔn)flv流時注意檢查官方平臺下一定要把編碼模式改為H.264文章來源:http://www.zghlxwxcb.cn/news/detail-761531.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-761531.html
到了這里,關(guān)于大華攝像頭實時預(yù)覽(spring boot+websocket+flv.js)Java開發(fā)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!