From f92baab6f759847f43db5819bf996bdc3df3115b Mon Sep 17 00:00:00 2001 From: Ironsp <1522278303@qq.com> Date: Fri, 26 Jun 2026 11:56:48 +0800 Subject: [PATCH] =?UTF-8?q?feat=20hls=E5=8A=A0=E8=BD=BD,=E6=96=AD=E7=BD=91?= =?UTF-8?q?=E5=A4=9A=E6=AC=A1=E5=8A=A0=E8=BD=BDm3u8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/video.ts | 18 ++++++++- src/views/equipmentList/list.vue | 69 +++----------------------------- 2 files changed, 21 insertions(+), 66 deletions(-) diff --git a/src/utils/video.ts b/src/utils/video.ts index 2dce39b..b5947b6 100644 --- a/src/utils/video.ts +++ b/src/utils/video.ts @@ -1,5 +1,6 @@ import Hls from 'hls.js'; let hlsInstance: Hls | null = null; +// let hlsRetryCount = 0; export const initHls = (monitorVideoUrl, monitorVideoRef) => { const url = monitorVideoUrl.value; @@ -17,7 +18,18 @@ export const initHls = (monitorVideoUrl, monitorVideoRef) => { } if (Hls.isSupported()) { - const hls = new Hls(); + const hls = new Hls({ + // ★ ZLM 注册流需要时间,配置自动重试 + manifestLoadingMaxRetry: 5, // m3u8 加载最多重试20次 + manifestLoadingRetryDelay: 500, // 每次重试间隔500ms + manifestLoadingTimeOut: 5000, // m3u8 请求超时5秒 + levelLoadingMaxRetry: 20, // ts 分片加载最多重试20次 + levelLoadingRetryDelay: 500, + fragLoadingMaxRetry: 20, // 片段加载重试 + fragLoadingRetryDelay: 500, + lowLatencyMode: false, // 关掉低延迟模式,更稳定 + startLevel: -1 // 自动选择码率 + }); hls.loadSource(url); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { @@ -25,6 +37,8 @@ export const initHls = (monitorVideoUrl, monitorVideoRef) => { try { video.play().catch(() => {}); } catch (e) {} + // 加载成功,重置重试计数 + // hlsRetryCount = 0; }); // ★ 错误自动恢复:hls.js 拿到空 m3u8 或 ts 加载失败时自动重试 @@ -32,8 +46,8 @@ export const initHls = (monitorVideoUrl, monitorVideoRef) => { if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: - // 网络错误(m3u8 404 / ts 404)→ 重新开始加载 hls.startLoad(); + ElMessage.error('网络错误, 请检查网络或重试'); break; case Hls.ErrorTypes.MEDIA_ERROR: // 媒体解码错误 → 尝试恢复 diff --git a/src/views/equipmentList/list.vue b/src/views/equipmentList/list.vue index 0d2cc54..5b86c1c 100644 --- a/src/views/equipmentList/list.vue +++ b/src/views/equipmentList/list.vue @@ -158,7 +158,7 @@ import { EquipmentList, DeviceInfoForm, AlarmAreaData } from '@/api/equipmentLis import { listDemo, getDemo, delDemo, addDemo, updateDemo } from '@/api/demo/demo'; import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types'; import { log } from 'node:console'; -import Hls from 'hls.js'; +import { initHls, destroyHls } from '@/utils/video'; import { nextTick } from 'vue'; import { useVideosStore } from '@/store/modules/video'; @@ -190,68 +190,9 @@ const dialog1 = reactive({ const monitorVideoUrl = ref(''); const monitorVideoRef = ref(); -let hlsInstance: Hls | null = null; let visibilityHandler: (() => void) | null = null; const monitoringArea = ref(''); -const initHls = () => { - const url = monitorVideoUrl.value; - const video = monitorVideoRef.value; - if (!url || !video) return; - - // 销毁已有实例 - if (hlsInstance) { - try { - hlsInstance.destroy(); - } catch (e) { - // ignore - } - hlsInstance = null; - } - - if (Hls.isSupported()) { - const hls = new Hls(); - hls.loadSource(url); - hls.attachMedia(video); - hls.on(Hls.Events.MANIFEST_PARSED, () => { - // 自动播放(若浏览器允许) - try { - video.play().catch(() => {}); - } catch (e) {} - }); - hlsInstance = hls; - } else if (video.canPlayType('application/vnd.apple.mpegurl')) { - // Safari/原生支持 HLS - video.src = url; - video.addEventListener('loadedmetadata', () => { - try { - video.play().catch(() => {}); - } catch (e) {} - }); - } else { - proxy?.$modal.msgWarning('当前浏览器不支持 HLS 播放'); - } -}; - -const destroyHls = () => { - try { - if (hlsInstance) { - hlsInstance.destroy(); - hlsInstance = null; - } - } catch (e) {} - const video = monitorVideoRef.value; - if (video) { - // 清理原生 src,避免残留 - try { - video.pause(); - video.removeAttribute('src'); - // 对某些浏览器,需要调用 load() 来重置 media element - if (typeof video.load === 'function') video.load(); - } catch (e) {} - } -}; - type MonitorPoint = { x: number; y: number }; const monitorDrawCollecting = ref(false); const monitorDrawPoints = ref([]); @@ -419,7 +360,7 @@ const handleSee = async (row?: any) => { dialog1.visible = true; dialog1.title = '修改设备'; - destroyHls(); + destroyHls(monitorVideoRef); monitorVideoUrl.value = ''; await videosStore.getVideos(row.id); monitorVideoUrl.value = videosStore.hisUrl || ''; @@ -432,7 +373,7 @@ const handleSee = async (row?: any) => { // 等待 DOM 更新后初始化 Hls await nextTick(); - initHls(); + initHls(monitorVideoUrl, monitorVideoRef); // 等待视频加载完成后初始化 ResizeObserver,以便能正确获取视频尺寸 initVideoResizeObserver(); @@ -565,7 +506,7 @@ const cleanupVisibilityChange = () => { const handleMonitorDialogClose = (done: () => void) => { // 卸载 Hls 与清理 video - destroyHls(); + destroyHls(monitorVideoRef); monitorVideoUrl.value = ''; destroyVideoResizeObserver(); resetMonitorDraw(); @@ -710,7 +651,7 @@ const saveImage = async () => { const handleMonitorDialogOpen = () => { // 初始化 Hls 与 video - initHls(); + initHls(monitorVideoUrl, monitorVideoRef); initVideoResizeObserver(); monitorDrawCollecting.value = false; monitorDrawPoints.value = [];