For agentic workers: REQUIRED SUB-SKILL: Use
executing-plansto implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: 在当前 Spring 服务中提供由海康 SDK 实际控制的 HLS 历史回放,支持跳转、暂停/恢复和 1/2/4/8 倍速。
Architecture: 服务通过 NET_DVR_PlayBackByTime_V40 建立唯一可控回放句柄,从 NET_DVR_SetPlayBackESCallBack 获取 H.264 裸帧,异步写入本机 FFmpeg 标准输入,FFmpeg 推 RTMP 到本机 Docker ZLMediaKit,ZLM 输出 HLS。会话控制直接调用同一 SDK 句柄;定位时切换到新的 HLS 流代次。
Tech Stack: Java 8、Spring Boot 2.3、JNA/HCNetSDK、FFmpeg、Docker Desktop、ZLMediaKit、JUnit 4、Mockito。
User constraint: 在当前 master 本地直接修改,不创建 worktree、不暂存、不提交。
Files:
Create: deploy/zlmediakit/docker-compose.yml
Create: deploy/zlmediakit/config.ini
Modify: .gitignore
External: C:\tools\ffmpeg\bin\ffmpeg.exe
写入 compose,唯一暴露端口为:
services:
zlmediakit:
image: zlmediakit/zlmediakit:master
container_name: inspect-nvr-zlmediakit
restart: unless-stopped
ports:
- "127.0.0.1:18081:80"
- "127.0.0.1:1935:1935"
Run: docker info --format '{{.ServerVersion}}'
Expected: Docker server version is printed; if it is unavailable, start Docker Desktop and retry.
从 Windows FFmpeg 发行包解压到 C:\tools\ffmpeg,验证:
Run: C:\tools\ffmpeg\bin\ffmpeg.exe -version
Expected: command prints an FFmpeg version and exits with code 0.
config.ini 必须至少包含:
[http]
port=80
allow_cross_domains=1
[protocol]
enable_hls=1
enable_audio=0
hls_demand=0
[hls]
segDur=2
segNum=3
segRetain=5
compose 必须将本文件挂载为 /opt/media/conf/config.ini:ro;该镜像从 /opt/media/bin 启动 MediaServer 并以 ../conf/config.ini 作为配置文件。
Run: docker compose -f deploy/zlmediakit/docker-compose.yml up -d
Expected: inspect-nvr-zlmediakit is running and Test-NetConnection 127.0.0.1 -Port 18081 plus Test-NetConnection 127.0.0.1 -Port 1935 return TcpTestSucceeded : True.
在 .gitignore 增加:
/deploy/zlmediakit/.env
/deploy/zlmediakit/log/
/deploy/zlmediakit/www/
验证 git status --short 中不出现本机 .env、日志或 HLS 切片。
Files:
Modify: src/main/java/com/inspect/nvr/hikVision/utils/jna/HCNetSDK.java
Create: src/test/java/com/inspect/nvr/hik/service/HikSdkPlaybackGatewayTest.java
@Test
public void exposesAbsoluteSeekControlAndStructuredPacketLayout() {
assertEquals(26, HCNetSDK.NET_DVR_PLAYSETTIME);
assertTrue(new HCNetSDK.NET_DVR_PACKET_INFO_EX().size() > 80);
}
Run: mvn -Dtest=HikSdkPlaybackGatewayTest#exposesAbsoluteSeekControlAndStructuredPacketLayout test
Expected: compilation failure because NET_DVR_PLAYSETTIME is absent, or assertion failure because the packet structure is represented as a JNA union.
在控制常量区加入:
public static final int NET_DVR_PLAYSETTIME = 26;
将 NET_DVR_PACKET_INFO_EX 从 Union 改为 HCStructure,保留字段顺序和所有字段;它是 SDK 回调返回的顺序结构体,不是 union。
Run: mvn -Dtest=HikSdkPlaybackGatewayTest#exposesAbsoluteSeekControlAndStructuredPacketLayout test
Expected: PASS.
Files:
Create: src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackHlsSession.java
Create: src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackHlsControlRequest.java
Create: src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackHlsControlAction.java
Create: src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackHlsStatus.java
Create: src/main/java/com/inspect/nvr/hik/service/HikPlaybackHlsUrlBuilder.java
Create: src/test/java/com/inspect/nvr/hik/service/HikPlaybackHlsUrlBuilderTest.java
Create: src/test/java/com/inspect/nvr/hik/domain/HikNvrPlaybackHlsControlRequestTest.java
@Test
public void buildsLocalHlsUrlForStreamGeneration() {
assertEquals("http://127.0.0.1:18081/playback/a1-2/hls.m3u8",
builder.hlsUrl("a1", 2));
}
@Test
public void rejectsUnsupportedPlaybackSpeed() {
assertFalse(HikNvrPlaybackHlsControlRequest.isSupportedSpeed(3));
}
Run: mvn -Dtest=HikPlaybackHlsUrlBuilderTest,HikNvrPlaybackHlsControlRequestTest test
Expected: compilation failure because the classes do not exist.
HikNvrPlaybackHlsControlAction 只包含 PAUSE、RESUME、SET_SPEED、SEEK、STOP。isSupportedSpeed 只接受 1、2、4、8。HLS 地址固定为:
return httpBaseUrl + "/playback/" + sessionId + "-" + generation + "/hls.m3u8";
Run: mvn -Dtest=HikPlaybackHlsUrlBuilderTest,HikNvrPlaybackHlsControlRequestTest test
Expected: PASS.
Files:
Create: src/main/java/com/inspect/nvr/hik/service/HikPlaybackFrame.java
Create: src/main/java/com/inspect/nvr/hik/service/HikPlaybackFrameSink.java
Create: src/main/java/com/inspect/nvr/hik/service/HikFfmpegProcessFactory.java
Create: src/main/java/com/inspect/nvr/hik/service/HikFfmpegH264Publisher.java
Create: src/test/java/com/inspect/nvr/hik/service/HikFfmpegH264PublisherTest.java
@Test
public void usesPipeInputAndPublishesToSessionRtmpUrl() throws Exception {
publisher.start();
assertThat(processFactory.command()).contains("pipe:0");
assertThat(processFactory.command()).contains("rtmp://127.0.0.1:1935/playback/a1-1");
}
@Test
public void preservesIFrameWhenQueueIsFull() {
// fill bounded queue with B/P frames, then offer an I frame
assertTrue(publisher.offer(frameOfType(1)));
}
Run: mvn -Dtest=HikFfmpegH264PublisherTest test
Expected: compilation failure because the publisher does not exist.
FFmpeg 参数必须包含:
-hide_banner -loglevel warning -fflags +genpts -use_wallclock_as_timestamps 1
-f h264 -i pipe:0 -c:v copy -an -f flv rtmp://127.0.0.1:1935/playback/{streamId}
JNA 回调只调用 offer;写线程把 byte[] 写入 process.getOutputStream()。关闭时停止接收、关闭 stdin、等待进程有限时间后强制销毁。
Run: mvn -Dtest=HikFfmpegH264PublisherTest test
Expected: PASS.
Files:
Create: src/main/java/com/inspect/nvr/hik/service/HikSdkPlaybackGateway.java
Create: src/main/java/com/inspect/nvr/hik/service/HikSdkPlaybackHandle.java
Create: src/main/java/com/inspect/nvr/hik/service/HikSdkPlaybackGatewayImpl.java
Modify: src/main/java/com/inspect/nvr/hik/exception/HikNvrPlaybackException.java
Modify: src/test/java/com/inspect/nvr/hik/service/HikSdkPlaybackGatewayTest.java
@Test
public void startsSdkPlaybackWithOriginalSdkChannelAndActualRange() {
gateway.open(request, start, end, sink);
verify(sdk).NET_DVR_PlayBackByTime_V40(eq(7), vodCaptor.capture());
assertEquals(33, vodCaptor.getValue().struIDInfo.dwChannel);
verify(sdk).NET_DVR_PlayBackControl_V40(eq(91), eq(HCNetSDK.NET_DVR_PLAYSTART),
isNull(), eq(0), isNull(), any(IntByReference.class));
}
@Test
public void mapsSpeedEightToNormalThenThreeFastCommands() {
handle.setSpeed(8);
verifyControl(HCNetSDK.NET_DVR_PLAYNORMAL, 1);
verifyControl(HCNetSDK.NET_DVR_PLAYFAST, 3);
}
@Test
public void seeksUsingNetDvrTimePointer() {
handle.seek(LocalDateTime.of(2026, 7, 13, 12, 45));
verify(sdk).NET_DVR_PlayBackControl_V40(eq(91), eq(26), any(Pointer.class),
eq(new HCNetSDK.NET_DVR_TIME().size()), isNull(), any(IntByReference.class));
}
Run: mvn -Dtest=HikSdkPlaybackGatewayTest test
Expected: compilation failure because the gateway and handle do not exist.
open 必须:登录缓存中获取 userId;填充 NET_DVR_VOD_PARA.dwSize、struIDInfo.dwSize、实际 SDK 通道、北京时间起止时间、主/子码流;调用 NET_DVR_PlayBackByTime_V40;保存回调强引用;注册 ES 回调;最后调用 NET_DVR_PLAYSTART。
回调必须复制 pPacketBuffer 中 dwPacketSize 字节;只将视频 I/B/P 包写入当前 AtomicReference<HikPlaybackFrameSink>,忽略文件头、音频和私有包。close 必须先断开 sink,再 NET_DVR_StopPlayBack。
Run: mvn -Dtest=HikSdkPlaybackGatewayTest test
Expected: PASS.
Files:
Create: src/main/java/com/inspect/nvr/hik/service/HikNvrPlaybackHlsService.java
Create: src/main/java/com/inspect/nvr/hik/service/HikNvrPlaybackHlsSessionRegistry.java
Create: src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackHlsServiceTest.java
@Test
public void createsSessionFromOneActualRecordingRangeWithoutReturningRtspUrl() {
HikNvrPlaybackHlsSession session = service.create(request);
assertEquals(HikNvrPlaybackHlsStatus.PLAYING, session.getStatus());
assertFalse(serialized(session).contains("rtsp://"));
}
@Test
public void seekCreatesNextGenerationAndReturnsNewHlsUrl() {
HikNvrPlaybackHlsSession moved = service.control(id, seek("2026-07-13 12:45:00"));
assertEquals(2, moved.getGeneration());
assertTrue(moved.getHlsUrl().contains("-2/hls.m3u8"));
}
Run: mvn -Dtest=HikNvrPlaybackHlsServiceTest test
Expected: compilation failure because the service and registry do not exist.
创建流程:调用已有 HikNvrPlaybackService.search;空记录抛 404;多个记录抛 409;将唯一实际区间解析为 LocalDateTime;创建 publisher 和 SDK handle;登记会话。
定位流程:暂停旧句柄,停止旧 publisher,启动新代次 publisher,切换句柄 sink,调用 SDK PLAYSETTIME;失败时关闭旧句柄并以目标时间重建;恢复目标速度;更新 HLS URL 与状态。自动清理使用当前已有 @EnableScheduling,过期时调用同一个 stop 路径。
Run: mvn -Dtest=HikNvrPlaybackHlsServiceTest test
Expected: PASS.
Files:
Create: src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackHlsController.java
Modify: src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackExceptionHandler.java
Create: src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackHlsControllerTest.java
Modify: src/test/java/com/inspect/nvr/config/ApiDocumentationCoverageTest.java
mockMvc.perform(post("/hik/nvr/playback/hls/sessions")
.contentType(MediaType.APPLICATION_JSON).content(REQUEST_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.hlsUrl").value("http://127.0.0.1:18081/playback/a1-1/hls.m3u8"))
.andExpect(jsonPath("$.rtspUrl").doesNotExist());
mockMvc.perform(post("/hik/nvr/playback/hls/sessions/a1/controls")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"action\":\"SET_SPEED\",\"speed\":8}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.speed").value(8));
Run: mvn -Dtest=HikNvrPlaybackHlsControllerTest test
Expected: compilation failure because controller does not exist.
暴露:
POST /hik/nvr/playback/hls/sessions
GET /hik/nvr/playback/hls/sessions/{sessionId}
POST /hik/nvr/playback/hls/sessions/{sessionId}/controls
DELETE /hik/nvr/playback/hls/sessions/{sessionId}
所有请求、响应、枚举加中文 @Schema、@Operation 和错误码说明。HikNvrPlaybackException 增加 notFound、conflict 和 mediaFailure 工厂方法,统一由既有 ControllerAdvice 返回。
Run: mvn -Dtest=HikNvrPlaybackHlsControllerTest,ApiDocumentationCoverageTest test
Expected: PASS.
Files:
Modify: docs/superpowers/specs/2026-07-13-hik-nvr-sdk-controlled-hls-design.md(仅记录实际验证结论)
Run: mvn test -DskipTests=false
Expected: all tests pass with zero failures and zero errors.
Run: set HIK_NVR_PLAYBACK_HLS_FFMPEG_PATH, then start Spring on 18080.
Expected: application starts without affecting the user-owned process on port 8080.
Run: call POST /hik/nvr/playback/hls/sessions with a known continuous Beijing-time recording interval.
Expected: status=PLAYING, no RTSP credential in response, and hlsUrl returns an m3u8 playlist after ZLM receives the stream.
依次调用 PAUSE、RESUME、SET_SPEED 2/4/8、SEEK 到区间内时间,并在每一步检查 SDK 控制返回值、会话状态与 HLS URL/playlist;定位后确认 generation 增加且前端必须加载返回的新 URL。
Run: DELETE /hik/nvr/playback/hls/sessions/{sessionId}.
Expected: SDK NET_DVR_StopPlayBack 被调用、FFmpeg 进程退出、ZLM 不再持续接收 RTMP,查询该会话返回 404。
Run: git status --short and git diff --cached --name-status.
Expected: no staging or commit action by this work; preserve all pre-existing user changes.