feat(video): 实现基于视频流播放WebSocket的FLV功能

- 修改了Java后端代码,将视频转码服务从HLS改为FLV格式输出
- 删除了原有的HLS相关控制器和转码逻辑
- 新增了VideoWebSocketServer和VideoServerPool类来管理WebSocket连接和视频流传输- 更新了前端Vue组件,使用flv.js替代hls.js来播放视频流- 增加了WebSocket通信机制,通过二进制数据传输FLV视频流
- 移除了旧的转码状态检查和控制逻辑
- 优化了FFmpeg命令参数以适配FLV流媒体传输
- 添加了WebSocket服务器启动配置,并集成到应用启动流程中
dev_flv
wangyan 2025-11-01 20:56:11 +08:00
parent 54ca5e9e4c
commit ebba316be7
6 changed files with 140 additions and 157 deletions

View File

@ -1,9 +1,9 @@
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

View File

@ -1,12 +1,11 @@
VITE_BASE=/dist
#VITE_BASE_URL=http://10.199.64.27:8520/integrated_yjb/
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=wss://qaaqwh.qhdsafety.com/disconnected/
#VITE_ON_LINE_WEB_SOCKET_URL=ws://172.16.70.226:10103
#websocket 在线学习
VITE_LEARNING_WEB_SOCKET_URL=ws://183.251.104.38:10102
#VITE_LEARNING_WEB_SOCKET_URL=wss://qaaqwh.qhdsafety.com/onlinelearning/
#VITE_LEARNING_WEB_SOCKET_URL=ws://172.16.70.226:8899

View File

@ -22,6 +22,7 @@
"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",

View File

@ -8,13 +8,4 @@ export const getvideoInfoView = (params) => post("/videoInfo/goEdit", params); /
export const setvideoInfoDelete = (params) => post("/videoInfo/delete", params); // 删除
export const setUpToBi = (params) => post("/videoInfo/editZhiding", params); // 删除
export const setvideoInfoEdit = (params) => post("/videoInfo/edit", params);
export const startTransCode = (params) =>
post("/playVideo/startTranscode", params);
export const stopTransCode = (params) =>
post("/playVideo/stopTranscode", params);
export const 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,17 @@
<template>
<el-dialog v-model="dialogVisible" title="播放后台转码视频" width="50%">
<!-- 原生video播放器 - 移除了controls属性以去掉默认控制栏 -->
<el-dialog v-model="dialogVisible" title="播放视频监控" width="50%">
<video ref="videoRef" playsinline class="video-player"></video>
<!-- 底部操作按钮 -->
<template #footer>
<el-button type="primary" @click="dialogVisible = false">
关闭播放
</el-button>
<el-button type="primary" @click="handleClose"> </el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from "vue";
import Hls from "hls.js"; // HLS
import { ref, watch, onUnmounted, nextTick } from "vue";
import flvjs from "flv.js";
import { ElMessage } from "element-plus";
import { getTranscodeStatus, stopTransCode } from "@/request/video_info.js"; //
//
const props = defineProps({
@ -26,7 +21,7 @@ const props = defineProps({
},
src: {
type: String,
required: true, // HLS
required: true, // FLV
},
videoId: {
type: String,
@ -40,46 +35,26 @@ 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 flvPlayer = null; // FLV
// 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) {
onDialogOpen();
// 使nextTickDOM
nextTick(() => {
handlePlay();
});
} else {
//
destroyPlayer(); //
}
});
@ -101,97 +76,135 @@ const handlePlay = () => {
return;
}
// HLS.js
if (Hls.isSupported()) {
//
if (hlsInstance) {
hlsInstance.destroy();
}
// 使FLV
playWithFLV();
};
hlsInstance = new Hls({
maxBufferLength: 30, //
maxMaxBufferLength: 60,
});
// 使FLV
const playWithFLV = () => {
const video = videoRef.value;
if (!video) {
ElMessage.error("视频播放器初始化失败");
return;
}
//
if (flvPlayer) {
flvPlayer.destroy();
flvPlayer = null;
}
// flv.js
if (!flvjs.isSupported()) {
ElMessage.error("您的浏览器不支持FLV播放");
return;
}
// WebSocket URL
const wsUrl = "ws://localhost:8888?src=" + encodeURIComponent(props.src);
try {
// flv.js
flvPlayer = flvjs.createPlayer(
{
type: "flv",
isLive: true,
hasAudio: false, // false
hasVideo: true, //
url: wsUrl,
},
{
enableWorker: false,
enableStashBuffer: true,
stashInitialSize: 128,
lazyLoad: true,
lazyLoadMaxDuration: 3 * 60,
seekType: "range",
liveBufferLatencyChasing: true,
liveBufferLatencyMaxLatency: 1.5,
liveBufferLatencyMinRemain: 0.5,
}
);
// video
flvPlayer.attachMediaElement(video);
//
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;
}
flvPlayer.on(flvjs.Events.ERROR, (type, detail) => {
console.error("FLV播放器错误类型:", type);
console.error("FLV播放器错误详情:", detail);
let errorMsg = "FLV播放错误: ";
switch (type) {
case flvjs.ErrorTypes.NETWORK_ERROR:
errorMsg += "网络错误请检查WebSocket连接";
break;
case flvjs.ErrorTypes.MEDIA_ERROR:
errorMsg += "媒体格式错误视频流可能不是有效的FLV格式";
break;
case flvjs.ErrorTypes.OTHER_ERROR:
errorMsg += "其他错误";
break;
default:
errorMsg += "未知错误";
}
if (detail && detail.msg) {
errorMsg += " (" + detail.msg + ")";
}
ElMessage.error(errorMsg);
});
hlsInstance.loadSource(props.src);
hlsInstance.attachMedia(video);
//
flvPlayer.on(flvjs.Events.LOADING_COMPLETE, () => {
console.log("FLV加载完成");
});
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
video.play().then(() => {
isPlaying.value = true;
//
flvPlayer.load();
video
.play()
.then(() => {
ElMessage.success("视频开始播放");
})
.catch((err) => {
ElMessage.error("播放失败: " + err.message);
console.error("播放失败详细信息:", err);
});
});
} 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 (error) {
ElMessage.error("播放器创建失败: " + error.message);
console.error("播放器创建异常:", error);
}
};
//
//
const destroyPlayer = () => {
//
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
//
if (flvPlayer) {
flvPlayer.unload();
flvPlayer.detachMediaElement();
flvPlayer.destroy();
flvPlayer = 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 || "未知错误"}`);
}
}
//
onUnmounted(() => {
destroyPlayer();
});
//
const handleClose = () => {
destroyPlayer();
dialogVisible.value = false;
emit("onStop", props.videoId);
};
</script>

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>
@ -111,7 +108,6 @@
v-model:visible="data.transcodeVideoDialog.visible"
:src="data.transcodeVideoDialog.src"
:video-id="data.transcodeVideoDialog.id"
@on-stop="onTranscodeStopped"
/>
</div>
</template>
@ -131,7 +127,6 @@ 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"; //
const data = reactive({
addDialog: {
@ -157,26 +152,11 @@ const data = reactive({
},
transcodeVideoDialog: {
visible: false,
src: "http://localhost:8100/api/hls/stream.m3u8", // HLS访
src: "", //
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);
@ -201,13 +181,12 @@ const fnUpToBi = async (videomanagerId) => {
const fnPreviewVideo = async (row) => {
try {
const resData = await startTransCode({url: row.url, id: row.PLS_ID}); //
// 使RTSP URLstartTransCode
data.transcodeVideoDialog.visible = true; //
data.transcodeVideoDialog.id = row.PLS_ID; //
data.transcodeVideoDialog.src =
"http://172.16.70.226:7811/" + resData.videoUrl + "stream.m3u8";
data.transcodeVideoDialog.id = row.PLS_ID; // ID
data.transcodeVideoDialog.src = row.url; // 使RTSP URL
} catch (error) {
ElMessage.error("启动转码失败: " + (error.message || "未知错误"));
ElMessage.error("播放视频失败: " + (error.message || "未知错误"));
}
};