feat(video):重构视频监控模块并优化实时预览功能
- 移除旧的 DahuaVideoController 控制器及相关接口 - 在 NetSDKService 中实现基于 FFmpeg 的实时流转码与推送逻辑 - 新增对 H.264 和 HEVC 编码格式的识别与动态转码支持 - 实现 FLV 流封装及 WebSocket 推送机制 - 添加 WebSocket 会话管理与多用户连接跟踪 - 更新平台视频管理控制器中的通道标识字段 - 配置文件中增加大华设备连接参数- 调整日志级别为 debug 以便于开发调试 - 完善资源清理逻辑,确保登出时释放所有相关句柄和进程
parent
54ca5e9e4c
commit
ad58b16d00
|
|
@ -1,22 +1,19 @@
|
|||
<template>
|
||||
<el-dialog v-model="dialogVisible" title="播放后台转码视频" width="50%">
|
||||
<!-- 原生video播放器 - 移除了controls属性以去掉默认控制栏 -->
|
||||
<video ref="videoRef" playsinline class="video-player"></video>
|
||||
<el-dialog v-model="dialogVisible" title="播放实时视频" width="50%">
|
||||
<!-- 原生video播放器 -->
|
||||
<video ref="videoRef" playsinline controls class="video-player"></video>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="dialogVisible = false">
|
||||
关闭播放
|
||||
</el-button>
|
||||
<el-button type="primary" @click="closePlayer"> 关闭播放 </el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import Hls from "hls.js"; // 引入HLS解析库
|
||||
import { ref, watch, onBeforeUnmount } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getTranscodeStatus, stopTransCode } from "@/request/video_info.js"; // 后端“停止转码”接口
|
||||
import { stopWebRtcStream } from "@/request/webrtc.js";
|
||||
|
||||
// 接收父组件传递的属性
|
||||
const props = defineProps({
|
||||
|
|
@ -24,13 +21,9 @@ const props = defineProps({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
required: true, // 必须传递HLS流地址
|
||||
},
|
||||
videoId: {
|
||||
type: String,
|
||||
required: true, // 视频Id
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -40,171 +33,535 @@ const emit = defineEmits(["update:visible", "onStop"]);
|
|||
// 控制弹窗显示(内部响应props.visible)
|
||||
const dialogVisible = ref(props.visible);
|
||||
const videoRef = ref(null); // video元素引用
|
||||
const isPlaying = ref(false); // 视频播放状态
|
||||
let hlsInstance = null; // HLS实例(用于管理流解析)
|
||||
let mediaSource = null;
|
||||
let sourceBuffer = null;
|
||||
let queue = [];
|
||||
let isProcessing = false;
|
||||
let ws = null; // WebSocket连接
|
||||
let isPlayerDestroyed = false; // 标记播放器是否已被销毁
|
||||
let isMediaSourceOpen = false; // 标记MediaSource是否已打开
|
||||
let dataReceived = false; // 标记是否已接收到数据
|
||||
let dataReceiveCount = 0; // 记录接收到的数据包数量
|
||||
let closeReason = null; // 记录关闭原因
|
||||
|
||||
// 监听父组件visible变化,同步到内部dialogVisible
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
dialogVisible.value = newVal;
|
||||
}
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
dialogVisible.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 定义定时器变量
|
||||
let checkInterval = null;
|
||||
|
||||
// 弹窗打开时执行的方法
|
||||
const onDialogOpen = () => {
|
||||
// 每3秒调用一次接口
|
||||
checkInterval = setInterval(async () => {
|
||||
try {
|
||||
// 假设接口名为checkVideoProgress,需自行导入
|
||||
const response = await getTranscodeStatus({ id: props.videoId });
|
||||
if (response.progress > 2) {
|
||||
handlePlay();
|
||||
// 播放后清除定时器
|
||||
if (checkInterval) {
|
||||
clearInterval(checkInterval);
|
||||
checkInterval = null;
|
||||
}
|
||||
}
|
||||
// 可根据需要处理接口返回结果
|
||||
} catch (error) {}
|
||||
}, 3000); // 3秒间隔
|
||||
};
|
||||
|
||||
// 监听内部dialogVisible变化,通知父组件
|
||||
watch(dialogVisible, (newVal) => {
|
||||
emit("update:visible", newVal);
|
||||
if (newVal) {
|
||||
// 重置状态标记
|
||||
isPlayerDestroyed = false;
|
||||
isMediaSourceOpen = false;
|
||||
dataReceived = false;
|
||||
dataReceiveCount = 0;
|
||||
closeReason = null;
|
||||
onDialogOpen();
|
||||
} else {
|
||||
destroyPlayer(); // 弹窗关闭时销毁播放器
|
||||
}
|
||||
});
|
||||
|
||||
// 播放视频
|
||||
const handlePlay = () => {
|
||||
// 关闭播放器的方法
|
||||
const closePlayer = () => {
|
||||
closeReason = "user";
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 弹窗打开时执行的方法
|
||||
const onDialogOpen = () => {
|
||||
connectWebSocket();
|
||||
};
|
||||
|
||||
// 连接WebSocket
|
||||
const connectWebSocket = () => {
|
||||
if (!props.videoId) {
|
||||
ElMessage.error("视频ID为空");
|
||||
// 不要自动关闭对话框,让用户手动关闭
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前时间戳作为临时userId(确保每次连接都有唯一的标识)
|
||||
const tempUserId = Date.now().toString();
|
||||
|
||||
// 创建WebSocket连接
|
||||
const wsUrl = `ws://localhost:8898?videoId=${props.videoId}&userId=${tempUserId}`;
|
||||
console.log("正在连接WebSocket:", wsUrl);
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket连接已建立");
|
||||
initializeMediaSource();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
// 标记已接收到数据
|
||||
if (!dataReceived) {
|
||||
dataReceived = true;
|
||||
console.log("首次接收到视频数据");
|
||||
}
|
||||
dataReceiveCount++;
|
||||
|
||||
// 每接收100个数据包打印一次日志
|
||||
if (dataReceiveCount % 100 === 0) {
|
||||
console.log("已接收到 " + dataReceiveCount + " 个数据包");
|
||||
}
|
||||
|
||||
if (event.data instanceof Blob) {
|
||||
// 接收视频数据
|
||||
handleVideoData(event.data);
|
||||
} else if (event.data instanceof ArrayBuffer) {
|
||||
// 接收视频数据
|
||||
const blob = new Blob([event.data], { type: "video/webm" });
|
||||
handleVideoData(blob);
|
||||
} else {
|
||||
console.log("接收到未知类型数据:", typeof event.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
closeReason = "error";
|
||||
console.error("WebSocket连接出错:", error);
|
||||
ElMessage.error("WebSocket连接出错: " + error.message);
|
||||
// 发生错误时不要自动关闭对话框,让用户手动关闭
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log(
|
||||
"WebSocket连接已关闭, code: " + event.code + ", reason: " + event.reason
|
||||
);
|
||||
// 检查是否从未接收到数据
|
||||
if (!dataReceived) {
|
||||
ElMessage.warning("未接收到视频数据,可能是视频源问题");
|
||||
} else {
|
||||
console.log("共接收到 " + dataReceiveCount + " 个数据包");
|
||||
}
|
||||
|
||||
// 只有在非用户主动关闭的情况下才销毁播放器资源
|
||||
if (closeReason !== "user") {
|
||||
// WebSocket关闭时销毁播放器资源
|
||||
destroyPlayerResources();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 初始化MediaSource
|
||||
const initializeMediaSource = () => {
|
||||
// 检查播放器是否已被销毁
|
||||
if (isPlayerDestroyed) {
|
||||
console.log("播放器已被销毁,取消初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
const video = videoRef.value;
|
||||
if (!video) {
|
||||
ElMessage.error("视频播放器初始化失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 先清空之前的资源
|
||||
video.src = "";
|
||||
|
||||
// 检查src是否有效
|
||||
if (!props.src) {
|
||||
ElMessage.error("视频源地址为空");
|
||||
if (!window.MediaSource) {
|
||||
ElMessage.error("您的浏览器不支持MediaSource");
|
||||
return;
|
||||
}
|
||||
|
||||
// HLS.js 支持检测
|
||||
if (Hls.isSupported()) {
|
||||
// 如果已有实例,先销毁
|
||||
if (hlsInstance) {
|
||||
hlsInstance.destroy();
|
||||
}
|
||||
try {
|
||||
mediaSource = new MediaSource();
|
||||
video.src = URL.createObjectURL(mediaSource);
|
||||
|
||||
hlsInstance = new Hls({
|
||||
maxBufferLength: 30, // 增加缓冲长度
|
||||
maxMaxBufferLength: 60,
|
||||
});
|
||||
mediaSource.addEventListener("sourceopen", () => {
|
||||
console.log("MediaSource已打开");
|
||||
// 标记MediaSource已打开
|
||||
isMediaSourceOpen = true;
|
||||
|
||||
// 监听错误事件
|
||||
hlsInstance.on(Hls.Events.ERROR, (event, data) => {
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
hlsInstance.startLoad();
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
hlsInstance.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
// 无法恢复的错误,需要重新初始化
|
||||
destroyPlayer();
|
||||
handlePlay();
|
||||
// 再次检查播放器是否已被销毁
|
||||
if (isPlayerDestroyed) {
|
||||
console.log("播放器已被销毁,取消SourceBuffer初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查MediaSource状态
|
||||
if (mediaSource.readyState !== "open") {
|
||||
console.warn("MediaSource未处于open状态:", mediaSource.readyState);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建SourceBuffer,尝试多种编解码器
|
||||
// 根据后端FFmpeg配置,我们应优先尝试单一编解码器而不是复合编解码器
|
||||
const codecs = [
|
||||
'video/webm; codecs="vp8"', // 优先尝试VP8(H.264转码输出)
|
||||
'video/webm; codecs="vp9"', // 然后尝试VP9(H.265转码输出)
|
||||
'video/webm; codecs="vp8,vp9"',
|
||||
"video/webm",
|
||||
];
|
||||
|
||||
let codecInitialized = false;
|
||||
for (const codec of codecs) {
|
||||
try {
|
||||
sourceBuffer = mediaSource.addSourceBuffer(codec);
|
||||
codecInitialized = true;
|
||||
console.log("SourceBuffer初始化完成,使用编解码器: " + codec);
|
||||
break;
|
||||
} catch (e) {
|
||||
console.warn("尝试初始化编解码器失败: " + codec, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!codecInitialized) {
|
||||
throw new Error("无法初始化任何支持的编解码器");
|
||||
}
|
||||
|
||||
sourceBuffer.addEventListener("updateend", () => {
|
||||
console.log("SourceBuffer更新完成");
|
||||
isProcessing = false;
|
||||
processQueue();
|
||||
});
|
||||
|
||||
sourceBuffer.addEventListener('error', (e) => {
|
||||
console.error("SourceBuffer发生错误:", e);
|
||||
// 尝试获取更多错误信息
|
||||
if (sourceBuffer && sourceBuffer.onerror) {
|
||||
console.error("SourceBuffer.onerror:", sourceBuffer.onerror);
|
||||
}
|
||||
|
||||
isProcessing = false;
|
||||
// 尝试处理队列中的下一个数据包
|
||||
if (queue.length > 0) {
|
||||
setTimeout(() => {
|
||||
if (!isPlayerDestroyed) {
|
||||
processQueue();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加更新开始事件监听器
|
||||
sourceBuffer.addEventListener('updatestart', () => {
|
||||
console.log("SourceBuffer开始更新");
|
||||
});
|
||||
|
||||
// 添加更新事件监听器
|
||||
sourceBuffer.addEventListener('update', () => {
|
||||
console.log("SourceBuffer正在更新");
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isPlayerDestroyed) {
|
||||
ElMessage.error("初始化SourceBuffer失败: " + e.message);
|
||||
console.error("SourceBuffer初始化错误堆栈:", e.stack);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
hlsInstance.loadSource(props.src);
|
||||
hlsInstance.attachMedia(video);
|
||||
mediaSource.addEventListener("sourceclose", () => {
|
||||
console.log("MediaSource已关闭");
|
||||
isMediaSourceOpen = false;
|
||||
// MediaSource关闭时销毁播放器资源
|
||||
destroyPlayerResources();
|
||||
});
|
||||
|
||||
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
video.play().then(() => {
|
||||
isPlaying.value = true;
|
||||
});
|
||||
mediaSource.addEventListener("sourceended", () => {
|
||||
console.log(
|
||||
"MediaSource已结束,当前readyState:" + mediaSource.readyState
|
||||
);
|
||||
isMediaSourceOpen = false;
|
||||
// MediaSource结束时销毁播放器资源
|
||||
destroyPlayerResources();
|
||||
});
|
||||
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
||||
// Safari等原生支持HLS的浏览器
|
||||
video.src = props.src;
|
||||
video.addEventListener("loadedmetadata", () => {
|
||||
video
|
||||
.play()
|
||||
.then(() => {
|
||||
isPlaying.value = true;
|
||||
ElMessage.success("视频开始播放");
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(`播放失败:${err.message}`);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ElMessage.error("您的浏览器不支持HLS视频流播放,请更换浏览器");
|
||||
} catch (e) {
|
||||
if (!isPlayerDestroyed) {
|
||||
ElMessage.error("初始化播放器失败: " + e.message);
|
||||
console.error("初始化播放器错误堆栈:", e.stack);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 销毁播放器时清除定时器
|
||||
const destroyPlayer = () => {
|
||||
// 清除定时器
|
||||
if (checkInterval) {
|
||||
clearInterval(checkInterval);
|
||||
checkInterval = null;
|
||||
// 处理视频数据
|
||||
const handleVideoData = (data) => {
|
||||
// 检查播放器是否已被销毁
|
||||
if (isPlayerDestroyed) {
|
||||
console.log("播放器已被销毁,丢弃接收到的数据");
|
||||
return;
|
||||
}
|
||||
|
||||
// 原有逻辑保持不变
|
||||
isPlaying.value = false;
|
||||
if (hlsInstance) {
|
||||
hlsInstance.destroy();
|
||||
hlsInstance = null;
|
||||
// 检查WebSocket连接是否仍然打开
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
console.log("WebSocket连接已关闭,丢弃接收到的数据");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查MediaSource是否打开
|
||||
if (!isMediaSourceOpen || !mediaSource || mediaSource.readyState !== "open") {
|
||||
console.log(
|
||||
"MediaSource未打开,丢弃接收到的数据,当前状态:" +
|
||||
(mediaSource ? mediaSource.readyState : "mediaSource未定义")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查数据是否有效
|
||||
if (!data || data.size === 0) {
|
||||
console.log("接收到空数据,忽略");
|
||||
return;
|
||||
}
|
||||
|
||||
queue.push(data);
|
||||
processQueue();
|
||||
};
|
||||
|
||||
// 处理队列中的数据
|
||||
const processQueue = () => {
|
||||
// 检查播放器是否已被销毁
|
||||
if (isPlayerDestroyed) {
|
||||
console.log("播放器已被销毁,清空数据队列");
|
||||
queue = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查基本条件
|
||||
if (
|
||||
isProcessing ||
|
||||
queue.length === 0 ||
|
||||
!sourceBuffer ||
|
||||
sourceBuffer.updating
|
||||
) {
|
||||
if (queue.length > 0 && !isProcessing) {
|
||||
console.log(
|
||||
"满足处理条件但未处理,状态详情:isProcessing=" +
|
||||
isProcessing +
|
||||
", queue.length=" +
|
||||
queue.length +
|
||||
", sourceBuffer存在=" +
|
||||
!!sourceBuffer +
|
||||
", sourceBuffer.updating=" +
|
||||
(sourceBuffer ? sourceBuffer.updating : "N/A")
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查WebSocket连接状态
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
console.log("WebSocket连接已关闭,清空数据队列");
|
||||
queue = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查MediaSource状态
|
||||
if (!isMediaSourceOpen || !mediaSource || mediaSource.readyState !== "open") {
|
||||
console.log(
|
||||
"MediaSource未打开或已关闭,清空数据队列,当前状态:" +
|
||||
(mediaSource ? mediaSource.readyState : "mediaSource未定义")
|
||||
);
|
||||
// 如果MediaSource已结束,尝试重新初始化
|
||||
if (mediaSource && mediaSource.readyState === "ended") {
|
||||
console.log("尝试重新初始化MediaSource");
|
||||
initializeMediaSource();
|
||||
}
|
||||
queue = [];
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessing = true;
|
||||
const data = queue.shift();
|
||||
console.log("开始处理数据包,队列剩余长度:" + queue.length);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
// 全面检查所有相关对象的有效性
|
||||
if (isPlayerDestroyed) {
|
||||
console.log("播放器已被销毁,取消数据处理");
|
||||
isProcessing = false;
|
||||
queue = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查所有相关对象
|
||||
if (!mediaSource || !sourceBuffer || mediaSource.readyState !== 'open') {
|
||||
console.warn("MediaSource或SourceBuffer无效,无法添加数据,当前状态:" +
|
||||
(mediaSource ? mediaSource.readyState : "mediaSource未定义"));
|
||||
isProcessing = false;
|
||||
queue = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查sourceBuffer是否仍然属于mediaSource
|
||||
if (!mediaSource.sourceBuffers ||
|
||||
(Array.from(mediaSource.sourceBuffers).indexOf(sourceBuffer) === -1)) {
|
||||
console.warn("SourceBuffer已被移除,无法添加数据");
|
||||
isProcessing = false;
|
||||
queue = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查sourceBuffer是否正在更新
|
||||
if (sourceBuffer.updating) {
|
||||
console.warn("SourceBuffer正在更新,无法添加数据");
|
||||
isProcessing = false;
|
||||
// 重新将数据放回队列头部
|
||||
queue.unshift(data);
|
||||
return;
|
||||
}
|
||||
|
||||
const arrayBuffer = event.target.result;
|
||||
console.log("准备添加数据到SourceBuffer,数据大小:" + arrayBuffer.byteLength + " 字节");
|
||||
|
||||
// 检查数据是否为空
|
||||
if (arrayBuffer.byteLength === 0) {
|
||||
console.warn("尝试添加空数据到SourceBuffer,跳过");
|
||||
isProcessing = false;
|
||||
if (queue.length > 0) {
|
||||
processQueue();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试添加数据到SourceBuffer
|
||||
sourceBuffer.appendBuffer(arrayBuffer);
|
||||
console.log("数据已提交到SourceBuffer");
|
||||
} catch (e) {
|
||||
console.error("添加视频数据失败:", e);
|
||||
console.error("错误堆栈:", e.stack);
|
||||
isProcessing = false;
|
||||
// 发生错误时也清空队列
|
||||
queue = [];
|
||||
// 尝试恢复播放器
|
||||
if (!isPlayerDestroyed) {
|
||||
console.log("尝试恢复播放器");
|
||||
destroyPlayerResources();
|
||||
// 延迟重新连接
|
||||
setTimeout(() => {
|
||||
if (!isPlayerDestroyed) {
|
||||
connectWebSocket();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = (error) => {
|
||||
console.error("FileReader读取数据失败:", error);
|
||||
isProcessing = false;
|
||||
// 出错时继续处理队列中的下一个数据
|
||||
if (queue.length > 0) {
|
||||
isProcessing = false;
|
||||
processQueue();
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(data);
|
||||
};
|
||||
|
||||
// 销毁播放器资源但不关闭对话框
|
||||
const destroyPlayerResources = () => {
|
||||
// 标记播放器已被销毁
|
||||
isPlayerDestroyed = true;
|
||||
|
||||
// 关闭WebSocket连接
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
|
||||
// 清理MediaSource
|
||||
if (mediaSource) {
|
||||
try {
|
||||
// 重置MediaSource状态标记
|
||||
isMediaSourceOpen = false;
|
||||
|
||||
if (sourceBuffer && sourceBuffer.updating) {
|
||||
sourceBuffer.abort();
|
||||
}
|
||||
|
||||
// 从MediaSource中移除SourceBuffer
|
||||
if (sourceBuffer && mediaSource.readyState === "open") {
|
||||
mediaSource.removeSourceBuffer(sourceBuffer);
|
||||
}
|
||||
|
||||
// 尝试结束MediaSource
|
||||
if (mediaSource.readyState === "open") {
|
||||
mediaSource.endOfStream();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("清理MediaSource时出错:", e);
|
||||
}
|
||||
|
||||
try {
|
||||
URL.revokeObjectURL(videoRef.value.src);
|
||||
} catch (e) {
|
||||
console.warn("释放ObjectURL时出错:", e);
|
||||
}
|
||||
|
||||
mediaSource = null;
|
||||
sourceBuffer = null;
|
||||
}
|
||||
|
||||
// 清空队列
|
||||
queue = [];
|
||||
isProcessing = false;
|
||||
|
||||
// 暂停视频
|
||||
if (videoRef.value) {
|
||||
const video = videoRef.value;
|
||||
video.pause();
|
||||
video.src = "";
|
||||
try {
|
||||
videoRef.value.pause();
|
||||
videoRef.value.src = "";
|
||||
} catch (e) {
|
||||
console.warn("重置video元素时出错:", e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("播放器资源已销毁");
|
||||
};
|
||||
|
||||
// 销毁播放器(包括关闭对话框)
|
||||
const destroyPlayer = () => {
|
||||
// 如果播放器已经被销毁,则直接返回
|
||||
if (isPlayerDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeReason = "dialog";
|
||||
|
||||
// 销毁播放器资源
|
||||
destroyPlayerResources();
|
||||
|
||||
// 调用停止转码接口
|
||||
handleStopTranscode();
|
||||
};
|
||||
|
||||
// 停止转码(调用后端接口)
|
||||
const handleStopTranscode = async () => {
|
||||
try {
|
||||
await stopTransCode({ id: props.videoId }); // 调用后端“停止转码”接口
|
||||
emit("onStop"); // 通知父组件“转码已停止”
|
||||
dialogVisible.value = false; // 关闭弹窗
|
||||
// 只有在播放器未被销毁时才调用停止接口
|
||||
if (!isPlayerDestroyed) {
|
||||
await stopWebRtcStream({ id: props.videoId });
|
||||
emit("onStop");
|
||||
}
|
||||
// 不要自动设置dialogVisible.value = false,让用户手动关闭
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
if (error !== "cancel" && !isPlayerDestroyed) {
|
||||
ElMessage.error(`停止转码失败:${error.message || "未知错误"}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 组件卸载前清理
|
||||
onBeforeUnmount(() => {
|
||||
destroyPlayer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-player {
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
object-fit: contain; /* 保持视频比例 */
|
||||
background-color: #000; /* 增加黑色背景,使视频区域更明显 */
|
||||
}
|
||||
|
||||
.video-controls {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
object-fit: contain;
|
||||
background-color: #000;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@
|
|||
<el-button native-type="reset" @click="fnResetPagination"
|
||||
>重置</el-button
|
||||
>
|
||||
<el-button type="success" @click="handleStartTranscode"
|
||||
>播放后台转码视频</el-button
|
||||
<el-button type="success" @click="handleStartWebRtcStream"
|
||||
>播放实时视频(WebRTC)</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
|
@ -106,12 +106,11 @@
|
|||
@get-data="fnResetPagination"
|
||||
/>
|
||||
|
||||
<!-- 转码视频播放器(修改后:双向绑定visible + 传递src) -->
|
||||
<!-- WebRTC视频播放器 -->
|
||||
<play-video
|
||||
v-model:visible="data.transcodeVideoDialog.visible"
|
||||
:src="data.transcodeVideoDialog.src"
|
||||
:video-id="data.transcodeVideoDialog.id"
|
||||
@on-stop="onTranscodeStopped"
|
||||
v-model:visible="data.webRtcVideoDialog.visible"
|
||||
:video-id="data.webRtcVideoDialog.id"
|
||||
@on-stop="onWebRtcStreamStopped"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -130,8 +129,10 @@ import {ElMessage, ElMessageBox} from "element-plus";
|
|||
import SelectingPoints from "./components/selecting_points.vue";
|
||||
import LayoutVideo from "@/components/video/index.vue";
|
||||
import {setVideoManagerList} from "@/request/eightwork_videomanager.js";
|
||||
import PlayVideo from "@/views/video_manager/video_manager/components/playVideo.vue"; // 引入新播放器组件
|
||||
import {startTransCode, stopTransCode} from "@/request/video_info.js"; // 转码接口
|
||||
import PlayVideo from "@/views/video_manager/video_manager/components/playVideo.vue";
|
||||
|
||||
// 更新相关导入
|
||||
import {startWebRtcStream, stopWebRtcStream} from "@/request/webrtc.js";
|
||||
|
||||
const data = reactive({
|
||||
addDialog: {
|
||||
|
|
@ -155,42 +156,32 @@ const data = reactive({
|
|||
videomanagerId: "",
|
||||
visible: false,
|
||||
},
|
||||
transcodeVideoDialog: {
|
||||
webRtcVideoDialog: {
|
||||
visible: false,
|
||||
src: "http://localhost:8100/api/hls/stream.m3u8", // 后端转码后HLS流的访问路径
|
||||
id: "",
|
||||
},
|
||||
});
|
||||
|
||||
// 启动转码并显示播放器
|
||||
const handleStartTranscode = async () => {
|
||||
// 启动WebRTC流并显示播放器
|
||||
const handleStartWebRtcStream = async () => {
|
||||
try {
|
||||
await startTransCode(); // 调用后端“启动转码”接口
|
||||
data.transcodeVideoDialog.visible = true; // 显示播放器弹窗
|
||||
// 这里应该选择一个默认视频或让用户选择
|
||||
await startWebRtcStream();
|
||||
data.webRtcVideoDialog.visible = true;
|
||||
} catch (error) {
|
||||
ElMessage.error("启动转码失败: " + (error.message || "未知错误"));
|
||||
ElMessage.error("启动实时视频流失败: " + (error.message || "未知错误"));
|
||||
}
|
||||
};
|
||||
|
||||
// 转码停止后的回调
|
||||
const onTranscodeStopped = async () => {
|
||||
await stopTransCode(); // 调用后端“停止转码”接口
|
||||
// WebRTC流停止后的回调
|
||||
const onWebRtcStreamStopped = async () => {
|
||||
await stopWebRtcStream();
|
||||
};
|
||||
|
||||
// 列表数据逻辑(保持不变)
|
||||
const {list, pagination, searchForm, fnGetData, fnResetPagination} =
|
||||
useListData(getHkVideoManagerList);
|
||||
|
||||
// 其他方法(保持不变)
|
||||
// const fnSetPositioning = async (row) => {
|
||||
// data.selectingPointsDialog.id = row.PLS_ID ? row.PLS_ID : "";
|
||||
// data.selectingPointsDialog.indexCode = row.indexCode;
|
||||
// data.selectingPointsDialog.regionPathName = row.regionPathName;
|
||||
// data.selectingPointsDialog.camName = row.name;
|
||||
// data.selectingPointsDialog.videomanagerId = row.videomanagerId;
|
||||
// data.selectingPointsDialog.visible = true;
|
||||
// };
|
||||
|
||||
const fnUpToBi = async (videomanagerId) => {
|
||||
await ElMessageBox.confirm("确定要置顶吗?置顶后将会默认展示在Bi页", {
|
||||
type: "warning",
|
||||
|
|
@ -201,13 +192,11 @@ const fnUpToBi = async (videomanagerId) => {
|
|||
|
||||
const fnPreviewVideo = async (row) => {
|
||||
try {
|
||||
const resData = await startTransCode({url: row.url, id: row.PLS_ID}); // 调用后端“启动转码”接口
|
||||
data.transcodeVideoDialog.visible = true; // 显示播放器弹窗
|
||||
data.transcodeVideoDialog.id = row.PLS_ID; // 显示播放器弹窗
|
||||
data.transcodeVideoDialog.src =
|
||||
"http://172.16.70.226:7811/" + resData.videoUrl + "stream.m3u8";
|
||||
await startWebRtcStream({url: row.url, id: row.PLS_ID});
|
||||
data.webRtcVideoDialog.visible = true;
|
||||
data.webRtcVideoDialog.id = row.PLS_ID;
|
||||
} catch (error) {
|
||||
ElMessage.error("启动转码失败: " + (error.message || "未知错误"));
|
||||
ElMessage.error("启动实时视频流失败: " + (error.message || "未知错误"));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue