Ver código fonte

feat(video-center): 优化海康 H5Player 初始化及异步控制

- 改为按需初始化播放器资源
- 根据跨源隔离状态启用多线程解码
- 合并并串行处理播放器尺寸调整
- 防止分屏、全屏和停止操作重复触发
- 完善加载状态及页面卸载后的异常处理
Zimo 1 semana atrás
pai
commit
dcd37a0db5
1 arquivos alterados com 83 adições e 15 exclusões
  1. 83 15
      src/views/pms/video_center/haikang/index.vue

+ 83 - 15
src/views/pms/video_center/haikang/index.vue

@@ -35,6 +35,7 @@ const previewWindowStreamTypes = ref<Record<number, HikvisionStreamType>>({})
 const interruptedWindowIndexes = ref<number[]>([])
 const interruptedWindowIndexes = ref<number[]>([])
 const previewError = ref('')
 const previewError = ref('')
 const captureAllLoading = ref(false)
 const captureAllLoading = ref(false)
+const stopAllLoading = ref(false)
 const soundWindowIndex = ref<number>()
 const soundWindowIndex = ref<number>()
 const soundControlLoading = ref(false)
 const soundControlLoading = ref(false)
 const talkWindowIndex = ref<number>()
 const talkWindowIndex = ref<number>()
@@ -48,8 +49,10 @@ const hoveredWindowIndex = ref<number>()
 // 鼠标进入页面叠加按钮后 SDK 会触发 windowEventOut,单独记录按钮悬浮以保持栏可见。
 // 鼠标进入页面叠加按钮后 SDK 会触发 windowEventOut,单独记录按钮悬浮以保持栏可见。
 const hoveredControlWindowIndex = ref<number>()
 const hoveredControlWindowIndex = ref<number>()
 const splitPopoverVisible = ref(false)
 const splitPopoverVisible = ref(false)
+const splitControlLoading = ref(false)
 const sidebarCollapsed = ref(false)
 const sidebarCollapsed = ref(false)
 const stageFullscreen = ref(false)
 const stageFullscreen = ref(false)
+const fullscreenControlLoading = ref(false)
 
 
 type SplitCount = 1 | 4 | 9 | 16
 type SplitCount = 1 | 4 | 9 | 16
 
 
@@ -100,6 +103,8 @@ let componentUnmounted = false
 // ResizeObserver 负责容器尺寸变化,requestAnimationFrame 用于合并同一帧内的重复 resize。
 // ResizeObserver 负责容器尺寸变化,requestAnimationFrame 用于合并同一帧内的重复 resize。
 let playerResizeObserver: ResizeObserver | undefined
 let playerResizeObserver: ResizeObserver | undefined
 let playerResizeFrame: number | undefined
 let playerResizeFrame: number | undefined
+let playerResizeInProgress = false
+let playerResizePending = false
 
 
 const currentSplitColumns = computed(() => Math.sqrt(currentSplitCount.value))
 const currentSplitColumns = computed(() => Math.sqrt(currentSplitCount.value))
 const currentSplitRows = computed(() => currentSplitColumns.value)
 const currentSplitRows = computed(() => currentSplitColumns.value)
@@ -125,6 +130,7 @@ function clearPreviewError() {
 }
 }
 
 
 function showPreviewError(message: string) {
 function showPreviewError(message: string) {
+  if (componentUnmounted) return
   clearPreviewErrorTimer()
   clearPreviewErrorTimer()
   previewError.value = message
   previewError.value = message
   previewErrorTimer = setTimeout(() => {
   previewErrorTimer = setTimeout(() => {
@@ -416,7 +422,9 @@ function initPlayer(): Promise<HikvisionPlayerInstance> {
         szBasePath: getHikvisionPlayerBasePath(),
         szBasePath: getHikvisionPlayerBasePath(),
         iMaxSplit: MAX_SPLIT_COLUMNS,
         iMaxSplit: MAX_SPLIT_COLUMNS,
         iCurrentSplit: DEFAULT_SPLIT_COLUMNS,
         iCurrentSplit: DEFAULT_SPLIT_COLUMNS,
-        mseWorkerEnable: false,
+        // 指南建议高分辨率画面开启多线程解码;仅在页面具备跨源隔离时启用,
+        // 避免未配置 COOP/COEP 的部署环境错误使用 SharedArrayBuffer。
+        mseWorkerEnable: window.crossOriginIsolated,
         bSupporDoubleClickFull: true,
         bSupporDoubleClickFull: true,
         oStyle: { borderSelect: '#facc15' }
         oStyle: { borderSelect: '#facc15' }
       })
       })
@@ -526,8 +534,8 @@ function initPlayer(): Promise<HikvisionPlayerInstance> {
       // 初始化失败也要销毁半成品实例,否则 Worker、WASM 或媒体资源可能残留。
       // 初始化失败也要销毁半成品实例,否则 Worker、WASM 或媒体资源可能残留。
       await initializingPlayer?.JS_StopRealPlayAll().catch(() => undefined)
       await initializingPlayer?.JS_StopRealPlayAll().catch(() => undefined)
       await Promise.resolve(initializingPlayer?.JS_Destroy()).catch(() => undefined)
       await Promise.resolve(initializingPlayer?.JS_Destroy()).catch(() => undefined)
-      console.error('H5player 初始化失败', error)
       if (!componentUnmounted) {
       if (!componentUnmounted) {
+        console.error('H5player 初始化失败', error)
         showPreviewError(error instanceof Error ? error.message : '播放器初始化失败')
         showPreviewError(error instanceof Error ? error.message : '播放器初始化失败')
       }
       }
       throw error
       throw error
@@ -540,6 +548,11 @@ function initPlayer(): Promise<HikvisionPlayerInstance> {
 }
 }
 
 
 async function playCameraPreview(camera: HikvisionCameraNode) {
 async function playCameraPreview(camera: HikvisionCameraNode) {
+  if (stopAllLoading.value || splitControlLoading.value) {
+    showPreviewError('播放器正在切换状态,请稍后再选择监控点')
+    return
+  }
+
   // 点击监控点时固定目标窗口,之后用户切换选中窗口不会改变本次请求的播放位置。
   // 点击监控点时固定目标窗口,之后用户切换选中窗口不会改变本次请求的播放位置。
   const windowIndex = activeWindowIndex.value
   const windowIndex = activeWindowIndex.value
   const requestId = beginWindowPreviewRequest(windowIndex)
   const requestId = beginWindowPreviewRequest(windowIndex)
@@ -971,8 +984,13 @@ async function stopWindowPreview(windowIndex: number) {
 
 
 async function handleSplitChange(count: SplitCount, columns: HikvisionSplitColumns) {
 async function handleSplitChange(count: SplitCount, columns: HikvisionSplitColumns) {
   splitPopoverVisible.value = false
   splitPopoverVisible.value = false
-  if (currentSplitCount.value === count) return
+  if (splitControlLoading.value || currentSplitCount.value === count) return
+  if (stopAllLoading.value) {
+    showPreviewError('正在关闭全部监控画面,请稍后切换分屏')
+    return
+  }
 
 
+  splitControlLoading.value = true
   try {
   try {
     const currentPlayer = await initPlayer()
     const currentPlayer = await initPlayer()
 
 
@@ -1005,6 +1023,8 @@ async function handleSplitChange(count: SplitCount, columns: HikvisionSplitColum
   } catch (error) {
   } catch (error) {
     console.error('切换监控分屏失败', error)
     console.error('切换监控分屏失败', error)
     showPreviewError(error instanceof Error ? error.message : '切换分屏失败')
     showPreviewError(error instanceof Error ? error.message : '切换分屏失败')
+  } finally {
+    splitControlLoading.value = false
   }
   }
 }
 }
 
 
@@ -1019,17 +1039,37 @@ function handleNodeClick(data: HikvisionTreeNode) {
   void playCameraPreview(data)
   void playCameraPreview(data)
 }
 }
 
 
-function resizePlayer() {
+async function resizePlayer() {
   playerResizeFrame = undefined
   playerResizeFrame = undefined
+  const currentPlayer = player
+  if (!currentPlayer) return
+
+  // ResizeObserver 在侧栏动画期间可能连续触发。SDK resize 尚未结束时只保留一次尾调用,
+  // 避免多个 WASM/Canvas 尺寸计算并发堆积。
+  if (playerResizeInProgress) {
+    playerResizePending = true
+    return
+  }
+
   const playerContainer = playerContainerRef.value
   const playerContainer = playerContainerRef.value
   if (!playerContainer) {
   if (!playerContainer) {
-    player?.JS_Resize().catch(() => undefined)
     return
     return
   }
   }
 
 
   const { clientWidth, clientHeight } = playerContainer
   const { clientWidth, clientHeight } = playerContainer
   if (!clientWidth || !clientHeight) return
   if (!clientWidth || !clientHeight) return
-  player?.JS_Resize(clientWidth, clientHeight).catch(() => undefined)
+  playerResizeInProgress = true
+  try {
+    await currentPlayer.JS_Resize(clientWidth, clientHeight)
+  } catch {
+    // 容器变化和播放器销毁可能发生在同一帧,尺寸同步失败无需打断预览。
+  } finally {
+    playerResizeInProgress = false
+    if (playerResizePending && !componentUnmounted) {
+      playerResizePending = false
+      schedulePlayerResize()
+    }
+  }
 }
 }
 
 
 /** 合并同一帧内的尺寸变化,避免侧栏动画或布局变化时连续调用昂贵的 SDK resize。 */
 /** 合并同一帧内的尺寸变化,避免侧栏动画或布局变化时连续调用昂贵的 SDK resize。 */
@@ -1039,10 +1079,17 @@ function schedulePlayerResize() {
 }
 }
 
 
 async function stopAllPreviews() {
 async function stopAllPreviews() {
+  if (stopAllLoading.value) return
+  if (splitControlLoading.value) {
+    showPreviewError('正在切换分屏,请稍后关闭全部画面')
+    return
+  }
+
   // 先让所有未完成的接口请求失效,防止停止完成后某个旧请求又调用 JS_Play。
   // 先让所有未完成的接口请求失效,防止停止完成后某个旧请求又调用 JS_Play。
   cancelAllWindowPreviewRequests()
   cancelAllWindowPreviewRequests()
   clearPreviewError()
   clearPreviewError()
 
 
+  stopAllLoading.value = true
   try {
   try {
     const currentTalkWindowIndex = talkWindowIndex.value ?? talkTargetWindowIndex.value
     const currentTalkWindowIndex = talkWindowIndex.value ?? talkTargetWindowIndex.value
     if (currentTalkWindowIndex !== undefined) await stopWindowTalk(currentTalkWindowIndex)
     if (currentTalkWindowIndex !== undefined) await stopWindowTalk(currentTalkWindowIndex)
@@ -1056,16 +1103,23 @@ async function stopAllPreviews() {
   } catch (error) {
   } catch (error) {
     console.error('关闭全部监控画面失败', error)
     console.error('关闭全部监控画面失败', error)
     showPreviewError(error instanceof Error ? error.message : '关闭全部监控画面失败')
     showPreviewError(error instanceof Error ? error.message : '关闭全部监控画面失败')
+  } finally {
+    stopAllLoading.value = false
   }
   }
 }
 }
 
 
 async function toggleFullscreen() {
 async function toggleFullscreen() {
+  if (fullscreenControlLoading.value) return
+
+  fullscreenControlLoading.value = true
   try {
   try {
     const currentPlayer = await initPlayer()
     const currentPlayer = await initPlayer()
     await currentPlayer.JS_FullScreenDisplay(!stageFullscreen.value)
     await currentPlayer.JS_FullScreenDisplay(!stageFullscreen.value)
   } catch (error) {
   } catch (error) {
     console.error('切换监控全屏失败', error)
     console.error('切换监控全屏失败', error)
     showPreviewError(error instanceof Error ? error.message : '切换全屏失败')
     showPreviewError(error instanceof Error ? error.message : '切换全屏失败')
+  } finally {
+    fullscreenControlLoading.value = false
   }
   }
 }
 }
 
 
@@ -1084,6 +1138,7 @@ async function destroyPlayer() {
   playerResizeObserver = undefined
   playerResizeObserver = undefined
   if (playerResizeFrame !== undefined) cancelAnimationFrame(playerResizeFrame)
   if (playerResizeFrame !== undefined) cancelAnimationFrame(playerResizeFrame)
   playerResizeFrame = undefined
   playerResizeFrame = undefined
+  playerResizePending = false
   if (!player) return
   if (!player) return
 
 
   const currentPlayer = player
   const currentPlayer = player
@@ -1110,7 +1165,6 @@ onMounted(() => {
     playerResizeObserver.observe(playerContainerRef.value)
     playerResizeObserver.observe(playerContainerRef.value)
   }
   }
   void loadRootTree()
   void loadRootTree()
-  void initPlayer().catch(() => undefined)
 })
 })
 
 
 onBeforeUnmount(() => {
 onBeforeUnmount(() => {
@@ -1346,8 +1400,11 @@ onBeforeUnmount(() => {
                 type="button"
                 type="button"
                 class="stop-all-trigger"
                 class="stop-all-trigger"
                 aria-label="关闭全部监控画面"
                 aria-label="关闭全部监控画面"
+                :aria-busy="stopAllLoading"
+                :disabled="stopAllLoading"
                 @click="stopAllPreviews">
                 @click="stopAllPreviews">
-                <i class="i-lucide:video-off"></i>
+                <i v-if="stopAllLoading" class="i-lucide:loader-circle animate-spin"></i>
+                <i v-else class="i-lucide:video-off"></i>
               </button>
               </button>
             </el-tooltip>
             </el-tooltip>
 
 
@@ -1387,8 +1444,11 @@ onBeforeUnmount(() => {
                 class="layout-trigger"
                 class="layout-trigger"
                 :class="{ active: splitPopoverVisible }"
                 :class="{ active: splitPopoverVisible }"
                 aria-label="选择分屏布局"
                 aria-label="选择分屏布局"
-                :aria-expanded="splitPopoverVisible">
-                <i class="i-lucide:layout-grid"></i>
+                :aria-busy="splitControlLoading"
+                :aria-expanded="splitPopoverVisible"
+                :disabled="splitControlLoading">
+                <i v-if="splitControlLoading" class="i-lucide:loader-circle animate-spin"></i>
+                <i v-else class="i-lucide:layout-grid"></i>
               </button>
               </button>
             </el-tooltip>
             </el-tooltip>
 
 
@@ -1411,6 +1471,7 @@ onBeforeUnmount(() => {
                   :title="`${option.count} 分屏`"
                   :title="`${option.count} 分屏`"
                   :aria-label="`切换为 ${option.count} 分屏`"
                   :aria-label="`切换为 ${option.count} 分屏`"
                   :aria-pressed="currentSplitCount === option.count"
                   :aria-pressed="currentSplitCount === option.count"
+                  :disabled="splitControlLoading"
                   @click="handleSplitChange(option.count, option.columns)">
                   @click="handleSplitChange(option.count, option.columns)">
                   <span
                   <span
                     class="split-menu-icon"
                     class="split-menu-icon"
@@ -1426,8 +1487,11 @@ onBeforeUnmount(() => {
                 class="fullscreen-trigger"
                 class="fullscreen-trigger"
                 :aria-label="stageFullscreen ? '退出全屏' : '全屏'"
                 :aria-label="stageFullscreen ? '退出全屏' : '全屏'"
                 :aria-pressed="stageFullscreen"
                 :aria-pressed="stageFullscreen"
+                :aria-busy="fullscreenControlLoading"
+                :disabled="fullscreenControlLoading"
                 @click="toggleFullscreen">
                 @click="toggleFullscreen">
-                <i v-if="stageFullscreen" class="i-lucide:minimize"></i>
+                <i v-if="fullscreenControlLoading" class="i-lucide:loader-circle animate-spin"></i>
+                <i v-else-if="stageFullscreen" class="i-lucide:minimize"></i>
                 <i v-else class="i-lucide:maximize"></i>
                 <i v-else class="i-lucide:maximize"></i>
               </button>
               </button>
             </el-tooltip>
             </el-tooltip>
@@ -1899,20 +1963,24 @@ onBeforeUnmount(() => {
 }
 }
 
 
 .sidebar-toggle:hover,
 .sidebar-toggle:hover,
-.stop-all-trigger:hover,
+.stop-all-trigger:hover:not(:disabled),
 .capture-all-trigger:hover:not(:disabled),
 .capture-all-trigger:hover:not(:disabled),
 .sound-all-trigger:hover:not(:disabled),
 .sound-all-trigger:hover:not(:disabled),
 .sound-all-trigger.active,
 .sound-all-trigger.active,
-.fullscreen-trigger:hover,
+.fullscreen-trigger:hover:not(:disabled),
 .fullscreen-trigger[aria-pressed='true'],
 .fullscreen-trigger[aria-pressed='true'],
-.layout-trigger:hover,
+.layout-trigger:hover:not(:disabled),
 .layout-trigger.active {
 .layout-trigger.active {
   color: #fff;
   color: #fff;
   background: #2b3746;
   background: #2b3746;
 }
 }
 
 
+.stop-all-trigger:disabled,
 .capture-all-trigger:disabled,
 .capture-all-trigger:disabled,
-.sound-all-trigger:disabled {
+.sound-all-trigger:disabled,
+.fullscreen-trigger:disabled,
+.layout-trigger:disabled,
+.split-menu-button:disabled {
   color: #64748b;
   color: #64748b;
   cursor: not-allowed;
   cursor: not-allowed;
 }
 }