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_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掉线
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 在线学习
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/
#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/
#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/
#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 线
*/
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",
"echarts": "^5.4.3",
"element-plus": "^2.6.1",
"flv.js": "^1.6.2",
"hls.js": "^1.6.13",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"mp4box": "^0.5.2",
"mpegts.js": "^1.8.0",
"nanoid": "^5.0.4",
"normalize.css": "^8.0.1",
"pako": "^2.1.0",
@ -3362,6 +3364,11 @@
"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": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
@ -4110,6 +4117,15 @@
"dev": true,
"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": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@ -5445,6 +5461,15 @@
"integrity": "sha512-GcCH0fySxBurJtvr0dfhz0IxHZjc1RP+F+I8xw+LIwkU1a+7HJx8NCDiww1I5u4Hz6g4eR1JlGADEGJ9r4lSfA==",
"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": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -7681,6 +7706,11 @@
"dev": true,
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

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

View File

@ -1,5 +1,5 @@
<template>
<el-dialog v-model="dialogVisible" title="播放后台转码视频" width="50%">
<el-dialog v-model="dialogVisible" title="播放大华设备视频" width="50%">
<!-- 原生video播放器 - 移除了controls属性以去掉默认控制栏 -->
<video ref="videoRef" playsinline class="video-player"></video>
@ -11,12 +11,10 @@
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from "vue";
import Hls from "hls.js"; // HLS
import { ref, watch, nextTick } from "vue";
import mpegts from "mpegts.js"; // mpegts.js
import { ElMessage } from "element-plus";
import { getTranscodeStatus, stopTransCode } from "@/request/video_info.js"; //
//
const props = defineProps({
@ -24,24 +22,19 @@ const props = defineProps({
type: Boolean,
default: false,
},
src: {
type: String,
required: true, // HLS
},
videoId: {
type: String,
required: true, // Id
channel: {
type: Number,
default: 0, // 0
},
});
//
const emit = defineEmits(["update:visible", "onStop"]);
const emit = defineEmits(["update:visible"]);
// props.visible
const dialogVisible = ref(props.visible);
const videoRef = ref(null); // video
const isPlaying = ref(false); //
let hlsInstance = null; // HLS
let player = null; // MPEGTS
// visibledialogVisible
watch(
@ -51,27 +44,13 @@ watch(
}
);
//
let checkInterval = null;
//
const onDialogOpen = () => {
// 3
checkInterval = setInterval(async () => {
try {
// checkVideoProgress
const response = await getTranscodeStatus({ id: props.videoId });
if (response.progress > 2) {
// DOM
nextTick(() => {
//
handlePlay();
//
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
}
}
//
} catch (error) {}
}, 3000); // 3
});
};
// dialogVisible
@ -84,127 +63,110 @@ watch(dialogVisible, (newVal) => {
}
});
//
const handlePlay = () => {
const video = videoRef.value;
if (!video) {
ElMessage.error("视频播放器初始化失败");
// MPEGTS
const initMpegtsPlayer = () => {
//
if (player) {
player.destroy();
player = null;
}
// video
if (!videoRef.value) {
console.error("无法获取video元素");
return;
}
//
video.src = "";
// 使mpegts.js
if (mpegts.isSupported()) {
const videoElement = videoRef.value;
// src
if (!props.src) {
ElMessage.error("视频源地址为空");
return;
// 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}`;
//
if (player) {
player.destroy();
player = null;
}
// HLS.js
if (Hls.isSupported()) {
//
if (hlsInstance) {
hlsInstance.destroy();
}
hlsInstance = new Hls({
maxBufferLength: 30, //
maxMaxBufferLength: 60,
// mpegts.jsWebSocket
player = mpegts.createPlayer({
type: "flv",
isLive: true,
hasAudio: false,
hasVideo: true,
url: websocketUrl, // WebSocket URLmpegts.js
}, {
enableWorker: false,
lazyLoad: false,
reuseRedirectedURL: false
});
//
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();
break;
}
}
// video
player.attachMediaElement(videoElement);
//
player.on(mpegts.Events.MEDIA_INFO, (mediaInfo) => {
console.log('媒体信息:', mediaInfo);
});
hlsInstance.loadSource(props.src);
hlsInstance.attachMedia(video);
player.on(mpegts.Events.ERROR, (type, details) => {
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;
video.addEventListener("loadedmetadata", () => {
video
.play()
.then(() => {
isPlaying.value = true;
ElMessage.success("视频开始播放");
})
.catch((err) => {
ElMessage.error(`播放失败:${err.message}`);
});
//
player.load();
player.play().catch((e) => {
console.error("播放失败:", e);
ElMessage.error("播放失败: " + e.message);
});
} else {
ElMessage.error("您的浏览器不支持HLS视频流播放请更换浏览器");
ElMessage.error("浏览器不支持mpegts.js");
}
};
//
const destroyPlayer = () => {
//
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
//
const handlePlay = () => {
//
if (videoRef.value) {
videoRef.value.src = "";
}
//
isPlaying.value = false;
if (hlsInstance) {
hlsInstance.destroy();
hlsInstance = null;
//
playDahuaVideo();
};
//
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) {
const video = videoRef.value;
video.pause();
video.src = "";
}
handleStopTranscode();
};
//
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
>
<el-button type="success" @click="handleStartTranscode"
>播放后台转码视频</el-button
>
</el-form-item>
</el-col>
</el-row>
@ -106,12 +103,11 @@
@get-data="fnResetPagination"
/>
<!-- 转码视频播放器修改后双向绑定visible + 传递src -->
<!-- 大华设备视频播放器 -->
<play-video
v-model:visible="data.transcodeVideoDialog.visible"
:src="data.transcodeVideoDialog.src"
:video-id="data.transcodeVideoDialog.id"
@on-stop="onTranscodeStopped"
v-model:visible="data.dahuaVideoDialog.visible"
:channel="data.dahuaVideoDialog.channel"
@update:visible="handlePlayVideoClose"
/>
</div>
</template>
@ -130,8 +126,7 @@ 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"; //
const data = reactive({
addDialog: {
@ -155,42 +150,18 @@ const data = reactive({
videomanagerId: "",
visible: false,
},
transcodeVideoDialog: {
dahuaVideoDialog: {
visible: false,
src: "http://localhost:8100/api/hls/stream.m3u8", // HLS访
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} =
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",
@ -200,14 +171,16 @@ 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";
} catch (error) {
ElMessage.error("启动转码失败: " + (error.message || "未知错误"));
//
data.dahuaVideoDialog.visible = true;
data.dahuaVideoDialog.channel = row.channel;
console.log("播放大华设备视频:", row.channel);
};
const handlePlayVideoClose = (visible) => {
if (!visible) {
//
console.log("视频播放窗口已关闭");
}
};