feat hls加载,断网多次加载m3u8

This commit is contained in:
Ironsp
2026-06-26 11:56:48 +08:00
parent a27b0ac7f2
commit f92baab6f7
2 changed files with 21 additions and 66 deletions

View File

@@ -1,5 +1,6 @@
import Hls from 'hls.js'; import Hls from 'hls.js';
let hlsInstance: Hls | null = null; let hlsInstance: Hls | null = null;
// let hlsRetryCount = 0;
export const initHls = (monitorVideoUrl, monitorVideoRef) => { export const initHls = (monitorVideoUrl, monitorVideoRef) => {
const url = monitorVideoUrl.value; const url = monitorVideoUrl.value;
@@ -17,7 +18,18 @@ export const initHls = (monitorVideoUrl, monitorVideoRef) => {
} }
if (Hls.isSupported()) { 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.loadSource(url);
hls.attachMedia(video); hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => { hls.on(Hls.Events.MANIFEST_PARSED, () => {
@@ -25,6 +37,8 @@ export const initHls = (monitorVideoUrl, monitorVideoRef) => {
try { try {
video.play().catch(() => {}); video.play().catch(() => {});
} catch (e) {} } catch (e) {}
// 加载成功,重置重试计数
// hlsRetryCount = 0;
}); });
// ★ 错误自动恢复hls.js 拿到空 m3u8 或 ts 加载失败时自动重试 // ★ 错误自动恢复hls.js 拿到空 m3u8 或 ts 加载失败时自动重试
@@ -32,8 +46,8 @@ export const initHls = (monitorVideoUrl, monitorVideoRef) => {
if (data.fatal) { if (data.fatal) {
switch (data.type) { switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR: case Hls.ErrorTypes.NETWORK_ERROR:
// 网络错误m3u8 404 / ts 404→ 重新开始加载
hls.startLoad(); hls.startLoad();
ElMessage.error('网络错误, 请检查网络或重试');
break; break;
case Hls.ErrorTypes.MEDIA_ERROR: case Hls.ErrorTypes.MEDIA_ERROR:
// 媒体解码错误 → 尝试恢复 // 媒体解码错误 → 尝试恢复

View File

@@ -158,7 +158,7 @@ import { EquipmentList, DeviceInfoForm, AlarmAreaData } from '@/api/equipmentLis
import { listDemo, getDemo, delDemo, addDemo, updateDemo } from '@/api/demo/demo'; import { listDemo, getDemo, delDemo, addDemo, updateDemo } from '@/api/demo/demo';
import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types'; import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types';
import { log } from 'node:console'; import { log } from 'node:console';
import Hls from 'hls.js'; import { initHls, destroyHls } from '@/utils/video';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { useVideosStore } from '@/store/modules/video'; import { useVideosStore } from '@/store/modules/video';
@@ -190,68 +190,9 @@ const dialog1 = reactive<DialogOption>({
const monitorVideoUrl = ref(''); const monitorVideoUrl = ref('');
const monitorVideoRef = ref<HTMLVideoElement>(); const monitorVideoRef = ref<HTMLVideoElement>();
let hlsInstance: Hls | null = null;
let visibilityHandler: (() => void) | null = null; let visibilityHandler: (() => void) | null = null;
const monitoringArea = ref<string>(''); const monitoringArea = ref<string>('');
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 }; type MonitorPoint = { x: number; y: number };
const monitorDrawCollecting = ref(false); const monitorDrawCollecting = ref(false);
const monitorDrawPoints = ref<MonitorPoint[]>([]); const monitorDrawPoints = ref<MonitorPoint[]>([]);
@@ -419,7 +360,7 @@ const handleSee = async (row?: any) => {
dialog1.visible = true; dialog1.visible = true;
dialog1.title = '修改设备'; dialog1.title = '修改设备';
destroyHls(); destroyHls(monitorVideoRef);
monitorVideoUrl.value = ''; monitorVideoUrl.value = '';
await videosStore.getVideos(row.id); await videosStore.getVideos(row.id);
monitorVideoUrl.value = videosStore.hisUrl || ''; monitorVideoUrl.value = videosStore.hisUrl || '';
@@ -432,7 +373,7 @@ const handleSee = async (row?: any) => {
// 等待 DOM 更新后初始化 Hls // 等待 DOM 更新后初始化 Hls
await nextTick(); await nextTick();
initHls(); initHls(monitorVideoUrl, monitorVideoRef);
// 等待视频加载完成后初始化 ResizeObserver以便能正确获取视频尺寸 // 等待视频加载完成后初始化 ResizeObserver以便能正确获取视频尺寸
initVideoResizeObserver(); initVideoResizeObserver();
@@ -565,7 +506,7 @@ const cleanupVisibilityChange = () => {
const handleMonitorDialogClose = (done: () => void) => { const handleMonitorDialogClose = (done: () => void) => {
// 卸载 Hls 与清理 video // 卸载 Hls 与清理 video
destroyHls(); destroyHls(monitorVideoRef);
monitorVideoUrl.value = ''; monitorVideoUrl.value = '';
destroyVideoResizeObserver(); destroyVideoResizeObserver();
resetMonitorDraw(); resetMonitorDraw();
@@ -710,7 +651,7 @@ const saveImage = async () => {
const handleMonitorDialogOpen = () => { const handleMonitorDialogOpen = () => {
// 初始化 Hls 与 video // 初始化 Hls 与 video
initHls(); initHls(monitorVideoUrl, monitorVideoRef);
initVideoResizeObserver(); initVideoResizeObserver();
monitorDrawCollecting.value = false; monitorDrawCollecting.value = false;
monitorDrawPoints.value = []; monitorDrawPoints.value = [];