feat: full HLS.js support — detection via interceptors + deep probe, playback via hls.js in viewer #2
+54
-8
@@ -340,17 +340,32 @@ function wireHlsEvents(hls, video, panel) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Segment / fragment tracking ---
|
// --- Segment / fragment tracking ---
|
||||||
// FRAG_LOADING fires continuously during playback for each fragment.
|
// FRAG_LOADING fires continuously during playback as hls.js proactively
|
||||||
// Only transition to buffering when the video is actually stalled
|
// loads future segments. During smooth playback the browser's MSE buffer
|
||||||
// (paused or readyState < HAVE_FUTURE_DATA). This prevents the overlay
|
// keeps the video playing without interruption — readyState may briefly
|
||||||
// from flickering between "buffering" and "playing" during smooth playback.
|
// 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) => {
|
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.phase = 'buffering';
|
||||||
}
|
|
||||||
state.lastEvent = 'FRAG_LOADING';
|
state.lastEvent = 'FRAG_LOADING';
|
||||||
state.lastActivity = Date.now();
|
state.lastActivity = Date.now();
|
||||||
renderPanelState(video, panel);
|
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) => {
|
hls.on(HlsEvents.FRAG_LOADED, (_ev, _data) => {
|
||||||
@@ -367,6 +382,7 @@ function wireHlsEvents(hls, video, panel) {
|
|||||||
hls.on(HlsEvents.FRAG_BUFFERED, (_ev, _data) => {
|
hls.on(HlsEvents.FRAG_BUFFERED, (_ev, _data) => {
|
||||||
state.lastEvent = 'FRAG_BUFFERED';
|
state.lastEvent = 'FRAG_BUFFERED';
|
||||||
state.lastActivity = Date.now();
|
state.lastActivity = Date.now();
|
||||||
|
state.bufferedAhead = getBufferedAhead(video);
|
||||||
if (state.phase !== 'error' && video.readyState >= 3 && !video.paused) {
|
if (state.phase !== 'error' && video.readyState >= 3 && !video.paused) {
|
||||||
state.phase = 'playing';
|
state.phase = 'playing';
|
||||||
}
|
}
|
||||||
@@ -414,7 +430,12 @@ function wireHlsEvents(hls, video, panel) {
|
|||||||
state.lastEvent = 'BUFFER_RESET';
|
state.lastEvent = 'BUFFER_RESET';
|
||||||
state.lastActivity = Date.now();
|
state.lastActivity = Date.now();
|
||||||
state.bufferedAhead = 0;
|
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);
|
renderPanelState(video, panel);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -422,13 +443,18 @@ function wireHlsEvents(hls, video, panel) {
|
|||||||
hls.on(HlsEvents.LEVEL_SWITCHED, (_ev, data) => {
|
hls.on(HlsEvents.LEVEL_SWITCHED, (_ev, data) => {
|
||||||
state.lastEvent = 'LEVEL_SWITCHED';
|
state.lastEvent = 'LEVEL_SWITCHED';
|
||||||
state.lastActivity = Date.now();
|
state.lastActivity = Date.now();
|
||||||
state.phase = 'switching';
|
|
||||||
state.level = data.level;
|
state.level = data.level;
|
||||||
const levels = hls.levels;
|
const levels = hls.levels;
|
||||||
if (levels && levels[data.level]) {
|
if (levels && levels[data.level]) {
|
||||||
const lvl = levels[data.level];
|
const lvl = levels[data.level];
|
||||||
state.levelLabel = `${lvl.width}x${lvl.height}`;
|
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);
|
renderPanelState(video, panel);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -542,6 +568,11 @@ function wireVideoEvents(video) {
|
|||||||
onVideoEvent('waiting', s => { s.phase = 'buffering'; s.lastEvent = 'video.waiting'; });
|
onVideoEvent('waiting', s => { s.phase = 'buffering'; s.lastEvent = 'video.waiting'; });
|
||||||
onVideoEvent('stalled', s => { s.phase = 'buffering'; s.lastEvent = 'video.stalled'; });
|
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
|
// Track actual playback position (fragments are delivered out of order but
|
||||||
// the browser's timeupdate fires as they are played back sequentially).
|
// the browser's timeupdate fires as they are played back sequentially).
|
||||||
video.addEventListener('timeupdate', () => {
|
video.addEventListener('timeupdate', () => {
|
||||||
@@ -572,11 +603,26 @@ function wireVideoEvents(video) {
|
|||||||
* Returns a cleanup function.
|
* Returns a cleanup function.
|
||||||
*/
|
*/
|
||||||
function startBufferMonitor(video, panel) {
|
function startBufferMonitor(video, panel) {
|
||||||
|
let prevPhase = '';
|
||||||
|
let prevSegLoaded = -1;
|
||||||
|
let prevSegFailed = -1;
|
||||||
|
let prevLabel = '';
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
const s = panelStates.get(video);
|
const s = panelStates.get(video);
|
||||||
if (!s) { clearInterval(timer); return; }
|
if (!s) { clearInterval(timer); return; }
|
||||||
s.bufferedAhead = getBufferedAhead(video);
|
s.bufferedAhead = getBufferedAhead(video);
|
||||||
|
// 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);
|
renderPanelState(video, panel);
|
||||||
|
prevPhase = s.phase;
|
||||||
|
prevSegLoaded = s.segmentsLoaded;
|
||||||
|
prevSegFailed = s.segmentsFailed;
|
||||||
|
prevLabel = s.levelLabel;
|
||||||
|
}
|
||||||
// Refresh aggregate status bar if any panel changed state.
|
// Refresh aggregate status bar if any panel changed state.
|
||||||
renderAggregateState();
|
renderAggregateState();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|||||||
Reference in new issue
Block a user