利用海康CVR实现视频流历史回放

一、概况

我们有一个项目,有多路摄像头。甲方要求在应用系统(Web)上集成视频流历史回放功能,可以回放指定时间段的视频流,还可以将它们保存成文件,方便播放。

视频流存储肯定要专业的NVR(网络硬盘录像机)服务器来,自己去弄是不可想象。项目有一个海康CVR(中心级视频网络存储设备)服务器,有磁盘阵列,好多个T,容量挺大,用于存储各路视频流。它提供了SDK,可用于二次开发,支持外部读取历史视频。就靠它了。

我根据海康官方给出的sdk demo(java)版进行改造,改造成一个spring boot程序,然后部署在docker。目前运行状态尚算良好。

二、海康CVR简介

它有SDK可以进行二次开发,并提供了java、c#等语言的demo。

三、工作原理

我根据java版的demo进行改造,改造成一个Spring Boot项目,称为历史回放服务,接收前端和后端的请求,然后将视频流返回给前端,成为前端、后端与CVR的连结器。当然,对前端和后端来说,它并不知道CVR的存在,视频流都来自于后端。

下面是历史回放服务的功能介绍。

四、从CVR下载视频流

前端指定播放历史视频流的开始时间,历史回放服务接收到前端请求后,向CVR下载视频流,长度限制为1分钟。视频流下载以后,保存成一个临时的视频文件,然后使用ffmpeg解码,推给前端。

为什么要使用ffmpeg呢?原因是前端使用mpegts.js,播放的是flv格式的视频流。因此从CVR拿到视频流涉及到一个转码的问题。转码异常复杂,别想着自己写代码来完成,我只能依靠ffmpeg。

然后为什么视频流从CVR下载以后,要先保存成文件呢?主要是使用ffmpeg边接收边转码的话,总是报溢出的问题。搞来搞去,我也不懂,就先保存成文件,然后ffmpeg处理文件。另外保存成文件也有个好处,就是如果用户觉得这段视频有价值,按下保存按钮,那系统就可以将这份文件拷贝一份,放到正式文件目录,以后用户可以随时播放了。

前面说到,前端过来的播放历史视频流的请求,只包含了开始时间,没有结束时间。因为不需要,系统强制限定长度为1分钟。1分钟足够长了,超过1分钟没有意义,保存下来的文件也很大。

向CVR下载视频流的方式,有个巨大无比的坑,让我浪费了许多时间。官方提供了两种方式进行回放,一个是按时间,一个是按文件。我一开始想当然地按时间进行播放,发现下载的视频流长度是随机的,有时是20秒,有时是2、3秒甚至是0秒,毫无规律可言,并且达不到指定的长度。这让人抓狂。后来使用按文件回放就正常了。事实上按文件回放,也是要指定时间的,而且我们并不需要指定什么文件位置,调用起来跟按时间回放一样简单。

五、播放视频流历史文件

其实就是用ffmpeg处理保存下来的正式视频文件(见从CVR下载视频流),然后推给前端就好了,更简单。播放历史回放视频,通常要等待几秒钟,播放历史文件,一下子就有画面了。

六、部署

1、docker

我们应用服务器是linux,很自然将历史回放服务部署在docker。由于使用了ffmpeg,因此构建镜像时,需要把ffmpeg打包在内。Dockerfile内容如下:

1)Dockerfile

bash 复制代码
FROM eclipse-temurin:17-jre-jammy

# 设置非交互式安装
ENV DEBIAN_FRONTEND=noninteractive

# 合并所有依赖安装,并单独处理 Ubuntu 22.04 缺失的 libssl1.1
RUN apt-get update && \
    apt-get install -y --no-install-recommends wget && \
    # 从 Ubuntu 官方安全源下载并安装 libssl1.1
    wget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.24_amd64.deb && \
    dpkg -i libssl1.1_1.1.1f-1ubuntu2.24_amd64.deb && \
    rm libssl1.1_1.1.1f-1ubuntu2.24_amd64.deb && \
    # 安装 FFmpeg 和海康 SDK 其他所需的系统依赖库
    apt-get install -y --no-install-recommends \
        ffmpeg \
        libstdc++6 \
        libgl1 \
        libglib2.0-0 \
        libx11-6 \
        libxext6 \
        ca-certificates \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /usr/data

ENTRYPOINT ["sh", "/usr/data/run.sh"]

我一般是针对批处理文件构建镜像,然后在批处理文件里运行jar包。好处是更新程序,不需要重新构建镜像,只需覆盖jar包就好。详见拙作《docker部署可执行jar包》

2)run.sh

比如本Dockerfile最后一行写的run.sh,内容如下:

bash 复制代码
#!/bin/bash
# run.sh

# 设置系统动态库搜索路径(关键!)
export LD_LIBRARY_PATH=/usr/data/lib:$LD_LIBRARY_PATH

# 可选:进入工作目录(确保 user.dir 正确)
cd /usr/data

# 启动 Java 应用
exec java \
  -Djava.awt.headless=true \
  -Duser.timezone=Asia/Shanghai \
  -Djna.library.path=/usr/data/lib \
  -jar hkCvr-1.0.0.jar \
  --spring.config.location=/usr/data/application.yml

3)构建镜像

4)创建容器:

bash 复制代码
sudo docker run --restart=always -d -it --name=hkcvr \
--network docker-fixed-net \
--ip 172.18.0.14 \
 -p 8084:8084 \
 -v /home/work/docker/hkcvr:/usr/data \
 -v /home/work/docker/hkcvr/lib:/usr/data/lib \
 -v /home/work/docker/hkcvr/sdkLog:/usr/data/sdkLog \
 -v /data/hkcvr/temp_streams:/usr/temp_streams \
 -v /data/hkcvr/records:/usr/records \
 --workdir /usr/data \
 hkcvr:1.0.0

2、库

本服务依赖官方提供的各种库。因此构建时,需要把这些库都带上。

1)代码结构

首先,代码结构是这样的:

用瘦模式打包jar包,打包出来的jar包只有几百K。pom.xml长这样:

2)pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.monkey</groupId>
    <artifactId>hkCvr</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.boot.version>2.7.0</spring.boot.version>
    </properties>
    
    <!-- 依赖 -->
    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
    
        <!-- Spring Boot WebSocket -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
    
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </dependency>
    
        <!-- JNA -->
        <dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna</artifactId>
            <version>5.12.1</version>
        </dependency>
        
        <!-- JNA Platform (包含 Windows API) -->
        <dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna-platform</artifactId>
            <version>5.12.1</version>
        </dependency>
        
        <!-- 海康 SDK 示例包 (本地 JAR) -->
        <dependency>
            <groupId>com.sun.jna</groupId>
            <artifactId>examples</artifactId>
            <version>3.0.9</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/examples.jar</systemPath>
        </dependency>
        
        <!-- Apache Commons Exec - 用于执行 FFmpeg 进程 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-exec</artifactId>
            <version>1.3</version>
        </dependency>
        
        <!-- Apache HttpClient - HTTP状态码 -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.4.15</version>
        </dependency>
    </dependencies>

    <build>
        <sourceDirectory>src</sourceDirectory>
        <outputDirectory>classes</outputDirectory>
    
        <plugins>
            <!-- 编译插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
    
            <!-- 复制lib目录到target,与JAR同级(用于Docker部署) -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.3.1</version>
                <executions>
                    <execution>
                        <id>copy-lib-to-target</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${project.basedir}/lib</directory>
                                    <includes>
                                        <include>**/*</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <!-- Spring Boot Maven 插件 - 禁用repackage -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring.boot.version}</version>
                <configuration>
                    <skip>true</skip>
                </configuration>
            </plugin>

            <!-- 复制依赖插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.6.0</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                            <excludeTransitive>false</excludeTransitive>
                            <stripVersion>false</stripVersion>
                            <includeSystemScope>true</includeSystemScope>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <!-- JAR插件 - 添加Main-Class和Class-Path -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.NetSDKDemo.CvrApplication</mainClass>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                            <useUniqueVersions>false</useUniqueVersions>
                        </manifest>
                        <manifestEntries>
                            <Class-Path>lib/examples-3.0.9.jar</Class-Path>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3)target目录

存放打包文件的target目录:

4)加载库

官方给的库,分为windows和linux。我开发环境是windows,部署环境是linux。因此lib目录下,两种库都有。代码中做了适配:

java 复制代码
    private void initSDK() {
        try {
            String strDllPath;

            // 根据操作系统选择库文件
            if (osSelect.isLinux()) {
                strDllPath = System.getProperty("user.dir") + "/lib/libhcnetsdk.so";
            } else {
                strDllPath = System.getProperty("user.dir") + "\\lib\\HCNetSDK.dll";
            }

            hCNetSDK = (HCNetSDK) com.sun.jna.Native.loadLibrary(strDllPath, HCNetSDK.class);

            // Linux 系统需要额外配置组件库路径
            if (osSelect.isLinux()) {
                System.out.println("检测到 Linux 系统,配置 SDK 组件库路径...");

                // 设置OpenSSL 库路径
                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(HCNetSDK.NET_SDK_INIT_CFG_LIBEAY_PATH, ptrByteArray1.getPointer());
                System.arraycopy(strPath2.getBytes(), 0, ptrByteArray2.byValue, 0, strPath2.length());
                ptrByteArray2.write();
                hCNetSDK.NET_DVR_SetSDKInitCfg(HCNetSDK.NET_SDK_INIT_CFG_SSLEAY_PATH, ptrByteArray2.getPointer());

                // 设置 SDK 组件库路径
                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(HCNetSDK.NET_SDK_INIT_CFG_SDK_PATH, struComPath.getPointer());

                System.out.println("Linux 系统 SDK 组件库路径配置完成");
            }

            if (!hCNetSDK.NET_DVR_Init()) {
                System.err.println("SDK 初始化失败,错误码:" + hCNetSDK.NET_DVR_GetLastError());
                return;
            }

            System.out.println("SDK 初始化成功");
            loginDevice(deviceIp, devicePort, deviceUsername, devicePassword);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

附录

NVR (Network Video Recorder) - 网络硬盘录像机

它是现代视频监控系统的核心设备之一。NVR 通过网络接收 IP 摄像机(IPC)传输过来的数字视频流,并进行集中的存储、管理、解码显示和智能分析。它通常是一台嵌入式的"一体机",部署灵活,是目前中小规模监控中最主流的选择。

CVR (Central Video Recorder) - 中心级视频网络存储设备

它是一种安防视频监控专用的企业级存储技术。CVR 的核心特点是支持"视频流直写"技术,即前端摄像机的视频数据可以直接写入 CVR 存储设备中,省去了中间的存储服务器。它专为海量视频数据的安全存储和高并发读写而生,适合智慧城市、机场、地铁等大型项目。

也就是说,NVR包含了存储和播放,适合中小规模场景;CVR侧重存储,适合大型项目,当然它也能播放。

选型建议

选 NVR:如果你的监控需求是普通的家庭安防、小型超市、或者一栋写字楼(比如摄像头数量在 100 路以内),追求性价比、安装简单且需要手机远程查看,NVR 是最成熟且经济的选择。

选 CVR:如果你面对的是智慧城市、机场、地铁站或大型工业园区等项目,摄像头数量成百上千,要求 7×24 小时不间断录像,且对数据的安全性、存储性能和后期扩容有极高的要求,那么必须选择 CVR。

相关推荐
MR.欻21 小时前
ZLMediaKit 源码分析(四):RTP/RTCP 协议栈实现分析
c++·人工智能·vscode·ffmpeg·音视频
晓py1 天前
音视频基础概念入门_FFmpeg学习笔记
学习·ffmpeg·音视频
daqinzl1 天前
Mpegts.js+FFmpeg+WebSocket+Node实时视频流实现方案
websocket·ffmpeg·node·mpegts.js
qq_369224332 天前
打开剪辑/直播/播放器提示ffmpeg.dll丢失?专属场景修复方法汇总
ffmpeg·dll·dll修复·dll错误
愿天垂怜2 天前
【C++脚手架】ffmpeg 库的介绍与使用
linux·服务器·开发语言·c++·ide·git·ffmpeg
韶博雅2 天前
oracle优化用到的sql
sql·oracle·ffmpeg
kkoral3 天前
视频二进制流RAW文件转图片完整教程
运维·python·ffmpeg·音视频
weixin_421607554 天前
短剧出海的AI 视频翻译技术方案:从单集打样到批量交付的工程全链路
人工智能·ffmpeg
_oP_i4 天前
FFmpeg 安装
ffmpeg