You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

6.6 KiB

海康 NVR SDK 受控 HLS 回放设计

状态: 用户已确认直接实现;所有变更保留在当前本地工作区,不提交。

目标

让本机浏览器以 HLS 播放海康 NVR 的历史录像,并由服务端控制同一个海康 SDK 回放句柄,支持:

  • 指定北京时间开始、结束时间创建回放;
  • 暂停、恢复、正常速度;
  • 真正调用 NVR 回放控制实现 2 倍、4 倍、8 倍;
  • 跳转到任意录像时间点;
  • 不向浏览器返回 NVR RTSP 地址、NVR 密码或媒体服务控制密钥。

约束与结论

ZLMediaKit 自行拉取 RTSP 时会创建一条独立的 NVR RTSP 会话,无法让 Spring 服务通过 NET_DVR_PlayBackControl_V40 控制那条会话。因此不能使用“ZLM 直接拉 NVR RTSP”作为可控回放链路。

本实现采用 SDK 回放句柄作为唯一的控制源:SDK 的裸 H.264 帧回调写入本机 FFmpeg,FFmpeg 推 RTMP 给本机 ZLMediaKit,ZLMediaKit 再生成 HLS。这样暂停、快放、定位都作用于实际向浏览器输送帧数据的回放句柄。

当前已验证的 NVR 历史回放是 H.264。第一版只支持标准 H.264 视频;H.265 或私有/加密码流会在建立会话时失败并返回明确错误,不伪装成可播放 HLS。

架构

flowchart LR
    Browser["本机前端浏览器"] -->|"HLS m3u8"| ZLM["ZLMediaKit Docker\n127.0.0.1:18081"]
    App["Spring 服务 :8080"] -->|"SDK 登录、回放、控制"| NVR["海康 NVR :8000"]
    NVR -->|"H.264 裸帧回调"| App
    App -->|"stdin 原始 H.264"| FFmpeg["本机 FFmpeg"]
    FFmpeg -->|"RTMP"| ZLM
    Browser -->|"会话/控制 API"| App

每个 HLS 会话拥有一套独立资源:海康 SDK 回放句柄、持久化 JNA 回调对象、帧队列与写入线程、FFmpeg 进程,以及 ZLM 的 playback/{streamId} RTMP 流。

API

创建会话

POST /hik/nvr/playback/hls/sessions

请求体复用 HikNvrPlaybackRequest。服务先调用已有录像查询逻辑,得到实际连续录像区间;无录像返回 404,请求范围内存在多个不连续区间时返回 409 并要求前端先按一个连续区间重新发起请求。

成功响应:

{
  "sessionId": "4c3b0cf0-2a92-4ddc-a36a-7e5f31b01f10",
  "status": "PLAYING",
  "speed": 1,
  "actualTimeRange": ["20260713123000", "20260713130000"],
  "hlsUrl": "http://127.0.0.1:18081/playback/4c3b0cf02a924ddca36a7e5f31b01f10-1/hls.m3u8"
}

前端通过 hls.js(Chrome/Edge)或原生 HLS(Safari)加载 hlsUrl。前端不能再对 video.playbackRate 做 2/4/8 倍设置;倍速必须调用后端控制接口,避免把客户端倍速与 NVR 倍速叠加。

查询会话

GET /hik/nvr/playback/hls/sessions/{sessionId}

返回会话状态、当前速度、实际录像区间、当前 HLS 地址和失败原因(如有)。状态为 STARTINGPLAYINGPAUSEDSTOPPEDFAILED

控制会话

POST /hik/nvr/playback/hls/sessions/{sessionId}/controls

{
  "action": "SET_SPEED",
  "speed": 4
}

可用动作:

  • PAUSE:调用 NET_DVR_PLAYPAUSE
  • RESUME:调用 NET_DVR_PLAYRESTART
  • SET_SPEED:仅接受 1248。先调用 NET_DVR_PLAYNORMAL,再按指数次数调用 NET_DVR_PLAYFAST
  • SEEK:请求 targetTime(固定 yyyy-MM-dd HH:mm:ss,北京时间)。先校验目标时间位于当前实际录像区间,再调用 NET_DVR_PLAYSETTIME(控制码 26)。为避免旧 HLS 缓冲与新 GOP 混合,控制成功后会切换到新的流代次和新的 HLS 地址;若设备不支持绝对定位,则停止旧回放并以目标时间重建 SDK 回放句柄作为兼容回退;
  • STOP:停止 SDK 回放、关闭帧桥和 FFmpeg,并使会话进入 STOPPED

SEEK 的响应始终带回新的 hlsUrl。前端收到后销毁旧 hls.js 实例并加载新地址。

销毁会话

DELETE /hik/nvr/playback/hls/sessions/{sessionId}

STOP 等价并从会话表删除。服务关闭时也会关闭自己创建的所有会话。定时清理会在“请求录像时长 + 10 分钟”后关闭仍未结束的会话,防止孤儿 SDK 句柄和 FFmpeg 进程。

SDK、帧桥与倍速

创建 SDK 会话时使用 NET_DVR_PlayBackByTime_V40,注册 NET_DVR_SetPlayBackESCallBack,再调用 NET_DVR_PLAYSTART。回调每次提供一帧裸码流及帧类型、时间戳。JNA 回调中只复制帧数据并放入有界队列,绝不执行网络、FFmpeg 或 SDK 控制调用。

单独写入线程将 H.264 帧写入 FFmpeg 的标准输入。FFmpeg 不使用 -re,并基于接收时间生成时间戳后推送 RTMP,因此 NVR SDK 的快放输出会以更短的媒体时间进入 ZLM/HLS。队列压力时优先丢弃 B/P 帧,不丢 I 帧;连续无法写入则会停止会话并标记为 FAILED

海康 SDK 中的 NET_DVR_PLAYFAST 每调用一次将当前速度乘以 2;本设计通过先恢复 1 倍再调用 1、2、3 次,保证目标值准确为 2、4、8 倍。

本机媒体环境

  • Docker Desktop 启动后拉取 ZLMediaKit 镜像;
  • ZLM 仅映射 127.0.0.1:18081 -> 80(HLS/HTTP)和 127.0.0.1:1935 -> 1935(FFmpeg RTMP 推流);
  • ZLM 显式启用 HLS、2 秒切片、跨域响应;
  • FFmpeg 安装到 C:\tools\ffmpeg,应用从 HIK_NVR_HLS_FFMPEG_PATH 读取 ffmpeg.exe 路径;
  • 应用从 HIK_NVR_HLS_ZLM_RTMP_BASE_URLHIK_NVR_HLS_ZLM_HTTP_BASE_URL 读取 ZLM 地址,默认分别为 rtmp://127.0.0.1:1935http://127.0.0.1:18081
  • 本地 ZLM 配置和环境文件不含在 Git 提交中,且不开放到局域网。

错误处理与安全

  • ZLM/FFmpeg 未就绪:502,返回组件状态但不泄露 NVR 密码;
  • SDK 登录、创建回放、注册回调、控制失败:502,保留海康错误码;
  • 无录像:404;跨越多个不连续录像段:409;非法速度或跳转时间:400;会话不存在:404
  • 日志和异常响应统一脱敏 RTSP 用户信息;
  • NVR 密码只存在于入站请求、SDK 登录和内存中,浏览器响应不返回密码;
  • FFmpeg 命令的输入来自 SDK 标准输入,不把 RTSP 密码放到操作系统进程命令行。

验证标准

  1. 单元测试验证控制码、速度映射、SDK VOD 参数、HLS/RTMP URL、队列背压和资源清理。
  2. MVC 测试验证创建、查询、暂停、恢复、1/2/4/8 倍、定位、停止和异常响应。
  3. Docker 运行 ZLM 后,服务可创建 HLS 会话并在 127.0.0.1:18081 返回 m3u8。
  4. 使用当前 NVR 的已知录像区间手工验证:1 倍播放、暂停/恢复、2/4/8 倍切换、跳转到指定时间并重新加载返回的 HLS 地址。