111 lines
5.0 KiB
JavaScript
111 lines
5.0 KiB
JavaScript
'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);
|