一:前言

最近进行项目开发时遇到了需要前端直接调用摄像头,并直接进行播放的需求。原本计划通过海康威视官网的《WEB无插件开发包 V3.2》直接进行控制、交互,实现摄像头直接登录以及取流预览。但是前端人员现场驻场开发后反映各种兼容性问题频发,反正就是不能友好的进行预览播放。鉴于此我直接查询了官网上相关的sdk,然后选用了《设备网络SDK_Win64 V6.1.9.4_build20220412》进行开发java版本的转码工具。整体思路是在PS流中解析出H264的裸流然后通过websocket传给前端,前端基于wfs.js进行h264的裸流播放。

二:开发准备

下载开发SDK开发包,并先查看和熟悉sdk使用方法并先查看和熟悉sdk使用方法并先查看和熟悉sdk使用方法!(拜托不要一来就问源码呀,这就是我写的所有的代码了呀,甚至前端我都给你贴出来了,前端引入js、引入jquery-3.0.0.js不需要教吧。麻烦先在官网下载下来把项目运行起来啊,现在的做个开发的都这么浮夸的吗?)

海康开放平台sdk下载

下载wfs.js插件:GitHub – MarkRepo/wfs.js: use html5 video tag with MSE for raw h264 live streaming.

三:整体介绍

逻辑流转图:

由于博主太懒了,这里没有图。

3.1.后端逻辑:

①:java加载SDK包(dll)实现sdk加载和调用

②:调用sdk的NET_DVR_Login_V40(m_strLoginInfo, m_strDeviceInfo)接口实现登录。

③:调用sdk的NET_DVR_RealPlay_V40(userID, strClientInfo, fRealDataCallBack , null)接口实现预览取流。

④:在我们自定义的fRealDataCallBack回调函数中对取流数据进行解码,就是将数据进行截取以及转码操作,并将数据包进行存贮。

⑤:创建websocket类监听前端连接,区分摄像头后通过websocket进行实时推流。socket地址为’/wstest/{lUserID}’ 其中lUserID是登录并预览成功后返回的lUserID!

主要代码展示:

package com.xunshi.hikangvision.Controller;import com.xunshi.hikangvision.untils.PreverViewUntil;import com.xunshi.hikangvision.vo.LoginVo;import com.xunshi.hikangvision.vo.result.R;import org.springframework.web.bind.annotation.*;import java.util.List;@RestController@RequestMapping("/playvision")public class HKController {//@Resource PreverViewUntil preverViewUntil;/** * 登录并开启预览 * @param loginVo * @return */@PostMapping("/loginAndPlayView")public R loginAndPlayView(@RequestBody LoginVo loginVo){ return R.buildOkData(PreverViewUntil.loginAndPlayView(loginVo));}/** * 退出预览&登录 *///@ApiParam("loginAndPlayView接口中返回的lUserID")//@ApiParam("loginAndPlayView接口中返回的lPlayID")@GetMapping("/logoutPlayView")public R logoutPlayView(@RequestParam (value = "lUserID",required = true) String lUserID,@RequestParam (value = "lPlayID",required = false) String lPlayID){if(null!=lPlayID){PreverViewUntil.logoutPlayView(lUserID,lPlayID);}else{PreverViewUntil.logoutPlayView(lUserID);}return R.buildOk();}/** * 登录(判断是否在线),只登录不预览 * ---支持批量登录 * @param loginVos * @return */@PostMapping("/login")public R<List> login(@RequestBody List loginVos){return R.buildOkData(PreverViewUntil.login(loginVos));}/** * 预览某个摄像头 * @param loginVo * @return */@PostMapping("/playView")public R playView(@RequestBody LoginVo loginVo){return R.buildOkData(PreverViewUntil.playView(loginVo));}/** * 只关闭某个摄像头预览,但不退出登录 * @param loginVo * @return */@PostMapping("/logoutPlayViewOnly")public R logoutPlayViewOnly(@RequestBody LoginVo loginVo){return R.buildOkData(PreverViewUntil.logoutPlayViewOnly(loginVo));}}
package com.xunshi.hikangvision.untils;import com.sun.jna.Native;import com.sun.jna.Pointer;import com.xunshi.hikangvision.untils.Common.osSelect;import com.xunshi.hikangvision.vo.LoginVo;import com.xunshi.hikangvision.vo.MyBlockingQueue;import javax.annotation.PostConstruct;import javax.annotation.PreDestroy;import java.util.ArrayList;import java.util.List;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;//@Componentpublic class PreverViewUntil {static boolean isInit = false;//是否初始化static HCNetSDK hCNetSDK = null;static PlayCtrl playControl = null;static PreverViewUntil.FExceptionCallBack_Imp fExceptionCallBack;//异常捕获回调/** * 异常信息捕获接受类 */static class FExceptionCallBack_Imp implements HCNetSDK.FExceptionCallBack {public void invoke(int dwType, int lUserID, int lHandle, Pointer pUser) {switch(dwType){case HCNetSDK.EXCEPTION_AUDIOEXCHANGE://语音对讲时网络异常System.out.println("登录句柄:"+lUserID+"语音对讲异常");break;case HCNetSDK.EXCEPTION_ALARM://报警上传时网络异常System.out.println("登录句柄:"+lUserID+"报警上传时网络异常");break;case HCNetSDK.EXCEPTION_PREVIEW://网络预览时异常System.out.println("登录句柄:"+lUserID+"网络预览时异常");//TODO: 关闭网络预览break;case HCNetSDK.EXCEPTION_SERIAL://透明通道传输时异常System.out.println("登录句柄:"+lUserID+"透明通道传输时异常");//TODO: 关闭透明通道break;case HCNetSDK.EXCEPTION_RECONNECT://预览时重连System.out.println("登录句柄:"+lUserID+"预览时重连");break;default:System.out.println("登录句柄:"+lUserID+",异常事件类型:"+Integer.toHexString(dwType));System.out.println("具体错误参照 SDK网络使用手册中:NET_DVR_SetExceptionCallBack_V30 方法中的异常定义!");break;}return;}}/** * 动态库加载 * @return */private static boolean CreateSDKInstance() {if (hCNetSDK == null) {synchronized (HCNetSDK.class) {String strDllPath = "";try {if (osSelect.isWindows())//win系统加载库路径strDllPath = System.getProperty("user.dir") + "\\lib\\HCNetSDK.dll";else if (osSelect.isLinux())//Linux系统加载库路径strDllPath = System.getProperty("user.dir") + "/lib/libhcnetsdk.so";System.out.println("loadLibrary: " + strDllPath);hCNetSDK = (HCNetSDK) Native.loadLibrary(strDllPath, HCNetSDK.class);} catch (Exception ex) {System.out.println("loadLibrary: " + strDllPath + " Error: " + ex.getMessage());return false;}}}return true;}/** * 播放库加载 * @return */private static boolean CreatePlayInstance() {if (playControl == null) {synchronized (PlayCtrl.class) {String strPlayPath = "";try {if (osSelect.isWindows())//win系统加载库路径strPlayPath = System.getProperty("user.dir") + "\\lib\\PlayCtrl.dll";else if (osSelect.isLinux())//Linux系统加载库路径strPlayPath = System.getProperty("user.dir") + "/lib/libPlayCtrl.so";playControl=(PlayCtrl) Native.loadLibrary(strPlayPath,PlayCtrl.class);} catch (Exception ex) {System.out.println("loadLibrary: " + strPlayPath + " Error: " + ex.getMessage());return false;}}}return true;}/** * 类初始化时加载SDK * @PostConstruct 便于直接加载注入类 */@PostConstructpublic static void init() {System.out.println("加载海康威视SDK dll");System.out.println("初始化路径为:"+System.getProperty("user.dir") + "\\lib\\HCNetSDK.dll");if (hCNetSDK == null&&playControl==null) {if (!CreateSDKInstance()) {System.out.println("Load SDK fail");return;}if (!CreatePlayInstance()) {System.out.println("Load PlayCtrl fail");return;}}System.out.println("海康威视SDK dll加载成功");//linux系统建议调用以下接口加载组件库if (osSelect.isLinux()) {HCNetSDK.BYTE_ARRAY ptrByteArray1 = new HCNetSDK.BYTE_ARRAY(256);HCNetSDK.BYTE_ARRAY ptrByteArray2 = new HCNetSDK.BYTE_ARRAY(256);//这里是库的绝对路径,请根据实际情况修改,注意改路径必须有访问权限String strPath1 = System.getProperty("user.dir") + "/lib/libcrypto.so.1.1";String strPath2 = System.getProperty("user.dir") + "/lib/libssl.so.1.1";System.arraycopy(strPath1.getBytes(), 0, ptrByteArray1.byValue, 0, strPath1.length());ptrByteArray1.write();hCNetSDK.NET_DVR_SetSDKInitCfg(3, ptrByteArray1.getPointer());System.arraycopy(strPath2.getBytes(), 0, ptrByteArray2.byValue, 0, strPath2.length());ptrByteArray2.write();hCNetSDK.NET_DVR_SetSDKInitCfg(4, ptrByteArray2.getPointer());String strPathCom = System.getProperty("user.dir") + "/lib";HCNetSDK.NET_DVR_LOCAL_SDK_PATH struComPath = new HCNetSDK.NET_DVR_LOCAL_SDK_PATH();System.arraycopy(strPathCom.getBytes(), 0, struComPath.sPath, 0, strPathCom.length());struComPath.write();hCNetSDK.NET_DVR_SetSDKInitCfg(2, struComPath.getPointer());}System.out.println("开始初始化海康威视Sdk");//SDK初始化,一个程序只需要调用一次boolean initSuc = hCNetSDK.NET_DVR_Init();if (initSuc != true) {System.out.println("初始化海康威视Sdk失败");}System.out.println("海康威视Sdk初始化成功!");System.out.println("开始设置异常消息回调");//异常消息回调if(fExceptionCallBack == null){fExceptionCallBack = new PreverViewUntil.FExceptionCallBack_Imp();}Pointer pUser = null;if (!hCNetSDK.NET_DVR_SetExceptionCallBack_V30(0, 0, fExceptionCallBack, pUser)) {return ;}System.out.println("设置异常消息回调成功");System.out.println("开始设置启动SDK写日志");//启动SDK写日志hCNetSDK.NET_DVR_SetLogToFile(3, "..\\sdkLog\\", false);isInit = true;}//类销毁时清理sdk@PreDestroypublic void clearSdk() {if (null!=hCNetSDK){//SDK反初始化,释放资源,只需要退出时调用一次hCNetSDK.NET_DVR_Cleanup();}}/** * 摄像头登录(支持批量操作) * @param loginVos * @return */public static List login(List loginVos) {if(loginVos.size()<1) return loginVos;for (int i = 0; i < loginVos.size(); i++) {LoginVo loginVo = loginVos.get(i);if(!isInit){init();}//如果已经登录,就先退出登陆String userIdByIp = MyBlockingQueue.findUserIdByIp(loginVo.getIp());if(null!=userIdByIp){//自动判断是否在预览并退出PreverViewUntil.logoutPlayView(userIdByIp);}//登录设备,每一台设备分别登录; 登录句柄是唯一的,可以区分设备HCNetSDK.NET_DVR_USER_LOGIN_INFO m_strLoginInfo = new HCNetSDK.NET_DVR_USER_LOGIN_INFO();//设备登录信息HCNetSDK.NET_DVR_DEVICEINFO_V40 m_strDeviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40();//设备信息String m_sDeviceIP = "********";//设备ip地址m_sDeviceIP=loginVo.getIp();m_strLoginInfo.sDeviceAddress = new byte[HCNetSDK.NET_DVR_DEV_ADDRESS_MAX_LEN];System.arraycopy(m_sDeviceIP.getBytes(), 0, m_strLoginInfo.sDeviceAddress, 0, m_sDeviceIP.length());String m_sUsername = "*****";//设备用户名m_sUsername=loginVo.getUserName();m_strLoginInfo.sUserName = new byte[HCNetSDK.NET_DVR_LOGIN_USERNAME_MAX_LEN];System.arraycopy(m_sUsername.getBytes(), 0, m_strLoginInfo.sUserName, 0, m_sUsername.length());String m_sPassword = "******";//设备密码m_sPassword=loginVo.getPassword();m_strLoginInfo.sPassword = new byte[HCNetSDK.NET_DVR_LOGIN_PASSWD_MAX_LEN];System.arraycopy(m_sPassword.getBytes(), 0, m_strLoginInfo.sPassword, 0, m_sPassword.length());m_strLoginInfo.wPort = 8000; //SDK端口m_strLoginInfo.bUseAsynLogin = false; //是否异步登录:0- 否,1- 是m_strLoginInfo.write();String ipPortStr=loginVo.getIp()+":"+loginVo.getProt();System.out.println("开始登录:"+ipPortStr);int lUserID = hCNetSDK.NET_DVR_Login_V40(m_strLoginInfo, m_strDeviceInfo);if (lUserID == -1) {System.out.println(ipPortStr+"登录失败,错误码为" + hCNetSDK.NET_DVR_GetLastError());loginVo.setLoginStatus("0");loginVo.setLoginMessage("登录失败,错误码为" + hCNetSDK.NET_DVR_GetLastError());loginVo.setLUserID(null);} else {//判断DVR工作状态//HCNetSDK.NET_DVR_WORKSTATE_V30 devwork = new HCNetSDK.NET_DVR_WORKSTATE_V30();//boolean net_DVR_GetDVRWorkState_V30 = hCNetSDK.NET_DVR_GetDVRWorkState_V30(lUserID, devwork);//if (net_DVR_GetDVRWorkState_V30) {////设备的状态,0-正常,1-CPU占用率太高,超过85%,2-硬件错误,例如串口死掉//if(devwork.dwDeviceStatic!=0)//{////}//}//else//{////未知错误:获取DVR工作状态失败//}//这里直接认为登陆成功就能预览吧。。String successStr=m_sDeviceIP + ":设备登录成功! " + "设备序列号:" +new String(m_strDeviceInfo.struDeviceV30.sSerialNumber).trim();System.out.println(successStr);loginVo.setLoginMessage(successStr);loginVo.setLoginStatus("1");loginVo.setLUserID(lUserID);m_strDeviceInfo.read();//记录ip已经登录MyBlockingQueue.IPToUserIdMap.put(loginVo.getIp(),loginVo.getLUserID().toString());}}return loginVos;}/*** * 实时预览某个摄像机 * @param vo * @return */public static LoginVo playView(LoginVo vo){//注释掉的代码也可以参考,去掉注释可以运行//VideoDemo.getIPChannelInfo(lUserID); //获取IP通道//int lDChannel = (int)m_strDeviceInfo.struDeviceV30.byStartDChan + lChannel -1;String ipPortStr=vo.getIp()+":"+vo.getProt();System.out.println("预览通道号: " + vo.getLDChannel());System.out.println("尝试预览连接:"+ipPortStr);VidePreView.RealPlay(vo.getLUserID(), vo.getLDChannel());//预览if(VidePreView.lPlayStatus){vo.setPlsyStatus("1");vo.setPlsyMessage("预览请求成功!");vo.setLPlayID(VidePreView.lPlay);System.out.println("预览请求成功:"+vo);//创建数据体,等待视频流实时回调BlockingQueue bq = new ArrayBlockingQueue(10);MyBlockingQueue.bpMap.put(vo.getLUserID().toString(),bq);MyBlockingQueue.PlayToUserIdMap.put(vo.getLPlayID().toString(),vo.getLUserID().toString());}else{vo.setPlsyStatus("0");vo.setPlsyMessage("预览失败:"+VidePreView.lPlayErrorMassage);vo.setLPlayID(null);}return vo;}/** * 登录并播放某个摄像头视频 */public static LoginVo loginAndPlayView(LoginVo loginVo) {if(!isInit){init();}//如果已经登录,就先退出登陆String userIdByIp = MyBlockingQueue.findUserIdByIp(loginVo.getIp());if(null!=userIdByIp){//自动判断是否在预览并退出PreverViewUntil.logoutPlayView(userIdByIp);}//登录List loginVos = new ArrayList();loginVos.add(loginVo);loginVos = login(loginVos);String ipPortStr=loginVo.getIp()+":"+loginVo.getProt();if("1".equals(loginVo.getLoginStatus())){//预览if("1".equals(loginVos.get(0).getLoginStatus())){playView(loginVos.get(0));}else{System.out.println(ipPortStr+"----登陆成功,但是预览失败!");//登录并预览接口中,如果登陆成功但是预览失败,自动退出登录-清理数据体PreverViewUntil.logoutPlayView(loginVo.getLUserID().toString());}}else{System.out.println(ipPortStr+"----登陆失败!");}return loginVos.get(0);}/** * 某个摄像头退出登录 * @param lUserID登录句柄 * @param lPlayID预览句柄 */public static void logoutPlayView(String lUserID,String lPlayID) {if(null!=hCNetSDK&&MyBlockingQueue.bpMap.containsKey(lUserID)){if (hCNetSDK.NET_DVR_StopRealPlay(Integer.valueOf(lPlayID))){System.out.println("停止预览成功");}//退出程序时调用,每一台设备分别注销if (hCNetSDK.NET_DVR_Logout(Integer.valueOf(lUserID))) {System.out.println("注销登录成功");}//清理数据体MyBlockingQueue.clearByUserId(lUserID,true);}}/** * 某个摄像头退出登录,自动判断是否在预览并停止 * @param lUserID登录句柄 */public static void logoutPlayView(String lUserID) {if(null!=hCNetSDK &&MyBlockingQueue.bpMap.containsKey(lUserID)){String playId = MyBlockingQueue.findPlayIdByUserId(lUserID);if(null!=playId){if (hCNetSDK.NET_DVR_StopRealPlay(Integer.valueOf(playId))){System.out.println("停止预览成功");}}//退出程序时调用,注销登录if (hCNetSDK.NET_DVR_Logout(Integer.valueOf(lUserID))) {System.out.println("注销登录成功");}//清理数据体MyBlockingQueue.clearByUserId(lUserID,true);}}/** * 只关闭某个摄像头预览,但不退出登录 * @param loginVo */public static LoginVo logoutPlayViewOnly(LoginVo loginVo) {if(null!=hCNetSDK){if(null!=loginVo.getLPlayID()){if (hCNetSDK.NET_DVR_StopRealPlay(loginVo.getLPlayID())){System.out.println("停止预览成功");loginVo.setLPlayID(null);loginVo.setPlsyMessage("停止预览成功");loginVo.setLoginStatus("0");}else{//接口返回失败请调用NET_DVR_GetLastError获取错误码,通过错误码判断出错原因。loginVo.setPlsyMessage("停止预览失败:接口返回失败请调用NET_DVR_GetLastError获取错误码,通过错误码判断出错原因。");}}//清理数据体MyBlockingQueue.clearByUserId(loginVo.getLUserID().toString(),false);}return loginVo;}}
package com.xunshi.hikangvision.untils;import com.sun.jna.Pointer;import com.sun.jna.ptr.ByteByReference;import com.xunshi.hikangvision.vo.MyBlockingQueue;import java.io.IOException;import java.util.HashMap;import java.util.Map;import java.util.concurrent.BlockingQueue;import static com.xunshi.hikangvision.untils.PreverViewUntil.hCNetSDK;public class VidePreView {static FRealDataCallBack1 fRealDataCallBack;//预览回调函数实现static VideoDemo.fPlayEScallback fPlayescallback; //裸码流回调函数public static int lPlay = -1;//预览句柄public static boolean lPlayStatus = false;//预览是否成功public static String lPlayErrorMassage = "";//预览错误信息/** * 预览摄像头 * @param userID 登录时返回的id * @param iChannelNo 通过哪个通道预览 */public static void RealPlay(int userID, int iChannelNo) {if (userID == -1) {System.out.println("请先登录");lPlayStatus=false;lPlayErrorMassage="请先登录";return;}HCNetSDK.NET_DVR_PREVIEWINFO strClientInfo = new HCNetSDK.NET_DVR_PREVIEWINFO();strClientInfo.read();strClientInfo.hPlayWnd = null;//窗口句柄,从回调取流不显示一般设置为空strClientInfo.lChannel = iChannelNo;//通道号strClientInfo.dwStreamType=0; //0-主码流,1-子码流,2-三码流,3-虚拟码流,以此类推strClientInfo.dwLinkMode=0; //连接方式:0- TCP方式,1- UDP方式,2- 多播方式,3- RTP方式,4- RTP/RTSP,5- RTP/HTTP,6- HRUDP(可靠传输) ,7- RTSP/HTTPS,8- NPQstrClientInfo.bBlocked=1;strClientInfo.write();//回调函数定义必须是全局的if (fRealDataCallBack == null) {fRealDataCallBack = new FRealDataCallBack1();}//开启预览lPlay = hCNetSDK.NET_DVR_RealPlay_V40(userID, strClientInfo, fRealDataCallBack , null);if (lPlay == -1) {int iErr = hCNetSDK.NET_DVR_GetLastError();System.out.println("取流失败" + iErr);lPlayStatus=false;lPlayErrorMassage="取流失败,错误码:" + iErr;return;}System.out.println("取流成功");lPlayStatus=true;return;}//预览回调static class FRealDataCallBack1 implements HCNetSDK.FRealDataCallBack_V30 {//预览回调 lRealHandle预览句柄回调public void invoke(int lRealHandle, int dwDataType, ByteByReference pBuffer, int dwBufSize, Pointer pUser) {//System.out.println("码流数据回调, 数据类型: " + dwDataType + ", 数据长度:" + dwBufSize);//播放库解码switch (dwDataType) {case HCNetSDK.NET_DVR_SYSHEAD: //系统头case HCNetSDK.NET_DVR_STREAMDATA: //码流数据if ((dwBufSize > 0)) {//System.out.println("取流回调进行中.....");byte[] outputData = pBuffer.getPointer().getByteArray(0, dwBufSize);try {writeESH264(outputData,lRealHandle);//将流写入对应的实体} catch (IOException e) {e.printStackTrace();}}}}//多路视频的pes数据进行缓存,知道某一路视频的RTP包开头进入时进行取出返给前端Map EsBytesMap=new HashMap();/** * 提取H264的裸流写入文件 * @param outputData * @throws IOException */public void writeESH264(final byte[] outputData,int lRealHandle) throws IOException {if (outputData.length  0) {//System.out.println("回调的lRealHandle:"+lRealHandle);if(MyBlockingQueue.PlayToUserIdMap.containsKey(String.valueOf(lRealHandle))){String userId = MyBlockingQueue.PlayToUserIdMap.get(String.valueOf(lRealHandle));BlockingQueue blockingQueue = MyBlockingQueue.bpMap.get(userId);//System.out.println("当前的lPlayID:"+lRealHandle);//System.out.println("myBlockingQueue.bq is null?"+(null==blockingQueue));try {blockingQueue.put(allEsBytes);//将当前的某一路视频通道的上一个Rtp包放到队列中去MyBlockingQueue.bpMap.put(userId,blockingQueue);allEsBytes = null;EsBytesMap.put(playIdStr,allEsBytes);//置空当前通道的RTP包,下次回调就是pes包进行取流追加} catch (InterruptedException e) {e.printStackTrace();}}}}// 是00 00 01 eo开头的就是视频的pes包if ((outputData[0] & 0xff) == 0x00//&& (outputData[1] & 0xff) == 0x00//&& (outputData[2] & 0xff) == 0x01//&& (outputData[3] & 0xff) == 0xE0) {//// 去掉包头后的起始位置int from = 9 + outputData[8] & 0xff;int len = outputData.length - 9 - (outputData[8] & 0xff);// 获取es裸流byte[] esBytes = new byte[len];System.arraycopy(outputData, from, esBytes, 0, len);if (allEsBytes == null) {allEsBytes = esBytes;} else {byte[] newEsBytes = new byte[allEsBytes.length + esBytes.length];System.arraycopy(allEsBytes, 0, newEsBytes, 0, allEsBytes.length);System.arraycopy(esBytes, 0, newEsBytes, allEsBytes.length, esBytes.length);allEsBytes = newEsBytes;}EsBytesMap.put(playIdStr,allEsBytes);//当前视频通道的部分包数据进行缓存}}}}
package com.xunshi.hikangvision.sevice;import com.xunshi.hikangvision.untils.PreverViewUntil;import com.xunshi.hikangvision.vo.MyBlockingQueue;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import javax.websocket.*;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import java.io.IOException;import java.nio.ByteBuffer;import java.util.concurrent.BlockingQueue;import java.util.concurrent.atomic.AtomicInteger;/** * 前后端交互的类实现消息的接收推送 * @ServerEndpoint(value = "/wstest") 前端通过此URI和后端建立连接 */@Slf4j@ServerEndpoint(value = "/wstest/{lUserID}")@Componentpublic class OneWebSocket {/** 记录当前在线网页数量 */private static AtomicInteger onlineCount = new AtomicInteger(0);/** * 连接建立成功调用的方法 */@OnOpenpublic void onOpen(final Session session , @PathParam("lUserID") String lUserID) {onlineCount.addAndGet(1);System.out.println("当前已经登录用户句柄S:"+MyBlockingQueue.bpMap.keySet());log.info("有新连接加入sessionid:{},摄像头登录用户的句柄为:{} 当前在线socket(视频路数)数量:{}", session.getId(),lUserID, onlineCount);if(MyBlockingQueue.bpMap.containsKey(lUserID)){if(null==MyBlockingQueue.findPlayIdByUserId(lUserID)){System.out.println(String.format("警告:根据登录句柄%s,没有找到用户预览句柄",lUserID));}BlockingQueue blockingQueue = MyBlockingQueue.bpMap.get(lUserID);MyBlockingQueue.SessionToUserIdMap.put(session.getId(),lUserID);//这里按照逻辑来说这里绑定后就应该开启一个线层来干这个事情,查询了一下好像websocket就是多线程的直接干吧while (null!=session&&session.isOpen()&&null!=blockingQueue) {try {byte[] esBytes = (byte[]) blockingQueue.take();if(esBytes.length<1) {System.out.println("取流失败,无内容");continue;}ByteBuffer data = ByteBuffer.wrap(esBytes);session.getBasicRemote().sendBinary(data);} catch (InterruptedException e) {System.out.println("socket 数据发失败,错误信息为:"+e.getMessage());return;} catch (IOException e) {System.out.println("socket 数据发失败,错误信息为:"+e.getMessage());return;}}}else{System.out.println("当前没有找到用户登录句柄,无法播放:"+lUserID);}}/** * 连接关闭调用的方法 */@OnClosepublic void onClose(final Session session) {onlineCount.decrementAndGet(); // 在线数减1System.out.println(String.format("socket[%s]断开链接,查找并执行退出预览&登录",session.getId()));//执行退出操作 if(MyBlockingQueue.SessionToUserIdMap.containsKey(session.getId())) { String userId = MyBlockingQueue.SessionToUserIdMap.get(session.getId()); if(null!=userId) { System.out.println(String.format("找到正在登录id[%s]预览的的相关信息,执行停止预览并退出登录操作",userId)); PreverViewUntil.logoutPlayView(userId);//执行退出预览操作 } } else { System.out.println(String.format("没有找到该socket相关的登录预览信息,无需操作!")); }}/** * 收到客户端消息后调用的方法 * * @param message * 客户端发送过来的消息 */@OnMessagepublic void onMessage(final String message, final Session session) {log.info("服务端收到客户端[{}]的消息:{}", session.getId(), message);}@OnErrorpublic void onError(final Session session, final Throwable error) {System.out.println(String.format("socket[%s]发生错误,查找并执行退出预览&登录,错误消息是:"+error.getMessage(),session.getId()));//执行退出操作if(MyBlockingQueue.SessionToUserIdMap.containsKey(session.getId())){String userId = MyBlockingQueue.SessionToUserIdMap.get(session.getId());if(null!=userId){System.out.println(String.format("找到正在登录id[%s]预览的的相关信息,执行停止预览并退出登录操作",userId));PreverViewUntil.logoutPlayView(userId);//执行退出预览操作}}else{System.out.println(String.format("没有找到该socket相关的登录预览信息,无需操作!"));}}/** * 服务端发送消息给客户端 */private void sendMessage(final String message, final Session toSession) {try {log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);toSession.getBasicRemote().sendText(message);} catch (Exception e) {log.error("服务端发送消息给客户端失败:{}", e);}}}
package com.xunshi.hikangvision.vo;import java.util.HashMap;import java.util.Map;import java.util.concurrent.BlockingQueue;import java.util.concurrent.atomic.AtomicReference;public class MyBlockingQueue {//static public BlockingQueue bq = new ArrayBlockingQueue(10);//publicString socketSessionId; //前端和这个摄像头建立的websocketid//publicString lUserID="-1";//摄像头登录句柄//publicString lPlayID="-1";//摄像预览句柄static public Map SessionToUserIdMap=new HashMap();static public Map PlayToUserIdMap=new HashMap();static public Map IPToUserIdMap=new HashMap();static public Map bpMap=new HashMap();/** * 通过摄像头登录句柄进行数据清理 * @param userId */static public void clearByUserId(String userId,boolean logout){if(null==MyBlockingQueue.bpMap){System.out.println("bpMap is null ");MyBlockingQueue.bpMap=new HashMap();}if(null==MyBlockingQueue.PlayToUserIdMap){System.out.println("PlayToUserIdMap is null ");MyBlockingQueue.PlayToUserIdMap=new HashMap();}if(null==MyBlockingQueue.SessionToUserIdMap){System.out.println("SessionToUserIdMap is null ");MyBlockingQueue.SessionToUserIdMap=new HashMap();}for (String key : MyBlockingQueue.PlayToUserIdMap.keySet()){String value = MyBlockingQueue.PlayToUserIdMap.get(key);if(value.equals(userId)){MyBlockingQueue.PlayToUserIdMap.remove(key);break;}}for (String key : MyBlockingQueue.SessionToUserIdMap.keySet()){String value = MyBlockingQueue.SessionToUserIdMap.get(key);if(value.equals(userId)){MyBlockingQueue.SessionToUserIdMap.remove(key);break;}}if(logout){for (String key : MyBlockingQueue.IPToUserIdMap.keySet()){String value = MyBlockingQueue.IPToUserIdMap.get(key);if(value.equals(userId)){MyBlockingQueue.IPToUserIdMap.remove(key);break;}}}}/** * 通过ip查找是否正在登录或者预览 * @param Ip */static public String findUserIdByIp(String Ip){for (String key : MyBlockingQueue.IPToUserIdMap.keySet()){String value = MyBlockingQueue.IPToUserIdMap.get(key);if(key.equals(Ip)){return value;}}return null;}/** * 通过摄像头登录句柄查询当前用户是否正在预览 * @param userId */static public String findPlayIdByUserId(String userId){for (String key : MyBlockingQueue.PlayToUserIdMap.keySet()){String value = MyBlockingQueue.PlayToUserIdMap.get(key);if(value.equals(userId)){return key;}}return null;}}
package com.xunshi.hikangvision.vo.result;import lombok.AllArgsConstructor;import lombok.Getter;import lombok.Setter;import lombok.ToString;import lombok.experimental.Accessors;import org.springframework.http.HttpStatus;import org.springframework.util.ObjectUtils;import java.io.Serializable;/** * 响应信息主体 * * @param  * @author somewhere */@ToString@Accessors(chain = true)@AllArgsConstructorpublic class R implements Serializable {private static final long serialVersionUID = 1L;@Getter@Setterprivate int code = CommonConstants.SUCCESS;@Getter@Setterprivate HttpStatus httpStatus;@Getter@Setterprivate T data;private String[] messages = {};public R() {super();}public R(T data) {super();this.data = data;}public R(String... msg) {super();this.messages = msg;}public R(T data, String... msg) {super();this.data = data;this.messages = msg;}public R(T data, int code, String... msg) {super();this.data = data;this.code = code;this.messages = msg;}public R(Throwable e) {super();setMessage(e.getMessage());this.code = CommonConstants.FAIL;}public static R buildOk(String... messages) {return new R(messages);}public static  R buildOkData(T data, String... messages) {return new R(data, messages);}public static  R buildFailData(T data, String... messages) {return new R(data, CommonConstants.FAIL, messages);}public static  R buildFail(String... messages) {return new R(null, CommonConstants.FAIL, messages);}public static  R build(T data, int code, String... messages) {return new R(data, code, messages);}public static  R build(int code, String... messages) {return new R(null, code, messages);}public String getMessage() {return readMessages();}public void setMessage(String message) {addMessage(message);}public String readMessages() {StringBuilder sb = new StringBuilder();for (String message : messages) {sb.append(message);}return sb.toString();}public void addMessage(String message) {this.messages = ObjectUtils.addObjectToArray(messages, message);}}

3.2.前端逻辑流程

①:准备好摄像头参数:ip、端口、预览通道、用户名、密码,请求后端接口‘/playvision/login’实现登录,登陆成功后端会返回loginStatus(登录状态:0-失败1-成功)、lUserID(登陆摄像头的id)

②:请求预览接口’/playvision/playView’,将登录后返回的数据直接请求过来即可。请求成功后会返回plsyStatus(预览状态:0-失败1-成功)、lPlayID(预览成功的通道句柄id)

③:预览接口请求成功后,我们调用wfs.js插件进行实时预览。

h.264 To fmp4 video.rotate180 {width: 100%;height: 100%;transform: rotateX(180deg);-moz-transform: rotateX(180deg);-webkit-transform: rotateX(180deg);-o-transform: rotateX(180deg);-ms-transform: rotateX(180deg);}

h.264 To fmp4

var mypath = "http://127.0.0.1:10086";/** * 登录并预览按钮点击:先请求 登录+预览接口,然后返回将返回的lUserID拼接到socket地址后面,创建一个websocket。 * 然后创建wfs实列 ,执行wfs.attachWebsocket()和wfs.attachMedia(),最后将socket和vide标签绑定给创建的wfs即可 */$("#loginAndPlayView").click(function () {var loginVo = {"userName": "admin",//摄像头登录账号"password": "****", //摄像头登录账号"ip": "192.**.0.**", //摄像头ip"prot": "8000"//摄像头端口}$.ajax({type: 'post',url: mypath + "/playvision/loginAndPlayView",dataType: 'json',contentType: "application/json;charset=UTF-8",data: JSON.stringify(loginVo),xhrFields: { withCredentials: false },success: function (res) {console.log(res)if (res.code && res.data && res.data.loginStatus == "1") {var luserID = res.data.luserID;var lplayID = res.data.lplayID;if (Wfs.isSupported()) {var video1 = document.getElementById("video1");var wfs = new Wfs();var my_websocket_path = "ws://127.0.0.1:10086";var socket = new WebSocket(my_websocket_path + "/wstest/" + luserID);wfs.attachMedia(video1, luserID + "_" + lplayID);//绑定video标签wfs.attachWebsocket(socket, luserID + "_" + lplayID)//绑定socket}}else {alert("播放失败,错误信息:" + ((res.data && res.data.loginMessage) ? res.data.loginMessage : ""))}}});})

④:注意摄像头参数配置需要配置编码为H.264

3.3 接口介绍

3.3.1登录并预览

通过`/playvision/loginAndPlayView`发起Post请求登陆摄像头并开启预览:

{"userName": 0, //用户名"password": 1, //密码"ip": "",//摄像头所在ip"prot": 80, //摄像头所在端口"lDChannel": 1//使用预览通道}

3.3.2退出预览&登录
通过`/playvision/logoutPlayView” />[{“userName”: 0, //用户名”password”: 1, //密码”ip”: “”,//摄像头所在ip”prot”: 80, //摄像头所在端口”lDChannel”: 1//使用预览通道},{“userName”: 0, //用户名”password”: 1, //密码”ip”: “”,//摄像头所在ip”prot”: 80, //摄像头所在端口”lDChannel”: 1//使用预览通道}]

3.3.4 开启某个摄像头预览
通过`/playvision/playView`发起Post请求登陆摄像头

{"lUserID":1, //登录接口返回的登录用户id(必填)"userName": 0, //用户名"password": 1, //密码"ip": "",//摄像头所在ip"prot": 80, //摄像头所在端口"lDChannel": 1//使用预览通道}

3.3.5 只关闭某个摄像头预览,但不退出登录
通过`/playvision/logoutPlayViewOnly`发起Post请求登陆摄像头

{"lUserID":1,//登录接口返回的登录用户id(必填)"lPlayID":1,//预览接口返回的预览句柄id(必填)"userName": 0, //用户名"password": 1, //密码"ip": "",//摄像头所在ip"prot": 80, //摄像头所在端口"lDChannel": 1//使用预览通道}

3.3.6 TODO 待办
代码封装、命名等比较仓促,没有很好很认真的封装,很多类、结构都只是开发探索时进行的编码,请读者深刻理解后自行进行封装和运用!!

3.3.7常见错误定义:

EXCEPTION_EXCHANGE 0x8000 用户交互时异常(注册心跳超时,心跳间隔为2分钟)EXCEPTION_AUDIOEXCHANGE 0x8001 语音对讲异常EXCEPTION_ALARM 0x8002 报警异常EXCEPTION_PREVIEW 0x8003 网络预览异常EXCEPTION_SERIAL 0x8004 透明通道异常EXCEPTION_RECONNECT 0x8005 预览时重连EXCEPTION_ALARMRECONNECT 0x8006 报警时重连EXCEPTION_SERIALRECONNECT 0x8007 透明通道重连SERIAL_RECONNECTSUCCESS 0x8008 透明通道重连成功EXCEPTION_PLAYBACK 0x8010 回放异常EXCEPTION_DISKFMT 0x8011 硬盘格式化EXCEPTION_PASSIVEDECODE 0x8012 被动解码异常EXCEPTION_EMAILTEST 0x8013 邮件测试异常EXCEPTION_BACKUP 0x8014 备份异常PREVIEW_RECONNECTSUCCESS 0x8015 预览时重连成功ALARM_RECONNECTSUCCESS 0x8016 报警时重连成功RESUME_EXCHANGE 0x8017 用户交互恢复NETWORK_FLOWTEST_EXCEPTION 0x8018 网络流量检测异常EXCEPTION_PICPREVIEWRECONNECT 0x8019 图片预览重连PICPREVIEW_RECONNECTSUCCESS 0x8020 图片预览重连成功EXCEPTION_PICPREVIEW 0x8021 图片预览异常EXCEPTION_MAX_ALARM_INFO 0x8022 报警信息缓存已达上限EXCEPTION_LOST_ALARM 0x8023 报警丢失EXCEPTION_PASSIVETRANSRECONNECT 0x8024 被动转码重连PASSIVETRANS_RECONNECTSUCCESS 0x8025 被动转码重连成功EXCEPTION_PASSIVETRANS 0x8026 被动转码异常EXCEPTION_RELOGIN 0x8040 用户重登陆RELOGIN_SUCCESS 0x8041 用户重登陆成功EXCEPTION_PASSIVEDECODE_RECONNNECT 0x8042 被动解码重连EXCEPTION_CLUSTER_CS_ARMFAILED 0x8043 集群报警异常EXCEPTION_RELOGIN_FAILED 0x8044 重登陆失败,停止重登陆EXCEPTION_PREVIEW_RECONNECT_CLOSED 0x8045 关闭预览重连功能EXCEPTION_ALARM_RECONNECT_CLOSED 0x8046 关闭报警重连功能EXCEPTION_SERIAL_RECONNECT_CLOSED 0x8047 关闭透明通道重连功能EXCEPTION_PIC_RECONNECT_CLOSED 0x8048 关闭回显重连功能EXCEPTION_PASSIVE_DECODE_RECONNECT_CLOSED 0x8049 关闭被动解码重连功能EXCEPTION_PASSIVE_TRANS_RECONNECT_CLOSED 0x804a 关闭被动转码重连功能

3.4 demo代码:

HaiKangVisionTool: 通过HTTP进行控制、交互,实现摄像头直接ip登录以及取流、转码前端通过websocket直接预览的功能。