yb пре 1 недеља
родитељ
комит
0fbb57f030

+ 85 - 12
src/api/ptz.ts

@@ -1,15 +1,21 @@
 /**
  * PTZ 云台控制 API
- * 通过独立的 PTZ 后端服务调用海康威视 ISAPI 协议
+ * 通过独立的 PTZ 后端服务调用 ONVIF 标准协议
+ * 支持厂家: HIKVISION (海康威视), ANPVIZ
  */
 
 // ==================== 类型定义 ====================
 
+// 支持的摄像头厂家
+export type CameraVendor = 'HIKVISION' | 'ANPVIZ' | 'DAHUA' | 'CT-IP500' | 'SVBC'
+
 export interface PTZConfig {
   host: string
   username: string
   password: string
   channel?: number
+  cameraId?: number
+  vendor?: CameraVendor
 }
 
 export interface PTZCommand {
@@ -52,10 +58,57 @@ export const PTZ_ZOOM_DIRECTIONS = {
 export type PTZDirectionKey = keyof typeof PTZ_DIRECTIONS
 export type PTZZoomKey = keyof typeof PTZ_ZOOM_DIRECTIONS
 
+// PTZ 方向命令
+export type PTZCommandType =
+  | 'up'
+  | 'down'
+  | 'left'
+  | 'right'
+  | 'up_left'
+  | 'up_right'
+  | 'down_left'
+  | 'down_right'
+  | 'stop'
+
 // ==================== 核心 API ====================
 
 /**
- * 发送 PTZ 控制命令 (统一接口)
+ * 发送 PTZ 控制命令 (ONVIF 统一接口)
+ * 支持: HIKVISION, ANPVIZ
+ */
+async function sendPTZCommand(
+  config: PTZConfig,
+  command: PTZCommandType,
+  speed: number = DEFAULT_SPEED
+): Promise<PTZResult> {
+  try {
+    const response = await fetch(`${PTZ_API_BASE}/ptz/control`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        host: config.host,
+        username: config.username,
+        password: config.password,
+        channel: config.channel || 1,
+        cameraId: config.cameraId,
+        vendor: config.vendor || 'HIKVISION',
+        command,
+        speed
+      })
+    })
+
+    const data = await response.json()
+    return data.code === 200
+      ? { success: true, data: data.data }
+      : { success: false, error: data.msg || 'Unknown error' }
+  } catch (error) {
+    return { success: false, error: String(error) }
+  }
+}
+
+/**
+ * 发送 PTZ 控制命令 (旧接口 - 使用 pan/tilt/zoom)
+ * 注意: 缩放功能可能仅部分摄像头支持
  */
 async function sendCommand(config: PTZConfig, command: PTZCommand): Promise<PTZResult> {
   try {
@@ -67,6 +120,8 @@ async function sendCommand(config: PTZConfig, command: PTZCommand): Promise<PTZR
         username: config.username,
         password: config.password,
         channel: config.channel || 1,
+        cameraId: config.cameraId,
+        vendor: config.vendor || 'HIKVISION',
         pan: command.pan,
         tilt: command.tilt,
         zoom: command.zoom
@@ -74,7 +129,9 @@ async function sendCommand(config: PTZConfig, command: PTZCommand): Promise<PTZR
     })
 
     const data = await response.json()
-    return data.code === 200 ? { success: true, data: data.data } : { 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) }
   }
@@ -97,7 +154,9 @@ export async function getPTZStatus(config: PTZConfig): Promise<PTZResult & { dat
     })
 
     const data = await response.json()
-    return data.code === 200 ? { success: true, data: data.data?.raw } : { 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) }
   }
@@ -105,25 +164,39 @@ export async function getPTZStatus(config: PTZConfig): Promise<PTZResult & { dat
 
 // ==================== 方向控制 ====================
 
+// 方向键映射到命令
+const DIRECTION_TO_COMMAND: Record<PTZDirectionKey, PTZCommandType> = {
+  UP: 'up',
+  DOWN: 'down',
+  LEFT: 'left',
+  RIGHT: 'right',
+  UP_LEFT: 'up_left',
+  UP_RIGHT: 'up_right',
+  DOWN_LEFT: 'down_left',
+  DOWN_RIGHT: 'down_right',
+  STOP: 'stop'
+}
+
 /**
  * 开始 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: 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
-  })
+export function startPTZ(
+  config: PTZConfig,
+  direction: PTZDirectionKey,
+  speed: number = DEFAULT_SPEED
+): Promise<PTZResult> {
+  // 使用新的 command 接口,支持所有厂家
+  const command = DIRECTION_TO_COMMAND[direction]
+  return sendPTZCommand(config, command, speed)
 }
 
 /**
  * 停止 PTZ 移动
  */
 export function stopPTZ(config: PTZConfig): Promise<PTZResult> {
-  return startPTZ(config, 'STOP')
+  return sendPTZCommand(config, 'stop')
 }
 
 // ==================== 缩放控制 ====================

+ 100 - 15
src/components/monitor/CameraSelector.vue

@@ -59,6 +59,7 @@
 import { ref, computed, watch } from 'vue'
 import { Search, CircleCheck, CircleCheckFilled } from '@element-plus/icons-vue'
 import { adminListCameras } from '@/api/camera'
+import type { CameraVendor } from '@/composables/useMonitorStore'
 
 interface CameraItem {
   id: string
@@ -66,6 +67,11 @@ interface CameraItem {
   streamType: 'webrtc' | 'cloudflare'
   streamUrl: string
   online: boolean
+  // PTZ 配置
+  vendor?: CameraVendor
+  ptzHost?: string
+  ptzUsername?: string
+  ptzPassword?: string
 }
 
 interface Props {
@@ -77,7 +83,18 @@ const props = defineProps<Props>()
 
 const emit = defineEmits<{
   'update:visible': [value: boolean]
-  select: [camera: { id: string; name: string; streamType: 'webrtc' | 'cloudflare'; streamUrl: string }]
+  select: [
+    camera: {
+      id: string
+      name: string
+      streamType: 'webrtc' | 'cloudflare'
+      streamUrl: string
+      vendor?: CameraVendor
+      ptzHost?: string
+      ptzUsername?: string
+      ptzPassword?: string
+    }
+  ]
 }>()
 
 const searchText = ref('')
@@ -91,22 +108,86 @@ const filteredCameras = computed(() => {
   return cameras.value.filter((camera) => camera.name.toLowerCase().includes(keyword))
 })
 
-// 预设的 WebRTC 摄像头流列表
+// 预设的 WebRTC 摄像头流列表 (包含 PTZ 配置,统一使用 viewer 账号)
 const presetWebrtcCameras: CameraItem[] = [
-  // ANPVIZ
-  { id: 'anpviz', name: 'ANPVIZ 主码流', streamType: 'webrtc', streamUrl: 'anpviz', online: true },
-  { id: 'anpviz_raw', name: 'ANPVIZ 原始流', streamType: 'webrtc', streamUrl: 'anpviz_raw', online: true },
-  { id: 'anpviz_sub', name: 'ANPVIZ 子码流', streamType: 'webrtc', streamUrl: 'anpviz_sub', online: true },
+  // ANPVIZ - 带 PTZ 配置
+  {
+    id: 'anpviz',
+    name: 'ANPVIZ 主码流',
+    streamType: 'webrtc',
+    streamUrl: 'anpviz',
+    online: true,
+    vendor: 'ANPVIZ',
+    ptzHost: '192.168.0.96',
+    ptzUsername: 'viewer',
+    ptzPassword: 'Wxc767718929'
+  },
+  {
+    id: 'anpviz_raw',
+    name: 'ANPVIZ 原始流',
+    streamType: 'webrtc',
+    streamUrl: 'anpviz_raw',
+    online: true,
+    vendor: 'ANPVIZ',
+    ptzHost: '192.168.0.96',
+    ptzUsername: 'viewer',
+    ptzPassword: 'Wxc767718929'
+  },
+  {
+    id: 'anpviz_sub',
+    name: 'ANPVIZ 子码流',
+    streamType: 'webrtc',
+    streamUrl: 'anpviz_sub',
+    online: true,
+    vendor: 'ANPVIZ',
+    ptzHost: '192.168.0.96',
+    ptzUsername: 'viewer',
+    ptzPassword: 'Wxc767718929'
+  },
   // CT-IP500
-  { id: 'ct-ip500', name: 'CT-IP500 主码流', streamType: 'webrtc', streamUrl: 'ct-ip500', online: true },
-  { id: 'ct-ip500_sub', name: 'CT-IP500 子码流', streamType: 'webrtc', streamUrl: 'ct-ip500_sub', online: true },
-  // HIKVISION 海康威视
-  { id: 'hikvision', name: '海康威视 主码流', streamType: 'webrtc', streamUrl: 'hikvision', online: true },
-  { id: 'hikvision_sub', name: '海康威视 子码流', streamType: 'webrtc', streamUrl: 'hikvision_sub', online: true },
+  {
+    id: 'ct-ip500',
+    name: 'CT-IP500 主码流',
+    streamType: 'webrtc',
+    streamUrl: 'ct-ip500',
+    online: true,
+    vendor: 'CT-IP500'
+  },
+  {
+    id: 'ct-ip500_sub',
+    name: 'CT-IP500 子码流',
+    streamType: 'webrtc',
+    streamUrl: 'ct-ip500_sub',
+    online: true,
+    vendor: 'CT-IP500'
+  },
+  // HIKVISION 海康威视 - 带 PTZ 配置
+  {
+    id: 'hikvision',
+    name: '海康威视 主码流',
+    streamType: 'webrtc',
+    streamUrl: 'hikvision',
+    online: true,
+    vendor: 'HIKVISION',
+    ptzHost: '192.168.0.64',
+    ptzUsername: 'viewer',
+    ptzPassword: 'Wxc767718929'
+  },
+  {
+    id: 'hikvision_sub',
+    name: '海康威视 子码流',
+    streamType: 'webrtc',
+    streamUrl: 'hikvision_sub',
+    online: true,
+    vendor: 'HIKVISION',
+    ptzHost: '192.168.0.64',
+    ptzUsername: 'viewer',
+    ptzPassword: 'Wxc767718929'
+  },
   // SVBC
-  { id: 'svbc', name: 'SVBC 主码流', streamType: 'webrtc', streamUrl: 'svbc', online: true },
-  { id: 'svbc_raw', name: 'SVBC 原始流', streamType: 'webrtc', streamUrl: 'svbc_raw', online: true },
-  { id: 'svbc_sub', name: 'SVBC 子码流', streamType: 'webrtc', streamUrl: 'svbc_sub', online: true }
+  { id: 'svbc', name: 'SVBC 主码流', streamType: 'webrtc', streamUrl: 'svbc', online: true, vendor: 'SVBC' },
+  { id: 'svbc_raw', name: 'SVBC 原始流', streamType: 'webrtc', streamUrl: 'svbc_raw', online: true, vendor: 'SVBC' },
+  { id: 'svbc_sub', name: 'SVBC 子码流', streamType: 'webrtc', streamUrl: 'svbc_sub', online: true, vendor: 'SVBC' }
 ]
 
 // 加载摄像头列表
@@ -145,7 +226,11 @@ function handleConfirm() {
     id: selectedCamera.value.id,
     name: selectedCamera.value.name,
     streamType: selectedCamera.value.streamType,
-    streamUrl: selectedCamera.value.streamUrl
+    streamUrl: selectedCamera.value.streamUrl,
+    vendor: selectedCamera.value.vendor,
+    ptzHost: selectedCamera.value.ptzHost,
+    ptzUsername: selectedCamera.value.ptzUsername,
+    ptzPassword: selectedCamera.value.ptzPassword
   })
 }
 

+ 37 - 12
src/components/monitor/PtzOverlay.vue

@@ -107,13 +107,30 @@ import {
   ZoomIn,
   ZoomOut
 } from '@element-plus/icons-vue'
-import { startPTZ, stopPTZ, startZoom, stopZoom, type PTZConfig, type PTZDirectionKey } from '@/api/ptz'
+import {
+  startPTZ,
+  stopPTZ,
+  startZoom,
+  stopZoom,
+  type PTZConfig,
+  type PTZDirectionKey,
+  type CameraVendor
+} from '@/api/ptz'
 
 interface Props {
   cameraId?: string
+  vendor?: CameraVendor
+  host?: string
+  username?: string
+  password?: string
 }
 
-const props = defineProps<Props>()
+const props = withDefaults(defineProps<Props>(), {
+  vendor: 'HIKVISION',
+  host: '192.168.0.64',
+  username: 'admin',
+  password: ''
+})
 
 const emit = defineEmits<{
   'ptz-action': [action: string, params?: unknown]
@@ -121,40 +138,48 @@ const emit = defineEmits<{
 
 const zoomValue = ref(0)
 
-// 默认 PTZ 配置 (后续可以从摄像头配置中获取)
-const ptzConfig: PTZConfig = {
-  host: '192.168.0.64',
-  username: 'admin',
-  password: 'Wxc767718929'
+// 根据 props 生成 PTZ 配置
+function getPtzConfig(): PTZConfig {
+  return {
+    host: props.host,
+    username: props.username,
+    password: props.password,
+    cameraId: props.cameraId ? Number(props.cameraId) : undefined,
+    vendor: props.vendor
+  }
 }
 
 const ptzSpeed = 50
 
 async function handleDirection(direction: PTZDirectionKey) {
   emit('ptz-action', 'direction', { direction })
-  await startPTZ(ptzConfig, direction, ptzSpeed)
+  const config = getPtzConfig()
+  await startPTZ(config, direction, ptzSpeed)
 }
 
 async function handleDirectionStop() {
   emit('ptz-action', 'stop')
-  await stopPTZ(ptzConfig)
+  const config = getPtzConfig()
+  await stopPTZ(config)
 }
 
 async function handleZoomChange(val: number) {
+  const config = getPtzConfig()
   if (val === 0) {
-    await stopZoom(ptzConfig)
+    await stopZoom(config)
     return
   }
 
   const direction = val > 0 ? 'IN' : 'OUT'
   const speed = Math.abs(val)
   emit('ptz-action', 'zoom', { direction, speed })
-  await startZoom(ptzConfig, direction, speed)
+  await startZoom(config, direction, speed)
 }
 
 async function handleZoomRelease() {
   zoomValue.value = 0
-  await stopZoom(ptzConfig)
+  const config = getPtzConfig()
+  await stopZoom(config)
 }
 </script>
 

+ 8 - 1
src/components/monitor/VideoCell.vue

@@ -53,7 +53,14 @@
       <!-- 右下角 PTZ 控制面板 -->
       <transition name="fade">
         <div v-show="isHovering" class="video-cell__ptz">
-          <PtzOverlay :camera-id="slotData.cameraId" @ptz-action="handlePtzAction" />
+          <PtzOverlay
+            :camera-id="slotData.cameraId"
+            :vendor="slotData.vendor"
+            :host="slotData.ptzHost"
+            :username="slotData.ptzUsername"
+            :password="slotData.ptzPassword"
+            @ptz-action="handlePtzAction"
+          />
         </div>
       </transition>
     </template>

+ 8 - 0
src/composables/useMonitorStore.ts

@@ -1,11 +1,19 @@
 import { ref, computed } from 'vue'
 
+// 摄像头厂家类型
+export type CameraVendor = 'HIKVISION' | 'ANPVIZ' | 'DAHUA' | 'CT-IP500' | 'SVBC'
+
 export interface GridSlot {
   position: number
   cameraId?: string
   cameraName?: string
   streamType: 'webrtc' | 'cloudflare'
   streamUrl?: string
+  // PTZ 配置
+  vendor?: CameraVendor
+  ptzHost?: string
+  ptzUsername?: string
+  ptzPassword?: string
 }
 
 export interface MonitorTab {

+ 53 - 10
src/views/demo/webrtc-stream.vue

@@ -204,7 +204,15 @@ import {
   ZoomOut
 } from '@element-plus/icons-vue'
 import VideoPlayer from '@/components/VideoPlayer.vue'
-import { startPTZ, stopPTZ, PTZ_DIRECTIONS, startZoom, stopZoom, PTZ_ZOOM_DIRECTIONS } from '@/api/ptz'
+import {
+  startPTZ,
+  stopPTZ,
+  PTZ_DIRECTIONS,
+  startZoom,
+  stopZoom,
+  PTZ_ZOOM_DIRECTIONS,
+  type CameraVendor
+} from '@/api/ptz'
 
 const playerRef = ref<InstanceType<typeof VideoPlayer>>()
 
@@ -220,10 +228,29 @@ const playConfig = reactive({
   muted: true
 })
 
-// PTZ 配置
+// 摄像头 PTZ 配置映射 (统一使用 viewer 账号)
+const cameraPtzConfigs: Record<string, { vendor: CameraVendor; host: string; username: string; password: string }> = {
+  // ANPVIZ
+  anpviz: { vendor: 'ANPVIZ', host: '192.168.0.96', username: 'viewer', password: 'Wxc767718929' },
+  anpviz_raw: { vendor: 'ANPVIZ', host: '192.168.0.96', username: 'viewer', password: 'Wxc767718929' },
+  anpviz_sub: { vendor: 'ANPVIZ', host: '192.168.0.96', username: 'viewer', password: 'Wxc767718929' },
+  // CT-IP500 (无 PTZ)
+  'ct-ip500': { vendor: 'CT-IP500', host: '', username: '', password: '' },
+  'ct-ip500_sub': { vendor: 'CT-IP500', host: '', username: '', password: '' },
+  // HIKVISION 海康威视
+  hikvision: { vendor: 'HIKVISION', host: '192.168.0.64', username: 'viewer', password: 'Wxc767718929' },
+  hikvision_sub: { vendor: 'HIKVISION', host: '192.168.0.64', username: 'viewer', password: 'Wxc767718929' },
+  // SVBC (无 PTZ)
+  svbc: { vendor: 'SVBC', host: '', username: '', password: '' },
+  svbc_raw: { vendor: 'SVBC', host: '', username: '', password: '' },
+  svbc_sub: { vendor: 'SVBC', host: '', username: '', password: '' }
+}
+
+// PTZ 配置 (根据选择的摄像头动态更新)
 const ptzConfig = reactive({
+  vendor: 'HIKVISION' as CameraVendor,
   host: '192.168.0.64',
-  username: 'admin',
+  username: 'viewer',
   password: 'Wxc767718929'
 })
 
@@ -305,6 +332,17 @@ function startPlay() {
 // 切换摄像头流
 function handleStreamChange(streamName: string) {
   addLog(`切换摄像头: ${streamName}`, 'info')
+
+  // 更新 PTZ 配置
+  const ptzSettings = cameraPtzConfigs[streamName]
+  if (ptzSettings) {
+    ptzConfig.vendor = ptzSettings.vendor
+    ptzConfig.host = ptzSettings.host
+    ptzConfig.username = ptzSettings.username
+    ptzConfig.password = ptzSettings.password
+    addLog(`PTZ 配置已切换到: ${ptzSettings.vendor} (${ptzSettings.host || '无PTZ'})`, 'info')
+  }
+
   if (isPlaying.value) {
     // 如果正在播放,重新连接新的流
     playerRef.value?.reconnect()
@@ -368,7 +406,7 @@ function onError(error: any) {
 // PTZ 控制
 async function handlePTZ(direction: keyof typeof PTZ_DIRECTIONS) {
   if (!ptzConfig.host || !ptzConfig.username || !ptzConfig.password) {
-    ElMessage.warning('请先配置摄像头信息')
+    ElMessage.warning('当前摄像头不支持 PTZ 控制')
     return
   }
 
@@ -376,14 +414,15 @@ async function handlePTZ(direction: keyof typeof PTZ_DIRECTIONS) {
     {
       host: ptzConfig.host,
       username: ptzConfig.username,
-      password: ptzConfig.password
+      password: ptzConfig.password,
+      vendor: ptzConfig.vendor
     },
     direction,
     ptzSpeed.value
   )
 
   if (result.success) {
-    addLog(`PTZ 移动: ${direction} (速度: ${ptzSpeed.value})`, 'info')
+    addLog(`PTZ 移动: ${direction} (${ptzConfig.vendor}, 速度: ${ptzSpeed.value})`, 'info')
   } else {
     addLog(`PTZ 控制失败: ${result.error}`, 'error')
   }
@@ -395,7 +434,8 @@ async function handlePTZStop() {
   const result = await stopPTZ({
     host: ptzConfig.host,
     username: ptzConfig.username,
-    password: ptzConfig.password
+    password: ptzConfig.password,
+    vendor: ptzConfig.vendor
   })
 
   if (!result.success) {
@@ -416,7 +456,8 @@ async function handleZoomChange(val: number) {
     await stopZoom({
       host: ptzConfig.host,
       username: ptzConfig.username,
-      password: ptzConfig.password
+      password: ptzConfig.password,
+      vendor: ptzConfig.vendor
     })
     return
   }
@@ -428,7 +469,8 @@ async function handleZoomChange(val: number) {
     {
       host: ptzConfig.host,
       username: ptzConfig.username,
-      password: ptzConfig.password
+      password: ptzConfig.password,
+      vendor: ptzConfig.vendor
     },
     direction,
     speed
@@ -443,7 +485,8 @@ async function handleZoomRelease() {
   await stopZoom({
     host: ptzConfig.host,
     username: ptzConfig.username,
-    password: ptzConfig.password
+    password: ptzConfig.password,
+    vendor: ptzConfig.vendor
   })
   addLog('缩放停止', 'info')
 }

+ 11 - 2
src/views/monitor/index.vue

@@ -52,7 +52,7 @@ import { Plus, FolderChecked } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 import VideoGrid from '@/components/monitor/VideoGrid.vue'
 import CameraSelector from '@/components/monitor/CameraSelector.vue'
-import { useMonitorStore, type GridSlot } from '@/composables/useMonitorStore'
+import { useMonitorStore, type GridSlot, type CameraVendor } from '@/composables/useMonitorStore'
 import { useI18n } from 'vue-i18n'
 
 const { t } = useI18n()
@@ -97,6 +97,10 @@ function handleCameraSelected(camera: {
   name: string
   streamType: 'webrtc' | 'cloudflare'
   streamUrl: string
+  vendor?: CameraVendor
+  ptzHost?: string
+  ptzUsername?: string
+  ptzPassword?: string
 }) {
   if (!currentTab.value) return
 
@@ -105,7 +109,12 @@ function handleCameraSelected(camera: {
     cameraId: camera.id,
     cameraName: camera.name,
     streamType: camera.streamType,
-    streamUrl: camera.streamUrl
+    streamUrl: camera.streamUrl,
+    // PTZ 配置
+    vendor: camera.vendor,
+    ptzHost: camera.ptzHost,
+    ptzUsername: camera.ptzUsername,
+    ptzPassword: camera.ptzPassword
   }
 
   const existingSlots = currentTab.value.slots.filter((s) => s.position !== selectedPosition.value)