Răsfoiți Sursa

去除ws-raw的直播插件

liujintao 6 zile în urmă
părinte
comite
fdb0b04fbc

+ 0 - 1
components.d.ts

@@ -25,7 +25,6 @@ declare module 'vue' {
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
-    MyVideo: typeof import('./src/components/myVideo.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     WebRTCVideo: typeof import('./src/components/WebRTCVideo.vue')['default']

+ 0 - 1
index.html

@@ -9,6 +9,5 @@
   <body>
     <div id="app"></div>
     <script type="module" src="/src/main.ts"></script>
-    <script type="module" src="/wfs-min.js"></script>
   </body>
 </html>

+ 0 - 50
package-lock.json

@@ -15,7 +15,6 @@
         "nprogress": "^0.2.0",
         "pinia": "^2.1.7",
         "qrcode.vue": "^3.6.0",
-        "vconsole": "^3.15.1",
         "vue": "^3.3.11",
         "vue-router": "^4.2.5"
       },
@@ -53,15 +52,6 @@
         "node": ">=6.0.0"
       }
     },
-    "node_modules/@babel/runtime": {
-      "version": "7.29.2",
-      "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/runtime/-/runtime-7.29.2.tgz",
-      "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
     "node_modules/@ctrl/tinycolor": {
       "version": "3.6.1",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
@@ -1610,29 +1600,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/copy-text-to-clipboard": {
-      "version": "3.2.2",
-      "resolved": "https://mirrors.huaweicloud.com/repository/npm/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.2.tgz",
-      "integrity": "sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/core-js": {
-      "version": "3.49.0",
-      "resolved": "https://mirrors.huaweicloud.com/repository/npm/core-js/-/core-js-3.49.0.tgz",
-      "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
-      "hasInstallScript": true,
-      "license": "MIT",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/core-js"
-      }
-    },
     "node_modules/csstype": {
       "version": "3.1.3",
       "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
@@ -2157,11 +2124,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/mutation-observer": {
-      "version": "1.0.3",
-      "resolved": "https://mirrors.huaweicloud.com/repository/npm/mutation-observer/-/mutation-observer-1.0.3.tgz",
-      "integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA=="
-    },
     "node_modules/nanoid": {
       "version": "3.3.11",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.11.tgz",
@@ -2818,18 +2780,6 @@
         "url": "https://github.com/sponsors/antfu"
       }
     },
-    "node_modules/vconsole": {
-      "version": "3.15.1",
-      "resolved": "https://mirrors.huaweicloud.com/repository/npm/vconsole/-/vconsole-3.15.1.tgz",
-      "integrity": "sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/runtime": "^7.17.2",
-        "copy-text-to-clipboard": "^3.0.1",
-        "core-js": "^3.11.0",
-        "mutation-observer": "^1.0.3"
-      }
-    },
     "node_modules/vite": {
       "version": "5.4.21",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/vite/-/vite-5.4.21.tgz",

+ 0 - 1
package.json

@@ -17,7 +17,6 @@
     "nprogress": "^0.2.0",
     "pinia": "^2.1.7",
     "qrcode.vue": "^3.6.0",
-    "vconsole": "^3.15.1",
     "vue": "^3.3.11",
     "vue-router": "^4.2.5"
   },

BIN
public/mp4-encoder.wasm


Fișier diff suprimat deoarece este prea mare
+ 0 - 2621
public/wasmconverter.js


BIN
public/wasmconverter.wasm


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/wfs-min.js


+ 1 - 1
src/components/WebRTCVideo.vue

@@ -125,7 +125,7 @@ let heartbeatTimer: ReturnType<typeof setInterval> | null = null
 let heartbeatDelay = 0
 let wsConnectStartTime = 0
 const HEARTBEAT_INTERVAL = 2000
-const WS_CONNECT_TIMEOUT = 3000 // WebSocket 握手超时 3 秒(正常只需几毫秒)
+const WS_CONNECT_TIMEOUT = 2000 // WebSocket 握手超时 2 秒(正常只需几毫秒)
 
 // 拖拽
 const scale = ref(1)

+ 0 - 583
src/components/myVideo.vue

@@ -1,583 +0,0 @@
-<template>
-  <div class="preview-container">
-    <div ref="videoContainer" class="video_container">
-      <video
-        id="player"
-        ref="videoElement"
-        autoplay
-        muted
-        oncontextmenu="return false;"
-        playsinline
-        poster="../assets/black.jpg"
-        @mousedown="startDrag"
-        @mousemove="onDrag"
-        @mouseup="endDrag"
-        @mouseleave="endDrag"
-      />
-    </div>
-  </div>
-  <div class="video-control">
-    <div class="video-control-left">
-      <!--刷新键-->
-      <div class="control-btn pull-left" title="刷新">
-        <el-icon
-          class="control-btn"
-          color="inherit"
-          @click="refreshWebSocket"
-        >
-          <refresh />
-        </el-icon>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, onUnmounted, readonly, ref } from 'vue'
-import { Refresh } from '@element-plus/icons-vue'
-
-const props = defineProps({
-  dragFlag: {
-    type: Boolean,
-    default: false
-  }
-})
-
-// 获取 WebSocket 服务器地址
-const wsHost = import.meta.env.MODE === 'development'
-  ? import.meta.env.VITE_HOST_IP
-  : window.location.host
-
-const wsVideoPort = 8000
-const wsHeartbeatPort = 8001
-
-const emit = defineEmits(['video-error'])
-
-// Refs
-const videoContainer = ref<HTMLElement | null>(null)
-const videoElement = ref<HTMLVideoElement | null>(null)
-
-// 视频缩放/拖拽状态
-const scale = ref(1)
-const position = ref({ x: 0, y: 0 })
-let dragState = {
-  dragging: false,
-  startX: 0,
-  startY: 0
-}
-
-// WebSocket 状态
-let wfsObj: WfsInstance | null = null
-let heartbeatWs: WebSocket | null = null
-let heartbeatTimer: ReturnType<typeof setTimeout> | null = null
-let reconnectTimer: ReturnType<typeof setTimeout> | null = null
-let reconnectAttempts = 0
-const maxReconnectAttempts = 5
-
-// mediaError 恢复计数(不占用 reconnectAttempts 配额)
-let mediaRecoveryAttempts = 0
-const maxMediaRecoveryAttempts = 10
-
-// 防止 handleMediaError 被连续触发多次
-let isRecovering = false
-
-// 页面可见性状态
-let lastTimeStamp = 0
-let lastVideoTime = 0
-let isPageHidden = false
-
-// ==================== 工具函数 ====================
-
-const debounce = (func: (...args: any[]) => void, wait: number) => {
-  let timeout: ReturnType<typeof setTimeout>
-  return (...args: any[]) => {
-    clearTimeout(timeout)
-    timeout = setTimeout(() => func(...args), wait)
-  }
-}
-
-// ==================== 缩放和拖拽 ====================
-
-const zoom = (event: WheelEvent) => {
-  if (!props.dragFlag) return
-  event.preventDefault()
-
-  const zoomSpeed = 0.1
-  const delta = event.deltaY < 0 ? zoomSpeed : -zoomSpeed
-  scale.value = Math.max(1, Math.min(3, scale.value + delta))
-  applyTransform()
-}
-
-const startDrag = (event: MouseEvent) => {
-  if (!props.dragFlag) return
-  dragState.dragging = true
-  dragState.startX = event.clientX - position.value.x
-  dragState.startY = event.clientY - position.value.y
-  event.preventDefault()
-}
-
-const onDrag = (event: MouseEvent) => {
-  if (!dragState.dragging || !props.dragFlag) return
-  position.value.x = event.clientX - dragState.startX
-  position.value.y = event.clientY - dragState.startY
-  applyTransform()
-}
-
-const endDrag = () => {
-  dragState.dragging = false
-}
-
-const applyTransform = () => {
-  if (!videoElement.value || !videoContainer.value || !props.dragFlag) return
-
-  const videoWidth = videoElement.value.offsetWidth * scale.value
-  const videoHeight = videoElement.value.offsetHeight * scale.value
-  const containerWidth = videoContainer.value.offsetWidth
-  const containerHeight = videoContainer.value.offsetHeight
-
-  const maxX = Math.max(0, (videoWidth - containerWidth) / 2)
-  const maxY = Math.max(0, (videoHeight - containerHeight) / 2)
-
-  position.value.x = Math.min(maxX, Math.max(-maxX, position.value.x))
-  position.value.y = Math.min(maxY, Math.max(-maxY, position.value.y))
-
-  videoElement.value.style.transform =
-    `translate(${position.value.x}px, ${position.value.y}px) scale(${scale.value})`
-}
-
-// ==================== WFS 视频流管理 ====================
-
-/**
- * 彻底重置 video 元素上的 MediaSource,
- * 防止 WFS 内部 doAppending 访问残留的 mediaElement.error 导致
- * "Cannot read properties of undefined (reading 'error')" 崩溃。
- */
-const resetVideoElement = () => {
-  const video = videoElement.value
-  if (!video) return
-
-  // 暂停播放
-  video.pause()
-
-  // 断开 MediaSource 绑定
-  video.removeAttribute('src')
-  video.srcObject = null
-
-  // 强制浏览器释放旧的 MediaSource / SourceBuffer
-  video.load()
-}
-
-const createWebSocket = () => {
-  try {
-    if (!window.Wfs?.isSupported()) {
-      throw new Error('WFS not supported')
-    }
-
-    const video = videoElement.value
-    if (!video) {
-      throw new Error('Video element not found')
-    }
-
-    const socketURL = `ws://${wsHost}:${wsVideoPort}/websocket`
-
-    wfsObj = new window.Wfs({
-      wsMinPacketInterval: 2000,
-      wsMaxPacketInterval: 8000
-    })
-
-    wfsObj.attachMedia(video, 'ch1', 'H264Raw', socketURL)
-
-    wfsObj.on(window.Wfs.Events.ERROR, handleWfsError)
-    wfsObj.on(window.Wfs.Events.MANIFEST_PARSED, () => {
-      // 连接成功,重置所有恢复计数
-      reconnectAttempts = 0
-      mediaRecoveryAttempts = 0
-      isRecovering = false
-    })
-  } catch (error) {
-    console.error('Failed to create WebSocket:', error)
-    emit('video-error', error)
-    scheduleReconnect()
-  }
-}
-
-const handleWfsError = (_eventName: string, data: any) => {
-  console.error('WFS Error:', data)
-
-  if (!data.fatal) return
-
-  emit('video-error', data)
-
-  switch (data.type) {
-    case window.Wfs.ErrorTypes.MEDIA_ERROR:
-      handleMediaError()
-      break
-    case window.Wfs.ErrorTypes.NETWORK_ERROR:
-      console.warn('Network error, scheduling reconnect...')
-      scheduleReconnect()
-      break
-    default:
-      console.error('Unrecoverable error, scheduling reconnect...')
-      scheduleReconnect()
-      break
-  }
-}
-
-/**
- * mediaError 恢复策略:
- *
- * error code 3 (MEDIA_ERR_DECODE) 表示浏览器解码失败。
- * WFS 内部在连续 3 次 doAppending 检测到 error 后触发 fatal。
- *
- * 恢复方式:完整重建(销毁 WFS + 重置 video + 重建 WebSocket + 重建心跳)
- * 后端会从新的关键帧开始推流,解码器状态干净。
- *
- * mediaError 的恢复不消耗 reconnectAttempts 配额,
- * 因为这不是网络问题,而是解码问题,每次重建都有很大概率成功。
- * 给 10 次机会,持续失败才放弃。
- */
-const handleMediaError = () => {
-  if (isRecovering) return
-  isRecovering = true
-
-  mediaRecoveryAttempts++
-
-  if (mediaRecoveryAttempts <= maxMediaRecoveryAttempts) {
-    console.warn(
-      `Media error recovery ${mediaRecoveryAttempts}/${maxMediaRecoveryAttempts}, rebuilding...`
-    )
-    cleanup()
-    setTimeout(() => {
-      isRecovering = false
-      createWebSocket()
-      createHeartbeatWs()
-    }, 300)
-  } else {
-    console.error('Media error recovery exhausted')
-    mediaRecoveryAttempts = 0
-    isRecovering = false
-    emit('video-error', { type: 'mediaRecoveryFailed', fatal: true })
-  }
-}
-
-// ==================== 心跳 WebSocket ====================
-
-const createHeartbeatWs = () => {
-  try {
-    const socketURL = `ws://${wsHost}:${wsHeartbeatPort}/websocket`
-    heartbeatWs = new WebSocket(socketURL)
-
-    heartbeatWs.onopen = () => {
-      console.log('Heartbeat WebSocket connected')
-      startHeartbeat()
-    }
-
-    heartbeatWs.onmessage = (event: MessageEvent) => {
-      handleHeartbeatMessage(event)
-    }
-
-    heartbeatWs.onclose = () => {
-      console.log('Heartbeat WebSocket disconnected')
-      stopHeartbeat()
-    }
-
-    heartbeatWs.onerror = (error) => {
-      console.error('Heartbeat WebSocket error:', error)
-    }
-  } catch (error) {
-    console.error('Failed to create heartbeat WebSocket:', error)
-  }
-}
-
-const handleHeartbeatMessage = (event: MessageEvent) => {
-  try {
-    if (event.data instanceof Blob) {
-      const reader = new FileReader()
-      reader.readAsText(event.data, 'utf-8')
-      reader.onload = () => {
-        try {
-          const data = JSON.parse(reader.result as string)
-          processHeartbeatData(data)
-        } catch (e) {
-          console.error('Failed to parse heartbeat blob:', e)
-        }
-      }
-    } else if (typeof event.data === 'string') {
-      if (event.data.includes('pong')) {
-        startHeartbeat()
-        return
-      }
-      const data = JSON.parse(event.data)
-      processHeartbeatData(data)
-    }
-  } catch (error) {
-    console.error('Failed to parse heartbeat message:', error)
-  }
-}
-
-const processHeartbeatData = (data: any) => {
-  if (data.serialNum && heartbeatWs?.readyState === WebSocket.OPEN) {
-    heartbeatWs.send(JSON.stringify({ serialNum: data.serialNum }))
-  }
-}
-
-// 心跳机制
-const heartbeatConfig = {
-  timeout: 1000,
-  maxRetries: 3,
-  retryCount: 0
-}
-
-const startHeartbeat = () => {
-  stopHeartbeat()
-
-  heartbeatTimer = setTimeout(() => {
-    if (heartbeatWs?.readyState === WebSocket.OPEN) {
-      heartbeatWs.send('ping')
-      heartbeatConfig.retryCount = 0
-    } else {
-      handleHeartbeatFailure()
-    }
-  }, heartbeatConfig.timeout)
-}
-
-const stopHeartbeat = () => {
-  if (heartbeatTimer) {
-    clearTimeout(heartbeatTimer)
-    heartbeatTimer = null
-  }
-}
-
-const handleHeartbeatFailure = () => {
-  heartbeatConfig.retryCount++
-  if (heartbeatConfig.retryCount >= heartbeatConfig.maxRetries) {
-    console.warn('Heartbeat failed, reconnecting...')
-    scheduleReconnect()
-  }
-}
-
-// ==================== 重连机制 ====================
-
-const scheduleReconnect = () => {
-  if (reconnectAttempts >= maxReconnectAttempts) {
-    console.error('Max reconnection attempts reached')
-    emit('video-error', { type: 'reconnectFailed', fatal: true })
-    return
-  }
-
-  // 指数退避,最大 10 秒
-  const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000)
-  reconnectAttempts++
-
-  if (reconnectTimer) {
-    clearTimeout(reconnectTimer)
-  }
-
-  reconnectTimer = setTimeout(() => {
-    console.log(`Reconnection attempt ${reconnectAttempts}/${maxReconnectAttempts}`)
-    cleanup()
-    setTimeout(() => {
-      createWebSocket()
-      createHeartbeatWs()
-    }, 300)
-  }, delay)
-}
-
-// ==================== 清理 ====================
-
-const destroyWfs = () => {
-  if (wfsObj) {
-    try {
-      // 先移除事件监听,防止 destroy 过程中再触发 error 回调
-      wfsObj.off(window.Wfs.Events.ERROR, handleWfsError)
-      wfsObj.destroy()
-    } catch (error) {
-      console.error('Error destroying WFS:', error)
-    }
-    wfsObj = null
-  }
-}
-
-const cleanup = () => {
-  stopHeartbeat()
-
-  if (reconnectTimer) {
-    clearTimeout(reconnectTimer)
-    reconnectTimer = null
-  }
-
-  destroyWfs()
-  resetVideoElement()
-
-  if (heartbeatWs) {
-    try {
-      heartbeatWs.close()
-    } catch (error) {
-      console.error('Error closing heartbeat WebSocket:', error)
-    }
-    heartbeatWs = null
-  }
-}
-
-// 手动刷新:重置所有计数
-const refreshWebSocket = () => {
-  cleanup()
-  reconnectAttempts = 0
-  mediaRecoveryAttempts = 0
-  isRecovering = false
-
-  setTimeout(() => {
-    createWebSocket()
-    createHeartbeatWs()
-  }, 500)
-}
-
-// ==================== 页面可见性处理 ====================
-
-const handleVisibilityChange = () => {
-  const video = videoElement.value
-  if (!video) return
-
-  if (document.hidden) {
-    isPageHidden = true
-    lastTimeStamp = Date.now()
-    lastVideoTime = video.currentTime
-  } else if (isPageHidden) {
-    isPageHidden = false
-    const elapsed = (Date.now() - lastTimeStamp) / 1000
-
-    // 如果离开超过 30 秒,直接刷新连接避免 SourceBuffer 积压
-    if (elapsed > 30) {
-      console.log('Page was hidden for too long, refreshing stream...')
-      refreshWebSocket()
-    } else {
-      video.currentTime = lastVideoTime + elapsed
-    }
-  }
-}
-
-// 防抖缩放(约 60fps)
-const debouncedZoom = debounce(zoom, 16)
-
-// ==================== 生命周期 ====================
-
-onMounted(() => {
-  if (videoContainer.value) {
-    videoContainer.value.addEventListener('wheel', debouncedZoom, { passive: false })
-  }
-
-  document.addEventListener('visibilitychange', handleVisibilityChange)
-  window.addEventListener('beforeunload', cleanup)
-
-  refreshWebSocket()
-})
-
-onUnmounted(() => {
-  if (videoContainer.value) {
-    videoContainer.value.removeEventListener('wheel', debouncedZoom)
-  }
-  document.removeEventListener('visibilitychange', handleVisibilityChange)
-  window.removeEventListener('beforeunload', cleanup)
-
-  cleanup()
-})
-
-defineExpose({
-  zoom: debouncedZoom,
-  startDrag,
-  onDrag,
-  endDrag,
-  refreshWebSocket,
-  scale: readonly(scale),
-  position: readonly(position)
-})
-</script>
-
-<style lang="scss" scoped>
-.preview-container {
-  position: relative;
-  width: 100%;
-  height: 100%;
-}
-
-.video_container {
-  overflow: hidden;
-  position: relative;
-  width: 100%;
-  height: 100%;
-  background-color: #000;
-
-  video {
-    width: 100%;
-    height: 100%;
-    object-fit: contain;
-    cursor: grab;
-
-    &:active {
-      cursor: grabbing;
-    }
-  }
-}
-
-.video-control {
-  position: absolute;
-  width: 100%;
-  height: 32px;
-  line-height: 32px;
-  background: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.3));
-  left: 0;
-  right: 0;
-  bottom: 0;
-  backdrop-filter: blur(4px);
-  transition: opacity 0.3s ease;
-
-  &:hover {
-    opacity: 1;
-  }
-
-  .video-control-left {
-    position: absolute;
-    left: 8px;
-    height: 32px;
-    display: flex;
-    align-items: center;
-  }
-
-  .control-btn {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    width: 24px;
-    height: 24px;
-    background: rgba(255, 255, 255, 0.1);
-    cursor: pointer;
-    font-size: 16px;
-    color: white;
-    border-radius: 4px;
-    transition: all 0.2s ease;
-    border: none;
-
-    &:hover {
-      background: rgba(255, 255, 255, 0.2);
-      transform: scale(1.1);
-    }
-
-    &:active {
-      transform: scale(0.95);
-    }
-  }
-}
-
-@media (max-width: 768px) {
-  .video-control {
-    height: 40px;
-    line-height: 40px;
-
-    .control-btn {
-      width: 32px;
-      height: 32px;
-      font-size: 18px;
-    }
-  }
-}
-</style>

+ 0 - 7
src/main.ts

@@ -9,13 +9,6 @@ import { View, Setting, User, Lock, Expand, Fold, Refresh, Camera, Microphone }
 import './assets/main.css'
 import 'element-plus/dist/index.css'
 
-// 开发环境启用 vConsole 方便调试
-if (import.meta.env.MODE === 'development') {
-  import('vconsole').then(({ default: VConsole }) => {
-    new VConsole()
-  })
-}
-
 const app = createApp(App)
 
 // 注册需要的图标组件

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff