# 海康 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。 ## 架构 ```mermaid 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` 并要求前端先按一个连续区间重新发起请求。 成功响应: ```json { "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 地址和失败原因(如有)。状态为 `STARTING`、`PLAYING`、`PAUSED`、`STOPPED` 或 `FAILED`。 ### 控制会话 `POST /hik/nvr/playback/hls/sessions/{sessionId}/controls` ```json { "action": "SET_SPEED", "speed": 4 } ``` 可用动作: - `PAUSE`:调用 `NET_DVR_PLAYPAUSE`; - `RESUME`:调用 `NET_DVR_PLAYRESTART`; - `SET_SPEED`:仅接受 `1`、`2`、`4`、`8`。先调用 `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_URL` 与 `HIK_NVR_HLS_ZLM_HTTP_BASE_URL` 读取 ZLM 地址,默认分别为 `rtmp://127.0.0.1:1935` 和 `http://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 地址。