feat(video): 实现后台转码视频播放功能

- 新增“播放后台转码视频”按钮,用于启动转码并播放HLS流- 替换原有大华设备直接播放逻辑为调用后端转码接口
- 引入HLS.js库以支持HLS流播放,并增加错误处理机制
- 修改播放器组件,移除默认控制栏,增加定时检测转码进度逻辑
- 实现转码开始与停止接口调用,增强播放流程的可控性
- 更新弹窗标题与关闭逻辑,优化用户体验
- 调整视频播放器样式,提升视觉效果与兼容性
dev
wangyan 2025-10-20 17:52:44 +08:00
parent b269359715
commit 54ca5e9e4c
2 changed files with 221 additions and 152 deletions

View File

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

View File

@ -2,9 +2,9 @@
<div>
<el-card>
<el-form
:model="searchForm"
label-width="60px"
@submit.prevent="fnResetPagination"
:model="searchForm"
label-width="60px"
@submit.prevent="fnResetPagination"
>
<el-row>
<el-col :span="6">
@ -16,7 +16,10 @@
<el-form-item label-width="10px">
<el-button type="primary" native-type="submit">搜索</el-button>
<el-button native-type="reset" @click="fnResetPagination"
>重置</el-button
>重置</el-button
>
<el-button type="success" @click="handleStartTranscode"
>播放后台转码视频</el-button
>
</el-form-item>
</el-col>
@ -26,9 +29,9 @@
<layout-card>
<layout-table
v-model:pagination="pagination"
:data="list"
@get-data="fnGetData"
v-model:pagination="pagination"
:data="list"
@get-data="fnGetData"
>
<!-- 表格列定义保持不变 -->
<el-table-column label="序号" width="70">
@ -42,9 +45,9 @@
<el-table-column property="PLS_ID" label="是否定位">
<template #default="{ row }">
<el-popover
placement="top-start"
trigger="hover"
content="定位后才能在Bi页上展示"
placement="top-start"
trigger="hover"
content="定位后才能在Bi页上展示"
>
<template #reference>
<el-tag v-if="row.PLS_ID" type="success"> </el-tag>
@ -56,25 +59,25 @@
<el-table-column label="操作" width="250">
<template #default="{ row }">
<el-button
v-if="row.ISSHOW === 0"
type="primary"
text
link
@click="fnUpToBi(row.videomanagerId)"
>置顶</el-button
v-if="row.ISSHOW === 0"
type="primary"
text
link
@click="fnUpToBi(row.videomanagerId)"
>置顶</el-button
>
<!-- <el-button type="primary" text link @click="fnSetPositioning(row)">-->
<!-- {{ row.PLS_ID ? "修改定位" : "添加定位" }}-->
<!-- </el-button>-->
<el-button link type="primary" @click="fnPreviewVideo(row)"
>播放视频</el-button
>播放视频</el-button
>
<el-button
v-if="row.videomanagerId"
link
type="danger"
@click="fnDeleteVideo(row)"
>移除定位</el-button
v-if="row.videomanagerId"
link
type="danger"
@click="fnDeleteVideo(row)"
>移除定位</el-button
>
</template>
</el-table-column>
@ -83,37 +86,38 @@
<!-- 其他弹窗保持不变 -->
<add
v-model:visible="data.addDialog.Visible"
v-model:form="data.addDialog.form"
:type="data.addDialog.type"
@get-data="fnResetPagination"
v-model:visible="data.addDialog.Visible"
v-model:form="data.addDialog.form"
:type="data.addDialog.type"
@get-data="fnResetPagination"
/>
<layout-video
v-if="data.videoDialog.visible"
v-model:visible="data.videoDialog.visible"
:src="data.videoDialog.src"
v-if="data.videoDialog.visible"
v-model:visible="data.videoDialog.visible"
:src="data.videoDialog.src"
/>
<selecting-points
:id="data.selectingPointsDialog.id"
v-model:visible="data.selectingPointsDialog.visible"
:index-code="data.selectingPointsDialog.indexCode"
:region-path-name="data.selectingPointsDialog.regionPathName"
:cam-name="data.selectingPointsDialog.camName"
:videomanager-id="data.selectingPointsDialog.videomanagerId"
@get-data="fnResetPagination"
:id="data.selectingPointsDialog.id"
v-model:visible="data.selectingPointsDialog.visible"
:index-code="data.selectingPointsDialog.indexCode"
:region-path-name="data.selectingPointsDialog.regionPathName"
:cam-name="data.selectingPointsDialog.camName"
:videomanager-id="data.selectingPointsDialog.videomanagerId"
@get-data="fnResetPagination"
/>
<!-- 大华视频播放器 -->
<!-- 转码视频播放器修改后双向绑定visible + 传递src -->
<play-video
v-model:visible="data.dahuaVideoDialog.visible"
:src="data.dahuaVideoDialog.src"
:channel="data.dahuaVideoDialog.channel"
v-model:visible="data.transcodeVideoDialog.visible"
:src="data.transcodeVideoDialog.src"
:video-id="data.transcodeVideoDialog.id"
@on-stop="onTranscodeStopped"
/>
</div>
</template>
<script setup>
import { serialNumber } from "@/assets/js/utils.js";
import {serialNumber} from "@/assets/js/utils.js";
import useListData from "@/assets/js/useListData.js";
import {
getHkVideoManagerList,
@ -121,13 +125,13 @@ import {
setVideoManagerDelete,
} from "@/request/video_manager.js";
import Add from "./components/add.vue";
import { reactive } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {reactive} from "vue";
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 {loginDahua} from "@/request/video_info.js";
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"; //
const data = reactive({
addDialog: {
@ -151,16 +155,31 @@ const data = reactive({
videomanagerId: "",
visible: false,
},
dahuaVideoDialog: {
transcodeVideoDialog: {
visible: false,
src: "",
channel: 0,
src: "http://localhost:8100/api/hls/stream.m3u8", // HLS访
id: "",
},
});
//
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 {list, pagination, searchForm, fnGetData, fnResetPagination} =
useListData(getHkVideoManagerList);
//
// const fnSetPositioning = async (row) => {
@ -176,28 +195,19 @@ const fnUpToBi = async (videomanagerId) => {
await ElMessageBox.confirm("确定要置顶吗置顶后将会默认展示在Bi页", {
type: "warning",
});
await setUpToBi({ videomanagerId });
ElMessage({ message: "操作成功", type: "success" });
await setUpToBi({videomanagerId});
ElMessage({message: "操作成功", type: "success"});
};
const fnPreviewVideo = async (row) => {
try {
//
const loginResult = await loginDahua();
console.log("大华设备登录结果:", loginResult);
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}`;
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) {
console.error("播放视频过程出错:", error);
ElMessage.error("播放视频失败: " + (error.message || "未知错误"));
ElMessage.error("启动转码失败: " + (error.message || "未知错误"));
}
};
@ -211,8 +221,8 @@ const fnDeleteVideo = async (row) => {
return;
}
await ElMessageBox.confirm(
"移除定位将会结束视频推送,并删除推送信息。确定要移除定位吗?",
{ type: "warning" }
"移除定位将会结束视频推送,并删除推送信息。确定要移除定位吗?",
{type: "warning"}
);
await setVideoManagerDelete({
VIDEOMANAGER_ID: row.videomanagerId,