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.
 
 

14 KiB

Hikvision NVR Playback Search Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a documented HTTP endpoint that queries a Hikvision NVR for actual recording coverage and returns Beijing-time timestamp ranges plus credential-bearing RTSP playback URLs.

Architecture: A controller under com.inspect.nvr.hik delegates to a focused service. The service reuses HikLoginService, queries recordings with NET_DVR_FindFile_V40/NET_DVR_FindNextFile_V40, clips and merges actual file intervals, and delegates URL construction to a pure HikPlaybackUrlBuilder. Controller advice converts validation failures to HTTP 400 and device/SDK failures to HTTP 502.

Tech Stack: Java 8, Spring Boot 2.3.4, Hikvision HCNetSDK through JNA, Lombok, springdoc-openapi 1.8.0, JUnit 4, Mockito, Spring MockMvc.

Execution constraint: Keep all changes local. Do not stage or commit any file.


File map

Create:

  • src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackRequest.java — HTTP input and Swagger field contract.
  • src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackRecord.java — one actual continuous range and its RTSP URL.
  • src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackResponse.java — successful search response.
  • src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackErrorResponse.java — parameter or SDK error response.
  • src/main/java/com/inspect/nvr/hik/exception/HikNvrPlaybackException.java — status and SDK error carrier.
  • src/main/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilder.java — pure Beijing-to-UTC RTSP URL builder.
  • src/main/java/com/inspect/nvr/hik/service/HikNvrPlaybackService.java — validation, SDK query and interval processing.
  • src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackController.javaPOST /hik/nvr/playback/search.
  • src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackExceptionHandler.java — scoped HTTP error mapping.
  • src/test/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilderTest.java — URL and timestamp tests.
  • src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackServiceTest.java — SDK state and interval tests.
  • src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackControllerTest.java — HTTP response tests.

Modify:

  • src/test/java/com/inspect/nvr/config/ApiDocumentationCoverageTest.java — include the new controller, handler and schemas.

Task 1: RTSP URL contract and domain models

Files:

  • Test: src/test/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilderTest.java

  • Create: src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackRequest.java

  • Create: src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackRecord.java

  • Create: src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackResponse.java

  • Create: src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackErrorResponse.java

  • Create: src/main/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilder.java

Create a JUnit 4 test that fixes the time-zone, track and credential-encoding behavior:

@Test
public void buildsMainStreamPlaybackUrlUsingUtcQueryTimes() {
    HikNvrPlaybackRequest request = request("admin", "p@ss word", 1, 1);

    String url = builder.build(
            request,
            LocalDateTime.of(2026, 7, 13, 12, 30, 0),
            LocalDateTime.of(2026, 7, 13, 13, 0, 0));

    assertEquals("rtsp://admin:p%40ss%20word@192.168.1.100:554/Streaming/tracks/101"
            + "?starttime=20260713T043000Z&endtime=20260713T050000Z", url);
}

@Test
public void buildsSubStreamTrackForNvrChannelThirtyThree() {
    HikNvrPlaybackRequest request = request("operator", "secret", 33, 2);

    String url = builder.build(
            request,
            LocalDateTime.of(2026, 7, 13, 12, 0, 0),
            LocalDateTime.of(2026, 7, 13, 13, 0, 0));

    assertTrue(url.contains("/Streaming/tracks/3302?"));
}

Run:

$env:JAVA_HOME='C:\Program Files\Java\jdk1.8.0_191'
mvn -Dtest=HikPlaybackUrlBuilderTest test

Expected: test compilation fails because HikPlaybackUrlBuilder and the new domain classes do not exist.

Use Lombok @Data, @Builder, @NoArgsConstructor, @AllArgsConstructor and class/field @Schema descriptions. The request fields are exactly:

private String nvrIp;
private Integer sdkPort;
private Integer rtspPort;
private Integer channel;
private Integer streamType;
private String username;
@Schema(description = "NVR登录密码", accessMode = Schema.AccessMode.WRITE_ONLY)
private String password;
private String startTime;
private String endTime;

HikNvrPlaybackRecord contains List<String> timeRange and String rtspUrl. HikNvrPlaybackResponse contains int code, String message, boolean hasRecord, and List<HikNvrPlaybackRecord> records, with a factory that always returns code 0 and a non-null list. HikNvrPlaybackErrorResponse contains int code and String message.

Implement HikPlaybackUrlBuilder.build(request, start, end) with:

private static final ZoneId BEIJING_ZONE = ZoneId.of("Asia/Shanghai");
private static final DateTimeFormatter RTSP_TIME =
        DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").withZone(ZoneOffset.UTC);

int trackId = request.getChannel() * 100 + request.getStreamType();
String utcStart = RTSP_TIME.format(start.atZone(BEIJING_ZONE).toInstant());
String utcEnd = RTSP_TIME.format(end.atZone(BEIJING_ZONE).toInstant());

Percent-encode every UTF-8 user-info byte except ASCII letters, digits, -, ., _, and ~. Assemble the URL without logging it.

Run mvn -Dtest=HikPlaybackUrlBuilderTest test.

Expected: both tests pass with zero failures.

Task 2: SDK recording search and interval merging

Files:

  • Test: src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackServiceTest.java

  • Create: src/main/java/com/inspect/nvr/hik/exception/HikNvrPlaybackException.java

  • Create: src/main/java/com/inspect/nvr/hik/service/HikNvrPlaybackService.java

Use Mockito to return a login ID of 7, find handle 88, and populate NET_DVR_FINDDATA_V40 outputs. The main behavior test must feed these file intervals:

2026-07-13 12:20:00 - 12:40:00
2026-07-13 12:30:00 - 12:50:00
2026-07-13 12:55:00 - 13:10:00

For a request from 12:00:00 to 13:00:00, assert exactly:

assertEquals(Arrays.asList("20260713122000", "20260713125000"),
        response.getRecords().get(0).getTimeRange());
assertEquals(Arrays.asList("20260713125500", "20260713130000"),
        response.getRecords().get(1).getTimeRange());
assertTrue(response.isHasRecord());
verify(hcNetSDK).NET_DVR_FindClose_V30(88);

Add separate tests that assert:

  • NET_DVR_FILE_NOFIND produces hasRecord=false and an empty list.

  • NET_DVR_ISFINDING followed by success is retried and returns data.

  • NET_DVR_FILE_EXCEPTION throws HikNvrPlaybackException with the value returned by NET_DVR_GetLastError and still closes handle 88.

  • an invalid end time is rejected before HikLoginService.login is invoked.

Run mvn -Dtest=HikNvrPlaybackServiceTest test.

Expected: test compilation fails because HikNvrPlaybackService and HikNvrPlaybackException do not exist.

HikNvrPlaybackService.search must:

LocalDateTime requestStart = parseBeijing(request.getStartTime());
LocalDateTime requestEnd = parseBeijing(request.getEndTime());
if (!requestEnd.isAfter(requestStart)) {
    throw HikNvrPlaybackException.badRequest("结束时间必须晚于开始时间");
}

Validate nonblank NVR IP, username and password; ports in 1..65535; channel greater than zero; stream type in {1, 2}; and reject URL delimiter characters in the NVR address. Build NvrInfo, call HikLoginService.login, and create a NET_DVR_FILECOND_V40 with:

condition.lChannel = request.getChannel();
condition.dwFileType = 0xff;
condition.dwIsLocked = 0xff;
condition.byFindType = 0;
condition.byQuickSearch = 0;
condition.byStreamType = request.getStreamType() == 1 ? (byte) 3 : (byte) 1;
condition.struStartTime = toSdkTime(requestStart);
condition.struStopTime = toSdkTime(requestEnd);
condition.write();

Call NET_DVR_FindFile_V40; a negative handle becomes an HTTP-502 exception with NET_DVR_GetLastError(). In a try/finally, loop over NET_DVR_FindNextFile_V40:

switch (status) {
    case HCNetSDK.NET_DVR_FILE_SUCCESS:
        rawRanges.add(rangeFrom(findData));
        findingAttempts = 0;
        break;
    case HCNetSDK.NET_DVR_ISFINDING:
        waitForSearch(++findingAttempts);
        break;
    case HCNetSDK.NET_DVR_FILE_NOFIND:
    case HCNetSDK.NET_DVR_NOMOREFILE:
        return buildResponse(request, requestStart, requestEnd, rawRanges);
    default:
        throw sdkFailure("查询海康NVR录像失败", hcNetSDK.NET_DVR_GetLastError());
}

Limit NET_DVR_ISFINDING to 100 attempts with a 50 ms wait, restore the interrupt flag when interrupted, and close with NET_DVR_FindClose_V30(findHandle) in finally.

For each returned interval, use max(fileStart, requestStart) and min(fileEnd, requestEnd), discard empty intervals, sort by start, and merge only when next.start <= current.end. Format response pairs using yyyyMMddHHmmss, and build one RTSP URL per merged interval.

Run:

mvn -Dtest=HikNvrPlaybackServiceTest,HikPlaybackUrlBuilderTest test

Expected: all service and URL tests pass; Mockito verifies the find handle is closed.

Task 3: HTTP endpoint, error mapping and Swagger coverage

Files:

  • Test: src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackControllerTest.java

  • Create: src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackController.java

  • Create: src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackExceptionHandler.java

  • Modify: src/test/java/com/inspect/nvr/config/ApiDocumentationCoverageTest.java

Configure standalone MockMvc with the controller and advice. Assert a valid POST returns status 200 and JSON paths:

andExpect(jsonPath("$.hasRecord").value(true))
andExpect(jsonPath("$.records[0].timeRange[0]").value("20260713123000"))
andExpect(jsonPath("$.records[0].timeRange[1]").value("20260713130000"))
andExpect(jsonPath("$.records[0].rtspUrl").value(expectedUrl));

Make the mocked service throw HikNvrPlaybackException.badRequest("结束时间必须晚于开始时间") and assert HTTP 400. Make it throw an SDK exception with code 7 and assert HTTP 502 plus $.code == 7.

Run mvn -Dtest=HikNvrPlaybackControllerTest test.

Expected: test compilation fails because the controller and handler do not exist.

Create a controller with:

@RestController
@RequestMapping("/hik/nvr/playback")
@Tag(name = "海康NVR回放", description = "查询海康NVR历史录像并生成RTSP回放地址")
public class HikNvrPlaybackController {
    @PostMapping("/search")
    public ResponseEntity<HikNvrPlaybackResponse> search(
            @io.swagger.v3.oas.annotations.parameters.RequestBody(
                    description = "海康NVR历史录像查询参数", required = true)
            @RequestBody HikNvrPlaybackRequest request) {
        return ResponseEntity.ok(playbackService.search(request));
    }
}

Add Chinese @Operation and @ApiResponses for 200, 400 and 502 with concrete response schemas. The advice must use @RestControllerAdvice(assignableTypes = HikNvrPlaybackController.class) and return HikNvrPlaybackErrorResponse without logging credentials or RTSP URLs.

Run mvn -Dtest=HikNvrPlaybackControllerTest test.

Expected: all controller tests pass.

Change the scan root to com.inspect.nvr, add com.inspect.nvr.hik.controller.HikNvrPlaybackController to expected controllers, change expected handler count from 35 to 36, and add the four new API models to API_MODEL_TYPES. Run:

mvn -Dtest=ApiDocumentationCoverageTest test

Expected before all schema annotations are complete: failure identifies any missing model or field description/example. Complete only the reported Swagger metadata until the test passes. Password must remain WRITE_ONLY with no example.

Run mvn -Dtest=ApiDocumentationCoverageTest test.

Expected: all coverage assertions pass with 8 controllers and 36 handler methods.

Task 4: Full verification and local handoff

Files:

  • Verify all files created or modified in Tasks 1–3.

Run:

$env:JAVA_HOME='C:\Program Files\Java\jdk1.8.0_191'
$env:Path="$env:JAVA_HOME\bin;$env:Path"
mvn test -DskipTests=false

Expected: build success with all existing and new tests passing.

Run:

git diff --check -- src/main/java/com/inspect/nvr/hik src/test/java/com/inspect/nvr/hik src/test/java/com/inspect/nvr/config/ApiDocumentationCoverageTest.java
rg -n 'log\..*(password|rtspUrl)|toString\(\).*HikNvrPlaybackRequest' src/main/java/com/inspect/nvr/hik

Expected: git diff --check exits 0 and the sensitive logging search returns no matches.

Capture git diff --cached --name-only and compare it with the pre-task staged list. Do not run git add, git commit, branch creation, reset or checkout.

Report the local endpoint, example request/response, test count, and the important limit that the URL contains credentials because the user explicitly requested it. State that no files were committed or newly staged.