发布时间:2023-11-13 08:00
最近进行项目开发时遇到了需要前端直接调用摄像头,并直接进行播放的需求。原本计划通过海康威视官网的《WEB无插件开发包 V3.2》直接进行控制、交互,实现摄像头直接登录以及取流预览。但是前端人员现场驻场开发后反映各种兼容性问题频发,反正就是不能友好的进行预览播放。鉴于此我直接查询了官网上相关的sdk,然后选用了《设备网络SDK_Win64 V6.1.9.4_build20220412》进行开发java版本的转码工具。整体思路是在PS流中解析出H264的裸流然后通过websocket传给前端,前端基于wfs.js进行h264的裸流播放。
下载开发SDK开发包,并先查看和熟悉sdk使用方法!!
下载wfs.js插件:GitHub - MarkRepo/wfs.js: use html5 video tag with MSE for raw h264 live streaming.
逻辑流转图:
由于博主太懒了,这里没有图。
①: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<LoginVo> 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<LoginVo>> login(@RequestBody List<LoginVo> loginVos)
{
return R.buildOkData(PreverViewUntil.login(loginVos));
}
/**
* 预览某个摄像头
* @param loginVo
* @return
*/
@PostMapping("/playView")
public R<LoginVo> playView(@RequestBody LoginVo loginVo)
{
return R.buildOkData(PreverViewUntil.playView(loginVo));
}
/**
* 只关闭某个摄像头预览,但不退出登录
* @param loginVo
* @return
*/
@PostMapping("/logoutPlayViewOnly")
public R<LoginVo> 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;
//@Component
public 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 便于直接加载注入类
*/
@PostConstruct
public 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
@PreDestroy
public void clearSdk() {
if (null!=hCNetSDK)
{
//SDK反初始化,释放资源,只需要退出时调用一次
hCNetSDK.NET_DVR_Cleanup();
}
}
/**
* 摄像头登录(支持批量操作)
* @param loginVos
* @return
*/
public static List<LoginVo> login(List<LoginVo> 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<LoginVo> 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- NPQ
strClientInfo.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<String,byte[]> EsBytesMap=new HashMap<>();
/**
* 提取H264的裸流写入文件
* @param outputData
* @throws IOException
*/
public void writeESH264(final byte[] outputData,int lRealHandle) throws IOException {
if (outputData.length <= 0) {
return;
}
String playIdStr = String.valueOf(lRealHandle);
byte[] allEsBytes = null;//当前这个通道的一个Rtp包数据
if(!EsBytesMap.containsKey(playIdStr))
{
EsBytesMap.put(playIdStr,allEsBytes);
}
else
{
allEsBytes=EsBytesMap.get(playIdStr);
}
if ((outputData[0] & 0xff) == 0x00//
&& (outputData[1] & 0xff) == 0x00//
&& (outputData[2] & 0xff) == 0x01//
&& (outputData[3] & 0xff) == 0xBA) {// RTP包开头
// 一个完整的帧解析完成后将解析的数据放入BlockingQueue,websocket获取后发送给前端
if (allEsBytes != null && allEsBytes.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}")
@Component
public class OneWebSocket {
/** 记录当前在线网页数量 */
private static AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 连接建立成功调用的方法
*/
@OnOpen
public 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);
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(final Session session) {
onlineCount.decrementAndGet(); // 在线数减1
System.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
* 客户端发送过来的消息
*/
@OnMessage
public void onMessage(final String message, final Session session) {
log.info("服务端收到客户端[{}]的消息:{}", session.getId(), message);
}
@OnError
public 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);
// public String socketSessionId; //前端和这个摄像头建立的websocketid
// public String lUserID="-1"; //摄像头登录句柄
// public String lPlayID="-1"; //摄像预览句柄
static public Map<String,String> SessionToUserIdMap=new HashMap<>();
static public Map<String,String> PlayToUserIdMap=new HashMap<>();
static public Map<String,String> IPToUserIdMap=new HashMap<>();
static public Map<String,BlockingQueue> 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;
}
}
①:准备好摄像头参数:ip、端口、预览通道、用户名、密码,请求后端接口‘/playvision/login’实现登录,登陆成功后端会返回loginStatus(登录状态:0-失败1-成功)、lUserID(登陆摄像头的id)
②:请求预览接口'/playvision/playView',将登录后返回的数据直接请求过来即可。请求成功后会返回plsyStatus(预览状态:0-失败1-成功)、lPlayID(预览成功的通道句柄id)
③:预览接口请求成功后,我们调用wfs.js插件进行实时预览。
<!DOCTYPE html>
<html>
<head>
<title>h.264 To fmp4</title>
<script src="js/jquery-3.0.0.js"> </script>
<script src="js/wfs.js"></script>
<!-- <link href="js/jquery/jquery-ui.css" rel="stylesheet" type="text/css" /> -->
<style type="text/css" media="screen">
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);
}
</style>
</head>
<body>
<h2>h.264 To fmp4</h2>
<div class="wfsjs">
<video id="video1" muted="muted" controls="controls" style="width: 100%;height: 100%;"
autoplay="autoplay" muted></video>
<div class="ratio"></div>
</div>
<script>
//先请求 登录+预览接口 然后返回将返回的lUserID传递进入wfs.js进行websocket初始化
//wfs.js 里面的onMediaAttached 连接 new WebSocket('ws://127.0.0.1:10086/wstest'+"/"+lUserID);
var lUserID="xxxx";
window.onload = function() {
if (Wfs.isSupported()) {
var video1 = document.getElementById("video1");
var wfs = new Wfs();
wfs.attachMedia(video1, 'ch1');
}
};
</script>
</body>
</html>
④:注意摄像头参数配置需要配置编码为H.264
3.3.1登录并预览
通过`/playvision/loginAndPlayView`发起Post请求登陆摄像头并开启预览:
{
"userName": 0, //用户名
"password": 1, //密码
"ip": "", //摄像头所在ip
"prot": 80, //摄像头所在端口
"lDChannel": 1 //使用预览通道
}
3.3.2退出预览&登录
通过`/playvision/logoutPlayView?lUserID=xx&lPlayID=xx`发起get请求退出摄像头的预览和登录
3.3.3批量登录(可用做判断摄像头是否在线)
通过`/playvision/login`发起Post请求登陆摄像头
[{
"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 关闭被动转码重连功能