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

feat: enhance PTZ control API with command and zoom functionalities

- Refactor PTZ command handling to use a unified interface for sending commands
- Introduce zoom control methods for starting and stopping zoom actions
- Update direction constants to use unit vectors for more precise control
- Improve type definitions for PTZ commands and results for better clarity
yb 2 недель назад
Родитель
Сommit
5a7b0eb07f
4 измененных файлов с 273 добавлено и 55 удалено
  1. 92 51
      src/api/ptz.ts
  2. 1 0
      src/components.d.ts
  3. 93 2
      src/views/demo/cloudflare-stream.vue
  4. 87 2
      src/views/demo/webrtc-stream.vue

+ 92 - 51
src/api/ptz.ts

@@ -3,66 +3,78 @@
  * 通过独立的 PTZ 后端服务调用海康威视 ISAPI 协议
  */
 
+// ==================== 类型定义 ====================
+
 export interface PTZConfig {
-  host: string // 摄像头 IP 地址
-  username: string // 用户名
-  password: string // 密码
-  channel?: number // 通道号,默认 1
+  host: string
+  username: string
+  password: string
+  channel?: number
+}
+
+export interface PTZCommand {
+  pan: number
+  tilt: number
+  zoom: number
 }
 
-export interface PTZDirection {
-  pan: number // -100 ~ 100, 负左正右
-  tilt: number // -100 ~ 100, 负下正上
+export interface PTZResult {
+  success: boolean
+  error?: string
+  data?: unknown
 }
 
-// PTZ 后端服务地址
+// ==================== 常量 ====================
+
 const PTZ_API_BASE = 'http://localhost:3002'
+const DEFAULT_SPEED = 50
 
-// 方向预设值
+// 方向预设值 (单位向量)
 export const PTZ_DIRECTIONS = {
-  UP: { pan: 0, tilt: 50 },
-  DOWN: { pan: 0, tilt: -50 },
-  LEFT: { pan: -50, tilt: 0 },
-  RIGHT: { pan: 50, tilt: 0 },
-  UP_LEFT: { pan: -50, tilt: 50 },
-  UP_RIGHT: { pan: 50, tilt: 50 },
-  DOWN_LEFT: { pan: -50, tilt: -50 },
-  DOWN_RIGHT: { pan: 50, tilt: -50 },
+  UP: { pan: 0, tilt: 1 },
+  DOWN: { pan: 0, tilt: -1 },
+  LEFT: { pan: -1, tilt: 0 },
+  RIGHT: { pan: 1, tilt: 0 },
+  UP_LEFT: { pan: -1, tilt: 1 },
+  UP_RIGHT: { pan: 1, tilt: 1 },
+  DOWN_LEFT: { pan: -1, tilt: -1 },
+  DOWN_RIGHT: { pan: 1, tilt: -1 },
   STOP: { pan: 0, tilt: 0 }
 } as const
 
+// 缩放预设值 (单位向量)
+export const PTZ_ZOOM_DIRECTIONS = {
+  IN: { zoom: 1 },
+  OUT: { zoom: -1 },
+  STOP: { zoom: 0 }
+} as const
+
+export type PTZDirectionKey = keyof typeof PTZ_DIRECTIONS
+export type PTZZoomKey = keyof typeof PTZ_ZOOM_DIRECTIONS
+
+// ==================== 核心 API ====================
+
 /**
- * 发送 PTZ 控制命令
- * 通过独立的 PTZ 后端服务,支持 Digest Auth
+ * 发送 PTZ 控制命令 (统一接口)
  */
-export async function sendPTZCommand(
-  config: PTZConfig,
-  direction: PTZDirection
-): Promise<{ success: boolean; error?: string }> {
+async function sendCommand(config: PTZConfig, command: PTZCommand): Promise<PTZResult> {
   try {
     const response = await fetch(`${PTZ_API_BASE}/ptz/control`, {
       method: 'POST',
-      headers: {
-        'Content-Type': 'application/json'
-      },
+      headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({
         host: config.host,
         username: config.username,
         password: config.password,
         channel: config.channel || 1,
-        pan: direction.pan,
-        tilt: direction.tilt,
-        zoom: 0
+        pan: command.pan,
+        tilt: command.tilt,
+        zoom: command.zoom
       })
     })
 
     const data = await response.json()
-
-    if (data.code === 200) {
-      return { success: true }
-    } else {
-      return { success: false, error: data.msg || 'Unknown error' }
-    }
+    return data.code === 200 ? { success: true, data: data.data } : { success: false, error: data.msg || 'Unknown error' }
   } catch (error) {
     return { success: false, error: String(error) }
   }
@@ -71,15 +83,11 @@ export async function sendPTZCommand(
 /**
  * 获取 PTZ 状态
  */
-export async function getPTZStatus(
-  config: PTZConfig
-): Promise<{ success: boolean; data?: string; error?: string }> {
+export async function getPTZStatus(config: PTZConfig): Promise<PTZResult & { data?: string }> {
   try {
     const response = await fetch(`${PTZ_API_BASE}/ptz/status`, {
       method: 'POST',
-      headers: {
-        'Content-Type': 'application/json'
-      },
+      headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({
         host: config.host,
         username: config.username,
@@ -89,27 +97,60 @@ export async function getPTZStatus(
     })
 
     const data = await response.json()
-
-    if (data.code === 200) {
-      return { success: true, data: data.data?.raw }
-    } else {
-      return { success: false, error: data.msg || 'Unknown error' }
-    }
+    return data.code === 200 ? { success: true, data: data.data?.raw } : { success: false, error: data.msg || 'Unknown error' }
   } catch (error) {
     return { success: false, error: String(error) }
   }
 }
 
+// ==================== 方向控制 ====================
+
 /**
  * 开始 PTZ 移动
+ * @param direction 方向: UP, DOWN, LEFT, RIGHT, UP_LEFT, UP_RIGHT, DOWN_LEFT, DOWN_RIGHT, STOP
+ * @param speed 速度 1-100,默认 50
  */
-export function startPTZ(config: PTZConfig, direction: keyof typeof PTZ_DIRECTIONS) {
-  return sendPTZCommand(config, PTZ_DIRECTIONS[direction])
+export function startPTZ(config: PTZConfig, direction: PTZDirectionKey, speed: number = DEFAULT_SPEED): Promise<PTZResult> {
+  const dir = PTZ_DIRECTIONS[direction]
+  return sendCommand(config, {
+    pan: dir.pan * speed,
+    tilt: dir.tilt * speed,
+    zoom: 0
+  })
 }
 
 /**
  * 停止 PTZ 移动
  */
-export function stopPTZ(config: PTZConfig) {
-  return sendPTZCommand(config, PTZ_DIRECTIONS.STOP)
+export function stopPTZ(config: PTZConfig): Promise<PTZResult> {
+  return startPTZ(config, 'STOP')
 }
+
+// ==================== 缩放控制 ====================
+
+/**
+ * 开始缩放
+ * @param direction 缩放方向: IN, OUT, STOP
+ * @param speed 速度 1-100,默认 50
+ */
+export function startZoom(config: PTZConfig, direction: PTZZoomKey, speed: number = DEFAULT_SPEED): Promise<PTZResult> {
+  const zoom = PTZ_ZOOM_DIRECTIONS[direction]
+  return sendCommand(config, {
+    pan: 0,
+    tilt: 0,
+    zoom: zoom.zoom * speed
+  })
+}
+
+/**
+ * 停止缩放
+ */
+export function stopZoom(config: PTZConfig): Promise<PTZResult> {
+  return startZoom(config, 'STOP')
+}
+
+// ==================== 便捷方法 ====================
+
+export const zoomIn = (config: PTZConfig, speed?: number) => startZoom(config, 'IN', speed)
+export const zoomOut = (config: PTZConfig, speed?: number) => startZoom(config, 'OUT', speed)
+export const zoomStop = stopZoom

+ 1 - 0
src/components.d.ts

@@ -49,6 +49,7 @@ declare module 'vue' {
     ElResult: typeof import('element-plus/es')['ElResult']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElSlider: typeof import('element-plus/es')['ElSlider']
     ElSpace: typeof import('element-plus/es')['ElSpace']
     ElStatistic: typeof import('element-plus/es')['ElStatistic']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']

+ 93 - 2
src/views/demo/cloudflare-stream.vue

@@ -99,6 +99,25 @@
             <el-icon><BottomRight /></el-icon>
           </div>
         </div>
+
+        <!-- 缩放控制 -->
+        <div class="zoom-controls">
+          <div class="zoom-header">
+            <el-icon><ZoomOut /></el-icon>
+            <span>缩放</span>
+            <el-icon><ZoomIn /></el-icon>
+          </div>
+          <el-slider
+            v-model="zoomValue"
+            :min="-100"
+            :max="100"
+            :step="10"
+            :show-tooltip="true"
+            :format-tooltip="formatZoomTooltip"
+            @input="handleZoomChange"
+            @change="handleZoomRelease"
+          />
+        </div>
       </div>
     </div>
 
@@ -160,10 +179,12 @@ import {
   TopRight,
   BottomLeft,
   BottomRight,
-  Refresh
+  Refresh,
+  ZoomIn,
+  ZoomOut
 } from '@element-plus/icons-vue'
 import VideoPlayer from '@/components/VideoPlayer.vue'
-import { startPTZ, stopPTZ, PTZ_DIRECTIONS } from '@/api/ptz'
+import { startPTZ, stopPTZ, PTZ_DIRECTIONS, startZoom, stopZoom, PTZ_ZOOM_DIRECTIONS } from '@/api/ptz'
 
 const playerRef = ref<InstanceType<typeof VideoPlayer>>()
 
@@ -188,6 +209,10 @@ const ptzConfig = reactive({
   password: 'Wxc767718929'
 })
 
+// PTZ 速度和缩放
+const ptzSpeed = ref(50)
+const zoomValue = ref(0)
+
 // 当前播放状态
 const currentSrc = ref('')
 const currentVideoId = ref('')
@@ -331,6 +356,51 @@ async function handlePTZStop() {
     addLog(`PTZ 停止失败: ${result.error}`, 'error')
   }
 }
+
+// 缩放滑块控制
+function formatZoomTooltip(val: number) {
+  if (val === 0) return '停止'
+  return val > 0 ? `放大 ${val}` : `缩小 ${Math.abs(val)}`
+}
+
+async function handleZoomChange(val: number) {
+  if (!ptzConfig.host || !ptzConfig.username || !ptzConfig.password) return
+
+  if (val === 0) {
+    await stopZoom({
+      host: ptzConfig.host,
+      username: ptzConfig.username,
+      password: ptzConfig.password
+    })
+    return
+  }
+
+  const direction = val > 0 ? 'IN' : 'OUT'
+  const speed = Math.abs(val)
+
+  await startZoom(
+    {
+      host: ptzConfig.host,
+      username: ptzConfig.username,
+      password: ptzConfig.password
+    },
+    direction,
+    speed
+  )
+}
+
+async function handleZoomRelease() {
+  // 松开滑块时回到中间并停止
+  zoomValue.value = 0
+  if (!ptzConfig.host) return
+
+  await stopZoom({
+    host: ptzConfig.host,
+    username: ptzConfig.username,
+    password: ptzConfig.password
+  })
+  addLog('缩放停止', 'info')
+}
 </script>
 
 <style lang="scss" scoped>
@@ -445,6 +515,27 @@ async function handlePTZStop() {
       background-color: var(--color-primary-light-9);
     }
   }
+
+  .zoom-controls {
+    margin-top: 15px;
+    padding-top: 15px;
+    border-top: 1px solid var(--border-color);
+
+    .zoom-label {
+      font-size: 12px;
+      color: var(--text-secondary);
+      margin-bottom: 10px;
+    }
+
+    .zoom-btns {
+      display: flex;
+      gap: 8px;
+
+      .el-button {
+        flex: 1;
+      }
+    }
+  }
 }
 
 .control-section {

+ 87 - 2
src/views/demo/webrtc-stream.vue

@@ -96,6 +96,31 @@
             <el-icon><BottomRight /></el-icon>
           </div>
         </div>
+
+        <!-- 缩放控制 -->
+        <div class="zoom-controls">
+          <div class="zoom-label">缩放</div>
+          <div class="zoom-btns">
+            <el-button
+              type="primary"
+              :icon="ZoomIn"
+              @mousedown="handleZoom('IN')"
+              @mouseup="handleZoomStop"
+              @mouseleave="handleZoomStop"
+            >
+              放大
+            </el-button>
+            <el-button
+              type="primary"
+              :icon="ZoomOut"
+              @mousedown="handleZoom('OUT')"
+              @mouseup="handleZoomStop"
+              @mouseleave="handleZoomStop"
+            >
+              缩小
+            </el-button>
+          </div>
+        </div>
       </div>
     </div>
 
@@ -193,10 +218,12 @@ import {
   TopRight,
   BottomLeft,
   BottomRight,
-  Refresh
+  Refresh,
+  ZoomIn,
+  ZoomOut
 } from '@element-plus/icons-vue'
 import VideoPlayer from '@/components/VideoPlayer.vue'
-import { startPTZ, stopPTZ, PTZ_DIRECTIONS } from '@/api/ptz'
+import { startPTZ, stopPTZ, PTZ_DIRECTIONS, startZoom, stopZoom, PTZ_ZOOM_DIRECTIONS } from '@/api/ptz'
 
 const playerRef = ref<InstanceType<typeof VideoPlayer>>()
 
@@ -380,6 +407,43 @@ async function handlePTZStop() {
     addLog(`PTZ 停止失败: ${result.error}`, 'error')
   }
 }
+
+// 缩放控制
+async function handleZoom(direction: keyof typeof PTZ_ZOOM_DIRECTIONS) {
+  if (!ptzConfig.host || !ptzConfig.username || !ptzConfig.password) {
+    ElMessage.warning('请先配置摄像头信息')
+    return
+  }
+
+  const result = await startZoom(
+    {
+      host: ptzConfig.host,
+      username: ptzConfig.username,
+      password: ptzConfig.password
+    },
+    direction
+  )
+
+  if (result.success) {
+    addLog(`缩放: ${direction === 'IN' ? '放大' : '缩小'}`, 'info')
+  } else {
+    addLog(`缩放失败: ${result.error}`, 'error')
+  }
+}
+
+async function handleZoomStop() {
+  if (!ptzConfig.host) return
+
+  const result = await stopZoom({
+    host: ptzConfig.host,
+    username: ptzConfig.username,
+    password: ptzConfig.password
+  })
+
+  if (!result.success) {
+    addLog(`缩放停止失败: ${result.error}`, 'error')
+  }
+}
 </script>
 
 <style lang="scss" scoped>
@@ -499,6 +563,27 @@ async function handlePTZStop() {
     }
   }
 
+  .zoom-controls {
+    margin-top: 15px;
+    padding-top: 15px;
+    border-top: 1px solid var(--border-color);
+
+    .zoom-label {
+      font-size: 12px;
+      color: var(--text-secondary);
+      margin-bottom: 10px;
+    }
+
+    .zoom-btns {
+      display: flex;
+      gap: 8px;
+
+      .el-button {
+        flex: 1;
+      }
+    }
+  }
+
   .ptz-speed {
     display: flex;
     align-items: center;