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

您所在的位置:网站首页 大华摄像机一直重启怎么回事 大华网络摄像头视频播放(基于vue+SpringBoot)

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

2024-06-25 15:06:00| 来源: 网络整理| 查看: 265

引言

近期接到一个需求,需要实现下面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



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭