Просмотр исходного кода

feat(pms): 新增海康视频预览、语音对讲及云台控制接口

Zimo 1 неделя назад
Родитель
Сommit
bd11118350

+ 112 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/hikvision/HikvisionController.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.pms.controller.admin.hikvision;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.pms.controller.admin.hikvision.vo.HikvisionPtzControlReqVO;
 import cn.iocoder.yudao.module.pms.controller.admin.hikvision.vo.HikvisionTreeNodeRespVO;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
@@ -15,10 +16,13 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
 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.validation.Valid;
 import javax.validation.constraints.NotBlank;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -52,6 +56,14 @@ public class HikvisionController {
     private static final String SUB_REGIONS_ADDRESS = "/api/resource/v2/regions/subRegions";
     /** 根据区域编号查询直属资源,不会查询子区域中的资源。 */
     private static final String SUB_RESOURCES_ADDRESS = "/api/irds/v2/resource/subResources";
+    /** 获取监控点实时预览地址。 */
+    private static final String PREVIEW_URL_ADDRESS = "/api/video/v2/cameras/previewURLs";
+    /** 获取监控点对讲地址。 */
+    private static final String TALK_URL_ADDRESS = "/api/video/v1/cameras/talkURLs";
+    /** 根据监控点编号进行云台控制。 */
+    private static final String PTZ_CONTROL_ADDRESS = "/api/video/v1/ptzs/controlling";
+    /** Web 页面通过海康 H5player 使用 WebSocket Secure 协议。 */
+    private static final String H5PLAYER_PROTOCOL = "wss";
     /** 返回给前端的区域节点类型。 */
     private static final String NODE_TYPE_REGION = "region";
     /** 返回给前端的监控点节点类型。 */
@@ -97,6 +109,106 @@ public class HikvisionController {
         }
     }
 
+    /**
+     * 获取监控点的 WSS 实时预览地址。
+     *
+     * <p>
+     * 海康接口除 {@code cameraIndexCode} 外的参数均为可选参数。这里固定传入
+     * {@code protocol=wss},并将前端选择的 {@code streamType} 原样传给海康平台。
+     * </p>
+     *
+     * @param cameraIndexCode 监控点唯一标识
+     * @param streamType      码流类型;0 为主码流,1 为子码流,默认主码流
+     * @return WSS 实时预览地址
+     */
+    @GetMapping("/preview-url")
+    @Operation(summary = "获取海康监控点 WSS 实时预览地址")
+    public CommonResult<String> getPreviewUrl(
+            @Parameter(description = "监控点唯一标识", required = true) @RequestParam("cameraIndexCode") @NotBlank(message = "监控点编号不能为空") String cameraIndexCode,
+            @Parameter(description = "码流类型:0 主码流,1 子码流", example = "0") @RequestParam(value = "streamType", defaultValue = "0") Integer streamType) {
+        try {
+            Map<String, Object> params = new HashMap<>();
+            params.put("cameraIndexCode", cameraIndexCode);
+            params.put("streamType", streamType);
+            params.put("protocol", H5PLAYER_PROTOCOL);
+            JSONObject data = parseArtemisData(invokeArtemis(PREVIEW_URL_ADDRESS, params));
+            String url = data.getString("url");
+            if (StrUtil.isBlank(url)) {
+                throw new IllegalStateException("海康预览地址响应缺少 url");
+            }
+            return success(url);
+        } catch (Exception e) {
+            log.error("获取海康监控点预览地址失败,cameraIndexCode: {}, streamType: {}",
+                    cameraIndexCode, streamType, e);
+            return error(500, "获取海康监控点预览地址失败");
+        }
+    }
+
+    /**
+     * 获取监控点的 WSS 对讲地址。
+     *
+     * <p>
+     * 海康对讲地址有效期为 5 分钟。接口固定传入 {@code protocol=wss},传输协议使用平台默认的
+     * TCP;前端应在开始对讲时调用本接口获取最新地址。
+     * </p>
+     *
+     * @param cameraIndexCode 监控点唯一标识
+     * @return WSS 对讲地址
+     */
+    @GetMapping("/talk-url")
+    @Operation(summary = "获取海康监控点 WSS 对讲地址")
+    public CommonResult<String> getTalkUrl(
+            @Parameter(description = "监控点唯一标识", required = true) @RequestParam("cameraIndexCode") @NotBlank(message = "监控点编号不能为空") String cameraIndexCode) {
+        try {
+            Map<String, Object> params = new HashMap<>();
+            params.put("cameraIndexCode", cameraIndexCode);
+            params.put("protocol", H5PLAYER_PROTOCOL);
+            JSONObject data = parseArtemisData(invokeArtemis(TALK_URL_ADDRESS, params));
+            String url = data.getString("url");
+            if (StrUtil.isBlank(url)) {
+                throw new IllegalStateException("海康对讲地址响应缺少 url");
+            }
+            return success(url);
+        } catch (Exception e) {
+            log.error("获取海康监控点对讲地址失败,cameraIndexCode: {}", cameraIndexCode, e);
+            return error(500, "获取海康监控点对讲地址失败");
+        }
+    }
+
+    /**
+     * 控制监控点云台。
+     *
+     * <p>
+     * speed 和 presetIndex 是可选参数,仅在前端传值时转发给海康平台,避免不支持这些参数的命令
+     * 收到无意义字段。持续动作由前端先发送 action=0 开始,再发送 action=1 停止。
+     * </p>
+     *
+     * @param reqVO 云台控制参数
+     * @return 是否控制成功
+     */
+    @PostMapping("/ptz-control")
+    @Operation(summary = "控制海康监控点云台")
+    public CommonResult<Boolean> controlPtz(@Valid @RequestBody HikvisionPtzControlReqVO reqVO) {
+        try {
+            Map<String, Object> params = new HashMap<>();
+            params.put("cameraIndexCode", reqVO.getCameraIndexCode());
+            params.put("action", reqVO.getAction());
+            params.put("command", reqVO.getCommand());
+            if (reqVO.getSpeed() != null) {
+                params.put("speed", reqVO.getSpeed());
+            }
+            if (reqVO.getPresetIndex() != null) {
+                params.put("presetIndex", reqVO.getPresetIndex());
+            }
+            parseArtemisData(invokeArtemis(PTZ_CONTROL_ADDRESS, params));
+            return success(true);
+        } catch (Exception e) {
+            log.error("控制海康监控点云台失败,cameraIndexCode: {}, action: {}, command: {}",
+                    reqVO.getCameraIndexCode(), reqVO.getAction(), reqVO.getCommand(), e);
+            return error(500, "控制海康监控点云台失败");
+        }
+    }
+
     /**
      * 获取根区域,并将根区域的下一层节点放入 children。
      *

+ 56 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/hikvision/vo/HikvisionPtzControlReqVO.java

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.pms.controller.admin.hikvision.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.AssertTrue;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+/**
+ * 海康监控点云台控制请求。
+ */
+@Schema(description = "管理后台 - Hikvision 监控点云台控制 Request VO")
+@Data
+public class HikvisionPtzControlReqVO {
+
+    private static final String COMMAND_PATTERN = "(?i)^(LEFT|RIGHT|UP|DOWN|ZOOM_IN|ZOOM_OUT|LEFT_UP|"
+            + "LEFT_DOWN|RIGHT_UP|RIGHT_DOWN|FOCUS_NEAR|FOCUS_FAR|IRIS_ENLARGE|IRIS_REDUCE|WIPER_SWITCH|"
+            + "START_RECORD_TRACK|STOP_RECORD_TRACK|START_TRACK|STOP_TRACK|GOTO_PRESET)$";
+
+    @Schema(description = "监控点唯一标识", requiredMode = Schema.RequiredMode.REQUIRED,
+            example = "748d84750e3a4a5bbad3cd4af9ed5101")
+    @NotBlank(message = "监控点编号不能为空")
+    private String cameraIndexCode;
+
+    @Schema(description = "操作类型:0 开始,1 停止", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+    @NotNull(message = "操作类型不能为空")
+    @Min(value = 0, message = "操作类型只能为 0 或 1")
+    @Max(value = 1, message = "操作类型只能为 0 或 1")
+    private Integer action;
+
+    @Schema(description = "云台命令,不区分大小写", requiredMode = Schema.RequiredMode.REQUIRED, example = "LEFT")
+    @NotBlank(message = "云台命令不能为空")
+    @Pattern(regexp = COMMAND_PATTERN, message = "云台命令不受支持")
+    private String command;
+
+    @Schema(description = "云台速度,范围 1-100;不传时由海康平台使用默认值", example = "50")
+    @Min(value = 1, message = "云台速度不能小于 1")
+    @Max(value = 100, message = "云台速度不能大于 100")
+    private Integer speed;
+
+    @Schema(description = "预置点编号;command=GOTO_PRESET 时必填,通常不超过 300", example = "1")
+    @Min(value = 1, message = "预置点编号不能小于 1")
+    private Integer presetIndex;
+
+    @AssertTrue(message = "调用预置点时预置点编号不能为空")
+    @JsonIgnore
+    public boolean isPresetIndexValid() {
+        return command == null || !"GOTO_PRESET".equalsIgnoreCase(command) || presetIndex != null;
+    }
+
+}