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)初始化和设备登录操作的工具类,其作用主要集中在:
- 加载 SDK 动态库 HCNetSDK.dll
- 初始化 SDK
- 登录/登出海康摄像头设备
@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 的推流进程并登出摄像头。
摄像头按需推流管理的核心类,其作用主要集中在:
- 推流启动、停止推流
- 连接计数管理
- FFmpeg 启动与停止
- 播放地址生成
- 设备登录控制
@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秒左右即可成功播放。