'use strict'; (function (root) { class LivePlayer { static key(cameras, quality) { // Polling readiness, labels and recording status must not recreate video. return JSON.stringify(cameras.map(c => [c.id, !!c.enabled])) + quality; } constructor(video, panel, source, options = {}) { this.video = video; this.panel = panel; this.source = source; this.Hls = options.Hls || root.Hls; this.clock = options.clock || root; this.now = options.now || (() => Date.now()); this.hls = null; this.timer = null; this.stopped = false; this.attempt = 0; this.played = false; this.userPaused = false; this.connecting = true; this.failed = false; this.lastTime = 0; this.lastProgress = this.now(); this.stableSince = 0; this.handlers = { playing: () => { if (this.timer !== null) this.clock.clearTimeout(this.timer); this.timer = null; this.played = true; this.connecting = false; this.failed = false; this.userPaused = false; this.lastProgress = this.now(); this.lastTime = video.currentTime; this.stableSince = this.now(); panel.querySelector('.video-empty').hidden = true; panel.querySelector('.video-error').hidden = true; }, pause: () => { if (!this.connecting && this.played && !video.ended && !video.error && video.readyState >= 2) this.userPaused = true; }, play: () => { this.userPaused = false; this.lastProgress = this.now(); if (this.failed) this.retryIfNeeded(); }, error: () => this.retry('画面暂时中断'), ended: () => this.retry('视频连接已结束') }; for (const [event, handler] of Object.entries(this.handlers)) video.addEventListener(event, handler); this.watchdog = this.clock.setInterval(() => this.watch(), 5000); this.start(); } message(text) { const el = this.panel.querySelector('.video-error'); el.textContent = text; el.hidden = false; } start() { if (this.stopped) return; this.connecting = true; this.failed = false; this.release(); this.lastProgress = this.now(); this.lastTime = 0; const Hls = this.Hls; if (Hls?.isSupported()) { const hls = new Hls({ maxBufferLength: 20, backBufferLength: 5, liveSyncDurationCount: 3, liveMaxLatencyDurationCount: 8, maxLiveSyncPlaybackRate: 1.2, manifestLoadingTimeOut: 60000, levelLoadingTimeOut: 15000, fragLoadingTimeOut: 15000, manifestLoadingMaxRetry: 2, levelLoadingMaxRetry: 2 }); this.hls = hls; hls.on(Hls.Events.ERROR, (_, data) => { if (this.hls === hls && data.fatal) this.retry('画面暂时中断'); }); hls.on(Hls.Events.MANIFEST_PARSED, () => { if (this.hls === hls && !this.stopped) this.video.play().catch(() => {}); }); hls.loadSource(this.source); hls.attachMedia(this.video); } else if (this.video.canPlayType('application/vnd.apple.mpegurl')) { this.video.src = this.source; this.video.play().catch(() => {}); } else { this.userPaused = true; this.message('当前浏览器不支持视频播放,请使用较新的浏览器。'); } } retry(reason) { if (this.stopped || this.timer !== null) return; this.failed = true; if (this.userPaused) return; const delay = Math.min(30000, 2000 * 2 ** Math.min(this.attempt++, 4)); this.message(reason + ',正在自动重新连接…'); this.timer = this.clock.setTimeout(() => { this.timer = null; if (!this.stopped) this.start(); }, delay); } watch() { if (this.stopped || this.timer !== null || this.userPaused) return; const current = this.video.currentTime; if (this.video.readyState >= 2 && this.video.videoWidth > 0 && current > this.lastTime + 0.1) { this.lastTime = current; this.lastProgress = this.now(); if (this.stableSince && this.now() - this.stableSince > 30000) this.attempt = 0; return; } const grace = this.connecting ? 65000 : 25000; if (this.now() - this.lastProgress > grace) this.retry('画面停顿'); } retryIfNeeded() { if (!this.failed || this.stopped) return; if (this.timer !== null) this.clock.clearTimeout(this.timer); this.timer = null; this.start(); } release() { const old = this.hls; this.hls = null; if (old) old.destroy(); this.video.pause(); this.video.removeAttribute('src'); this.video.load(); } destroy() { this.stopped = true; this.connecting = true; if (this.timer !== null) this.clock.clearTimeout(this.timer); this.timer = null; this.clock.clearInterval(this.watchdog); for (const [event, handler] of Object.entries(this.handlers)) this.video.removeEventListener(event, handler); this.release(); } } if (typeof module !== 'undefined' && module.exports) module.exports = LivePlayer; else root.ZhaoLivePlayer = LivePlayer; })(typeof window !== 'undefined' ? window : globalThis);