Recover Chromium HLS audio at ad discontinuities
This commit is contained in:
107
public/js/components/HlsAudioRecovery.js
Normal file
107
public/js/components/HlsAudioRecovery.js
Normal file
@ -0,0 +1,107 @@
|
||||
/** Chromium can retain an invalid audio decoder state across HLS ad discontinuities. */
|
||||
class HlsAudioRecovery {
|
||||
constructor(hls, video, events, isCurrent, options = {}) {
|
||||
this.hls = hls;
|
||||
this.video = video;
|
||||
this.events = events;
|
||||
this.isCurrent = isCurrent;
|
||||
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
|
||||
this.chromium = options.chromium ?? /(?:Chrome|Chromium|Edg)\//.test(userAgent);
|
||||
this.now = options.now || (() => Date.now());
|
||||
this.beforeRecovery = options.beforeRecovery;
|
||||
this.lastCC = null;
|
||||
this.lastRecovery = -Infinity;
|
||||
this.attempts = [];
|
||||
this.pendingBoundary = false;
|
||||
this.disposed = false;
|
||||
|
||||
this.onFragment = (event, data) => {
|
||||
if (!this.active() || !data?.frag || data.frag.sn === 'initSegment') return;
|
||||
const cc = data.frag.cc;
|
||||
if (!Number.isInteger(cc) || cc === this.lastCC) return;
|
||||
const previous = this.lastCC;
|
||||
this.lastCC = cc;
|
||||
if (previous === null || !this.chromium) return;
|
||||
this.pendingBoundary = true;
|
||||
this.onPlaying();
|
||||
};
|
||||
this.onPlaying = () => {
|
||||
if (!this.active() || !this.pendingBoundary || this.video.paused) return;
|
||||
this.pendingBoundary = false;
|
||||
this.requestRecovery('audio discontinuity');
|
||||
};
|
||||
this.onDestroy = () => this.dispose();
|
||||
this.onManifest = () => {
|
||||
if (!this.active()) return;
|
||||
this.lastCC = null;
|
||||
this.pendingBoundary = false;
|
||||
this.cancelRecovery();
|
||||
};
|
||||
hls.on(events.FRAG_CHANGED, this.onFragment);
|
||||
hls.on(events.DESTROYING, this.onDestroy);
|
||||
hls.on(events.MANIFEST_LOADING, this.onManifest);
|
||||
video.addEventListener?.('playing', this.onPlaying);
|
||||
}
|
||||
|
||||
active() {
|
||||
return !this.disposed && this.isCurrent();
|
||||
}
|
||||
|
||||
requestRecovery(reason) {
|
||||
if (!this.active()) return false;
|
||||
if (this.timer !== undefined || this.onAttached) return true;
|
||||
const now = this.now();
|
||||
this.attempts = this.attempts.filter(time => now - time < 30000);
|
||||
if (now - this.lastRecovery < 2000 || this.attempts.length >= 3) return false;
|
||||
|
||||
// Let HLS finish dispatching the fragment/error event before detaching its media.
|
||||
this.timer = setTimeout(() => {
|
||||
this.timer = undefined;
|
||||
if (!this.active()) return;
|
||||
const position = this.video.currentTime;
|
||||
const resume = !this.video.paused && !this.video.ended;
|
||||
const restoreCaptions = this.beforeRecovery?.();
|
||||
this.previousStartPosition = this.hls.config.startPosition;
|
||||
if (Number.isFinite(position) && position >= 0) this.hls.config.startPosition = position;
|
||||
this.lastRecovery = this.now();
|
||||
this.attempts.push(this.lastRecovery);
|
||||
this.onAttached = () => {
|
||||
if (!this.active() || !this.onAttached) return;
|
||||
this.hls.off(this.events.MEDIA_ATTACHED, this.onAttached);
|
||||
this.onAttached = null;
|
||||
this.hls.config.startPosition = this.previousStartPosition;
|
||||
if (!this.active()) return;
|
||||
restoreCaptions?.();
|
||||
if (resume) this.video.play().catch(() => {});
|
||||
else this.video.pause();
|
||||
};
|
||||
this.hls.on(this.events.MEDIA_ATTACHED, this.onAttached);
|
||||
console.info(`[HLS] Refreshing media decoder: ${reason}`);
|
||||
this.hls.recoverMediaError();
|
||||
}, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
cancelRecovery() {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = undefined;
|
||||
if (this.onAttached) {
|
||||
this.hls.off(this.events.MEDIA_ATTACHED, this.onAttached);
|
||||
this.hls.config.startPosition = this.previousStartPosition;
|
||||
this.onAttached = null;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.disposed) return;
|
||||
this.disposed = true;
|
||||
this.cancelRecovery();
|
||||
this.hls.off(this.events.FRAG_CHANGED, this.onFragment);
|
||||
this.hls.off(this.events.DESTROYING, this.onDestroy);
|
||||
this.hls.off(this.events.MANIFEST_LOADING, this.onManifest);
|
||||
this.video.removeEventListener?.('playing', this.onPlaying);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') window.HlsAudioRecovery = HlsAudioRecovery;
|
||||
if (typeof module !== 'undefined') module.exports = HlsAudioRecovery;
|
||||
Reference in New Issue
Block a user