admin-vben5/apps/web-antd/src/views/sis/video/index.vue
2025-07-25 21:31:43 +08:00

382 lines
9.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<Page class="h-full w-full">
<!-- 设备分组区域 -->
<div class="flex h-full gap-[8px]">
<ChannelTree class="h-[83vh] w-[260px]" @check="onNodeChecked" />
<!-- 设备分组区域 -->
<div class="bg-background flex-1">
<div class="video-play-area flex h-[calc(100%-30px)] flex-wrap">
<div
v-for="i in playerNum"
:style="playerStyle"
class="player"
:class="`layer-${i} ${currentSelectPlayerIndex == i ? selected : ''}`"
@click="playerSelect(i)"
>
<video
style="width: 100%; height: 100%"
:ref="setItemRef"
muted
autoplay
></video>
</div>
</div>
<div class="player-area flex h-[30px] gap-[5px]">
<div @click="onPlayerNumChanged(1)" class="h-[20px] w-[20px]">
<Svg1FrameIcon style="width: 100%; height: 100%" />
</div>
<div @click="onPlayerNumChanged(2)" class="h-[20px] w-[20px]">
<Svg4FrameIcon style="width: 100%; height: 100%" />
</div>
<div @click="onPlayerNumChanged(3)" class="h-[20px] w-[20px]">
<Svg9FrameIcon style="width: 100%; height: 100%" />
</div>
<div @click="onPlayerNumChanged(4)" class="h-[20px] w-[20px]">
<Svg16FrameIcon style="width: 100%; height: 100%" />
</div>
</div>
</div>
</div>
</Page>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, toRaw } from 'vue';
import { Page } from '@vben/common-ui';
import ChannelTree from './channel-tree.vue';
import mpegts from 'mpegts.js';
import { message } from 'ant-design-vue';
import { addStreamProxy } from '#/api/sis/stream';
import {
Svg16FrameIcon,
Svg1FrameIcon,
Svg4FrameIcon,
Svg9FrameIcon,
} from '@vben/icons';
const selected = 'selected';
const itemRefs = ref<HTMLVideoElement[]>([]);
const setItemRef = (el: any) => {
if (el) {
itemRefs.value.push(el);
}
};
/**
* 屏幕播放器数量
*/
const playerNum = ref(1);
/**
* 屏幕播放器样式
*/
const playerStyle = ref({
width: '100%',
height: '100%',
});
const currentSelectPlayerIndex = ref(-1);
function playerSelect(index: number) {
if (index === currentSelectPlayerIndex.value) {
currentSelectPlayerIndex.value = -1;
return;
}
currentSelectPlayerIndex.value = index;
}
/**
* 播放器数量
* @param val
*/
function onPlayerNumChanged(val: number) {
// 1个屏幕
const changeBeforeNum = playerNum.value;
let changeNum = 1;
if (val === 1) {
playerStyle.value = {
width: '100%',
height: '100%',
};
}
// 4个屏幕 2*2
else if (val === 2) {
playerStyle.value = {
width: '50%',
height: '50%',
};
changeNum = 4;
}
// 9个屏幕 3*3
else if (val === 3) {
playerStyle.value = {
width: '33.33%',
height: '33.33%',
};
changeNum = 9;
}
// 16个屏幕 4*4
else {
playerStyle.value = {
width: '25%',
height: '25%',
};
changeNum = 16;
}
playerNum.value = changeNum;
// 缩小布局
if (changeBeforeNum > changeNum) {
const playerArr = [];
for (let i = 0; i < playerList.length; i++) {
const playerBox = playerList[i];
if (playerBox) {
playerArr.push(playerBox);
}
playerList[i] = null;
}
for (let i = 0; i < playerArr.length; i++) {
const play = playerArr[i];
if (i < changeNum) {
// 获取播放元素
changeElPlayer(play, i);
} else {
closePlayVieo(play.player);
}
}
}
}
/**
* 处理带有子节点的数据
* @param node
* @param newNode
*/
function handleParentNoe(node: any, newNode: any[] = []) {
if (node.children && node.children.length >= 1) {
node.children.forEach((item: any) => {
if (item.level === 2) {
newNode.push(toRaw(item.data));
}
if (item.children && item.children.length >= 1) {
handleParentNoe(item.children, newNode);
}
});
}
}
// 播放器数据, 每一个位置代表页面上行的一个矩形
const playerList: any[] = [];
/**
* 节点选中时间处理
* @param _val 选中节点id
* @param checked 是否选中
* @param node 节点数据
*/
function onNodeChecked(
_val: any,
{ checked, node }: { checked: boolean; node: any },
) {
// 此次操作需要新增或者删除节点
let checkNode: any = [];
if (node.level === 1) {
handleParentNoe(node, checkNode);
} else {
checkNode.push(toRaw(node.data));
}
// 新增
if (checked) {
/**
* 如果当前页面有选择播放未知,并且播放视频只有一个,则播放到制定位置
*/
if (currentSelectPlayerIndex.value !== -1 && checkNode.length == 1) {
doPlayer(checkNode[0], currentSelectPlayerIndex.value - 1);
}
// 批量播放 currentSelectPlayerIndex 将不再生效
else {
checkNode.forEach((item: any) => {
// 判断当前摄像数据是否已经在播放,如果在播放则此次不进行播放处理.默认选中最后一个
let firstFreePlayerIndex = playerNum.value - 1;
// 记录第一个空播放器是否已找到
let isRecord = false;
// 此次选中的设备是否播放
let isPlayer = true;
for (let i = 0; i < playerNum.value; i++) {
// 记录第一个出现空播放器的索引
const playerData = playerList[i];
// 没找到空播放器并且此次循环播放器为null
if (!isRecord && !playerData) {
firstFreePlayerIndex = i;
isRecord = true;
}
if (playerData && playerData.data.id == item.id) {
isRecord = false;
break;
}
}
if (isPlayer) {
doPlayer(item, firstFreePlayerIndex);
}
});
}
}
// 删除
else {
checkNode.forEach((item: any) => {
for (let i = 0; i < playerNum.value; i++) {
const player = playerList[i];
if (player && player.data.id === item.id) {
closePlayer(i);
}
}
});
}
}
function changeElPlayer(playerInfo: any, index: number) {
const playerData = playerInfo.data;
const oldPlayer = playerInfo.player;
if (oldPlayer) {
closePlayVieo(oldPlayer);
}
const videoConfig = {
type: 'flv',
url: playerData.url,
isLive: true,
hasAudio: false,
hasVideo: true,
enableWorker: true, // 启用分离的线程进行转码
enableStashBuffer: false, // 关闭IO隐藏缓冲区
stashInitialSize: 256, // 减少首帧显示等待时长
};
const playerConfig = {
enableErrorRecover: true, // 启用错误恢复
autoCleanupMaxBackwardDuration: 30,
autoCleanupMinBackwardDuration: 10,
};
const player = mpegts.createPlayer(videoConfig, playerConfig);
const videoElement = itemRefs.value[index];
if (videoElement) {
player.attachMediaElement(videoElement);
player.load();
player.play();
playerList[index] = {
player,
data: playerData,
};
} else {
console.log('视频播放元素获取异常');
}
}
/**
* 开始播放视频流
* @param nodeData 播放的节点数据
* @param index 播放器的索引信息
*/
function doPlayer(nodeData: any, index: number) {
if (mpegts.isSupported()) {
const params = {
videoIp: nodeData.deviceIp,
videoPort: nodeData.devicePort,
factoryNo: nodeData.factoryNo,
account: nodeData.deviceAccount,
pwd: nodeData.devicePwd,
channelId: nodeData.channelNo,
};
addStreamProxy(params).then((res) => {
const url = res.wsFlv;
// 将url 绑定到 nodeData
nodeData.url = url;
closePlayer(index);
const videoConfig = {
type: 'flv',
url: url,
isLive: true,
hasAudio: false,
hasVideo: true,
enableWorker: true, // 启用分离的线程进行转码
enableStashBuffer: false, // 关闭IO隐藏缓冲区
stashInitialSize: 256, // 减少首帧显示等待时长
};
const playerConfig = {
enableErrorRecover: true, // 启用错误恢复
autoCleanupMaxBackwardDuration: 30,
autoCleanupMinBackwardDuration: 10,
};
const player = mpegts.createPlayer(videoConfig, playerConfig);
const videoElement = itemRefs.value[index];
if (videoElement) {
player.attachMediaElement(videoElement);
player.load();
player.play();
playerList[index] = {
player,
data: nodeData,
};
} else {
console.log('视频播放元素获取异常');
}
});
} else {
message.error('浏览器不支持播放');
}
}
function closePlayVieo(plInfo: any) {
if (plInfo) {
try {
plInfo.pause(); // 暂停
plInfo.unload(); // 卸载
plInfo.destroy(); // 销毁
} catch (e) {
console.log('播放器关闭失败e=', e);
}
}
}
function closePlayer(index: number) {
// 如果播放器存在,尝试关闭
const pData = playerList[index];
if (pData) {
try {
const player = pData.player;
player.pause(); // 暂停
player.unload(); // 卸载
player.destroy(); // 销毁
playerList[index] = null;
} catch (e) {
console.log('播放器关闭失败e=', e);
}
}
}
onMounted(() => {
// 初始化不加载任何视频
});
onUnmounted(() => {
for (let i = 0; i < playerList.length; i++) {
closePlayer(i);
}
});
</script>
<style scoped>
.player {
border: 1px solid #e4e4e7;
cursor: pointer;
}
.player.selected {
border: 2px solid deepskyblue;
}
.player-area {
display: flex;
align-items: center;
cursor: pointer;
}
</style>