Java对接大华云睿摄像头(人流统计/客流量统计)
环境准备(注意区分开发环境和正式服务器环境)
- 开发环境
win11 + IDEA + spring boot + mysql - 正式服务器环境
Linux - 下载开发环境和正式环境的SDK
SDK下载地址:https://support.dahuatech.com/tools/sdkExploit
** 注意选择对应的语言和操作系统 **
打开下载的(win64)SDK,查看人流统计部分实现逻辑
- 在SDK中找到人流统计部分的代码逻辑(HumanNumberStatistic.class 和 HumanNumberStatisticFrame.class)
- 启动HumanNumberStatistic,查看摄像头的参数面板
填入设备的外网IP地址、外网端口、用户名、密码,登录设备选择需要订阅的通道,这个通常在设备的管理后台可以查到,需要预先知晓每个摄像头对应的通道号。
点击只能订阅,在摄像头有人流变化时发生回调。
注意:智能订阅的同时,不能点击实时预览,会造成人流回调没有数据,具体原因尚未明确。如下图,订阅的通道发生回调后,就能在事件列表中展示数据。
基于这样的逻辑,我们可以将SDK中的逻辑整合在项目中来。
整合SDK中的逻辑到项目中
根据SDK中大华的JAVA_编程指导手册,了解整个事件的订阅和回调流程。
每一个通道都需要经过这个过程。将SDK打包成jar包(这里需要打两个版本的,就是下载下来的两个开发包,一个win64,一个Linux64,下面会用到)。
jar包可以打入本地maven仓库,在pom.xml中引用,或者直接在项目中添加到libs文件夹,并在pom.xml中引用。两种方式如下。
- 打包到本地maven仓库
mvn install:install-file -Dfile=D:\netsdk-1.0-demo.jar -DgroupId=com.netsdk -DartifactId=netsdk -Dversion=1.0 -Dpackaging=jar -DgeneratePom=true
然后就可以在maven中直接引用maven坐标了。
<dependency>
<groupId>com.netsdk</groupId>
<artifactId>netsdk</artifactId>
<version>1.0</version>
</dependency>
- 不打入本地maven,直接在pom中引用
<dependency>
<groupId>com.netsdk</groupId>
<artifactId>netsdk</artifactId>
<version>1.0-demo</version>
<scope>system</scope>
<systemPath>${basedir}/src/main/resources/libs/netsdk-1.0-demo.jar</systemPath>
</dependency>
<!-- 注意打包到线上要用Linux包里的jar包 -->
<dependency>
<groupId>com.netsdk</groupId>
<artifactId>netsdk</artifactId>
<version>1.0-demo</version>
<scope>system</scope>
<systemPath>${basedir}/src/main/resources/libs/linux/netsdk-1.0-demo.jar</systemPath>
</dependency>
创建数据表(非必须)
由于我的项目中需要用到统计年月日的数据,所以需要将每天的数据记录下来。
设计了如下表(t_human_statistic_info.sql),用于之后统计
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_dahua_human_statistic_info
-- ----------------------------
CREATE TABLE IF NOT EXISTS `t_dahua_human_statistic_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`date` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '统计日期 (格式:yyyy/MM/dd)',
`channel` int NOT NULL COMMENT '通道号',
`entered_today` int NOT NULL COMMENT '今日进入总数',
`entered_total` int NOT NULL COMMENT '进入总数',
`exited_today` int NOT NULL COMMENT '今日出去总数',
`exited_total` int NOT NULL COMMENT '出去总数',
`event_time` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '回调事件最新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
配置人流初始化和回调监听
根据SDK中的流程调用,并配置各自的回调处理(回调处理中根据各自的业务需求去实现)
import com.netsdk.common.EventTaskCommonQueue;
import com.netsdk.demo.module.LoginModule;
import com.netsdk.demo.module.VideoStateSummaryModule;
import com.netsdk.lib.NetSDKLib;
import com.sun.jna.Pointer;
import java.sql.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 人流统计sdk集成
*
* @author Joi
*/
public class HumanNumberModel {
private final String DEVICE_IP = "设备公网IP";
private final int DEVICE_PORT = 8888;// 设备公网端口
private final String DEVICE_USERNAME = "admin";// 用户名
private final String DEVICE_PASSWORD = "123456";// 密码
/**
* 设备断线通知回调
*/
private static final HumanNumberModel.DisConnectCallBack disConnectCB = new HumanNumberModel.DisConnectCallBack();
/**
* 网络连接恢复
*/
private static final HumanNumberModel.HaveReConnectCallBack haveReConnectCB = new HumanNumberModel.HaveReConnectCallBack();
/**
* 默认通道0
*/
private final int channelId;
/**
* sdk lib实例
*/
private static final NetSDKLib netSdk = NetSDKLib.NETSDK_INSTANCE;
/**
* sdk lib实例
*/
private static final NetSDKLib config = NetSDKLib.CONFIG_INSTANCE;
public HumanNumberModel(Integer channelId) {
// 打开工程,SDK初始化,注册断线和重连回调函数
LoginModule.init(disConnectCB, haveReConnectCB);
// 登录
LoginModule.login(DEVICE_IP, DEVICE_PORT, DEVICE_USERNAME, DEVICE_PASSWORD);
this.channelId = channelId;
VideoStateSummaryModule.attachVideoStatSummary(channelId, HumanNumberModel.humanNumberStatisticCB);
System.out.println("人数统计初始化完成~~已登录~~订阅通道 channelId:" + channelId);
}
/**
* 设备断线回调: 通过 CLIENT_Init 设置该回调函数,当设备出现断线时,SDK会调用该函数
*/
private static class DisConnectCallBack implements NetSDKLib.fDisConnect {
@Override
public void invoke(NetSDKLib.LLong m_hLoginHandle, String pchDVRIP, int nDVRPort, Pointer dwUser) {
System.out.printf("Device[%s] Port[%d] DisConnectCallBack!\n", pchDVRIP, nDVRPort);
}
}
private static class HaveReConnectCallBack implements NetSDKLib.fHaveReConnect {
@Override
public void invoke(NetSDKLib.LLong m_hLoginHandle, String pchDVRIP, int nDVRPort, Pointer dwUser) {
System.out.printf("ReConnect Device[%s] Port[%d]\n", pchDVRIP, nDVRPort);
// 断线后需要重新订阅
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
VideoStateSummaryModule.reAttachAllVideoStatSummary(humanNumberStatisticCB);
}
});
service.shutdown();
}
}
/**
* 人数统计回调事件
*/
public static fHumanNumberStatisticCallBack humanNumberStatisticCB = fHumanNumberStatisticCallBack.getInstance();
public static class fHumanNumberStatisticCallBack implements NetSDKLib.fVideoStatSumCallBack {
private static fHumanNumberStatisticCallBack instance = new fHumanNumberStatisticCallBack();
public static fHumanNumberStatisticCallBack getInstance() {
return instance;
}
private EventTaskCommonQueue eventTaskQueue = new EventTaskCommonQueue();
public fHumanNumberStatisticCallBack() {
eventTaskQueue.init();
}
@Override
public void invoke(NetSDKLib.LLong lAttachHandle, NetSDKLib.NET_VIDEOSTAT_SUMMARY stVideoState, int dwBufLen, Pointer dwUser) {
System.out.println("消息回调开始!");
SummaryInfo summaryInfo = new SummaryInfo(
stVideoState.nChannelID, stVideoState.stuTime.toStringTime(),
stVideoState.stuEnteredSubtotal.nToday,
stVideoState.stuEnteredSubtotal.nHour,
stVideoState.stuEnteredSubtotal.nTotal,
stVideoState.stuExitedSubtotal.nToday,
stVideoState.stuExitedSubtotal.nHour,
stVideoState.stuExitedSubtotal.nTotal);
System.out.printf("\nChannel[%d] GetTime[%s]\n" +
"进入人数统计 People In Information[Total[%d] Hour[%d] Today[%d]]\n" +
"出去人数统计 People Out Information[Total[%d] Hour[%d] Today[%d]]\n%n",
summaryInfo.nChannelID, summaryInfo.eventTime,
summaryInfo.enteredTotal, summaryInfo.enteredHour, summaryInfo.enteredToday,
summaryInfo.exitedTotal, summaryInfo.exitedHour, summaryInfo.exitedToday);
// 查询向数据库中插入数据
try {
// 加载数据库驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 声明数据库view的URL
String url = "jdbc:mysql://localhost:3306/dahua?useUnicode=true&characterEncoding=utf-8&useSSL=false";
// 数据库用户名
String user = "root";
// 数据库密码
String password = "123456";
// 建立数据库连接,获得连接对象conn
Connection conn = DriverManager.getConnection(url, user, password);
// 查询当天是否有这条数据
String selectSql = "SELECT `id`,`date`,`channel`,`entered_today`,`entered_total`,`exited_today`,`exited_total`,`event_time` FROM t_dahua_human_statistic_info WHERE `date` = ? and `channel` = ?";
PreparedStatement selectPreparedStatement = conn.prepareStatement(selectSql);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String date = LocalDate.now().format(formatter);
selectPreparedStatement.setString(1, date);
selectPreparedStatement.setInt(2, summaryInfo.nChannelID);
ResultSet resultSet = selectPreparedStatement.executeQuery();
if (resultSet.next()) {
String updateSql = "UPDATE t_dahua_human_statistic_info SET `entered_today` = ?,`entered_total` = ?,`exited_today` = ?,`exited_total` = ?,`event_time` = ? WHERE `date` = ? AND `channel` = ?";
// 创建一个Statment对象
PreparedStatement ps = conn.prepareStatement(updateSql);
// 为sql语句中第一个问号赋值
ps.setInt(1, summaryInfo.enteredToday);
// 为sql语句中第二个问号赋值
ps.setInt(2, summaryInfo.enteredTotal);
// 为sql语句中第三个问号赋值
ps.setInt(3, summaryInfo.exitedToday);
// 为sql语句中第四个问号赋值
ps.setInt(4, summaryInfo.exitedTotal);
ps.setString(5, summaryInfo.eventTime);
ps.setString(6, date);
ps.setInt(7, summaryInfo.nChannelID);
// 执行sql语句
ps.executeUpdate();
System.out.println("人数信息更新完毕!!! SummaryInfo: " + summaryInfo);
} else {
String sql = "insert into t_dahua_human_statistic_info (date,channel,entered_today,entered_total,exited_today,exited_total,event_time) " +
"values(?,?,?,?,?,?,?)"; // 生成一条sql语句
// 创建一个Statment对象
PreparedStatement ps = conn.prepareStatement(sql);
// 为sql语句中第一个问号赋值
ps.setString(1, date);
// 为sql语句中第二个问号赋值
ps.setInt(2, summaryInfo.nChannelID);
// 为sql语句中第三个问号赋值
ps.setInt(3, summaryInfo.enteredToday);
// 为sql语句中第四个问号赋值
ps.setInt(4, summaryInfo.enteredTotal);
ps.setInt(5, summaryInfo.exitedToday);
ps.setInt(6, summaryInfo.exitedTotal);
ps.setString(7, summaryInfo.eventTime);
// 执行sql语句
ps.executeUpdate();
System.out.println("人数信息插入完毕!!! SummaryInfo: " + summaryInfo);
}
// 关闭数据库连接对象
conn.close();
} catch (ClassNotFoundException e) {
System.out.println("ClassNotFoundException 加载数据库连接失败,保存SummaryInfo出错! " + e.getMessage());
} catch (SQLException e) {
System.out.println("SQLException 执行SQL出错!" + e.getMessage());
e.printStackTrace();
}
System.out.println("消息回调结束!");
}
}
public static class SummaryInfo {
public int nChannelID;
/**
* 事件事件
* 格式:yyyy/MM/dd HH:mm:ss
* 例如:2023/07/14 17:40:19
*/
public String eventTime;
public int enteredToday;
public int enteredHour;
public int enteredTotal;
public int exitedToday;
public int exitedHour;
public int exitedTotal;
public SummaryInfo() {
}
public SummaryInfo(int nChannelID, String eventTime,
int enteredToday, int enteredHour,
int enteredTotal, int exitedToday,
int exitedHour, int exitedTotal) {
this.nChannelID = nChannelID;
this.eventTime = eventTime;
this.enteredToday = enteredToday;
this.enteredHour = enteredHour;
this.enteredTotal = enteredTotal;
this.exitedToday = exitedToday;
this.exitedHour = exitedHour;
this.exitedTotal = exitedTotal;
}
}
/**
* 登出
*/
public void logout() {
VideoStateSummaryModule.detachAllVideoStatSummary(); // 退订阅
LoginModule.logout(); // 退出登录
}
}
调用人流统计
项目启动时,配置需要监听的通道号,在摄像头有人流变化时,回调结果
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Configuration;
/**
* Spring boot启动后调用
* 也可以通过实现CommandLineRunner接口来登录硬件设备
*
* @author Joi
*/
@Configuration
public class ApplicationService implements ApplicationRunner {
@Value("${dahua.camera.ip}")
private String DEVICE_IP;
@Value("${dahua.camera.port}")
private int DEVICE_PORT;
@Value("${dahua.camera.username}")
private String DEVICE_USERNAME;
@Value("${dahua.camera.password}")
private String DEVICE_PASSWORD;
@Override
public void run(ApplicationArguments args) throws Exception {
// 初始化人流统计 需要监听的通道号
new HumanNumberModel(0);
new HumanNumberModel(1);
new HumanNumberModel(2);
new HumanNumberModel(3);
new HumanNumberModel(4);
new HumanNumberModel(5);
new HumanNumberModel(6);
new HumanNumberModel(7);
new HumanNumberModel(8);
new HumanNumberModel(9);
new HumanNumberModel(10);
new HumanNumberModel(11);
new HumanNumberModel(12);
}
}
执行结果如下:
常见问题
- 正式服务器(Linux)找不.so文件
- 复制so文件到lib路径
linux系统的so库一般存储与“/usr/lib/”路径中,可将动态库复制到该路径中。