WholeProcessPlatform/frontend/src/views/shiPinJianKong/components/LiveVideoBox.vue

365 lines
8.4 KiB
Vue
Raw Normal View History

<template>
<div
class="live-video-box"
:class="{ 'no-title': !showTitle }"
:style="{ height }"
>
<!-- 有视频时显示播放器 -->
<div v-if="videoUrl" class="video-container">
<!-- 视频标题和关闭按钮 -->
<div v-if="showTitle" class="live-video-title">
{{ videoData?.flnm }}
<div class="title-actions">
<CloseOutlined @click="handleClose" />
</div>
</div>
<div :id="playerId" class="video-player"></div>
</div>
<!-- 无视频时显示占位 -->
<div v-else class="video-empty">
<i class="iconfont icon-jiankong"></i>
<div>请选择视频</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
import { CloseOutlined } from '@ant-design/icons-vue';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
// 注册 HLS 支持video.js 8.x 内置了 HLS 支持)
interface VideoData {
flnm?: string;
stnm?: string;
url?: string;
urls?: string[];
[key: string]: any;
}
const props = withDefaults(
defineProps<{
videoData?: VideoData | null;
height?: string;
monitorType?: 'live' | 'record'; // 监控类型live-实时视频, record-录像
showTitle?: boolean; // 是否显示标题栏
}>(),
{
showTitle: true
}
);
const emit = defineEmits<{
close: [nodeKey?: string];
}>();
// 生成唯一ID
const playerId = `video-player-${Math.random().toString(36).substr(2, 9)}`;
let player: any = null;
// 获取第一个有效的URL
const videoUrl = computed(() => {
if (!props.videoData) return '';
// 优先使用url字段
if (props.videoData.url) return props.videoData.url;
// 其次使用urls数组的第一个
if (
props.videoData.urls &&
Array.isArray(props.videoData.urls) &&
props.videoData.urls.length > 0
) {
return props.videoData.urls[0];
}
return '';
});
// 初始化 video.js 播放器
const initPlayer = () => {
if (!videoUrl.value) return;
// 销毁旧播放器
if (player) {
player.dispose();
player = null;
}
try {
// 创建 video 元素
const videoElement = document.createElement('video');
videoElement.className = 'video-js vjs-default-skin vjs-big-play-centered';
videoElement.setAttribute('id', playerId);
videoElement.setAttribute('playsinline', 'playsinline');
videoElement.setAttribute('webkit-playsinline', 'webkit-playsinline');
// 将 video 元素添加到容器
const container = document.getElementById(playerId);
if (container) {
container.innerHTML = '';
container.appendChild(videoElement);
}
// 根据 monitorType 判断是否为直播流
// live: 实时视频流m3u8显示直播UI
// record: 录像文件,显示进度条
const isLiveStream = props.monitorType === 'live';
// 根据 URL 后缀或监控类型确定视频 MIME Type
const getVideoType = (url: string): string => {
const lowerUrl = url.toLowerCase();
// 优先根据 URL 后缀判断
if (lowerUrl.includes('.m3u8')) {
return 'application/x-mpegURL'; // HLS 流
} else if (lowerUrl.includes('.mp4')) {
return 'video/mp4';
} else if (lowerUrl.includes('.webm')) {
return 'video/webm';
} else if (lowerUrl.includes('.ogg') || lowerUrl.includes('.ogv')) {
return 'video/ogg';
} else if (lowerUrl.includes('.avi')) {
return 'video/x-msvideo';
} else if (lowerUrl.includes('.mov')) {
return 'video/quicktime';
} else if (lowerUrl.includes('.wmv')) {
return 'video/x-ms-wmv';
} else if (lowerUrl.includes('.flv')) {
return 'video/x-flv';
}
// 如果 URL 中没有明确后缀,根据监控类型判断
return isLiveStream ? 'application/x-mpegURL' : 'video/mp4';
};
const videoType = getVideoType(videoUrl.value);
// 初始化 video.js
player = videojs(videoElement, {
autoplay: true,
muted: true,
controls: true,
fluid: false,
width: '100%',
height: '100%',
preload: 'auto',
playbackRates: [0.5, 1, 1.5, 2],
liveui: isLiveStream, // 直播流显示直播UI
// 录像模式下禁用直播相关功能
inactivityTimeout: 0, // 禁用不活动超时
controlBar: {
children: [
'playToggle',
'volumePanel',
'currentTimeDisplay',
'timeDivider',
'durationDisplay',
'progressControl',
'liveDisplay',
'remainingTimeDisplay',
'customControlSpacer',
'playbackRateMenuButton',
'fullscreenToggle'
]
},
html5: {
hls: {
enableLowInitialPlaylist: true,
smoothQualityChange: true
}
},
sources: [
{
src: videoUrl.value,
type: videoType // 指定视频类型
}
]
});
// 如果是录像模式,隐藏直播指示器
if (!isLiveStream) {
player.ready(() => {
const liveDisplay = player.controlBar.getChild('liveDisplay');
if (liveDisplay) {
liveDisplay.el().style.display = 'none';
}
});
}
// 监听错误事件
player.on('error', () => {
const error = player.error();
console.error('播放器错误:', error);
// 保持最后一帧画面,不显示错误提示
// 在后台静默重试
let retryCount = 0;
const maxRetries = 10;
const attemptReload = () => {
if (retryCount < maxRetries && player && videoUrl.value) {
retryCount++;
setTimeout(() => {
try {
player.src({
src: videoUrl.value,
type: videoType // 指定视频类型
});
player.load();
player.play();
} catch (e) {
if (retryCount < maxRetries) {
attemptReload();
}
}
}, 3000);
}
};
attemptReload();
});
// 监听加载完成
player.on('loadeddata', () => {});
// 监听播放状态
player.on('play', () => {});
// 监听暂停
player.on('pause', () => {});
// 监听等待(缓冲)
player.on('waiting', () => {});
} catch (error) {
// 静默处理错误
}
};
// 关闭视频
const handleClose = () => {
// 传递节点的 key 给父组件
emit('close', props.videoData?.key);
};
// 监听视频URL变化
watch(
videoUrl,
(newUrl, oldUrl) => {
if (newUrl && newUrl !== oldUrl) {
// URL 变化时延迟初始化,确保 DOM 已更新
setTimeout(() => initPlayer(), 200);
} else if (newUrl && !oldUrl) {
// 首次加载
setTimeout(() => initPlayer(), 100);
}
},
{ immediate: true }
);
onMounted(() => {
if (videoUrl.value) {
initPlayer();
}
});
onUnmounted(() => {
// 清理播放器
if (player) {
player.dispose();
player = null;
}
});
</script>
<style scoped lang="scss">
.live-video-box {
width: 100%;
height: 100%;
position: relative;
background-color: #000;
border-radius: 4px;
overflow: hidden;
.live-video-title {
width: 100%;
height: 40px;
background-color: #2f6b98;
display: flex;
padding: 8px 16px;
color: #fff;
font-size: 13px;
justify-content: space-between;
align-items: center;
.title-actions {
display: flex;
gap: 8px;
align-items: center;
span {
font-size: 16px;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
}
}
}
}
.video-container {
width: 100%;
height: calc(100% - 40px);
position: relative;
.video-player {
width: 100%;
height: 100%;
// video.js 样式优化
:deep(.video-js) {
width: 100%;
height: 100%;
background-color: #000;
video {
object-fit: contain;
}
}
}
}
&.no-title {
.video-container {
height: 100%;
}
}
.video-empty {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #666;
background-color: #1a1a1a;
i {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
div {
font-size: 13px;
color: #999;
}
}
}
</style>