当前位置: 首页 > web >正文

JAVA + 海康威视SDK + FFmpeg+ SRS 实现海康威视摄像头二次开发

一、问题表述

我们公司自研的物联网平台,需要对项目设计的海康威视摄像头进行接入,并实现预览、回放、回放视频下载等功能。之前都是通过开源的平台进行中转后接入我们平台,现在要求使用海康官方SDK进行开发,实现相应功能。

二、解决思路

在这个系统中,整体架构基于 Java 后端,集成海康威视 SDK(HCNetSDK)实现摄像头控制与视频获取,通过 FFmpeg 将 RTSP 视频流转为 RTMP 协议推送至 SRS 流媒体服务器,最终前端通过浏览器使用 HTTP-FLV 协议实现实时播放或录像回放。整个系统结合海康 SDK 的稳定性、FFmpeg 的强大转码能力和 SRS 的高性能直播服务,形成了一套稳定、可扩展、支持按需拉流、录像回放和下载的摄像头视频服务平台。

三、整体技术栈角色说明

技术作用
Java后端服务开发(控制摄像头、启动推流、接口服务等)
海康威视 SDK(HCNetSDK)用于从摄像头设备中获取实时视频流或录像(回放)
FFmpeg把从摄像头获取的 RTSP 流,转封装为 RTMP 推送到 SRS
SRS(Simple Realtime Server)实时音视频服务器,负责接收 RTMP 推流,提供 FLV/HLS/RTC 播放地址
浏览器前端使用 video.js/flv.js 播放 SRS 提供的地址,如 http://ip:port/live/123.flv

四、海康SDK二次开发----前期准备

1. 下载海康威视官方SDK

1.1 进入海康威视SDK下载管网,下载适合自己版本的SDK包。(注意:我们公司服务部署在Windows系统的服务器上,所以我后续使用的是Windows 64 版本)

在这里插入图片描述

1.2 解压SDK包

SDK包内容解析:

文件夹名称作用
Demo示例各种开发语言的Demo实例,我们主要关注Java的
开发文档设备网络SDK使用手册.chm是对SDK中的方法、参数、流程的解释,开发过程中主要用到了这个,其他的文档没怎么使用过
库文件dll文件引用,二次开发的核心
头文件C/C++ 中的头文件,Java开发涉及不到
HCNetSDKCom文件夹必须加载到工程.txt说明文件,大家启动Demo一定要看一下

下载解压后如图:
在这里插入图片描述

2. 本地部署SRS

具体参考全网最详细 Windows 部署 SRS(Simple Realtime Server) 流媒体服务器。

3. 本地部署FFmpeg

具体参考全网最详细 Windows 部署 FFmpeg 跨平台音视频处理工具。

4. 本地安装 VLC Media Player 播放器

具体参考Windows 安装 VLC Media Player 多媒体播放器。

5. 摄像头准备

有一台海康威视平台的摄像头,且支持 RTSP 协议。如图即可:
在这里插入图片描述

五、海康SDK二次开发----编码准备

代码结构预览
在这里插入图片描述

1.1 SRS 信息配置

在 application.yaml 中配置 SRS 的信息,主要配置 IP(部署SRS的服务器Ip) 和端口(默认是8080,可以自己进行更改,我改了9299)。

rtsp:ip: 127.0.0.1port: 9299

1.2 摄像头信息存储

我们把摄像头信息存储在数据库,每次预览时根据前端的摄像头的ID,登录并预览相应的摄像头。具体接口根据自己的项目需求编写即可。

1.2.1 数据库建表

CREATE TABLE camera_device_info (camera_id   INT AUTO_INCREMENT PRIMARY KEY COMMENT '摄像头设备ID',camera_name VARCHAR(255) COMMENT '设备名称',ip          VARCHAR(255) NOT NULL COMMENT 'IP',port        SMALLINT NOT NULL COMMENT '端口',username    VARCHAR(255) COMMENT '用户名',password    VARCHAR(255) COMMENT '密码',stream_key  VARCHAR(255) NOT NULL COMMENT '流编号',remark      TEXT COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='摄像头设备信息';

1.2.2 编写查询接口

此处只提供需要实现的功能,具体实现不做提供。

public interface CameraDeviceInfoService {/*** 获取摄像头信息列表*/List<CameraDeviceInfoListRespVO> listCameraDeviceInfo(String cameraName);
}

1.3 添加海康威视SDK

1.3.1 提取所需代码

在下列文件夹中找到相应的代码复制到自己的项目中,一个接口、一个文件夹。

......\CH-HCNetSDKV6.1.9.48_build20230410_win64\Demo示例\4- Java 开发示例\1-预览回放下载\

复制后效果如图所示:
在这里插入图片描述

1.3.2 提取 .dll 文件及 jar 包

在resources 目录下新建 lib文件夹,从下列文件夹 中复制 .dll 文件及 jar 包到此处(为了避免出错,直接复制改文件夹下的所有文件)。

......\CH-HCNetSDKV6.1.9.48_build20230410_win64\库文件

复制后效果如图所示:
在这里插入图片描述

1.3.3 加载jar包及配置打包插件

在pom.xml加载jar到项目中,并设置打包插件,避免打包时遗漏jar包。

<dependencies><!-- 海康威视 相关 --><dependency><groupId>net.java.jna</groupId><artifactId>jna</artifactId><version>1.0.0</version><scope>system</scope><systemPath>${project.basedir}/src/main/resources/lib/jna.jar</systemPath></dependency><dependency><groupId>net.java.examples</groupId><artifactId>examples</artifactId><version>1.0.0</version><scope>system</scope><systemPath>${project.basedir}/src/main/resources/lib/examples.jar</systemPath></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><includeSystemScope>true</includeSystemScope></configuration></plugin></plugins></build>

六、海康SDK二次开发----预览开发

1. 海康威视 SDK 工具类

HKUtils 类是一个封装了海康威视 SDK(HCNetSDK)初始化和设备登录操作的工具类,其作用主要集中在:

  1. 加载 SDK 动态库 HCNetSDK.dll
  2. 初始化 SDK
  3. 登录/登出海康摄像头设备
@Slf4j
@Component
public class HKUtils {private HCNetSDK hCNetSDK;/*** 初始化海康 SDK,加载 HCNetSDK.dll*/@PostConstructpublic void initSdk() {try {String dllPath = "C:\\lib\\HCNetSDK.dll";File dllFile = new File(dllPath);if (!dllFile.exists()) {log.error("HCNetSDK.dll 不存在:{}", dllPath);return;}hCNetSDK = (HCNetSDK) Native.loadLibrary(dllFile.getAbsolutePath(), HCNetSDK.class);log.info("HCNetSDK.dll 初始化成功");} catch (Exception e) {log.error("SDK 初始化失败:{}", e.getMessage(), e);}}/*** 登录摄像头设备** @param ip         摄像头 IP* @param port       端口号* @param username   登录用户名* @param password   登录密码* @param cameraName 用于日志标识* @return 登录成功返回 userId,失败返回负数*/public int loginDevice(String ip, short port, String username, String password, String cameraName) {HCNetSDK.NET_DVR_USER_LOGIN_INFO loginInfo = new HCNetSDK.NET_DVR_USER_LOGIN_INFO();HCNetSDK.NET_DVR_DEVICEINFO_V40 deviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40();System.arraycopy(ip.getBytes(), 0, loginInfo.sDeviceAddress, 0, ip.length());System.arraycopy(username.getBytes(), 0, loginInfo.sUserName, 0, username.length());System.arraycopy(password.getBytes(), 0, loginInfo.sPassword, 0, password.length());loginInfo.wPort = port;loginInfo.bUseAsynLogin = false;int userId = hCNetSDK.NET_DVR_Login_V40(loginInfo, deviceInfo);if (userId < 0) {log.error("摄像头 [{}] 登录失败,错误码:{}", cameraName, hCNetSDK.NET_DVR_GetLastError());} else {log.info("摄像头 [{}] 登录成功", cameraName);}return userId;}/*** 登出设备** @param userId 登录句柄*/public void logout(int userId) {if (userId >= 0) {hCNetSDK.NET_DVR_Logout(userId);}}
}

2. 摄像头流管理器

这里的实现逻辑是,每一个摄像头的推流的FLV播放地址是唯一的,但是它可以让很多人同时预览。当有人退出预览时,会让这个FFmpeg 推流进程的连接数 -1,反之则 +1。当连接数 =0 时,则会停止 FFmpeg 的推流进程并登出摄像头。
摄像头按需推流管理的核心类,其作用主要集中在:

  1. 推流启动、停止推流
  2. 连接计数管理
  3. FFmpeg 启动与停止
  4. 播放地址生成
  5. 设备登录控制
@Slf4j
@Component
@RequiredArgsConstructor
public class StreamManager {private final CameraDeviceInfoMapper cameraDeviceInfoMapper;// RTMP 推流服务器配置@Value("${rtsp.ip}")private String rtmpServerIp;// RTMP 推流服务器配置@Value("${rtsp.port}")private short rtmpServerPort;@Resourceprivate HKUtils hkUtils;// cameraId -> 推流信息映射private final Map<Long, CameraStream> CAMERA_STREAMS = new ConcurrentHashMap<>();// cameraId -> userId 的登录记录private final Map<Long, Integer> LOGIN_USER_MAP = new ConcurrentHashMap<>();// 登录记录(供 start 时使用)public void recordLoginUser(Long cameraId, int userId) {LOGIN_USER_MAP.put(cameraId, userId);}/*** 启动推流,如果已有流存在则增加连接数** @param cameraId 摄像头ID* @param rtspUrl  RTSP 视频源地址* @return 播放地址(RTMP 格式)* @throws IOException 启动失败时抛出*/public synchronized String startStreaming(Long cameraId, String rtspUrl) throws IOException {CameraDeviceInfoDO cameraDeviceInfoDO = cameraDeviceInfoMapper.selectById(cameraId);if (cameraDeviceInfoDO == null) {throw exception(CAMERA_NOT_EXIST);}if (!CAMERA_STREAMS.containsKey(cameraId)) {// 启动 FFmpeg 转推流String rtmpUrl = buildPlayUrl(cameraDeviceInfoDO.getStreamKey());Process ffmpegProcess = launchFfmpeg(rtspUrl, rtmpUrl);CAMERA_STREAMS.put(cameraId, new CameraStream(ffmpegProcess));log.info("摄像头 [{}] 推流启动成功", cameraId);} else {// 已推流,增加连接数CameraStream stream = CAMERA_STREAMS.get(cameraId);stream.setConnectionCount(stream.getConnectionCount() + 1);log.debug("摄像头 [{}] 增加连接数,当前连接数: {}", cameraId, stream.getConnectionCount());}return buildPlayUrl(cameraDeviceInfoDO.getStreamKey());}/*** 停止摄像头推流连接,如果连接数为0则销毁进程** @param cameraId 摄像头ID* @return 是否成功停止连接*/public synchronized boolean stopStreaming(Long cameraId) {CameraDeviceInfoDO camera = cameraDeviceInfoMapper.selectById(cameraId);if (camera == null) {log.warn("摄像头 [{}] 不存在", cameraId);return false;}CameraStream stream = CAMERA_STREAMS.get(cameraId);if (stream == null) {log.warn("摄像头 [{}] 无推流进程", camera.getCameraName());return false;}// 连接数减一,若为0则销毁进程int count = Math.max(0, stream.getConnectionCount() - 1);stream.setConnectionCount(count);log.debug("摄像头 [{}] 减少连接,剩余: {}", cameraId, count);if (count == 0) {stream.getFfmpegProcess().destroy();CAMERA_STREAMS.remove(cameraId);log.info("摄像头 [{}] 所有连接断开,推流停止", camera.getCameraName());// 退出登录Integer userId = LOGIN_USER_MAP.remove(cameraId);if (userId != null) {hkUtils.logout(userId);log.info("摄像头 [{}] 已退出登录", camera.getCameraName());}}return true;}/*** 检查摄像头是否正在推流** @param cameraId 摄像头ID* @return 是否正在推流*/public boolean isStreaming(Long cameraId) {return CAMERA_STREAMS.containsKey(cameraId);}/*** 构造播放地址** @param streamKey 流唯一标识* @return 播放地址*/private String buildPlayUrl(String streamKey) {return String.format("rtmp://%s:%d/live/camera_%s", rtmpServerIp, rtmpServerPort, streamKey);}/*** 启动 FFmpeg 进程,从 RTSP 推送至 RTMP** @param inputUrl  RTSP 视频流* @param outputUrl RTMP 推流地址* @return FFmpeg 进程* @throws IOException 启动失败*/private Process launchFfmpeg(String inputUrl, String outputUrl) throws IOException {List<String> command = Arrays.asList("C:\\lib\\ffempg\\ffmpeg-master-latest-win64-gpl-shared\\bin\\ffmpeg.exe",// 强制TCP传输RTSP"-rtsp_transport", "tcp",// 输入流地址"-i", inputUrl,// 禁用音频流"-an",// 直接复制视频流,无需解码编码"-c:v", "copy",// 输出格式为FLV"-f", "flv",// 推流目标地址outputUrl);return new ProcessBuilder(command).redirectErrorStream(true).start();}
}

3. 推流信息管理类

用于记录每一个 FFmpeg 进程信息及其连接数。

@Data
public class CameraStream {/*** FFmpeg 推流进程*/private Process ffmpegProcess;/*** 当前观看连接数*/private int connectionCount;public CameraStream(Process process) {this.ffmpegProcess = process;this.connectionCount = 1;}
}

4. Service 服务层

Service 服务层,通过摄像头ID 控制开始推流与停止推流

public interface CameraDeviceInfoService {/*** 获取摄像头信息列表*/List<CameraDeviceInfoListRespVO> listCameraDeviceInfo(String cameraName);/*** 启动摄像头推流(按需拉流)** @param cameraId 摄像头 ID*/String startPreview(Long cameraId) throws IOException;/*** 停止推流** @param cameraId 摄像头 ID*/Boolean stopPreview(Long cameraId);}

5. serviceImpl 服务实现类

serviceImpl 服务实现类,都是对海康威视 SDK 工具类和摄像头流管理器的方法的调用,注意调用方法时的逻辑。

@Service
@Slf4j
public class CameraDeviceInfoServiceImpl implements CameraDeviceInfoService {@Resourceprivate CameraDeviceInfoMapper cameraDeviceInfoMapper;@Resourceprivate HKUtils hkUtils;@Resourceprivate StreamManager streamManager;// RTMP 推流服务器配置@Value("${rtsp.ip}")private String rtmpServerIp;// RTMP 推流服务器配置@Value("${rtsp.port}")private short rtmpServerPort;private static final String PLAY_URL_PREFIX = "http://112.29.108.215:9929/live/";@Overridepublic List<CameraDeviceInfoListRespVO> listCameraDeviceInfo(String cameraName) {// 此处代码省略........}/*** 启动摄像头推流(按需拉流)** @param cameraId 摄像头 ID* @return 播放地址*/@Overridepublic String startPreview(Long cameraId) {CameraDeviceInfoDO cameraDeviceInfoDO = cameraDeviceInfoMapper.selectById(cameraId);if (cameraDeviceInfoDO == null) {throw exception(CAMERA_NOT_EXIST);}// 登录设备if (!streamManager.isStreaming(cameraId)) {int userId = hkUtils.loginDevice(cameraDeviceInfoDO.getIp(), cameraDeviceInfoDO.getPort(), cameraDeviceInfoDO.getUsername(), cameraDeviceInfoDO.getPassword(), cameraDeviceInfoDO.getCameraName());streamManager.recordLoginUser(cameraId, userId);}// 启动推流try {// 构造 RTSP 拉流地址String rtspUrl = String.format("rtsp://%s:%s@%s:%d/%s",cameraDeviceInfoDO.getUsername(),cameraDeviceInfoDO.getPassword(),cameraDeviceInfoDO.getIp(),554,"/h264/ch1/main/av_stream");streamManager.startStreaming(cameraId, rtspUrl);} catch (IOException e) {log.error("摄像头 [{}] 推流失败", cameraDeviceInfoDO.getCameraName(), e);throw new RuntimeException("推流失败:" + e.getMessage());}// 返回前端需要的播放地址格式return "http://" + rtmpServerIp + ":" + rtmpServerPort + "/live/camera_" + cameraDeviceInfoDO.getStreamKey() + ".flv";}/*** 停止推流** @param cameraId 摄像头 ID*/@Overridepublic Boolean stopPreview(Long cameraId) {CameraDeviceInfoDO camera = cameraDeviceInfoMapper.selectById(cameraId);if (camera == null) {return false;}return streamManager.stopStreaming(cameraId);}}

6. controller 接口

提供给前端使用的接口,可以根据自己项目的需要继续增加。

@Tag(name = "监控系统--摄像头信息")
@RestController
@RequestMapping("/camera/device_info")
public class CameraDeviceInfoController {@ResourceCameraDeviceInfoService cameraDeviceInfoService;@GetMapping("/list")@Operation(summary = "获取摄像头信息列表")public CommonResult<List<CameraDeviceInfoListRespVO>> selectEnterpriseInfoList(@RequestParam(value = "cameraName", required = false) String cameraName) {return success(cameraDeviceInfoService.listCameraDeviceInfo(cameraName));}@GetMapping("/start/preview/{cameraId}")@Operation(summary = "开启摄像头预览推流")public CommonResult<String> startPreview(@PathVariable("cameraId") Long cameraId) throws IOException {return success(cameraDeviceInfoService.startPreview(cameraId));}@PostMapping("/stop/preview/{cameraId}")@Operation(summary = "停止摄像头预览推流")public CommonResult<Boolean> stopPreview(@PathVariable("cameraId") Long cameraId) {return success(cameraDeviceInfoService.stopPreview(cameraId));}
}

7. FLV 播放地址测试

调用开启摄像头预览推流接口会返还一个 HTTP-FLV 协议的播放地址,使用 VLC Media Player 进行测试播放。

在这里插入图片描述
输入获取的地址进行播放,稍等3秒左右即可成功播放。
在这里插入图片描述

七、后续的视频回放、回放视频下载、云台控制、截图等功能的开发,将在后续持续更新,敬请关注。

http://www.xdnf.cn/news/16375.html

相关文章:

  • 服务器托管:网站经常被攻击该怎么办?
  • 学习游戏制作记录(克隆技能)7.25
  • 秋招Day19 - 分布式 - 分布式锁
  • 初识决策树-理论部分
  • 肺癌预测模型实战案例
  • 【自动化运维神器Ansible】Ansible常用模块之Copy模块详解
  • 文件包含学习总结
  • 滑动窗口-7
  • 主要分布在背侧海马体(dHPC)CA1区域(dCA1)的时空联合细胞对NLP中的深层语义分析的积极影响和启示
  • ClickHouse 常用的使用场景
  • AWS WebRTC:我们的业务模式
  • [python][flask]flask蓝图使用方法
  • 【软件工程】构建软件合规防护网:双阶段检查机制的实践之道
  • Android studio自带的Android模拟器都是x86架构的吗,需要把arm架构的app翻译成x86指令?
  • FP16 和 BF16
  • 函数-变量的作用域和生命周期
  • 老题新解|奇偶数判断
  • 从Taro的Dialog.open出发,学习远程控制组件之【事件驱动】
  • OAuth 2.0 安全最佳实践 (RFC 9700) password 授权类型已经不推荐使用了,将在计划中移除
  • JS与Go:编程语言双星的碰撞与共生
  • vue2+node+express+MongoDB项目安装启动启动
  • go语言基础教程:【2】基础语法:基本数据类型(整形和浮点型)
  • js实现宫格布局图片放大交互动画
  • android app适配Android 15可以在Android studio自带的模拟器上进行吗,还是说必须在真机上进行
  • 无人机视觉模块技术解析
  • 【LeetCode Solutions】LeetCode 热题 100 题解(1 ~ 5)
  • [CSS]让overflow不用按shift可以滚轮水平滚动(纯CSS)
  • 【数据库】AI驱动未来:电科金仓新一代数据库一体机如何重构性能边界?
  • 半相合 - 脐血联合移植
  • Kingbasepostgis 安装实践