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

281 lines
7.1 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 :auto-content-height="true">
<div class="flex h-full gap-[8px]">
<div class="h-full tree-box">
<DpTree class="h-full w-[300px]" @checked="onNodeChecked" />
</div>
<div class="bg-background flex-1">
<div class="video-play-area flex h-full 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>
</div>
</Page>
</template>
<script setup lang="ts">
import DpTree from './dp-tree.vue';
import { Page } from '@vben/common-ui';
import { onMounted, onUnmounted, ref, toRaw } from 'vue';
import mpegts from 'mpegts.js';
import { addFFmpegStreamProxy, addStreamProxy } from '#/api/sis/stream';
import { message } from 'ant-design-vue';
import { checkHEVCSupport } from '#/utils/video';
import type { AddStreamProxyResult } from '#/api/sis/stream/model';
/**
* 屏幕播放器数量
*/
const selected = 'selected';
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;
}
const itemRefs = ref<HTMLVideoElement[]>([]);
const setItemRef = (el: any) => {
if (el) {
itemRefs.value.push(el);
}
};
/**
* 处理带有子节点的数据
* @param node
* @param newNode
*/
function handleParentNoe(node: any, newNode: any[] = []) {
node.forEach((item: any) => {
if (item.level === 6) {
newNode.push(toRaw(item.data));
}
if (item.children && item.children.length >= 1) {
handleParentNoe(item.children, newNode);
}
});
}
/**
* 节点选中时间处理
* @param _val 选中节点id
* @param checked 是否选中
* @param node 节点数据
*/
function onNodeChecked(
_val: any,
{ checked, node }: { checked: boolean; node: any },
) {
// 此次操作需要新增或者删除节点
let checkNode: any = [];
handleParentNoe([node], checkNode);
// 新增
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);
}
}
});
}
}
// 播放器数据, 每一个位置代表页面上行的一个矩形
const playerList: any[] = [];
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));
}
}
}
/**
* 开始播放视频流
* @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.flv;
// 将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,
el: videoElement,
};
} else {
console.log('视频播放元素获取异常');
}
});
} else {
message.error('浏览器不支持播放');
}
}
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>
.player {
border: 1px solid #e4e4e7;
cursor: pointer;
video {
width: 100%;
height: 100%;
object-fit: fill;
}
}
.player.selected {
border: 2px solid deepskyblue;
}
</style>