Explorar o código

Merge branch 'video_splitview' of shuzhihua/pms-iot-vue into master

yanghao hai 9 horas
pai
achega
048d5b811f
Modificáronse 2 ficheiros con 156 adicións e 89 borrados
  1. 1 1
      .env.local
  2. 155 88
      src/views/pms/video_center/sip/splitview.vue

+ 1 - 1
.env.local

@@ -4,7 +4,7 @@ NODE_ENV=development
 VITE_DEV=true
 
 # 请求路径  http://192.168.188.149:48080  https://iot.deepoil.cc  http://172.26.0.3:48080
-VITE_BASE_URL='https://iot.deepoil.cc'
+VITE_BASE_URL='https://aims.deepoil.cc'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server

+ 155 - 88
src/views/pms/video_center/sip/splitview.vue

@@ -7,11 +7,7 @@
       :body-style="{ padding: '0px' }"
       class="border-none"
     >
-      <el-container
-        v-loading="loading"
-        style="height: 100%"
-        :element-loading-text="t('sip.splitview998531-0')"
-      >
+      <el-container style="height: 100%">
         <el-aside width="250px" style="background-color: #ffffff">
           <DeviceTree :click-event="clickEvent" />
         </el-aside>
@@ -56,23 +52,25 @@
             </el-button>
           </div>
           <div style="height: 85vh; display: flex; flex-wrap: wrap">
-            <!-- 只渲染实际可用的通道数量,不超过当前分屏数量 -->
+            <!-- 渲染所有分屏数量的播放框 -->
             <div
-              v-for="i in Math.min(spilt, availableChannels.length || spilt)"
+              v-for="i in spilt"
               :key="i"
               class="play-box"
               :style="liveStyle"
               :class="{ redborder: playerIdx == i - 1 }"
               @click="playerIdx = i - 1"
             >
+              <!-- 显示序号,如果该位置没有视频或不在可用通道范围内 -->
               <div
-                v-if="!videoUrl[i - 1]"
+                v-if="!videoUrl[i - 1] || i - 1 >= availableChannels.length"
                 style="color: #ffffff; font-size: 30px; font-weight: bold"
               >
                 {{ i }}
               </div>
+              <!-- 只有当有视频URL时才渲染播放器 -->
               <player
-                v-else
+                v-if="videoUrl[i - 1]"
                 :ref="(el) => setPlayerRef(el, i - 1)"
                 :videourl="videoUrl[i - 1]"
                 :playerInfo="
@@ -88,28 +86,6 @@
                 @destroy="destroy"
               />
             </div>
-
-            <!-- 如果通道数少于分屏数,显示空白占位 -->
-            <div
-              v-for="i in Math.max(0, spilt - availableChannels.length)"
-              :key="`empty-${i}`"
-              class="play-box"
-              :style="liveStyle"
-            >
-              <div
-                style="
-                  color: #ffffff;
-                  font-size: 30px;
-                  font-weight: bold;
-                  display: flex;
-                  align-items: center;
-                  justify-content: center;
-                  height: 100%;
-                "
-              >
-                {{ availableChannels.length + i }}
-              </div>
-            </div>
           </div>
         </el-main>
       </el-container>
@@ -178,42 +154,119 @@ const liveStyle = computed(() => {
   return style
 })
 
-// 监听分屏变化
+const loadAdditionalChannelsForSplit = async (newSplit, oldSplit) => {
+  loading.value = true
+  try {
+    const currentlyLoaded = Math.min(oldSplit, availableChannels.value.length)
+    const toBeLoaded = Math.min(newSplit, availableChannels.value.length)
+
+    // 使用数组存储所有要执行的异步操作
+    const operations = []
+
+    for (let i = currentlyLoaded; i < toBeLoaded; i++) {
+      const channelData = availableChannels.value[i]
+
+      if (channelData._playUrl) {
+        // 同步操作:立即设置URL
+        setPlayUrl(channelData._playUrl, i)
+      } else {
+        // 异步操作:稍后并行执行
+        operations.push({
+          index: i,
+          channelData,
+          playData: {
+            deviceSipId: channelData.deviceId,
+            channelSipId: channelData.basicData?.channelSipId || channelData.id,
+            name: channelData.name || `通道${i + 1}`,
+            ...channelData.basicData
+          }
+        })
+      }
+    }
+
+    // 并行执行所有sendDevicePush请求
+    if (operations.length > 0) {
+      const promises = operations.map(({ playData, index, channelData }) =>
+        sendDevicePush(playData, index)
+          .then(() => {
+            // 成功后的回调
+            if (videoUrl.value[index]) {
+              channelData._playUrl = videoUrl.value[index]
+            }
+            return { success: true, index }
+          })
+          .catch((error) => {
+            console.error(`通道${index + 1}加载失败:`, error)
+            return { success: false, index, error }
+          })
+      )
+
+      // 使用allSettled而不是all,这样即使有失败也不会中断其他请求
+      await Promise.allSettled(promises)
+    }
+  } catch (error) {
+    console.error('加载新增通道出错:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 方法2:并行销毁,提高效率
+const destroyAllPlayersAsync = async () => {
+  if (!playerRefs.value || playerRefs.value.length === 0) {
+    console.log('没有播放器需要销毁')
+    return true
+  }
+
+  console.log(`开始并行销毁 ${playerRefs.value.length} 个播放器`)
+
+  try {
+    // 收集所有销毁 Promise
+    const destroyPromises = playerRefs.value
+      .filter((player) => player && typeof player.destroy === 'function')
+      .map((player, index) => {
+        return Promise.resolve().then(async () => {
+          try {
+            await player.destroy()
+            console.log(`播放器 ${index} 销毁完成`)
+            return { success: true, index }
+          } catch (error) {
+            console.error(`播放器 ${index} 销毁失败:`, error)
+            return { success: false, index, error }
+          }
+        })
+      })
+
+    // 并行执行所有销毁
+    const results = await Promise.allSettled(destroyPromises)
+
+    // 统计结果
+    const successCount = results.filter((r) => r.status === 'fulfilled' && r.value.success).length
+    const failedCount = results.length - successCount
+
+    console.log(`销毁完成: 成功 ${successCount} 个, 失败 ${failedCount} 个`)
+
+    // 清空引用数组
+    playerRefs.value = []
+
+    return failedCount === 0
+  } catch (error) {
+    console.error('并行销毁过程中出错:', error)
+    return false
+  }
+}
+
 watch(
   () => spilt.value,
   async (newSplit, oldSplit) => {
-    // 只有当分屏数增加(例如从1到4,或从4到9)且有当前选中设备时才执行
-    if (newSplit > oldSplit && currentDevice.value) {
-      console.log(
-        `分屏从 ${oldSplit} 变为 ${newSplit},自动填充设备 ${currentDevice.value.name} 的通道`
-      )
-
-      // 延迟执行,确保DOM更新完成
+    // 先销毁所有现有播放器,确保资源释放
+    await destroyAllPlayersAsync()
+    // 如果当前有选中的设备
+    if (currentDevice.value && availableChannels.value.length > 0) {
       await nextTick()
 
-      loading.value = true
-      try {
-        // 重新分配通道到所有播放器
-        await assignChannelsToPlayers(availableChannels.value)
-      } catch (error) {
-        console.error('分屏变化时填充通道出错:', error)
-        ElMessage.error('自动填充通道失败')
-      } finally {
-        loading.value = false
-      }
-    }
-    // 当分屏数减少时,清空超出当前通道数的播放器
-    else if (newSplit < oldSplit) {
-      // 清空超出当前分屏数或超出通道数的播放器
-      const maxVisible = Math.min(newSplit, availableChannels.value.length)
-      for (let i = maxVisible; i < Math.max(oldSplit, availableChannels.value.length); i++) {
-        const newVideoUrls = [...videoUrl.value]
-        if (newVideoUrls[i]) {
-          newVideoUrls[i] = ''
-          videoUrl.value = newVideoUrls
-          clear(i + 1) // 清理存储的数据
-        }
-      }
+      // 只加载新出现的播放器,不重新加载已有播放器
+      await loadAdditionalChannelsForSplit(newSplit, oldSplit)
     }
   },
   { immediate: false }
@@ -244,45 +297,39 @@ const destroy = (idx) => {
 const currentDevice = ref(null) // 存储当前选中的设备信息
 // 点击事件
 let playerInfo = ref([])
+
 const clickEvent = async (data) => {
   // 情况1:点击的是设备节点 (type === 0)
   if (data.type === 0) {
+    // 重置选中的空闲槽位
+    selectedEmptySlot.value = -1
+
     currentDevice.value = {
       id: data.userData?.serialNumber,
       name: data.name,
       data: data
     }
-    const deviceId = data.userData?.serialNumber // 从树节点数据中提取设备ID
+    const deviceId = data.userData?.serialNumber
     if (!deviceId) return
 
     loading.value = true
     try {
-      // 1. 获取该设备下的所有通道
       const channels = await getDeviceChannels(deviceId)
-
-      // 只保留摄像头类型的通道
       const cameraChannels = channels.filter((channel) => channel.basicData.model === 'Camera')
       availableChannels.value = cameraChannels
 
-      // 清空之前的数据
       playerInfo.value = []
       cameraChannels.forEach((channel) => {
         playerInfo.value.push(channel)
       })
 
-      console.log('playerInfo>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>', playerInfo.value)
-      console.log('availableChannels>>>>>>>>>>>>>>>>>>>>>>>>>>>>>', availableChannels.value)
-      console.log('videourl**************************', videoUrl.value)
-
       if (!cameraChannels || cameraChannels.length === 0) {
         console.warn('该设备下没有可用的摄像头通道')
-        // 清空所有播放器
         const newVideoUrls = Array(spilt.value).fill('')
         videoUrl.value = newVideoUrls
         return
       }
 
-      // 2. 将通道智能分配给播放器
       await assignChannelsToPlayers(cameraChannels)
     } catch (error) {
       console.error('处理设备通道时出错:', error)
@@ -290,9 +337,20 @@ const clickEvent = async (data) => {
       loading.value = false
     }
   }
-  // 情况2:点击的是具体的通道节点,保持原有逻辑不变
+  // 情况2:点击的是具体的通道节点
   else if (data.userData?.channelSipId) {
-    sendDevicePush(data.userData)
+    // 检查是否有选中的空闲槽位
+    let targetIndex = selectedEmptySlot.value !== -1 ? selectedEmptySlot.value : playerIdx.value
+
+    // 验证目标索引是否在有效范围内
+    if (targetIndex >= spilt.value) {
+      targetIndex = playerIdx.value // 回退到当前选中的播放器
+    }
+
+    // 重置选中的空闲槽位
+    selectedEmptySlot.value = -1
+
+    await sendDevicePush(data.userData, targetIndex)
   }
 }
 
@@ -310,39 +368,35 @@ const getDeviceChannels = async (deviceId) => {
 const assignChannelsToPlayers = async (channels) => {
   const currentSplit = spilt.value
   const actualChannels = Math.min(channels.length, currentSplit)
-
-  console.log(`分配 ${actualChannels} 个通道到最多 ${currentSplit} 个播放窗口`)
-
-  // 清空当前所有播放器
-  const newVideoUrls = Array(Math.max(currentSplit, channels.length)).fill('')
+  // 初始化所有播放器位置
+  const newVideoUrls = Array(currentSplit).fill('')
   videoUrl.value = newVideoUrls
 
   // 为每个可用通道分配播放器
   for (let i = 0; i < actualChannels; i++) {
-    // 延迟执行,避免同时发起大量请求
     await new Promise((resolve) => setTimeout(resolve, 50))
 
     const channelData = channels[i]
-    // 构建符合 sendDevicePush 要求的参数格式
     const playData = {
       deviceSipId: channelData.deviceId,
       channelSipId: channelData.basicData?.channelSipId || channelData.id,
       name: channelData.name || `通道${i + 1}`,
       ...channelData.basicData
     }
-    console.log(`将通道 ${playData.name} 分配给播放器 ${i}`)
+
     await sendDevicePush(playData, i)
   }
-
-  // 如果通道数少于当前分屏数,不需要做额外处理,因为已经清空了超出部分
 }
 
 // 通知设备上传媒体流
-const sendDevicePush = (itemData, targetIndex = null) => {
+const sendDevicePush = async (itemData, targetIndex = null) => {
   // 关键修改:允许从外部传入 targetIndex,默认使用当前激活的 playerIdx
   const playIndex = targetIndex !== null ? targetIndex : playerIdx.value
 
-  save(itemData, playIndex) // 保存数据也要使用正确的索引
+  // 确保索引在有效范围内
+  const validIndex = Math.min(playIndex, spilt.value - 1)
+
+  save(itemData, validIndex) // 保存数据也要使用正确的索引
 
   let deviceId = itemData.deviceSipId
   let channelId = itemData.channelSipId
@@ -357,7 +411,7 @@ const sendDevicePush = (itemData, targetIndex = null) => {
         itemData.playUrl = res.playurl
       }
       itemData.streamId = res.streamId
-      setPlayUrl(itemData.playUrl, playIndex) // 播放URL设置到指定位置
+      setPlayUrl(itemData.playUrl, validIndex) // 播放URL设置到指定位置
     })
     .finally(() => {
       loading.value = false
@@ -367,6 +421,7 @@ const sendDevicePush = (itemData, targetIndex = null) => {
 let playInfoRes = ref('')
 // 设置播放URL
 const setPlayUrl = (url, idx) => {
+  console.log('设置播放URL:', url, '索引:', idx)
   // 确保数组长度足够
   while (videoUrl.value.length <= idx) {
     videoUrl.value.push('')
@@ -389,6 +444,18 @@ const checkPlayByParam = () => {
   }
 }
 
+// 存储当前选中的空闲槽位
+const selectedEmptySlot = ref(-1)
+
+const handleEmpty = (i) => {
+  // 计算当前点击的空闲槽位的实际索引
+  const slotIndex = availableChannels.value.length + i - 1
+  selectedEmptySlot.value = slotIndex
+  playerIdx.value = slotIndex
+
+  console.log('选中空闲槽位:', slotIndex)
+}
+
 // 截图处理
 const shot = (e) => {
   var base64ToBlob = function (code) {