package com.inspect.nvr.hik.controller; import com.inspect.nvr.dahua.DahuaIvsVendorService; import com.inspect.nvr.domain.Infrared.NvrInfo; import com.inspect.nvr.hik.domain.HikNvrLiveStreamResponse; import com.inspect.nvr.hik.domain.HikNvrPlaybackErrorResponse; import com.inspect.nvr.hik.domain.HikNvrPresetListResponse; import com.inspect.nvr.hik.domain.HikNvrUiActionRequest; import com.inspect.nvr.hik.domain.HikNvrUiActionResponse; import com.inspect.nvr.hik.exception.HikNvrPlaybackException; import com.inspect.nvr.hik.service.HikNvrCredentialService; import com.inspect.nvr.hik.service.HikNvrLivePublicBaseUrlResolver; import com.inspect.nvr.hik.service.HikNvrLiveStreamService; import com.inspect.nvr.hik.service.HikNvrUiActionService; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; @Slf4j @RestController @CrossOrigin @RequestMapping("/hik/nvr/playback/hls/ui/actions") public class HikNvrUiActionController { /** 海康云台、预置位和抓图操作服务。 */ @Resource private HikNvrUiActionService actionService; /** 海康实时拉流服务。 */ @Resource private HikNvrLiveStreamService liveStreamService; /** 按请求来源地址生成实时预览对外播放地址。 */ @Resource private HikNvrLivePublicBaseUrlResolver publicBaseUrlResolver; /** 按页面选择的 NVR IP 从数据库读取服务端凭据。 */ @Resource private HikNvrCredentialService credentialService; /** 大华工作台预置位保存能力。 */ @Resource private DahuaIvsVendorService dahuaVendorService; /** * 接收海康 NVR 云台控制指令。 */ @PostMapping("/ptz") /** 代理海康云台动作请求。 */ public ResponseEntity ptz(@RequestBody HikNvrUiActionRequest request) { // 绑定页面所选 NVR 后执行云台动作,避免相同通道号被发送到默认设备。 return ResponseEntity.ok(actionServiceFor(request).ptz(request)); } /** * 调用指定通道的海康预置位。 */ @PostMapping("/preset") /** 代理海康预置位调用请求。 */ public ResponseEntity preset(@RequestBody HikNvrUiActionRequest request) { // 绑定页面所选 NVR 后执行预置位。 return ResponseEntity.ok(actionServiceFor(request).preset(request)); } /** * 保存当前云台位置为指定的海康预置位。 */ @PostMapping("/preset/create") /** 代理海康预置位保存请求。 */ public ResponseEntity createPreset(@RequestBody HikNvrUiActionRequest request) { // 先按页面 NVR IP 读取数据库厂商,浏览器不能自行选择 SDK。 NvrInfo nvr = nvrInfoFor(request); // 大华工作台复用同一路由,由 NetSDK 保存页面指定编号的预置位。 if (isDahua(nvr.getNvrCompany())) { return ResponseEntity.ok(dahuaVendorService.createPreset(request)); } // 海康设备继续沿用原有 SDK/ISAPI 保存实现。 return ResponseEntity.ok(actionService.forNvr(nvr).createPreset(request)); } /** * 读取指定通道的海康预置位列表。 */ @GetMapping("/presets") public ResponseEntity listPresets( @RequestParam("channel") Integer channel, @RequestParam("nvrIp") String nvrIp) { // 创建内部请求对象并读取 NVR 当前预置位列表。 HikNvrUiActionRequest request = new HikNvrUiActionRequest(); request.setNvrIp(nvrIp); request.setChannel(channel); // 绑定页面所选 NVR 后委托海康 ISAPI 列表读取能力。 return ResponseEntity.ok(actionServiceFor(request).listPresets(request)); } /** * 抓取海康 NVR 当前通道的 JPEG 图片。 */ @PostMapping(value = "/snapshot", produces = MediaType.IMAGE_JPEG_VALUE) /** 代理海康 JPEG 抓图请求。 */ public ResponseEntity snapshot(@RequestBody HikNvrUiActionRequest request) { // 绑定页面所选 NVR 后调用海康抓图服务获取 JPEG 字节。 byte[] image = actionServiceFor(request).snapshot(request); // 从请求中提取通道号用于响应文件名和响应头。 int channel = request == null || request.getChannel() == null ? 0 : request.getChannel(); String filename = "nvr-ch" + channel + "-" + System.currentTimeMillis() + ".jpg"; return ResponseEntity.ok() .contentType(MediaType.IMAGE_JPEG) .contentLength(image.length) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"") .header("X-Capture-Channel", String.valueOf(channel)) .body(image); } /** * 启动指定通道的实时拉流任务。 */ @PostMapping("/live/start") /** 代理实时拉流启动请求。 */ public ResponseEntity startLive( @RequestBody HikNvrUiActionRequest request, HttpServletRequest httpRequest) { // 从页面请求提取设备树完整 cameraCode 和可选码流类型。 String cameraCode = request == null ? null : request.getCameraCode(); Integer streamType = request == null ? null : request.getStreamType(); // 内网与外网两个 IP 共用本服务时,flvUrl/hlsUrl 必须指向本次请求实际访问的地址。 String publicBaseUrl = publicBaseUrlResolver.resolve(httpRequest); // 多跳网关会改写 Host,起流时记录一次候选头,便于现场定位播放地址为何与访问地址不一致。 log.info("[live] 播放地址解析 host={} xForwardedHost={} xPublicHost={} xForwardedFor={} origin={} resolved={}", httpRequest.getHeader("Host"), httpRequest.getHeader("X-Forwarded-Host"), httpRequest.getHeader("X-Public-Host"), httpRequest.getHeader("X-Forwarded-For"), httpRequest.getHeader("Origin"), publicBaseUrl); // 实时服务通过现有 IVS 媒体接口解析 cameraCode 并取得 RTSP 源地址。 return ResponseEntity.ok( liveStreamService.start(cameraCode, streamType, publicBaseUrl)); } /** * 停止指定通道的实时拉流任务。 */ @PostMapping("/live/stop") /** 代理实时拉流停止请求。 */ public ResponseEntity stopLive(@RequestBody HikNvrUiActionRequest request) { // 从页面请求提取要停止的完整 cameraCode。 String cameraCode = request == null ? null : request.getCameraCode(); // 只停止该 cameraCode 对应的流,不影响设备树中的其他摄像机。 return ResponseEntity.ok(liveStreamService.stop(cameraCode)); } /** * 停止本服务创建的全部实时拉流任务。 */ @PostMapping("/live/stop-all") /** 运维与批量任务收尾使用:一次释放所有 NVR 实时会话。 */ public ResponseEntity stopAllLive() { // 批量清理不需要 cameraCode,直接释放全部会话和排队任务。 return ResponseEntity.ok(liveStreamService.stopAll()); } /** * 查询指定通道的实时拉流状态。 */ @GetMapping("/live/status") public ResponseEntity liveStatus( @RequestParam("cameraCode") String cameraCode, HttpServletRequest httpRequest) { // 按设备树完整 cameraCode 查询唯一实时拉流状态。 // 状态轮询同样按请求来源返回播放地址,保证与外网/内网访问方一致。 return ResponseEntity.ok(liveStreamService.status( cameraCode, publicBaseUrlResolver.resolve(httpRequest))); } /** * 为页面请求绑定数据库中已启用的目标 NVR。 * * @param request 页面操作请求,必须包含所选 NVR IP * @return 绑定数据库目标 NVR 的海康操作服务 */ private HikNvrUiActionService actionServiceFor(HikNvrUiActionRequest request) { return actionService.forNvr(nvrInfoFor(request)); } /** 按页面请求读取并校验数据库中的目标 NVR。 */ private NvrInfo nvrInfoFor(HikNvrUiActionRequest request) { // 多 NVR 环境必须明确目标 IP,禁止按固定配置或隐式默认设备执行控制。 if (request == null || !hasText(request.getNvrIp())) { throw HikNvrPlaybackException.badRequest("NVR IP不能为空"); } // 只允许绑定数据库中已启用的 NVR,页面无法直接提供账号密码。 return credentialService.resolveByIp(request.getNvrIp().trim()); } /** 判断数据库厂商字段是否表示大华。 */ private boolean isDahua(String vendor) { return vendor != null && (vendor.trim().toUpperCase().contains("DAHUA") || vendor.contains("大华")); } /** * 判断页面提交的 NVR IP 是否包含有效文本。 * * @param value 待检查文本 * @return 非空且不全为空白时返回 true */ private boolean hasText(String value) { // 空文本不能定位数据库 NVR。 return value != null && !value.trim().isEmpty(); } }