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

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

View File

@ -1,9 +1,12 @@
VITE_BASE=/ VITE_BASE=/
# VITE_BASE_URL=http://192.168.0.25:8095/ # VITE_BASE_URL=http://192.168.0.25:8095/
VITE_BASE_URL=http://192.168.0.37:8095/ VITE_BASE_URL=http://192.168.4.40:8095/
#websocket t掉线 #websocket t掉线
VITE_ON_LINE_WEB_SOCKET_URL=ws://192.168.0.37:8869 VITE_ON_LINE_WEB_SOCKET_URL=ws://192.168.4.40:8869
#websocket 在线学习 #websocket 在线学习
VITE_LEARNING_WEB_SOCKET_URL=ws://192.168.0.37:8899 VITE_LEARNING_WEB_SOCKET_URL=ws://192.168.4.40:8899
#websocket 大华设备视频
VITE_DAHUA_WEB_SOCKET_URL=ws://192.168.4.40:8866

View File

@ -4,9 +4,12 @@ VITE_BASE_URL=http://172.16.70.226:8081/sx_yjb/
#VITE_BASE_URL=https://qaaqwh.qhdsafety.com/integrated_whb/ #VITE_BASE_URL=https://qaaqwh.qhdsafety.com/integrated_whb/
#websocket t掉线 #websocket t掉线
VITE_ON_LINE_WEB_SOCKET_URL=ws://183.251.104.38:10103 VITE_ON_LINE_WEB_SOCKET_URL=ws://172.16.70.226:8869
# VITE_ON_LINE_WEB_SOCKET_URL=wss://qaaqwh.qhdsafety.com/disconnected/ # VITE_ON_LINE_WEB_SOCKET_URL=wss://qaaqwh.qhdsafety.com/disconnected/
#websocket 在线学习 #websocket 在线学习
VITE_LEARNING_WEB_SOCKET_URL=ws://183.251.104.38:10102 VITE_LEARNING_WEB_SOCKET_URL=ws://172.16.70.226:8899
#VITE_LEARNING_WEB_SOCKET_URL=wss://qaaqwh.qhdsafety.com/onlinelearning/ #VITE_LEARNING_WEB_SOCKET_URL=wss://qaaqwh.qhdsafety.com/onlinelearning/
#websocket 大华设备视频
VITE_DAHUA_WEB_SOCKET_URL=ws://172.16.70.226:8866

4
env.d.ts vendored
View File

@ -19,4 +19,8 @@ interface ImportMetaEnv {
* websocket 线 * websocket 线
*/ */
readonly VITE_LEARNING_WEB_SOCKET_URL: string readonly VITE_LEARNING_WEB_SOCKET_URL: string
/**
* websocket
*/
readonly VITE_DAHUA_WEB_SOCKET_URL: string
} }

30
package-lock.json generated
View File

@ -20,12 +20,14 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"element-plus": "^2.6.1", "element-plus": "^2.6.1",
"flv.js": "^1.6.2",
"hls.js": "^1.6.13", "hls.js": "^1.6.13",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"mp4box": "^0.5.2", "mp4box": "^0.5.2",
"mpegts.js": "^1.8.0",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pako": "^2.1.0", "pako": "^2.1.0",
@ -3362,6 +3364,11 @@
"es6-symbol": "^3.1.1" "es6-symbol": "^3.1.1"
} }
}, },
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmmirror.com/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"node_modules/es6-symbol": { "node_modules/es6-symbol": {
"version": "3.1.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
@ -4110,6 +4117,15 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/flv.js": {
"version": "1.6.2",
"resolved": "https://registry.npmmirror.com/flv.js/-/flv.js-1.6.2.tgz",
"integrity": "sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==",
"dependencies": {
"es6-promise": "^4.2.8",
"webworkify-webpack": "^2.1.5"
}
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@ -5445,6 +5461,15 @@
"integrity": "sha512-GcCH0fySxBurJtvr0dfhz0IxHZjc1RP+F+I8xw+LIwkU1a+7HJx8NCDiww1I5u4Hz6g4eR1JlGADEGJ9r4lSfA==", "integrity": "sha512-GcCH0fySxBurJtvr0dfhz0IxHZjc1RP+F+I8xw+LIwkU1a+7HJx8NCDiww1I5u4Hz6g4eR1JlGADEGJ9r4lSfA==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/mpegts.js": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/mpegts.js/-/mpegts.js-1.8.0.tgz",
"integrity": "sha512-ZtujqtmTjWgcDDkoOnLvrOKUTO/MKgLHM432zGDI8oPaJ0S+ebPxg1nEpDpLw6I7KmV/GZgUIrfbWi3qqEircg==",
"dependencies": {
"es6-promise": "^4.2.5",
"webworkify-webpack": "github:xqq/webworkify-webpack"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -7681,6 +7706,11 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/webworkify-webpack": {
"version": "2.1.5",
"resolved": "git+ssh://git@github.com/xqq/webworkify-webpack.git#24d1e719b4a6cac37a518b2bb10fe124527ef4ef",
"license": "MIT"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -22,12 +22,14 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"element-plus": "^2.6.1", "element-plus": "^2.6.1",
"flv.js": "^1.6.2",
"hls.js": "^1.6.13", "hls.js": "^1.6.13",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"mp4box": "^0.5.2", "mp4box": "^0.5.2",
"mpegts.js": "^1.8.0",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pako": "^2.1.0", "pako": "^2.1.0",

View File

@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="dialogVisible" title="播放后台转码视频" width="50%"> <el-dialog v-model="dialogVisible" title="播放大华设备视频" width="50%">
<!-- 原生video播放器 - 移除了controls属性以去掉默认控制栏 --> <!-- 原生video播放器 - 移除了controls属性以去掉默认控制栏 -->
<video ref="videoRef" playsinline class="video-player"></video> <video ref="videoRef" playsinline class="video-player"></video>
@ -11,12 +11,10 @@
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<script setup> <script setup>
import { ref, watch } from "vue"; import { ref, watch, nextTick } from "vue";
import Hls from "hls.js"; // HLS import mpegts from "mpegts.js"; // mpegts.js
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { getTranscodeStatus, stopTransCode } from "@/request/video_info.js"; //
// //
const props = defineProps({ const props = defineProps({
@ -24,54 +22,35 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
src: { channel: {
type: String, type: Number,
required: true, // HLS default: 0, // 0
},
videoId: {
type: String,
required: true, // Id
}, },
}); });
// //
const emit = defineEmits(["update:visible", "onStop"]); const emit = defineEmits(["update:visible"]);
// props.visible // props.visible
const dialogVisible = ref(props.visible); const dialogVisible = ref(props.visible);
const videoRef = ref(null); // video const videoRef = ref(null); // video
const isPlaying = ref(false); // let player = null; // MPEGTS
let hlsInstance = null; // HLS
// visibledialogVisible // visibledialogVisible
watch( watch(
() => props.visible, () => props.visible,
(newVal) => { (newVal) => {
dialogVisible.value = newVal; dialogVisible.value = newVal;
} }
); );
//
let checkInterval = null;
// //
const onDialogOpen = () => { const onDialogOpen = () => {
// 3 // DOM
checkInterval = setInterval(async () => { nextTick(() => {
try { //
// checkVideoProgress handlePlay();
const response = await getTranscodeStatus({ id: props.videoId }); });
if (response.progress > 2) {
handlePlay();
//
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
}
}
//
} catch (error) {}
}, 3000); // 3
}; };
// dialogVisible // dialogVisible
@ -84,127 +63,110 @@ watch(dialogVisible, (newVal) => {
} }
}); });
// // MPEGTS
const handlePlay = () => { const initMpegtsPlayer = () => {
const video = videoRef.value; //
if (!video) { if (player) {
ElMessage.error("视频播放器初始化失败"); player.destroy();
player = null;
}
// video
if (!videoRef.value) {
console.error("无法获取video元素");
return; return;
} }
// // 使mpegts.js
video.src = ""; if (mpegts.isSupported()) {
const videoElement = videoRef.value;
// WebSocket URL
const baseUrl = import.meta.env.VITE_DAHUA_WEB_SOCKET_URL || "ws://localhost:8866";
const separator = baseUrl.includes('?') ? '&' : '?';
const websocketUrl = `${baseUrl}${separator}user=user1&channel=${props.channel}`;
// src //
if (!props.src) { if (player) {
ElMessage.error("视频源地址为空"); player.destroy();
return; player = null;
}
// HLS.js
if (Hls.isSupported()) {
//
if (hlsInstance) {
hlsInstance.destroy();
} }
hlsInstance = new Hls({ // mpegts.jsWebSocket
maxBufferLength: 30, // player = mpegts.createPlayer({
maxMaxBufferLength: 60, type: "flv",
isLive: true,
hasAudio: false,
hasVideo: true,
url: websocketUrl, // WebSocket URLmpegts.js
}, {
enableWorker: false,
lazyLoad: false,
reuseRedirectedURL: false
}); });
// // video
hlsInstance.on(Hls.Events.ERROR, (event, data) => { player.attachMediaElement(videoElement);
if (data.fatal) {
switch (data.type) { //
case Hls.ErrorTypes.NETWORK_ERROR: player.on(mpegts.Events.MEDIA_INFO, (mediaInfo) => {
hlsInstance.startLoad(); console.log('媒体信息:', mediaInfo);
break;
case Hls.ErrorTypes.MEDIA_ERROR:
hlsInstance.recoverMediaError();
break;
default:
//
destroyPlayer();
handlePlay();
break;
}
}
}); });
hlsInstance.loadSource(props.src); player.on(mpegts.Events.ERROR, (type, details) => {
hlsInstance.attachMedia(video); console.error('播放器错误:', type, details);
ElMessage.error(`播放器错误: ${type}`);
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
video.play().then(() => {
isPlaying.value = true;
});
}); });
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
// SafariHLS //
video.src = props.src; player.load();
video.addEventListener("loadedmetadata", () => { player.play().catch((e) => {
video console.error("播放失败:", e);
.play() ElMessage.error("播放失败: " + e.message);
.then(() => {
isPlaying.value = true;
ElMessage.success("视频开始播放");
})
.catch((err) => {
ElMessage.error(`播放失败:${err.message}`);
});
}); });
} else { } else {
ElMessage.error("您的浏览器不支持HLS视频流播放请更换浏览器"); ElMessage.error("浏览器不支持mpegts.js");
} }
}; };
// //
const destroyPlayer = () => { const handlePlay = () => {
// //
if (checkInterval) { if (videoRef.value) {
clearInterval(checkInterval); videoRef.value.src = "";
checkInterval = null;
} }
// //
isPlaying.value = false; playDahuaVideo();
if (hlsInstance) { };
hlsInstance.destroy();
hlsInstance = null; //
const playDahuaVideo = () => {
if (!mpegts.isSupported()) {
ElMessage.error("您的浏览器不支持MPEG-TS视频播放");
return;
} }
try {
//
initMpegtsPlayer();
} catch (error) {
ElMessage.error("播放器初始化失败:" + error.message);
}
};
//
const destroyPlayer = () => {
// MPEGTSWebSocket
if (player) {
player.destroy();
player = null;
}
if (videoRef.value) { if (videoRef.value) {
const video = videoRef.value; const video = videoRef.value;
video.pause(); video.pause();
video.src = ""; video.src = "";
} }
handleStopTranscode();
}; };
</script>
//
const handleStopTranscode = async () => {
try {
await stopTransCode({ id: props.videoId }); //
emit("onStop"); //
dialogVisible.value = false; //
} catch (error) {
if (error !== "cancel") {
ElMessage.error(`停止转码失败:${error.message || "未知错误"}`);
}
}
};
</script>
<style scoped>
.video-player {
width: 100%;
min-height: 600px;
object-fit: contain; /* 保持视频比例 */
background-color: #000; /* 增加黑色背景,使视频区域更明显 */
}
.video-controls {
margin-top: 16px;
text-align: center;
}
</style>

View File

@ -18,9 +18,6 @@
<el-button native-type="reset" @click="fnResetPagination" <el-button native-type="reset" @click="fnResetPagination"
>重置</el-button >重置</el-button
> >
<el-button type="success" @click="handleStartTranscode"
>播放后台转码视频</el-button
>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -106,12 +103,11 @@
@get-data="fnResetPagination" @get-data="fnResetPagination"
/> />
<!-- 转码视频播放器修改后双向绑定visible + 传递src --> <!-- 大华设备视频播放器 -->
<play-video <play-video
v-model:visible="data.transcodeVideoDialog.visible" v-model:visible="data.dahuaVideoDialog.visible"
:src="data.transcodeVideoDialog.src" :channel="data.dahuaVideoDialog.channel"
:video-id="data.transcodeVideoDialog.id" @update:visible="handlePlayVideoClose"
@on-stop="onTranscodeStopped"
/> />
</div> </div>
</template> </template>
@ -130,8 +126,7 @@ import {ElMessage, ElMessageBox} from "element-plus";
import SelectingPoints from "./components/selecting_points.vue"; import SelectingPoints from "./components/selecting_points.vue";
import LayoutVideo from "@/components/video/index.vue"; import LayoutVideo from "@/components/video/index.vue";
import {setVideoManagerList} from "@/request/eightwork_videomanager.js"; import {setVideoManagerList} from "@/request/eightwork_videomanager.js";
import PlayVideo from "@/views/video_manager/video_manager/components/playVideo.vue"; // import PlayVideo from "@/views/video_manager/video_manager/components/playVideo.vue"; //
import {startTransCode, stopTransCode} from "@/request/video_info.js"; //
const data = reactive({ const data = reactive({
addDialog: { addDialog: {
@ -155,42 +150,18 @@ const data = reactive({
videomanagerId: "", videomanagerId: "",
visible: false, visible: false,
}, },
transcodeVideoDialog: { dahuaVideoDialog: {
visible: false, visible: false,
src: "http://localhost:8100/api/hls/stream.m3u8", // HLS访
id: "", id: "",
channel: 0 //
}, },
}); });
// //
const handleStartTranscode = async () => {
try {
await startTransCode(); //
data.transcodeVideoDialog.visible = true; //
} catch (error) {
ElMessage.error("启动转码失败: " + (error.message || "未知错误"));
}
};
//
const onTranscodeStopped = async () => {
await stopTransCode(); //
};
//
const {list, pagination, searchForm, fnGetData, fnResetPagination} = const {list, pagination, searchForm, fnGetData, fnResetPagination} =
useListData(getHkVideoManagerList); 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) => { const fnUpToBi = async (videomanagerId) => {
await ElMessageBox.confirm("确定要置顶吗置顶后将会默认展示在Bi页", { await ElMessageBox.confirm("确定要置顶吗置顶后将会默认展示在Bi页", {
type: "warning", type: "warning",
@ -200,14 +171,16 @@ const fnUpToBi = async (videomanagerId) => {
}; };
const fnPreviewVideo = async (row) => { const fnPreviewVideo = async (row) => {
try { //
const resData = await startTransCode({url: row.url, id: row.PLS_ID}); // data.dahuaVideoDialog.visible = true;
data.transcodeVideoDialog.visible = true; // data.dahuaVideoDialog.channel = row.channel;
data.transcodeVideoDialog.id = row.PLS_ID; // console.log("播放大华设备视频:", row.channel);
data.transcodeVideoDialog.src = };
"http://172.16.70.226:7811/" + resData.videoUrl + "stream.m3u8";
} catch (error) { const handlePlayVideoClose = (visible) => {
ElMessage.error("启动转码失败: " + (error.message || "未知错误")); if (!visible) {
//
console.log("视频播放窗口已关闭");
} }
}; };