海康摄像头(这里指的是智能摄像头,也就是内置算法的摄像头,热成像或者可见光摄像头),报警信息分为两种,监听和布防,本篇文档使用设备集成 API 实现的报警监听。
1. 说明
海康目前可以通过三种对接设备,分别是:设备集成 SDK 、设备集成 API、设备集成网关
官方说明:海康设备能力开放
设备集成 SDK
基于OTAP(Open Things Access Protocol,开放物联协议)开发的SDK,以动态链接库形式提供,包含Win64和Linux64版本。适用于“硬件设备没有固定IP地址”的网络环境,会覆盖接入AI智能前后端产品、通用前后端产品、门禁产品、交通产品、对讲产品、报警产品、热成像、传输产品、显示产品、控制产品等多种网络硬件设备。功能特性:支持设备注册、预览、回放、语音对讲、事件接收、OTA升级、网关与子设备、参数配置等。
PS:使用过,体验一般。可以参考海康SDK对接记录
设备集成 API
HTTP协议RESTful风格API接口, 可在Win32、Win64、Linux32、Linux64、Android、iOS、ARM等各种系统中使用,适用于“硬件与客户端处于同一个局域网或专网下有固定IP地址”或“硬件或硬件所接的路由器有固定公网IP地址或域名”的网络环境,支持AI智能前后端产品、通用前后端产品、门禁产品、交通产品、对讲产品、报警产品、热成像产品、控制产品等多种网络硬件设备。
PS:使用过,感觉比 SDK 好用些,需要签约,一些 API 接口在设备上,可能是不通用的。(神奇的海康)
设备集成网关
Device Gateway,是一款网关软件,以软件安装包形式提供,包含Win64、Linux64版本,支持接入视频设备(如网络摄像机、硬盘录像机等)和明眸门禁设备(兼容以前的门禁网关AC Gateway)。设备通过ISUP5.0协议接入网关,然后三方平台可使用HTTP/HTTPS协议和RTSP协议对接网关,从而实现视频或明眸门禁设备的管理、配置、控制操作、报警事件、取流等功能的集成。
- 接入视频设备,可以实现预览、回放、云台控制、语音对讲等功能。同时提供无插件SDK取流方式和Web SDK V3.3插件取流方式,可以实现BS浏览器集成视频预览和录像回放功能。其中无插件取流支持1路1080P,Web SDK V3.3插件取流支持最大分辨率为 2400W(如果是多路预览回放,分辨率之和不得超过 2400W), 支持同时预览回放的最大路数为 16 路。
- 接入明眸门禁设备,可以实现人员管理、人脸管理、卡片管理、指纹管理、远程控门等功能。
PS:没用过。
2.海康文档中的布防和监听说明
报警主机通过子系统实现防区分区管理和配置,每一个子系统下有多个防区,报警主机对子系统或者防区进行布防之后会自动检测报警触发事件然后上传。SDK接收报警主机上传的报警信息有两种方式:
1)报警布防方式,是指SDK主动连接设备,建立报警上传通道,设备发生报警之后发送给SDK,需要先注册登录设备。
2)报警监听方式,是指触发事件时设备主动连接SDK并且上传报警信息,SDK在设定的端口上监听和接收。需要先在设备端配置报警主机的IP和端口,和SDK监听的IP、端口需要一致。
布防和监听,是两种不同的方式,设备同时支持的情况下,只需要选择其中一种即可。
3.海康中监听方式报警信息的实现
报警服务器就是服务端,摄像头就是客户端。个人理解是,配置报警服务器以后,触发报警以后,摄像头将该报警报文传输到配置的报警服务器。这种情况适用于多个摄像头,同时往报警服务器上传报警信息。
1.海康摄像头后台配置
首先在海康的 web 页面,配置报警服务器的 IP 、端口、url 等。
智能分析-联动方式-勾选上传中心
2.代码实现接收报警消息
简单来说:就是监听某个端口,收到报警数据以后,然后解析数据。
以下代码来自海康给的示例代码:
/**
* @author zhengxiaohui
* @date 2024/1/15 19:13
* @desc ISAPI监听示例demo代码
*/
@Slf4j
public class AlarmListenDemo {
private static Thread listenThread;
/**
* 开始监听
*/
public void startListen() {
stopListen();
AlarmListenDemo.listenThread = new Thread(new ListenThread());
AlarmListenDemo.listenThread.start();
}
/**
* 停止监听
*/
public void stopListen() {
if (AlarmListenDemo.listenThread == null) {
return;
}
AlarmListenDemo.listenThread.interrupt();
}
}
/**
* @author zhengxiaohui
* @date 2024/1/18 17:38
* @desc 监听处理线程
*/
public class ListenThread implements Runnable {
static PropertiesUtil propertiesUtil = new PropertiesUtil("./config.properties");
private final AlarmDataParser alarmDataParser = new AlarmDataParser();
@Override
public void run() {
int listenPort = propertiesUtil.getIntegerProperty("custom.isapi.listen.port", 9999);
try {
ServerSocket serverSocket = new ServerSocket(listenPort);
System.out.println("启动监听, 监听端口:" + listenPort);
while (!Thread.currentThread().isInterrupted()) {
Socket accept = serverSocket.accept();
accept.setKeepAlive(true);
// System.out.println("设备(客户端)信息:" + accept.getInetAddress().getHostAddress());
if (accept.isConnected()) {
handleData(accept);
}
accept.close();
}
serverSocket.close();
System.out.println("停止监听完成");
} catch (InterruptedException e) {
// 线程被中断的处理逻辑
System.out.println("停止监听完成: " + e.getMessage());
} catch (Exception e) {
System.out.println("监听创建异常: " + e.getMessage());
}
}
private void handleData(Socket accept) throws Exception {
InputStream inputData = accept.getInputStream();
OutputStream outputData = accept.getOutputStream();
// 输出数据
ByteArrayOutputStream byOutputData = new ByteArrayOutputStream();
byte[] buffer = new byte[2 * 1024 * 1024];
int length = 0;
// 持续接收处理数据直到接收完毕
String recvAlarmData = "";
while ((length = inputData.read(buffer)) > 0) {
byOutputData.write(buffer, 0, length);
String recvData = byOutputData.toString();
recvAlarmData = recvAlarmData + recvData;
// 获取boundary
String strBoundary = "boundary=";
int beginIndex = recvData.indexOf(strBoundary);
beginIndex += strBoundary.length();
int lenIndex = recvData.indexOf("\r\n", beginIndex);
String strBoundaryMark = recvData.substring(beginIndex, lenIndex);
if (recvAlarmData.contains("--" + strBoundaryMark.trim() + "--")) {
//表单结束符判断接收结束
break;
}
}
String response = "HTTP/1.1 200 OK" +
"\r\n" +
"Connection: close" +
"\r\n\r\n";
outputData.write(response.getBytes());
outputData.flush();
outputData.close();
inputData.close();
//解析数据
response = parseAlarmInfoByte(byOutputData);
}
private String parseAlarmInfoByte(ByteArrayOutputStream byOutputData) throws Exception {
// 事件报文字节
byte[] byAlarmDataInfo = byOutputData.toByteArray();
int iDataLen = byAlarmDataInfo.length;
String szBoundaryMark = "boundary=";
String szContentTypeMark = "Content-Type: ";
int iTypeMarkLen = szContentTypeMark.getBytes("UTF-8").length;
String szContentLenMark = "Content-Length: ";
int iLenMarkLen = szContentLenMark.getBytes("UTF-8").length;
String szContentLenMark2 = "content-length: ";
int iLenMarkLen2 = szContentLenMark2.getBytes("UTF-8").length;
int iContentLen = 0;
String szEndMark = "\r\n";
int iMarkLen = szEndMark.getBytes("UTF-8").length;
String szEndMark2 = "\r\n\r\n";
int iMarkLen2 = szEndMark2.getBytes("UTF-8").length;
String szJson = "text/json";
String szJpg = "image/jpeg";
int iStartBoundary = doDataSearch(byAlarmDataInfo, szBoundaryMark.getBytes("UTF-8"), 0, byAlarmDataInfo.length);
iStartBoundary += szBoundaryMark.getBytes("UTF-8").length;
int iEndBoundary = doDataSearch(byAlarmDataInfo, szEndMark.getBytes("UTF-8"), iStartBoundary, byAlarmDataInfo.length);
byte[] byBoundary = new byte[iEndBoundary - iStartBoundary];
System.arraycopy(byAlarmDataInfo, iStartBoundary, byBoundary, 0, iEndBoundary - iStartBoundary);
String szBoundaryEndMark = "--" + new String(byBoundary).trim() + "--";
int iDateEnd = doDataSearch(byAlarmDataInfo, szBoundaryEndMark.getBytes("UTF-8"), 0, byAlarmDataInfo.length);
String szBoundaryMidMark = "--" + new String(byBoundary).trim();
int iBoundaryMidLen = szBoundaryMidMark.getBytes("UTF-8").length;
int startIndex = iEndBoundary;
String szContentType = "";
int[] iBoundaryPos = new int[11]; //boundary个数,这里最大解析10个
int iBoundaryNum = 0;
for (iBoundaryNum = 0; iBoundaryNum < 10; iBoundaryNum++) {
startIndex = doDataSearch(byAlarmDataInfo, szBoundaryMidMark.getBytes("UTF-8"), startIndex, iDateEnd);
if (startIndex < 0) {
break;
}
startIndex += iBoundaryMidLen;
iBoundaryPos[iBoundaryNum] = startIndex;
}
iBoundaryPos[iBoundaryNum] = iDateEnd;//最后一个是结束符
for (int i = 0; i < iBoundaryNum; i++) {
// Content-Type
int iStartType = doDataSearch(byAlarmDataInfo, szContentTypeMark.getBytes("UTF-8"), iBoundaryPos[i], iBoundaryPos[i + 1]);
if (iStartType > 0) {
iStartType += iTypeMarkLen;
int iEndType = doDataSearch(byAlarmDataInfo, szEndMark.getBytes("UTF-8"), iStartType, iBoundaryPos[i + 1]);
if (iEndType > 0) {
byte[] byType = new byte[iEndType - iStartType];
System.arraycopy(byAlarmDataInfo, iStartType, byType, 0, iEndType - iStartType);
szContentType = new String(byType).trim();
}
}
// Content-Length
int iStartLength = doDataSearch(byAlarmDataInfo, szContentLenMark.getBytes("UTF-8"), iBoundaryPos[i], iBoundaryPos[i + 1]);
if (iStartLength > 0) {
iStartLength += iLenMarkLen;
int iEndLength = doDataSearch(byAlarmDataInfo, szEndMark.getBytes("UTF-8"), iStartLength, iBoundaryPos[i + 1]);
if (iEndLength > 0) {
byte[] byLength = new byte[iEndLength - iStartLength];
System.arraycopy(byAlarmDataInfo, iStartLength, byLength, 0, iEndLength - iStartLength);
iContentLen = Integer.parseInt(new String(byLength).trim());
}
}
// Content-Length(兼容错误大小写)
int iStartLength2 = doDataSearch(byAlarmDataInfo, szContentLenMark2.getBytes("UTF-8"), iBoundaryPos[i], iBoundaryPos[i + 1]);
if (iStartLength2 > 0) {
iStartLength2 += iLenMarkLen2;
int iEndLength2 = doDataSearch(byAlarmDataInfo, szEndMark.getBytes("UTF-8"), iStartLength2, iBoundaryPos[i + 1]);
if (iEndLength2 > 0) {
byte[] byLength2 = new byte[iEndLength2 - iStartLength2];
System.arraycopy(byAlarmDataInfo, iStartLength2, byLength2, 0, iEndLength2 - iStartLength2);
iContentLen = Integer.parseInt(new String(byLength2).trim());
}
}
// 通过\r\n\r\n判断报文数据起始位置
int iStartData = doDataSearch(byAlarmDataInfo, szEndMark2.getBytes("UTF-8"), iBoundaryPos[i], iBoundaryPos[i + 1]);
if (iStartData > 0) {
iStartData += iMarkLen2;
// 有的报文可能没有Content-Length
if (iContentLen <= 0) {
iContentLen = iBoundaryPos[i + 1] - iStartData;
}
// 截取数据内容
byte[] byData = new byte[iContentLen];
System.arraycopy(byAlarmDataInfo, iStartData, byData, 0, iContentLen);
// 根据类型处理数据
int contentType = ContentTypeEnum.getEventType(szContentType);
String storeFolder = System.getProperty("user.dir") + "\\output\\listen\\event\\";
switch (contentType) {
case ContentTypeEnum.APPLICATION_JSON:
case ContentTypeEnum.APPLICATION_XML: {
String rawContent = new String(byData).trim();
alarmDataParser.parseAlarmInfo(contentType, storeFolder, rawContent, null);
break;
}
case ContentTypeEnum.IMAGE_JPEG:
case ContentTypeEnum.IMAGE_PNG:
case ContentTypeEnum.VIDEO_MPG:
case ContentTypeEnum.VIDEO_MPEG4:
case ContentTypeEnum.APPLICATION_ZIP: {
alarmDataParser.parseAlarmInfo(contentType, storeFolder, null, byData);
break;
}
default: {
System.out.println("未匹配到可以解析的content-type, 请自行补全处理!");
}
}
}
}
// 响应报文
String response = "";
// 消费交易事件 (实际如果没有消费机设备可以不需要消费机的处理代码)
String eventType = "";
String eventConfirm = "";
if (eventType.equals("ConsumptionEvent") || eventType.equals("TransactionRecordEvent") || eventType.equals("HealthInfoSyncQuery")) {
response = "HTTP/1.1 200 OK" +
"\r\n" +
"Content-Type: application/json; charset=\"UTF-8\"" +
"\r\n" +
"Content-Length: " + eventConfirm.length() +
"\r\n\r\n" + eventConfirm +
"\r\n";
} else {
response = "HTTP/1.1 200 OK" +
"\r\n" +
"Connection: close" +
"\r\n\r\n";
}
return response;
}
private int doDataSearch(byte[] bySrcData, byte[] keyData, int startIndex, int endIndex) {
if (bySrcData == null || keyData == null || bySrcData.length <= startIndex || bySrcData.length < keyData.length) {
return -1;
}
if (endIndex > bySrcData.length) {
endIndex = bySrcData.length;
}
int iPos, jIndex;
for (iPos = startIndex; iPos < endIndex; iPos++) {
if (bySrcData.length < keyData.length + iPos) {
break;
}
for (jIndex = 0; jIndex < keyData.length; jIndex++) {
if (bySrcData[iPos + jIndex] != keyData[jIndex]) {
break;
}
}
if (jIndex == keyData.length) {
return iPos;
}
}
return -1;
}
}
PS:
如果你看到这里,希望我的分享,可以帮到你,感谢你的阅读,愿我们在代码世界变得更强!