feat(video):重构视频监控模块并优化实时预览功能

- 移除旧的 DahuaVideoController 控制器及相关接口
- 在 NetSDKService 中实现基于 FFmpeg 的实时流转码与推送逻辑
- 新增对 H.264 和 HEVC 编码格式的识别与动态转码支持
- 实现 FLV 流封装及 WebSocket 推送机制
- 添加 WebSocket 会话管理与多用户连接跟踪
- 更新平台视频管理控制器中的通道标识字段
- 配置文件中增加大华设备连接参数- 调整日志级别为 debug 以便于开发调试
- 完善资源清理逻辑,确保登出时释放所有相关句柄和进程
wangyan 2025-10-31 19:46:09 +08:00
parent 54ca5e9e4c
commit ad58b16d00
2 changed files with 504 additions and 158 deletions

View File

@ -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; //
// visibledialogVisible
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"', // VP8H.264
'video/webm; codecs="vp9"', // VP9H.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")) {
// SafariHLS
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;
}
// sourceBuffermediaSource
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();
}
// MediaSourceSourceBuffer
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>

View File

@ -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 || "未知错误"));
}
};
@ -233,4 +222,4 @@ const fnDeleteVideo = async (row) => {
};
</script>
<style scoped></style>
<style scoped></style>