大华网络摄像头视频播放(基于vue+SpringBoot)

365体育世界杯专用版 ⌛ 2025-10-27 13:51:43 ✍️ admin 👁️ 2798 ❤️ 792
大华网络摄像头视频播放(基于vue+SpringBoot)

引言

近期接到一个需求,需要实现下面2点。

1.实时预览大华网络摄像机(IPC);2.网络硬盘录像机(NVR)的回放。

笔者通过大华官网支持(support)的页面,下载了WEB相关的SDK(WEB无插件开发包),却发现无法实现公网访问。退而求其次寻求替代方案,最后确定使用Java后端转发视频流的方式来实现需求。

本文参考了下面2位作者的文章,在此表示非常感谢。

大华SDK+JAVA+4g网络摄像头进行二次开发(Cljxy~)

vue+flv.js+SpringBoot+websocket实现视频监控与回放(香草味糖葫芦)

开发前准备

1.大华IPC摄像头;2.大华NetSDK_JAVA;3.IntelliJ IDEA。

技术分析

【实时预览】

根据大华《NetSDK_JAVA编程指导手册》的流程图

目标:在【回调fRealDataCallBackEx 解析码流】这个步骤,得到码流推送至前端解码即可。

此流程图中没有外网访问的功能,该功能需要另一份开发文档【NetSDK_JAVA 主动注册】来实现。

【主动注册】

主动注册需要启用监听服务器监听9500端口,然后在网络摄像头的设置中按图设置好服务器的ip地址、端口、子设备ID(需要保证系统唯一性)。

设备会隔间30秒左右向服务器注册自己的信息(【ip地址】,【端口号】,【设备ID】)。然后在设备的信息基础上增加【设备账户】与【设备密码】,即可得到登录句柄。

什么是句柄? JAVA 开发人员很少接触、使用句柄。句柄是一个唯一的整数,作为对象的身份id,用于区分不同的对象和同类中的不同实例。程序可以通过句柄访问对象的部分信息。JAVA 中任何东西都可以视为对象。我们可以认为操纵的标识符实际是指向一个对象的“句柄"(Handle)。

【最终方案】

1.Java后端通过NetSDK得到IPC回调的FLV流;

2.后端与前端通过websocket进行数据的传输;

3.前端通过后端转发的FLV流,使用flv.js进行解析并播放。

代码实现

1.新建springboot项目

2.导入相关的依赖和资源

lib包与common包可以从大华的demo中直接复制过来。linnx64与win64按需要复制即可。要注意NetSDKLib.java文件在windows与liunx中有差异,请按实际需要选择。

3.SDK启动

按照大华示例,进行相应模块的编写,如LoginModule、AutoRegisterModule、ServiceCB、DisConnect、HaveReConnect的编写,可以复制相关示例或者自己按需编写。

public class LoginModule {

public static NetSDKLib netSdk = NetSDKLib.NETSDK_INSTANCE;

// 设备信息

public static NetSDKLib.NET_DEVICEINFO_Ex m_stDeviceInfo = new NetSDKLib.NET_DEVICEINFO_Ex();

// 登陆句柄

public static NetSDKLib.LLong m_hLoginHandle = new NetSDKLib.LLong(0);

private static boolean bInit = false;

private static boolean bLogopen = false;

/**

* 初始化

*/

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");

}

// 设置断线重连回调接口,设置过断线重连成功回调函数后,当设备出现断线情况,SDK内部会自动进行重连操作

// 此操作为可选操作,但建议用户进行设置

netSdk.CLIENT_SetAutoReconnect(haveReConnect, null);

//设置登录超时时间和尝试次数,可选

int waitTime = 5000; //登录请求响应超时时间设置为5S

int tryTimes = 1; //登录时尝试建立链接1次

netSdk.CLIENT_SetConnectTime(waitTime, tryTimes);

// 设置更多网络参数,NET_PARAM的nWaittime,nConnectTryNum成员与CLIENT_SetConnectTime

// 接口设置的登录设备超时时间和尝试次数意义相同,可选

NetSDKLib.NET_PARAM netParam = new NetSDKLib.NET_PARAM();

netParam.nConnectTime = 10000; // 登录时尝试建立链接的超时时间

netParam.nGetConnInfoTime = 3000; // 设置子连接的超时时间

netParam.nGetDevInfoTime = 3000;//获取设备信息超时时间,为0默认1000ms

netSdk.CLIENT_SetNetworkParam(netParam);

return true;

}

/**

* 清除环境

*/

public static void cleanup() {

if(bLogopen) {

netSdk.CLIENT_LogClose();

}

if(bInit) {

netSdk.CLIENT_Cleanup();

}

}

/**

* 登录设备

*/

public static boolean login(String m_strIp, int m_nPort, String m_strUser, String m_strPassword) {

//IntByReference nError = new IntByReference(0);

//入参

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();

//出参

NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY pstOutParam=new NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY();

pstOutParam.stuDeviceInfo=m_stDeviceInfo;

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;

}

/**

* 登出设备

*/

public static boolean logout() {

if(m_hLoginHandle.longValue() == 0) {

return false;

}

boolean bRet = netSdk.CLIENT_Logout(m_hLoginHandle);

if(bRet) {

m_hLoginHandle.setValue(0);

}

return bRet;

}

}

public class AutoRegisterModule {

public static final NetSDKLib DHNetSdkLib = NetSDKLib.NETSDK_INSTANCE;

// 监听服务句柄

public static NetSDKLib.LLong mServerHandler = new NetSDKLib.LLong(0);

// 主动注册监听回调

private static ServiceCB serviceCallback = new ServiceCB();

// 设备断线回调

private static DisConnect disConnectCallback = new DisConnect();

// 设备重连通知回调

private static HaveReConnect haveReConnect = new HaveReConnect();

// 设备列表

/**

* 开启服务

* @param address 本地IP地址

* @param port 本地端口, 建议9500

*/

public static boolean startServer(String address, int port) {

//SDK初始化,并设置回调

boolean flag = LoginModule.init(disConnectCallback, haveReConnect);

if (flag){

System.out.println("注册成功");

}

mServerHandler = DHNetSdkLib.CLIENT_ListenServer(address, port, 1000, serviceCallback, null);

if (0 == mServerHandler.longValue()) {

System.err.println("Failed to start server." + ToolKits.getErrorCodePrint());

} else {

System.out.printf("Start server, [Server address %s][Server port %d]\n", address, port);

}

return mServerHandler.longValue() != 0;

}

/**

* 结束服务

*/

public static boolean stopServer() {

boolean flag = false;

if(mServerHandler.longValue() != 0) {

flag = DHNetSdkLib.CLIENT_StopListenServer(mServerHandler);

mServerHandler.setValue(0);

System.out.println("Stop server!");

}

return flag;

}

public static boolean getServerState() {

return mServerHandler.longValue() != 0;

}

/**

* 设备断线回调: 通过 CLIENT_Init 设置该回调函数,当设备出现断线时,SDK会调用该函数

*/

private static class DisConnect implements NetSDKLib.fDisConnect {

public void invoke(NetSDKLib.LLong m_hLoginHandle, String pchDVRIP, int nDVRPort, Pointer dwUser) {

System.out.printf("Device[%s] Port[%d] DisConnect!\n", pchDVRIP, nDVRPort);

}

}

/**

* 设备重连回调: 通过 CLIENT_Init 设置该回调函数,当设备出现断线时,SDK会调用该函数

*/

private static class HaveReConnect implements NetSDKLib.fHaveReConnect {

public void invoke(NetSDKLib.LLong m_hLoginHandle, String pchDVRIP, int nDVRPort, Pointer dwUser) {

System.out.printf("ReConnect Device[%s] Port[%d]\n", pchDVRIP, nDVRPort);

}

}

}

public class ServiceCB implements NetSDKLib.fServiceCallBack {

@Override

public int invoke(NetSDKLib.LLong lHandle, final String pIp, final int wPort,

int lCommand, Pointer pParam, int dwParamLen,

Pointer dwUserData) {

// 将 pParam 转化为序列号

byte[] buffer = new byte[dwParamLen];

pParam.read(0, buffer, 0, dwParamLen);

String deviceId = "";

try {

deviceId = new String(buffer, "GBK").trim();

} catch (UnsupportedEncodingException e) {

e.printStackTrace();

}

System.out.printf("Register Device Info [Device address %s][port %s][DeviceID %s] \n", pIp, wPort, deviceId);

switch(lCommand) {

case NetSDKLib.EM_LISTEN_TYPE.NET_DVR_DISCONNECT: { // 验证期间设备断线回调

break;

}

case NetSDKLib.EM_LISTEN_TYPE.NET_DVR_SERIAL_RETURN: {// 设备注册携带序列号

List deviceList = DeviceListUtil.getDeviceList();

for (DevicesModule deviceModule : deviceList) {

if (deviceModule.getDeviceInfo().getDid().equals(deviceId)) {

//设置为虚拟pIp,wPort

deviceModule.getDeviceInfo().setDIp(pIp);

deviceModule.getDeviceInfo().setDPort(wPort);

Executors.newSingleThreadExecutor().submit(new Runnable() {

@Override

public void run() {

//设备登陆,标记状态

deviceModule.login();

}

});

}

}

break;

}

default:

break;

}

return 0;

}

}

这里主要是通过AutoRegisterModule.startServer(serverIP, serverPort)方法来启动自动注册监听,设备通过设置自动注册后,将自己的信息通过回调进ServiceCB中,然后根据设备信息登录来获取【设备登录句柄】。

4.websocket

websocket的示例很多,第一步是pom中引入

org.springframework.boot

spring-boot-starter-websocket

配置WebSocketConfig

@Configuration

public class WebSocketConfig {

/**

* 注入ServerEndpointExporter,

* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint

*/

@Bean

public ServerEndpointExporter serverEndpointExporter() {

return new ServerEndpointExporter();

}

}

编写

@ServerEndpoint("/device/monitor/{device}/{channel}")

@Component

@Slf4j

public class WebSocketServer {

public static VideoMonitorService service;

/**

* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全

*/

private final AtomicInteger onlineCount = new AtomicInteger(0);

/**

* 存放每个客户端对应的WebSocket对象,根据设备realPlayHandler建立session

*/

public static ConcurrentHashMap sessions = new ConcurrentHashMap<>();

/**

* 保存覆盖播放标识

*/

public static CopyOnWriteArrayList sessionList = new CopyOnWriteArrayList<>();

/**

* 有websocket client连接

* @param device 设备ID

* @param channel 预览句柄

* @param session

*/

@OnOpen

public void OnOpen(@PathParam("device") String device, @PathParam("channel") String channel, Session session) throws InterruptedException {

log.info("连接进入");

//设备ID+预览句柄组成唯一性标识

String uuid = device + channel;

if (sessions.containsKey(uuid)) {

sessions.put(uuid, session);

} else {

sessions.put(uuid, session);

addOnlineCount();

}

log.info("websocket connect.session: " + session);

}

/**

* 连接关闭调用的方法

* @param device 设备ID

* @param channel 预览句柄

* @param session websocket连接对象

*/

@OnClose

public void onClose(@PathParam("device") String device, @PathParam("channel") String channel, Session session){

String uuid = device + channel;

if (sessions.containsKey(uuid)) {

sessions.remove(uuid);

subOnlineCount();

try{

if(service != null){

long handle = AutoRegisterEventModule.findRealPlayInfoByDeviceIdAndChannelNum(uuid);

RealPlayInfo realPlayInfo = AutoRegisterEventModule.findRealPlayInfo(handle);

if(realPlayInfo != null){

System.out.println(uuid + ":websocket 断开连接!");

boolean b = false;

if("1".equals(realPlayInfo.getType())){

b = service.stopPlay(new NetSDKLib.LLong(handle),realPlayInfo.getDeviceId());

}else if("2".equals(realPlayInfo.getType())){

b = service.stopReport(new NetSDKLib.LLong(handle),realPlayInfo.getDeviceId());

}

if(b){

AutoRegisterEventModule.removeRealPlayInfo(handle);

}

}else{

}

}

}catch (Exception e){

e.printStackTrace();

}

}

}

/**

* 发生错误

* @param throwable e

*/

@OnError

public void onError(Throwable throwable) {

throwable.printStackTrace();

}

/**

* 收到客户端发来消息

* @param message 消息对象

*/

@OnMessage

public void onMessage(ByteBuffer message) {

log.info("服务端收到客户端发来的消息: {}", message);

}

/**

* 收到客户端发来消息

* @param message 字符串类型消息

*/

@OnMessage

public void onMessage(String message) {

log.info("服务端收到客户端发来的消息: {}", message);

}

/**

* 发送消息

* @param message 字符串类型的消息

*/

public void sendAll(String message) {

for (Map.Entry session : sessions.entrySet()) {

session.getValue().getAsyncRemote().sendText(message);

}

}

/**

* 发送binary消息

* @param buffer

*/

public void sendMessage(ByteBuffer buffer) {

for (Map.Entry session : sessions.entrySet()) {

session.getValue().getAsyncRemote().sendBinary(buffer);

}

}

/**

* 转发数据流

* @param realPlayHandler 预览句柄

* @param buffer 码流数据

*/

public void forwardDataFlow(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.getDeviceId()+realPlayInfo.getChannel();

Session session = sessions.get(key);

if (session != null) {

synchronized (session) {

try {

session.getBasicRemote().sendBinary(buffer);

} catch (IOException e) {

e.printStackTrace();

}

}

} else {

log.error("session is null.please check.", this);

}

}

/**

* 主动关闭websocket连接

* @param realPlayHandler 预览句柄

*/

public void closeSession(long realPlayHandler) {

try {

Session session = sessions.get(realPlayHandler);

if (session != null) {

session.close();

}

} catch (IOException e) {

e.printStackTrace();

}

}

/**

* 获取当前连接数

* @return

*/

public int getOnlineCount() {

return onlineCount.get();

}

/**

* 增加当前连接数

* @return

*/

public int addOnlineCount() {

return onlineCount.getAndIncrement();

}

/**

* 减少当前连接数

* @return

*/

public int subOnlineCount() {

return onlineCount.getAndDecrement();

}

public static boolean crrentSession(String key){

if(sessions.containsKey(key)){

return true;

}

return false;

}

}

主要方法是forwardDataFlow方法,在摄像头的流数据回调至NetSDKLib.fRealDataCallBackEx时,用此方法,可以转给对应的前端用于视频流的展示。

5.预览

将预览服务封装进一个service服务中,方便调用

public class VideoMonitorService {

@Autowired

private WebSocketRealDataCallback webSocketRealDataCallback;

/**

* 视频预览

*/

public long startRealPlay(String deviceId,int channelNum,int rType){

DevicesModule devicesModule = DeviceListUtil.getDeviceModuleByDeviceId(deviceId);

if(devicesModule != null)

return devicesModule.startRealPlay(channelNum,webSocketRealDataCallback,deviceId,rType);

return 0;

}

public boolean stopPlay(NetSDKLib.LLong playHandle, String deviceId){

DevicesModule devicesModule = DeviceListUtil.getDeviceModuleByDeviceId(deviceId);

if(devicesModule != null)

return devicesModule.stopPlay(playHandle);

return false;

}

}

通过调用VideoMonitorService中的startRealPlay方法,通过设备ID获取到已在自动注册成功后存储的设备登录句柄,然后再调用封装的startRealPlay方法来实现预览的回调

public long startRealPlay(NetSDKLib.LLong m_hLoginHandle, int channelNum, WebSocketRealDataCallback webSocketRealDataCallback, String deviceId, int rType) {

//入参对象

int emDataType = NetSDKLib.EM_REAL_DATA_TYPE.EM_REAL_DATA_TYPE_FLV_STREAM;

NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE inParam = new NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE();

inParam.emDataType = emDataType;

inParam.nChannelID = channelNum;

inParam.emAudioType = EM_AUDIO_DATA_TYPE.EM_AUDIO_DATA_TYPE_AAC.ordinal();

inParam.rType = rType;

inParam.cbRealData = webSocketRealDataCallback;

inParam.dwUser = null;

//返回对象

NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE stOut = new NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE();

//获取预览句柄

NetSDKLib.LLong lRealHandle = DHNetSdkLib.CLIENT_RealPlayByDataType(m_hLoginHandle, inParam, stOut, 5000);

//开启实时监控

if(lRealHandle.longValue() != 0) {

RealPlayInfo info = new RealPlayInfo(m_hLoginHandle.longValue(), emDataType, channelNum, NetSDKLib.NET_RealPlayType.NET_RType_Realplay,deviceId,"1",lRealHandle.longValue(),null);

realPlayHandlers.put(lRealHandle.longValue(), info);

} else {

System.err.printf("RealPlayByDataType Failed!Last Error[0x%x]\n", DHNetSdkLib.CLIENT_GetLastError());

}

return lRealHandle.longValue();

}

再通过WebSocketRealDataCallback implements NetSDKLib.fRealDataCallBackEx来获取设备流数据的回调。

public class WebSocketRealDataCallback implements NetSDKLib.fRealDataCallBackEx {

@Autowired

private WebSocketServer server;

@Override

public void invoke(NetSDKLib.LLong lRealHandle, int dwDataType, Pointer pBuffer, int dwBufSize, int param, Pointer dwUser) {

RealPlayInfo info = AutoRegisterEventModule.findRealPlayInfo(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) {

//sendBuffer(buffer, lRealHandle.longValue());

}

} else if ((dwDataType - 1000) == info.getEmDataType()) {

sendBuffer(pBuffer.getByteArray(0, dwBufSize), lRealHandle.longValue());

}

}

}

private void sendBuffer(byte[] bytes, long realPlayHandler) {

/**

* 发送流数据

* 使用pBuffer.getByteBuffer(0,dwBufSize)得到的是一个指向native pointer的ByteBuffer对象,其数据存储在native,

* 而webSocket发送的数据需要存储在ByteBuffer的成员变量hb,使用pBuffer的getByteBuffer得到的ByteBuffer其hb为null

* 所以,需要先得到pBuffer的字节数组,手动创建一个ByteBuffer

*/

ByteBuffer buffer = ByteBuffer.wrap(bytes);

server.forwardDataFlow(realPlayHandler, buffer);

}

}

设备流数据再通过websocket推送给前端,完成闭环。

注意事项

1.需要告诉摄像头回调的流形式需要是FLV格式 NetSDKLib.EM_REAL_DATA_TYPE.EM_REAL_DATA_TYPE_FLV_STREAM;

2.前端如果报错

[TransmuxingController] > DemuxException: type = CodecUnsupported, info = Flv: Unsupported codec in video frame: 12

是因为FLV流的格式不对,因为FLV流要求视频编码为H264格式,声音为AAC格式。你需要检查网络摄像机视频设置视频码流设置是否正确。

3.请务必阅读大华《NetSDK_JAVA开发_FAQ》开发文档。特别是开发环境windows,服务器环境liunx的情况。

最后前端效果图

下载资源

https://download.csdn.net/download/qq_37529783/88466488

相关推荐

365体育世界杯专用版 成语词典

成语词典

⌛ 09-11 👁️ 4559
365体育世界杯专用版 亚马逊跨境电商总部位置解析,全球布局与区域中心
365bet体育35元 女巫汤意大利面🍝的做法

女巫汤意大利面🍝的做法

⌛ 09-01 👁️ 9747
bt365网上娱乐 原神岩之印位置一览 岩之印全收集