feat: full HLS.js support — detection via interceptors + deep probe, playback via hls.js in viewer #2
+57
-11
@@ -340,17 +340,32 @@ function wireHlsEvents(hls, video, panel) {
|
||||
});
|
||||
|
||||
// --- Segment / fragment tracking ---
|
||||
// FRAG_LOADING fires continuously during playback for each fragment.
|
||||
// Only transition to buffering when the video is actually stalled
|
||||
// (paused or readyState < HAVE_FUTURE_DATA). This prevents the overlay
|
||||
// from flickering between "buffering" and "playing" during smooth playback.
|
||||
// FRAG_LOADING fires continuously during playback as hls.js proactively
|
||||
// loads future segments. During smooth playback the browser's MSE buffer
|
||||
// keeps the video playing without interruption — readyState may briefly
|
||||
// dip to HAVE_CURRENT_DATA (2) between fragments even though the user
|
||||
// sees seamless playback. If we set phase='buffering' on every
|
||||
// FRAG_LOADING, the overlay flickers between "buffering" and "playing".
|
||||
//
|
||||
// Fix: Only transition to 'buffering' from FRAG_LOADING during initial
|
||||
// load states (idle/loading). Once the video reaches 'playing', 'buffering',
|
||||
// or 'switching', defer to the video element's native events
|
||||
// (waiting/stalled/playing — wired in wireVideoEvents) as the authoritative
|
||||
// source for buffering state. Those native events only fire when actual
|
||||
// stalling occurs, not during normal proactive fragment fetching.
|
||||
hls.on(HlsEvents.FRAG_LOADING, (_ev, _data) => {
|
||||
if (video.paused || video.readyState < 3) {
|
||||
// Only set buffering during initial load. During active playback the
|
||||
// native 'waiting'/'stalled' events (see wireVideoEvents) are the
|
||||
// source of truth for when the video is actually stalled.
|
||||
if (state.phase === 'idle' || state.phase === 'loading') {
|
||||
state.phase = 'buffering';
|
||||
state.lastEvent = 'FRAG_LOADING';
|
||||
state.lastActivity = Date.now();
|
||||
renderPanelState(video, panel);
|
||||
}
|
||||
state.lastEvent = 'FRAG_LOADING';
|
||||
state.lastActivity = Date.now();
|
||||
renderPanelState(video, panel);
|
||||
// During playing/buffering/switching: ignore FRAG_LOADING entirely —
|
||||
// it fires continuously as hls.js proactively fetches future segments.
|
||||
// Re-rendering here causes overlay flickering even during smooth playback.
|
||||
});
|
||||
|
||||
hls.on(HlsEvents.FRAG_LOADED, (_ev, _data) => {
|
||||
@@ -367,6 +382,7 @@ function wireHlsEvents(hls, video, panel) {
|
||||
hls.on(HlsEvents.FRAG_BUFFERED, (_ev, _data) => {
|
||||
state.lastEvent = 'FRAG_BUFFERED';
|
||||
state.lastActivity = Date.now();
|
||||
state.bufferedAhead = getBufferedAhead(video);
|
||||
if (state.phase !== 'error' && video.readyState >= 3 && !video.paused) {
|
||||
state.phase = 'playing';
|
||||
}
|
||||
@@ -414,7 +430,12 @@ function wireHlsEvents(hls, video, panel) {
|
||||
state.lastEvent = 'BUFFER_RESET';
|
||||
state.lastActivity = Date.now();
|
||||
state.bufferedAhead = 0;
|
||||
state.segmentsLoaded = 0;
|
||||
// Do NOT reset segmentsLoaded — it's a cumulative metric across the
|
||||
// entire session. BUFFER_RESET fires during quality switches (hls.js
|
||||
// flushes and rebuilds the MediaSource buffer) but the segments were
|
||||
// still successfully loaded and played. Clearing it here makes the
|
||||
// overlay show "0 seg" mid-playback, which is the exact "fragmented /
|
||||
// lost state" the user sees.
|
||||
renderPanelState(video, panel);
|
||||
});
|
||||
|
||||
@@ -422,13 +443,18 @@ function wireHlsEvents(hls, video, panel) {
|
||||
hls.on(HlsEvents.LEVEL_SWITCHED, (_ev, data) => {
|
||||
state.lastEvent = 'LEVEL_SWITCHED';
|
||||
state.lastActivity = Date.now();
|
||||
state.phase = 'switching';
|
||||
state.level = data.level;
|
||||
const levels = hls.levels;
|
||||
if (levels && levels[data.level]) {
|
||||
const lvl = levels[data.level];
|
||||
state.levelLabel = `${lvl.width}x${lvl.height}`;
|
||||
}
|
||||
// Briefly show 'switching' to indicate a quality transition is in progress.
|
||||
// However, if there's meaningful buffer ahead, the switch is likely instant
|
||||
// (switching between already-buffered segments) — skip the intermediate state
|
||||
// to avoid the panel getting stuck at 'switching' when FRAG_BUFFERED doesn't fire.
|
||||
const buf = getBufferedAhead(video);
|
||||
state.phase = buf > 2 ? 'playing' : 'switching';
|
||||
renderPanelState(video, panel);
|
||||
});
|
||||
|
||||
@@ -542,6 +568,11 @@ function wireVideoEvents(video) {
|
||||
onVideoEvent('waiting', s => { s.phase = 'buffering'; s.lastEvent = 'video.waiting'; });
|
||||
onVideoEvent('stalled', s => { s.phase = 'buffering'; s.lastEvent = 'video.stalled'; });
|
||||
|
||||
// Video ended — on looped content this rarely fires (loop re-starts instantly),
|
||||
// but handle it for completeness. The 'ended' phase appears in the overlay
|
||||
// and aggregate state so the user knows playback reached the end of a segment.
|
||||
onVideoEvent('ended', s => { s.phase = 'ended'; s.lastEvent = 'video.ended'; });
|
||||
|
||||
// Track actual playback position (fragments are delivered out of order but
|
||||
// the browser's timeupdate fires as they are played back sequentially).
|
||||
video.addEventListener('timeupdate', () => {
|
||||
@@ -572,11 +603,26 @@ function wireVideoEvents(video) {
|
||||
* Returns a cleanup function.
|
||||
*/
|
||||
function startBufferMonitor(video, panel) {
|
||||
let prevPhase = '';
|
||||
let prevSegLoaded = -1;
|
||||
let prevSegFailed = -1;
|
||||
let prevLabel = '';
|
||||
const timer = setInterval(() => {
|
||||
const s = panelStates.get(video);
|
||||
if (!s) { clearInterval(timer); return; }
|
||||
s.bufferedAhead = getBufferedAhead(video);
|
||||
renderPanelState(video, panel);
|
||||
// Only re-render if something actually changed — avoids overlay flickering
|
||||
// on stably playing panels where only lastActivity/lastEvent differ.
|
||||
if (s.phase !== prevPhase ||
|
||||
s.segmentsLoaded !== prevSegLoaded ||
|
||||
s.segmentsFailed !== prevSegFailed ||
|
||||
s.levelLabel !== prevLabel) {
|
||||
renderPanelState(video, panel);
|
||||
prevPhase = s.phase;
|
||||
prevSegLoaded = s.segmentsLoaded;
|
||||
prevSegFailed = s.segmentsFailed;
|
||||
prevLabel = s.levelLabel;
|
||||
}
|
||||
// Refresh aggregate status bar if any panel changed state.
|
||||
renderAggregateState();
|
||||
}, 1000);
|
||||
|
||||
Reference in new issue
Block a user