状态: 用户已确认直接实现;所有变更保留在当前本地工作区,不提交。
让本机浏览器以 HLS 播放海康 NVR 的历史录像,并由服务端控制同一个海康 SDK 回放句柄,支持:
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 流。
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 地址和失败原因(如有)。状态为 STARTING、PLAYING、PAUSED、STOPPED 或 FAILED。
POST /hik/nvr/playback/hls/sessions/{sessionId}/controls
{
"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 会话时使用 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 倍。
127.0.0.1:18081 -> 80(HLS/HTTP)和 127.0.0.1:1935 -> 1935(FFmpeg RTMP 推流);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;502,返回组件状态但不泄露 NVR 密码;502,保留海康错误码;404;跨越多个不连续录像段:409;非法速度或跳转时间:400;会话不存在:404;127.0.0.1:18081 返回 m3u8。