package com.inspect.nvr.controller; import com.inspect.nvr.aop.TimeTrace; import com.inspect.nvr.domain.device.*; import com.inspect.nvr.domain.ivs.IvsPosition3dRequest; import com.inspect.nvr.domain.ivs.IvsPtzControlRequest; import com.inspect.nvr.domain.ivs.IvsPtzControlResponse; import com.inspect.nvr.ivs.service.IvsOpenApiService; import com.inspect.nvr.security.PtzLockService; import com.inspect.nvr.service.IvsCameraService; import com.inspect.nvr.utils.StringUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.io.ByteArrayInputStream; @Slf4j @RestController public class IvsCameraController { @Resource private IvsCameraService ivsCameraService; @Resource private IvsOpenApiService ivsOpenApiService; /** 云台控制权锁:body 携带角色字段的调用按 cameraCode 抢锁,北向机器调用跳过。 */ @Resource private PtzLockService ptzLockService; private static final Object captureLock = new Object(); private static final Object captureLock_pic = new Object(); /** * 查询指定摄像机在对应 NVR 上的预置位列表。 */ @CrossOrigin @GetMapping("/device/ptzpresetlist/{cameraCode}/{nvrCode}") public ResponseEntity ptzPresetList( @PathVariable("cameraCode") String cameraCode, @PathVariable("nvrCode") String nvrCode) { log.info("PTZ_PRESET_LIST cameraCode : {}, nvrCode: {}", cameraCode, nvrCode); IvsPresetListView ivsPresetListView = ivsOpenApiService.presetList(cameraCode, nvrCode); return ResponseEntity .ok() .body(ivsPresetListView); } /** * 接收 IVS 云台控制指令,并转发给厂商适配器执行。 */ @CrossOrigin @PostMapping({"/device/ptzcontrol"}) public ResponseEntity ptzControl( @RequestBody IvsPtzControlRequest param) { log.info("Ptz control request param: {}", param); // 请求体携带 username 时按 cameraCode 校验控制权;锁被更高优先持有时抛 HTTP 423。 ptzLockService.checkLock( param == null ? null : param.getCameraCode(), param == null ? null : param.getUsername(), param == null ? null : param.getJoinbrightToken()); return ResponseEntity.ok().body(ivsOpenApiService.ptzControl(param)); } @PostMapping({"/v1/device/ptzcontrol"}) public ResponseEntity ptzControl(@RequestBody PtzControlParam param) { log.info("Ptz control request param: {}", param); return ResponseEntity .ok() .body(ivsCameraService.ptzControl(param)); } /** * 接收前端视频框选坐标,并按框选方向执行 3D 定位放大或缩小。 * * @param param cameraCode 和 0~255 归一化起止坐标 * @return 厂商设备的执行结果 */ @CrossOrigin @PostMapping("/device/position3d") public ResponseEntity position3d( @RequestBody IvsPosition3dRequest param) { // 3D 定位与普通云台控制共用同一把 cameraCode 控制权锁。 ptzLockService.checkLock( param == null ? null : param.getCameraCode(), param == null ? null : param.getUsername(), param == null ? null : param.getJoinbrightToken()); // 由 IVS 门面按 cameraCode 选择厂商,并在厂商层完成坐标校验和 SDK 调用。 return ResponseEntity.ok().body(ivsOpenApiService.position3d(param)); } /** * 创建指定摄像机的平台抓图任务。 */ @GetMapping({"/platform/platformSnapshot2/{cameraCode}/{domainCode}"}) public ResponseEntity platformSnapshot2( @PathVariable("cameraCode") String cameraCode, @PathVariable("domainCode") String domainCode) { log.info("Platform Snapshot cameraCode: {}, domainCode: {}", cameraCode, domainCode); return ResponseEntity .ok() .body(ivsCameraService.platformSnapshot(cameraCode, domainCode, null)); } /** * 按任务或时间范围查询平台抓图结果。 */ @PostMapping({"/platform/snapshotList2"}) public ResponseEntity retrieveSnapshotList2( @RequestBody SnapshotInfoListParam param) { log.info("Snapshot list request param: {}", param); return ResponseEntity .ok() .body(ivsCameraService.getSnapshotList(param)); } /** * 兼容旧版路径参数格式查询平台抓图结果。 */ @GetMapping("/platform/snapshotlist/{cameraCode}/{domainCode}/{startTime}/{endTime}/{fromIndex}/{toIndex}/{snapType}") public ResponseEntity retrieveLegacySnapshotList( @PathVariable("cameraCode") String cameraCode, @PathVariable("domainCode") String domainCode, @PathVariable("startTime") String startTime, @PathVariable("endTime") String endTime, @PathVariable("fromIndex") Integer fromIndex, @PathVariable("toIndex") Integer toIndex, @PathVariable("snapType") Integer snapType) { SnapshotInfoListParam.SnapshotDetailInfo detail = SnapshotInfoListParam.SnapshotDetailInfo.builder() .startTime(startTime) .endTime(endTime) .fromIndex(fromIndex) .toIndex(toIndex) .snapType(snapType) .build(); SnapshotInfoListParam request = SnapshotInfoListParam.builder() .cameraCode(cameraCode) .domainCode(domainCode) .snapshotDetailInfo(detail) .build(); return ResponseEntity.ok(ivsCameraService.getSnapshotList(request)); } /** * @deprecated * use {@link #capture} instead. */ @Deprecated /** * 使用旧版会话标识下载抓图文件,并保留失败重试机制。 */ @GetMapping("/downloadfile_old") public ResponseEntity downloadFile( @RequestParam("filesessionid") String fileSessionId) { // synchronized (captureLock_pic) { log.info("Download File Get Stream, fileSessionId: {}", fileSessionId); ByteArrayInputStream byteArrayInputStream = null; //添加重试机制 final int MAX_RETRIES = 20; // 最大重试次数 boolean success = false; int retryCount = 0; while (!success && retryCount < MAX_RETRIES) { try { retryCount++; log.info("第{}次尝试抓图", retryCount); byteArrayInputStream = ivsCameraService.downloadFile(fileSessionId); if (StringUtils.isNotNull(byteArrayInputStream)) { success = true; } else { Thread.sleep(500); // 等待一秒后重试 log.error("第{}次抓图失败", retryCount); } } catch (Exception e) { log.error("第{}次抓图失败: {}", retryCount, e.getMessage()); } } return ResponseEntity .ok() .body(new InputStreamResource(byteArrayInputStream)); // } } @TimeTrace /** * 根据临时图片会话标识下载抓图文件。 */ @GetMapping("/downloadfile") public ResponseEntity capture( @RequestParam("filesessionid") String fileSessionId) { ByteArrayInputStream byteArrayInputStream = ivsCameraService.capture(fileSessionId); return ResponseEntity.ok().body(new InputStreamResource(byteArrayInputStream)); } @TimeTrace /** * 使用 Digest 认证直接抓取并返回 JPEG 图片。 */ @GetMapping("/digest") public ResponseEntity digest( @RequestParam("filesessionid") String fileSessionId) { byte[] bytes = ivsCameraService.captureDigest(fileSessionId); if (bytes == null || bytes.length == 0) { log.error("Digest 抓图失败,filesessionid={}", fileSessionId); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } return ResponseEntity.ok() .contentType(MediaType.IMAGE_JPEG) .contentLength(bytes.length) .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"digest.jpg\"") .body(bytes); } }