Files
admin-vben5/apps/web-antd/src/views/sis/video/index.vue
2025-08-27 20:40:49 +08:00

429 lines
11 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]">
<div class="h-full pb-[5px] c-tree bg-background">
<ChannelTree
class="w-[300px]"
@check="onNodeChecked"
/>
</div>
<!-- 设备分组区域 -->
<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 { addFFmpegStreamProxy, addStreamProxy } from '#/api/sis/stream';
import {
Svg16FrameIcon,
Svg1FrameIcon,
Svg4FrameIcon,
Svg9FrameIcon,
} from '@vben/icons';
import { checkHEVCSupport } from '#/utils/video';
import type { AddStreamProxyResult } from '#/api/sis/stream/model';
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 {
// 如果此次播放数量小于当前播能播放
const freeArr: number[] = []; // 空闲播放器数量
for (let i = 0; i < playerNum.value; i++) {
const playerData = playerList[i];
if (!playerData) {
freeArr.push(i);
}
}
// 要播放的视频数量,小于等于空闲播放器数量,则填充空闲即可
if (checkNode.length <= freeArr.length) {
for (let j = 0; j < checkNode.length; j++) {
doPlayer(checkNode[j], freeArr[j]);
}
}
// 直接覆盖原有的播放视频
else {
for (let i = 0; i < playerNum.value; i++) {
doPlayer(checkNode[i], i);
}
}
}
}
// 删除
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('视频播放元素获取异常');
}
}
function streamProxy(nodeData: any, cb: Function) {
let params = {};
if (nodeData.nvrIp) {
params = {
videoIp: nodeData.nvrIp,
videoPort: nodeData.nvrPort,
factoryNo: nodeData.nvrFactoryNo,
account: nodeData.nvrAccount,
pwd: nodeData.nvrPwd,
channelId: nodeData.nvrChannelNo,
};
} else {
params = {
videoIp: nodeData.deviceIp,
videoPort: nodeData.devicePort,
factoryNo: nodeData.factoryNo,
account: nodeData.deviceAccount,
pwd: nodeData.devicePwd,
channelId: nodeData.channelNo,
};
}
if (isSupportH265) {
addStreamProxy(params).then((res) => cb(res));
} else {
addFFmpegStreamProxy(params).then((res) => cb(res));
// addStreamProxy(params).then((res) => cb(res));
}
}
/**
* 开始播放视频流
* @param nodeData 播放的节点数据
* @param index 播放器的索引信息
*/
function doPlayer(nodeData: any, index: number = 0) {
console.log('index=', index);
if (mpegts.isSupported()) {
streamProxy(nodeData, (res: AddStreamProxyResult) => {
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);
}
}
}
function catchUp() {
playerList.forEach((playerData) => {
if (playerData) {
const { player, el } = playerData;
const end = player.buffered.end(player.buffered.length - 1);
const diff = end - el.currentTime;
if (diff > 2) {
// 如果延迟超过2秒
el.currentTime = end - 0.5; // 跳转到接近直播点
}
}
});
}
let isSupportH265 = false;
onMounted(() => {
// 检测浏览器是否支持h265
isSupportH265 = checkHEVCSupport();
setInterval(catchUp, 10000);
});
onUnmounted(() => {
for (let i = 0; i < playerList.length; i++) {
closePlayer(i);
}
});
</script>
<style scoped>
.player {
border: 1px solid #e4e4e7;
cursor: pointer;
video {
width: 100%;
height: 100%;
object-fit: fill;
}
}
.player.selected {
border: 2px solid deepskyblue;
}
.player-area {
display: flex;
align-items: center;
cursor: pointer;
}
</style>