| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- <template>
- <div style="height: 100%">
- <el-card id="devicePosition" shadow="never" style="height: calc(100% - 40px);border: 0;" :body-style="{ padding: '0px' }" class="border-none">
- <el-container
- v-loading="loading"
- style="height: 100%"
- :element-loading-text="t('sip.splitview998531-0')"
- >
- <el-aside width="250px" style="background-color: #ffffff">
- <DeviceTree :click-event="clickEvent" />
- </el-aside>
- <el-main style="padding: 0">
- <div
- height="5vh"
- style="text-align: left; font-size: 17px; line-height: 5vh; margin-bottom: 10px"
- >
- {{ t('sip.splitview998531-1') }}
- <el-button
- type="success"
- style="margin-left: 10px"
- :class="{ active: spilt == 1 }"
- @click="spilt = 1"
- :icon="FullScreen"
- plain
- size="small"
- >
- {{ t('sip.splitview998531-2') }}
- </el-button>
- <el-button
- type="info"
- style="margin-left: 10px"
- :class="{ active: spilt == 4 }"
- @click="spilt = 4"
- :icon="Menu"
- plain
- size="small"
- >
- {{ t('sip.splitview998531-3') }}
- </el-button>
- <el-button
- type="warning"
- style="margin-left: 10px"
- :class="{ active: spilt == 9 }"
- @click="spilt = 9"
- :icon="Grid"
- plain
- size="small"
- >
- {{ t('sip.splitview998531-4') }}
- </el-button>
- </div>
- <div style="height: 85vh; display: flex; flex-wrap: wrap">
- <div
- 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]"
- style="color: #ffffff; font-size: 30px; font-weight: bold"
- >{{ i }}</div
- >
- <player
- v-else
- :ref="(el) => setPlayerRef(el, i - 1)"
- :video-url="videoUrl[i - 1]"
- fluent
- autoplay
- @screenshot="shot"
- class="player-wrap"
- @destroy="destroy"
- />
- </div>
- </div>
- </el-main>
- </el-container>
- </el-card>
- </div>
- </template>
- <script setup>
- import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
- import { useRoute } from 'vue-router'
- import { FullScreen, Menu, Grid } from '@element-plus/icons-vue'
- import player from './components/player/jessibuca.vue'
- import DeviceTree from './components/player/DeviceTree.vue'
- import { startPlay } from '@/api/pms/video/channel'
- import { listSipDeviceChannel } from '@/api/pms/video/sipdevice';
- const { t } = useI18n()
- // 使用组合式API
- const route = useRoute()
- // refs
- const playerRefs = ref([])
- // 数据状态
- const videoUrl = ref([''])
- const spilt = ref(1) //分屏
- const playerIdx = ref(0) //激活播放器
- const updateLooper = ref(0) //数据刷新轮训标志
- const count = ref(15)
- const total = ref(0)
- const loading = ref(false)
- // 设置播放器引用
- const setPlayerRef = (el, index) => {
- if (el) {
- playerRefs.value[index] = el
- }
- }
- // 计算属性 - 直播样式
- const liveStyle = computed(() => {
- let style = { width: '81%', height: '99%' }
- switch (spilt.value) {
- case 4:
- style = { width: '40%', height: '49%' }
- break
- case 9:
- style = { width: '27%', height: '32%' }
- break
- }
- nextTick(() => {
- for (let i = 0; i < spilt.value; i++) {
- const player = playerRefs.value[i]
- if (player && typeof player.updatePlayerDomSize === 'function') {
- player.updatePlayerDomSize()
- }
- }
- })
- return style
- })
- // 监听分屏变化
- watch(
- () => spilt.value,
- async (newSplit, oldSplit) => {
- // 只有当分屏数增加(例如从1到4,或从4到9)且有当前选中设备时才执行
- if (newSplit > oldSplit && currentDevice.value) {
- console.log(`分屏从 ${oldSplit} 变为 ${newSplit},自动填充设备 ${currentDevice.value.name} 的通道`)
-
- // 延迟执行,确保DOM更新完成
- await nextTick()
-
- loading.value = true
- try {
- // 1. 重新获取当前设备的通道
- const channels = await getDeviceChannels(currentDevice.value.id)
-
- if (!channels || channels.length === 0) {
- console.warn('该设备下没有可用通道')
- return
- }
-
- // 2. 重新分配通道到所有播放器
- await assignChannelsToPlayers(channels)
-
- } catch (error) {
- console.error('分屏变化时填充通道出错:', error)
- ElMessage.error('自动填充通道失败')
- } finally {
- loading.value = false
- }
- }
- // 可选:当分屏数减少时,可以清空多余的播放器
- else if (newSplit < oldSplit) {
- // 清空超出当前分屏数的播放器
- for (let i = newSplit; i < oldSplit; i++) {
- const newVideoUrls = [...videoUrl.value]
- newVideoUrls[i] = ''
- videoUrl.value = newVideoUrls
- clear(i + 1) // 清理存储的数据
- }
- }
- },
- { immediate: false } // 不需要立即执行
- )
- // 监听路由变化
- watch(
- () => route.fullPath,
- () => {
- checkPlayByParam()
- }
- )
- // 生命周期钩子
- onMounted(() => {
- checkPlayByParam()
- })
- onUnmounted(() => {
- clearTimeout(updateLooper.value)
- })
- // 销毁事件
- const destroy = (idx) => {
- console.log(idx)
- clear(idx.substring(idx.length - 1))
- }
- const currentDevice = ref(null) // 存储当前选中的设备信息
- // 点击事件
- const clickEvent = async (data) => {
- // 情况1:点击的是设备节点 (type === 0)
- if (data.type === 0) {
- currentDevice.value = {
- id: data.userData?.serialNumber,
- name: data.name,
- data: data
- }
- const deviceId = data.userData?.serialNumber; // 从树节点数据中提取设备ID
- if (!deviceId) return;
- loading.value = true;
- try {
- // 1. 获取该设备下的所有通道
- const channels = await getDeviceChannels(deviceId);
- if (!channels || channels.length === 0) {
- console.warn('该设备下没有可用通道');
- return;
- }
- // 2. 将通道智能分配给播放器
- await assignChannelsToPlayers(channels);
- } catch (error) {
- console.error('处理设备通道时出错:', error);
- } finally {
- loading.value = false;
- }
- }
- // 情况2:点击的是具体的通道节点,保持原有逻辑不变
- else if (data.userData?.channelSipId) {
- sendDevicePush(data.userData);
- }
- };
- const getDeviceChannels = async (deviceId) => {
- try {
- const response = await listSipDeviceChannel(deviceId);
- console.log('获取设备通道列表成功:', response);
- // 假设接口返回的是通道数组,且每个通道对象结构符合 { basicData: { channelSipId, ... }, ... }
- return response || [];
- } catch (error) {
- console.error('获取设备通道列表失败:', error);
- return [];
- }
- };
- const assignChannelsToPlayers = async (channels) => {
- const currentSplit = spilt.value
- console.log(`分配 ${channels.length} 个通道到 ${currentSplit} 个播放窗口`)
-
- // 决定从哪个播放器窗口开始分配:始终从第一个窗口(0)开始
- // 这样保证每次分屏变化时都能从头开始重新分配
- const startPlayerIndex = 0
-
- // 决定从哪个通道开始取:总是从第一个通道开始
- const startChannelIndex = 0
-
- // 循环,为当前分屏布局中的每个位置分配通道
- for (let screenPosition = 0; screenPosition < currentSplit; screenPosition++) {
- const channelIndex = startChannelIndex + screenPosition
- const targetPlayerIndex = startPlayerIndex + screenPosition
-
- // 延迟执行,避免同时发起大量请求
- await new Promise(resolve => setTimeout(resolve, 50))
-
- if (channelIndex < channels.length) {
- // 有对应通道,开始播放
- const channelData = channels[channelIndex]
- // 构建符合 sendDevicePush 要求的参数格式
- const playData = {
- deviceSipId: channelData.deviceId,
- channelSipId: channelData.basicData?.channelSipId || channelData.id,
- name: channelData.name || `通道${channelIndex + 1}`,
- ...channelData.basicData
- }
- console.log(`将通道 ${playData.name} 分配给播放器 ${targetPlayerIndex}`)
- await sendDevicePush(playData, targetPlayerIndex)
- } else {
- // 没有更多通道,清空当前位置
- const newVideoUrls = [...videoUrl.value]
- newVideoUrls[targetPlayerIndex] = ''
- videoUrl.value = newVideoUrls
- // 清理存储的数据
- clear(targetPlayerIndex + 1)
- console.log(`播放器 ${targetPlayerIndex} 没有可用通道,已清空`)
- }
- }
- }
- // 通知设备上传媒体流
- const sendDevicePush = (itemData, targetIndex = null) => {
- // 关键修改:允许从外部传入 targetIndex,默认使用当前激活的 playerIdx
- const playIndex = targetIndex !== null ? targetIndex : playerIdx.value;
- save(itemData, playIndex); // 保存数据也要使用正确的索引
- let deviceId = itemData.deviceSipId;
- let channelId = itemData.channelSipId;
- loading.value = true;
- return startPlay(deviceId, channelId)
- .then((response) => {
- let res = response;
- if (window.location.protocol === 'http:') {
- itemData.playUrl = res.ws_flv;
- } else {
- itemData.playUrl = res.wss_flv;
- }
- itemData.streamId = res.streamId;
- setPlayUrl(itemData.playUrl, playIndex); // 播放URL设置到指定位置
- })
- .finally(() => {
- loading.value = false;
- });
- };
- // 设置播放URL
- const setPlayUrl = (url, idx) => {
- const newVideoUrls = [...videoUrl.value]
- newVideoUrls[idx] = url
- videoUrl.value = newVideoUrls
- setTimeout(() => {
- window.localStorage.setItem('videoUrl', JSON.stringify(videoUrl.value))
- }, 100)
- }
- // 检查播放参数
- const checkPlayByParam = () => {
- let { deviceId, channelId } = route.query
- if (deviceId && channelId) {
- sendDevicePush({ deviceId, channelId })
- }
- }
- // 截图处理
- const shot = (e) => {
- var base64ToBlob = function (code) {
- let parts = code.split(';base64,')
- let contentType = parts[0].split(':')[1]
- let raw = window.atob(parts[1])
- let rawLength = raw.length
- let uInt8Array = new Uint8Array(rawLength)
- for (let i = 0; i < rawLength; ++i) {
- uInt8Array[i] = raw.charCodeAt(i)
- }
- return new Blob([uInt8Array], {
- type: contentType
- })
- }
- let aLink = document.createElement('a')
- let blob = base64ToBlob(e)
- let evt = document.createEvent('HTMLEvents')
- evt.initEvent('click', true, true)
- aLink.download = 't("sip.splitview998531-5")'
- aLink.href = URL.createObjectURL(blob)
- aLink.click()
- }
- // 保存数据
- const save = (item, index) => {
- let dataStr = window.localStorage.getItem('playData') || '[]';
- let data = JSON.parse(dataStr);
- data[index] = item;
- window.localStorage.setItem('playData', JSON.stringify(data));
- };
- // 清除数据
- const clear = (idx) => {
- let dataStr = window.localStorage.getItem('playData') || '[]'
- let data = JSON.parse(dataStr)
- data[idx - 1] = null
- console.log(data)
- window.localStorage.setItem('playData', JSON.stringify(data))
- }
- </script>
- <style scoped>
- .btn {
- margin: 0 10px;
- }
- .btn:hover {
- color: #409eff;
- }
- .btn.active {
- color: #409eff;
- }
- .redborder {
- border: 2px solid red !important;
- }
- .play-box {
- background-color: #000000;
- border: 1px solid #505050;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: 10px;
- position: relative;
- border-radius: 5px;
- }
- .player-wrap {
- position: absolute;
- top: 0px;
- height: 100% !important;
- }
- </style>
|