|
|
@@ -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) {
|