feat(video): 实现大华设备视频预览功能

- 修改后端控制器路径从 /api/video为 /dahuaVideo
- 更新登录接口使用固定测试凭证
- 实现基于 StreamingResponseBody 的视频流传输
- 前端移除转码相关逻辑,集成大华播放器组件
- 添加设备登录和预览启动/停止接口
-优化视频播放器组件支持大华流媒体播放
- 调整播放器UI增加控制按钮和错误处理- 修复视频播放错误提示和资源释放逻辑
dev
wangyan 2025-10-20 17:21:06 +08:00
parent 93dac9ab39
commit b269359715
3 changed files with 100 additions and 165 deletions

View File

@ -14,3 +14,7 @@ export const stopTransCode = (params) =>
post("/playVideo/stopTranscode", params); post("/playVideo/stopTranscode", params);
export const getTranscodeStatus = (params) => export const getTranscodeStatus = (params) =>
post("/playVideo/getTranscodeStatus", params); post("/playVideo/getTranscodeStatus", params);
export const loginDahua = (params) =>
post("/dahuaVideo/login", params);
export const getDahuaSDKStatus = (params) =>
post("/dahuaVideo/status", params);

View File

@ -1,22 +1,29 @@
<template> <template>
<el-dialog v-model="dialogVisible" title="播放后台转码视频" width="50%"> <el-dialog v-model="dialogVisible" title="视频播放" width="50%" @close="handleStop">
<!-- 原生video播放器 - 移除了controls属性以去掉默认控制栏 --> <!-- 原生video播放器 -->
<video ref="videoRef" playsinline class="video-player"></video> <video
ref="videoRef"
playsinline
controls
class="video-player"
@error="handleVideoError"
></video>
<!-- 底部操作按钮 --> <!-- 底部操作按钮 -->
<template #footer> <template #footer>
<el-button type="primary" @click="dialogVisible = false"> <el-button type="primary" @click="handleStopPreview">
关闭播放 停止播放
</el-button>
<el-button @click="dialogVisible = false">
关闭
</el-button> </el-button>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<script setup> <script setup>
import { ref, watch } from "vue"; import { ref, watch, onBeforeUnmount } from "vue";
import Hls from "hls.js"; // HLS
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { getTranscodeStatus, stopTransCode } from "@/request/video_info.js"; //
// //
const props = defineProps({ const props = defineProps({
@ -26,61 +33,42 @@ const props = defineProps({
}, },
src: { src: {
type: String, type: String,
required: true, // HLS default: ""
},
videoId: {
type: String,
required: true, // Id
}, },
channel: {
type: Number,
default: 0
}
}); });
// //
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 hlsInstance = null; // HLS
// visibledialogVisible // visibledialogVisible
watch( watch(
() => props.visible, () => props.visible,
(newVal) => { (newVal) => {
dialogVisible.value = newVal; dialogVisible.value = newVal;
if (newVal) {
// DOM
setTimeout(() => {
handlePlay();
}, 100);
} else {
handleStop();
}
} }
); );
//
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 // dialogVisible
watch(dialogVisible, (newVal) => { watch(dialogVisible, (newVal) => {
emit("update:visible", newVal); emit("update:visible", newVal);
if (newVal) { if (!newVal) {
onDialogOpen(); handleStop();
} else {
destroyPlayer(); //
} }
}); });
@ -92,115 +80,68 @@ const handlePlay = () => {
return; return;
} }
//
video.src = "";
// src
if (!props.src) { if (!props.src) {
ElMessage.error("视频源地址为空"); ElMessage.error("视频源地址为空");
return; return;
} }
// HLS.js //
if (Hls.isSupported()) {
//
if (hlsInstance) {
hlsInstance.destroy();
}
hlsInstance = new Hls({
maxBufferLength: 30, //
maxMaxBufferLength: 60,
});
//
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;
}
}
});
hlsInstance.loadSource(props.src);
hlsInstance.attachMedia(video);
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.src = props.src;
video.addEventListener("loadedmetadata", () => {
video //
.play() video.play().then(() => {
.then(() => {
isPlaying.value = true;
ElMessage.success("视频开始播放"); ElMessage.success("视频开始播放");
}) }).catch((err) => {
.catch((err) => {
ElMessage.error(`播放失败:${err.message}`); ElMessage.error(`播放失败:${err.message}`);
}); });
});
} else {
ElMessage.error("您的浏览器不支持HLS视频流播放请更换浏览器");
}
}; };
// //
const destroyPlayer = () => { const handleStop = () => {
//
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
}
//
isPlaying.value = false;
if (hlsInstance) {
hlsInstance.destroy();
hlsInstance = null;
}
if (videoRef.value) {
const video = videoRef.value; const video = videoRef.value;
if (video) {
video.pause(); video.pause();
video.src = ""; video.removeAttribute('src'); // src
video.load(); //
} }
handleStopTranscode();
}; };
// //
const handleStopTranscode = async () => { const handleVideoError = (e) => {
console.error("视频播放错误:", e);
ElMessage.error("视频播放出现错误,请检查网络连接或设备状态");
};
//
const handleStopPreview = async () => {
try { try {
await stopTransCode({ id: props.videoId }); // const response = await fetch('/dahuaVideo/stopPreview', {
emit("onStop"); // method: 'POST',
dialogVisible.value = false; // });
} catch (error) { const result = await response.json();
if (error !== "cancel") { if (result.success) {
ElMessage.error(`停止转码失败:${error.message || "未知错误"}`); ElMessage.success("停止预览成功");
handleStop();
} else {
ElMessage.error("停止预览失败: " + result.message);
} }
} catch (error) {
ElMessage.error("停止预览失败: " + (error.message || "未知错误"));
} }
}; };
//
onBeforeUnmount(() => {
handleStop();
});
</script> </script>
<style scoped> <style scoped>
.video-player { .video-player {
width: 100%; width: 100%;
min-height: 600px; min-height: 600px;
object-fit: contain; /* 保持视频比例 */ object-fit: contain;
background-color: #000; /* 增加黑色背景,使视频区域更明显 */ background-color: #000;
} }
.video-controls { .video-controls {

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" :src="data.dahuaVideoDialog.src"
:video-id="data.transcodeVideoDialog.id" :channel="data.dahuaVideoDialog.channel"
@on-stop="onTranscodeStopped"
/> />
</div> </div>
</template> </template>
@ -130,8 +126,8 @@ 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"; // import {loginDahua} from "@/request/video_info.js";
const data = reactive({ const data = reactive({
addDialog: { addDialog: {
@ -155,28 +151,13 @@ const data = reactive({
videomanagerId: "", videomanagerId: "",
visible: false, visible: false,
}, },
transcodeVideoDialog: { dahuaVideoDialog: {
visible: false, visible: false,
src: "http://localhost:8100/api/hls/stream.m3u8", // HLS访 src: "",
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);
@ -201,13 +182,22 @@ const fnUpToBi = async (videomanagerId) => {
const fnPreviewVideo = async (row) => { const fnPreviewVideo = async (row) => {
try { try {
const resData = await startTransCode({ url: row.url, id: row.PLS_ID }); // //
data.transcodeVideoDialog.visible = true; // const loginResult = await loginDahua();
data.transcodeVideoDialog.id = row.PLS_ID; // console.log("大华设备登录结果:", loginResult);
data.transcodeVideoDialog.src =
"http://172.16.70.226:7811/" + resData.videoUrl + "stream.m3u8"; if (!loginResult.success) {
ElMessage.error("设备登录失败: " + loginResult.message);
return;
}
// SDK
data.dahuaVideoDialog.visible = true;
data.dahuaVideoDialog.channel = row.PLS_ID || 0;
data.dahuaVideoDialog.src = `/dahuaVideo/start-preview?channel=${row.PLS_ID || 0}`;
} catch (error) { } catch (error) {
ElMessage.error("启动转码失败: " + (error.message || "未知错误")); console.error("播放视频过程出错:", error);
ElMessage.error("播放视频失败: " + (error.message || "未知错误"));
} }
}; };