refactor(viewer): modularize HLS playback tracking with per-panel state machine
CI / test (pull_request) Successful in 27s
CI / test (pull_request) Successful in 27s
Decompose monolithic wirePlaybackTracking() into single-responsibility modules: - PanelState: per-panel playback state (phase, segments, buffer, quality) - EventWiring: attaches hls.js + video element event listeners - BufferMonitor: periodic buffer health check with timer lifecycle - PanelRenderer: per-panel overlay rendering (phase, quality, segments) - AggregateRenderer: global status bar aggregating all panel states - PanelFactory: DOM panel creation + HLS/direct playback routing Each module handles one concern: state tracking, event registration, buffer monitoring, overlay rendering, aggregate rendering, or DOM creation. Also fix: walk() dead code bug in m3u8DeepProbeFn where the string check was unreachable behind the object-type guard.
This commit is contained in:
1 parent
a128babde0
commit
bd093be652
4 files changed
+1327
-120
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -6,6 +6,7 @@ import './viewer_units.test.mjs';
|
|||||||
import './data_flow.test.mjs';
|
import './data_flow.test.mjs';
|
||||||
import './viewer_integration.test.mjs';
|
import './viewer_integration.test.mjs';
|
||||||
import './config_validation.test.mjs';
|
import './config_validation.test.mjs';
|
||||||
|
import './hls_qa.test.mjs';
|
||||||
import { run } from './runner.mjs';
|
import { run } from './runner.mjs';
|
||||||
|
|
||||||
const result = await run();
|
const result = await run();
|
||||||
|
|||||||
+2
-1
@@ -105,7 +105,7 @@ function m3u8DeepProbeFn() {
|
|||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
||||||
function walk(obj, path, depth) {
|
function walk(obj, path, depth) {
|
||||||
if (!obj || typeof obj !== 'object' || depth > 5) return;
|
if (depth > 5) return;
|
||||||
if (typeof obj === 'string') {
|
if (typeof obj === 'string') {
|
||||||
const match = obj.match(/https?:\/\/[^"'\s`)]+\.m3u8[^"'\s`)]*/gi);
|
const match = obj.match(/https?:\/\/[^"'\s`)]+\.m3u8[^"'\s`)]*/gi);
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -115,6 +115,7 @@ function m3u8DeepProbeFn() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!obj || typeof obj !== 'object') return;
|
||||||
for (const k of Object.keys(obj)) {
|
for (const k of Object.keys(obj)) {
|
||||||
walk(obj[k], `${path}.${k}`, depth + 1);
|
walk(obj[k], `${path}.${k}`, depth + 1);
|
||||||
}
|
}
|
||||||
|
|||||||
+156
-119
@@ -3,7 +3,15 @@
|
|||||||
* Reads video URLs from chrome.storage and renders them in a dynamic NxN grid.
|
* Reads video URLs from chrome.storage and renders them in a dynamic NxN grid.
|
||||||
* Supports HLS playback via hls.js (dynamic CDN load) and direct MP4/WebM.
|
* Supports HLS playback via hls.js (dynamic CDN load) and direct MP4/WebM.
|
||||||
*
|
*
|
||||||
* Fragmented playback tracking:
|
* Modular architecture:
|
||||||
|
*
|
||||||
|
* PanelState — per-panel playback state (phase, segments, buffer, quality)
|
||||||
|
* EventWiring — attaches hls.js + video element event listeners
|
||||||
|
* BufferMonitor — periodic buffer health check + timer lifecycle
|
||||||
|
* PanelRenderer — per-panel overlay rendering (phase, quality, segments)
|
||||||
|
* AggregateRenderer — global status bar aggregating all panel states
|
||||||
|
* PanelFactory — DOM panel creation + HLS/direct playback routing
|
||||||
|
*
|
||||||
* HLS delivery is inherently segmented (fragments load one at a time into
|
* HLS delivery is inherently segmented (fragments load one at a time into
|
||||||
* the MediaSource buffer). This module tracks the broken-up nature of
|
* the MediaSource buffer). This module tracks the broken-up nature of
|
||||||
* playback by wiring hls.js events for fragment loading, buffer updates,
|
* playback by wiring hls.js events for fragment loading, buffer updates,
|
||||||
@@ -44,11 +52,13 @@ const panelsEl = document.getElementById('panels');
|
|||||||
/** Track all Hls instances so we can destroy them on unload. */
|
/** Track all Hls instances so we can destroy them on unload. */
|
||||||
const hlsInstances = new Map();
|
const hlsInstances = new Map();
|
||||||
|
|
||||||
/**
|
/* ======================================================================== */
|
||||||
* Per-panel playback state for fragmented HLS tracking.
|
/* PanelState — per-panel playback state */
|
||||||
*
|
/* ======================================================================== */
|
||||||
* Each entry tracks the lifecycle of an HLS stream from manifest load
|
|
||||||
* through fragment arrivals, buffer growth, and quality switches.
|
/** Map<video, PanelState> — one state object per video panel. */
|
||||||
|
const panelStates = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} PanelState
|
* @typedef {object} PanelState
|
||||||
* @property {'idle'|'loading'|'buffering'|'playing'|'error'} phase
|
* @property {'idle'|'loading'|'buffering'|'playing'|'error'} phase
|
||||||
@@ -77,9 +87,6 @@ const hlsInstances = new Map();
|
|||||||
* Total media duration in seconds (from video.duration).
|
* Total media duration in seconds (from video.duration).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Map<video, PanelState> — one state object per video panel. */
|
|
||||||
const panelStates = new Map();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a fresh PanelState for a video element.
|
* Initialize a fresh PanelState for a video element.
|
||||||
*/
|
*/
|
||||||
@@ -95,7 +102,6 @@ function createPanelState() {
|
|||||||
lastEvent: 'none',
|
lastEvent: 'none',
|
||||||
lastActivity: 0,
|
lastActivity: 0,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
// Playback position tracking — updated by timeupdate events.
|
|
||||||
// HLS delivers fragments out-of-order, but the browser plays them back
|
// HLS delivers fragments out-of-order, but the browser plays them back
|
||||||
// sequentially. currentTime and duration give the user-facing progress.
|
// sequentially. currentTime and duration give the user-facing progress.
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
@@ -104,22 +110,26 @@ function createPanelState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ======================================================================== */
|
/* ======================================================================== */
|
||||||
/* HLS helpers */
|
/* Buffer helpers */
|
||||||
/* ======================================================================== */
|
/* ======================================================================== */
|
||||||
|
|
||||||
/** Destroy the Hls instance attached to a video element (if any). */
|
/**
|
||||||
function destroyHls(video) {
|
* Compute buffered seconds ahead of current playback position.
|
||||||
const inst = hlsInstances.get(video);
|
* Returns 0 if the video has no buffered ranges or is not playing.
|
||||||
if (inst) {
|
*/
|
||||||
inst.destroy();
|
function getBufferedAhead(video) {
|
||||||
hlsInstances.delete(video);
|
if (!video.buffered || video.buffered.length === 0) return 0;
|
||||||
|
const current = video.currentTime;
|
||||||
|
for (let i = 0; i < video.buffered.length; i++) {
|
||||||
|
if (video.buffered.end(i) > current) {
|
||||||
|
return video.buffered.end(i) - current;
|
||||||
}
|
}
|
||||||
panelStates.delete(video);
|
}
|
||||||
removePanelOverlay(video);
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ======================================================================== */
|
/* ======================================================================== */
|
||||||
/* Panel overlay helpers */
|
/* PanelRenderer — per-panel overlay rendering */
|
||||||
/* ======================================================================== */
|
/* ======================================================================== */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -152,9 +162,23 @@ function removePanelOverlay(video) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase emoji indicators for overlay display.
|
||||||
|
*/
|
||||||
|
const PHASE_ICONS = {
|
||||||
|
idle: '\u23F8', // pause
|
||||||
|
loading: '\u2B07', // down arrow
|
||||||
|
buffering: '\u27F7', // spinning
|
||||||
|
playing: '\u25B6', // play
|
||||||
|
error: '\u2717', // X
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the current PanelState into the overlay.
|
* Render the current PanelState into the overlay.
|
||||||
* Shows: phase indicator, quality, segments loaded/failed, buffer ahead.
|
* Shows: phase indicator, quality, segments loaded/failed, buffer ahead.
|
||||||
|
*
|
||||||
|
* This function is intentionally rendering-only — it reads state, formats it,
|
||||||
|
* and writes to the DOM. No side effects beyond setting aggregateStateDirty.
|
||||||
*/
|
*/
|
||||||
function renderPanelState(video, panel) {
|
function renderPanelState(video, panel) {
|
||||||
const state = panelStates.get(video);
|
const state = panelStates.get(video);
|
||||||
@@ -166,40 +190,31 @@ function renderPanelState(video, panel) {
|
|||||||
panelOverlays.set(video, overlay);
|
panelOverlays.set(video, overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase emoji/indicator
|
|
||||||
const phaseIcons = {
|
|
||||||
idle: '⏸',
|
|
||||||
loading: '⬇',
|
|
||||||
buffering: '◷',
|
|
||||||
playing: '▶',
|
|
||||||
error: '✗',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build status lines
|
// Build status lines
|
||||||
const lines = [];
|
const lines = [];
|
||||||
lines.push(`${phaseIcons[state.phase] || '?'} ${state.phase}`);
|
lines.push(`${PHASE_ICONS[state.phase] || '?'} ${state.phase}`);
|
||||||
if (state.level >= 0) {
|
if (state.level >= 0) {
|
||||||
lines.push(`📺 ${state.levelLabel}`);
|
lines.push(`\uD83D\uDD7A\uFE0F ${state.levelLabel}`);
|
||||||
}
|
}
|
||||||
if (state.segmentsLoaded > 0) {
|
if (state.segmentsLoaded > 0) {
|
||||||
const segText = state.segmentsFailed > 0
|
const segText = state.segmentsFailed > 0
|
||||||
? `📦 ${state.segmentsLoaded} seg (+${state.segmentsFailed} fail)`
|
? `\uD83D\uDCE6 ${state.segmentsLoaded} seg (+${state.segmentsFailed} fail)`
|
||||||
: `📦 ${state.segmentsLoaded} seg`;
|
: `\uD83D\uDCE6 ${state.segmentsLoaded} seg`;
|
||||||
lines.push(segText);
|
lines.push(segText);
|
||||||
}
|
}
|
||||||
lines.push(`💾 ${state.bufferedAhead.toFixed(1)}s buf`);
|
lines.push(`\uD83D\uDCBE ${state.bufferedAhead.toFixed(1)}s buf`);
|
||||||
// Playback progress: currentTime / duration (only when known)
|
// Playback progress: currentTime / duration (only when known)
|
||||||
if (state.duration > 0) {
|
if (state.duration > 0) {
|
||||||
const pct = ((state.currentTime / state.duration) * 100).toFixed(0);
|
const pct = ((state.currentTime / state.duration) * 100).toFixed(0);
|
||||||
lines.push(`⏱ ${state.currentTime.toFixed(1)}s / ${state.duration.toFixed(0)}s (${pct}%)`);
|
lines.push(`\u23F1 ${state.currentTime.toFixed(1)}s / ${state.duration.toFixed(0)}s (${pct}%)`);
|
||||||
} else if (state.currentTime > 0) {
|
} else if (state.currentTime > 0) {
|
||||||
lines.push(`⏱ ${state.currentTime.toFixed(1)}s (live/loop)`);
|
lines.push(`\u23F1 ${state.currentTime.toFixed(1)}s (live/loop)`);
|
||||||
}
|
}
|
||||||
if (state.fpsDropCount > 0) {
|
if (state.fpsDropCount > 0) {
|
||||||
lines.push(`⚠ ${state.fpsDropCount} fps-drop`);
|
lines.push(`\u26A0 ${state.fpsDropCount} fps-drop`);
|
||||||
}
|
}
|
||||||
if (state.errorMessage) {
|
if (state.errorMessage) {
|
||||||
lines.push(`❌ ${state.errorMessage}`);
|
lines.push(`\u274C ${state.errorMessage}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
overlay.textContent = lines.join(' ');
|
overlay.textContent = lines.join(' ');
|
||||||
@@ -219,24 +234,21 @@ function renderPanelState(video, panel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================================================================== */
|
||||||
|
/* AggregateRenderer — global status bar */
|
||||||
|
/* ======================================================================== */
|
||||||
|
|
||||||
|
let statusElRef = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aggregate playback status bar.
|
* Render aggregate playback state into the top status bar.
|
||||||
|
* Format: `2 playing | 1 buffering | 1 error | 47 seg total`
|
||||||
*
|
*
|
||||||
* HLS delivery is inherently segmented — fragments load one at a time into
|
* HLS delivery is inherently segmented — fragments load one at a time into
|
||||||
* the MediaSource buffer. Even when the phase is 'playing', the underlying
|
* the MediaSource buffer. Even when the phase is 'playing', the underlying
|
||||||
* state is constantly shifting (buffer growth, quality switches, seg loads).
|
* state is constantly shifting (buffer growth, quality switches, seg loads).
|
||||||
* This function collapses per-panel state into a single human-readable line
|
* This function collapses per-panel state into a single human-readable line
|
||||||
* so the user can see overall health at a glance.
|
* so the user can see overall health at a glance.
|
||||||
*
|
|
||||||
* Wires up to `aggregateStateDirty` — set by `renderPanelState` whenever
|
|
||||||
* a panel's state changes. The buffer-refresh tick checks the flag and
|
|
||||||
* re-renders only when necessary to keep CPU usage low.
|
|
||||||
*/
|
|
||||||
let statusElRef = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render aggregate playback state into the top status bar.
|
|
||||||
* Format: `▶ 2 playing | ◷ 1 buffering | ✗ 1 error | 📦 47 seg total`
|
|
||||||
*/
|
*/
|
||||||
function renderAggregateState() {
|
function renderAggregateState() {
|
||||||
if (!statusElRef) return;
|
if (!statusElRef) return;
|
||||||
@@ -263,13 +275,13 @@ function renderAggregateState() {
|
|||||||
|
|
||||||
const totalPanels = panelStates.size;
|
const totalPanels = panelStates.size;
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (playing > 0) parts.push(`▶ ${playing} playing`);
|
if (playing > 0) parts.push(`\u25B6 ${playing} playing`);
|
||||||
if (buffering > 0) parts.push(`◷ ${buffering} buffering`);
|
if (buffering > 0) parts.push(`\u27F7 ${buffering} buffering`);
|
||||||
if (errors > 0) parts.push(`✗ ${errors} error`);
|
if (errors > 0) parts.push(`\u2717 ${errors} error`);
|
||||||
if (idle > 0) parts.push(`⏸ ${idle} idle`);
|
if (idle > 0) parts.push(`\u23F8 ${idle} idle`);
|
||||||
|
|
||||||
if (totalSegs > 0) {
|
if (totalSegs > 0) {
|
||||||
let segPart = `📦 ${totalSegs} seg`;
|
let segPart = `\uD83D\uDCE6 ${totalSegs} seg`;
|
||||||
if (totalSegFails > 0) segPart += ` (+${totalSegFails} fail)`;
|
if (totalSegFails > 0) segPart += ` (+${totalSegFails} fail)`;
|
||||||
parts.push(segPart);
|
parts.push(segPart);
|
||||||
}
|
}
|
||||||
@@ -283,58 +295,28 @@ function renderAggregateState() {
|
|||||||
}
|
}
|
||||||
const active = playing + buffering;
|
const active = playing + buffering;
|
||||||
if (active > 0) {
|
if (active > 0) {
|
||||||
parts.push(`💾 ${(totalBuf / active).toFixed(1)}s avg buf`);
|
parts.push(`\uD83D\uDCBE ${(totalBuf / active).toFixed(1)}s avg buf`);
|
||||||
}
|
}
|
||||||
|
|
||||||
statusElRef.textContent = `Live grid ${totalPanels} panels: ${parts.join(' | ')} || Found ${totalPanels} video(s)`;
|
statusElRef.textContent = `Live grid ${totalPanels} panels: ${parts.join(' | ')} || Found ${totalPanels} video(s)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ======================================================================== */
|
/* ======================================================================== */
|
||||||
/* Fragmented playback tracker */
|
/* EventWiring — hls.js + video element event listeners */
|
||||||
/* ======================================================================== */
|
/* ======================================================================== */
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute buffered seconds ahead of current playback position.
|
|
||||||
* Returns 0 if the video has no buffered ranges or is not playing.
|
|
||||||
*/
|
|
||||||
function getBufferedAhead(video) {
|
|
||||||
if (!video.buffered || video.buffered.length === 0) return 0;
|
|
||||||
const current = video.currentTime;
|
|
||||||
// Find the buffer range that contains or is ahead of current time
|
|
||||||
for (let i = 0; i < video.buffered.length; i++) {
|
|
||||||
if (video.buffered.end(i) > current) {
|
|
||||||
return video.buffered.end(i) - current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach hls.js event listeners for fragmented playback tracking.
|
* Attach hls.js event listeners for fragmented playback tracking.
|
||||||
* This wires up every event that reveals the broken-up nature of HLS:
|
* Wires up every event that reveals the broken-up nature of HLS:
|
||||||
* segment loading, buffer mutations, quality switches, and decode issues.
|
* segment loading, buffer mutations, quality switches, and decode issues.
|
||||||
|
*
|
||||||
|
* Side effects: registers callbacks on `hls`, creates `PanelState`,
|
||||||
|
* sets up buffer monitor timer, patches hls.destroy for cleanup.
|
||||||
*/
|
*/
|
||||||
function wirePlaybackTracking(hls, video, panel) {
|
function wireHlsEvents(hls, video, panel) {
|
||||||
const state = createPanelState();
|
const state = createPanelState();
|
||||||
panelStates.set(video, state);
|
panelStates.set(video, state);
|
||||||
|
|
||||||
// Periodically update buffer health and re-render overlay
|
|
||||||
const bufferTimer = setInterval(() => {
|
|
||||||
const s = panelStates.get(video);
|
|
||||||
if (!s) { clearInterval(bufferTimer); return; }
|
|
||||||
s.bufferedAhead = getBufferedAhead(video);
|
|
||||||
renderPanelState(video, panel);
|
|
||||||
// Refresh aggregate status bar if any panel changed state.
|
|
||||||
renderAggregateState();
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
// Clean up timer on destroy (monkey-patch hls.destroy)
|
|
||||||
const origDestroy = hls.destroy.bind(hls);
|
|
||||||
hls.destroy = function () {
|
|
||||||
clearInterval(bufferTimer);
|
|
||||||
origDestroy();
|
|
||||||
};
|
|
||||||
|
|
||||||
const HlsEvents = window.Hls.Events;
|
const HlsEvents = window.Hls.Events;
|
||||||
|
|
||||||
// --- Manifest lifecycle ---
|
// --- Manifest lifecycle ---
|
||||||
@@ -493,15 +475,20 @@ function wirePlaybackTracking(hls, video, panel) {
|
|||||||
hlsInstances.delete(video);
|
hlsInstances.delete(video);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- Video element events (complement hls.js) ---
|
/**
|
||||||
|
* Wire video element lifecycle events (playing, waiting, stalled, etc.).
|
||||||
|
* These complement hls.js events by providing browser-level playback state.
|
||||||
|
*/
|
||||||
|
function wireVideoEvents(video) {
|
||||||
video.addEventListener('playing', () => {
|
video.addEventListener('playing', () => {
|
||||||
const s = panelStates.get(video);
|
const s = panelStates.get(video);
|
||||||
if (s) {
|
if (s) {
|
||||||
s.phase = 'playing';
|
s.phase = 'playing';
|
||||||
s.lastEvent = 'video.playing';
|
s.lastEvent = 'video.playing';
|
||||||
s.lastActivity = Date.now();
|
s.lastActivity = Date.now();
|
||||||
renderPanelState(video, panel);
|
renderPanelState(video, s._panel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -511,7 +498,7 @@ function wirePlaybackTracking(hls, video, panel) {
|
|||||||
s.phase = 'buffering';
|
s.phase = 'buffering';
|
||||||
s.lastEvent = 'video.waiting';
|
s.lastEvent = 'video.waiting';
|
||||||
s.lastActivity = Date.now();
|
s.lastActivity = Date.now();
|
||||||
renderPanelState(video, panel);
|
renderPanelState(video, s._panel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -521,7 +508,7 @@ function wirePlaybackTracking(hls, video, panel) {
|
|||||||
s.phase = 'buffering';
|
s.phase = 'buffering';
|
||||||
s.lastEvent = 'video.stalled';
|
s.lastEvent = 'video.stalled';
|
||||||
s.lastActivity = Date.now();
|
s.lastActivity = Date.now();
|
||||||
renderPanelState(video, panel);
|
renderPanelState(video, s._panel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -547,11 +534,69 @@ function wirePlaybackTracking(hls, video, panel) {
|
|||||||
if (s) {
|
if (s) {
|
||||||
s.lastEvent = 'video.seeked';
|
s.lastEvent = 'video.seeked';
|
||||||
s.lastActivity = Date.now();
|
s.lastActivity = Date.now();
|
||||||
renderPanelState(video, panel);
|
renderPanelState(video, s._panel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================================================================== */
|
||||||
|
/* BufferMonitor — periodic buffer health check */
|
||||||
|
/* ======================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a periodic timer that refreshes buffer health and re-renders overlays.
|
||||||
|
* Returns a cleanup function.
|
||||||
|
*/
|
||||||
|
function startBufferMonitor(video, panel) {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const s = panelStates.get(video);
|
||||||
|
if (!s) { clearInterval(timer); return; }
|
||||||
|
s.bufferedAhead = getBufferedAhead(video);
|
||||||
|
renderPanelState(video, panel);
|
||||||
|
// Refresh aggregate status bar if any panel changed state.
|
||||||
|
renderAggregateState();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================== */
|
||||||
|
/* HLS lifecycle helpers */
|
||||||
|
/* ======================================================================== */
|
||||||
|
|
||||||
|
/** Destroy the Hls instance attached to a video element (if any). */
|
||||||
|
function destroyHls(video) {
|
||||||
|
const inst = hlsInstances.get(video);
|
||||||
|
if (inst) {
|
||||||
|
inst.destroy();
|
||||||
|
hlsInstances.delete(video);
|
||||||
|
}
|
||||||
|
panelStates.delete(video);
|
||||||
|
removePanelOverlay(video);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================== */
|
||||||
|
/* PanelFactory — DOM panel creation */
|
||||||
|
/* ======================================================================== */
|
||||||
|
|
||||||
|
/** Show an error message inside a panel's video placeholder. */
|
||||||
|
function showError(panel, message) {
|
||||||
|
const err = document.createElement('div');
|
||||||
|
err.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#f87171;background:rgba(0,0,0,0.6);font-family:sans-serif;font-size:13px;text-align:center;padding:12px;';
|
||||||
|
err.textContent = message;
|
||||||
|
panel.appendChild(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a video element with standard autoplay attributes. */
|
||||||
|
function createVideoElement() {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.autoplay = true;
|
||||||
|
video.muted = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
return video;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play an HLS manifest on a <video> element using hls.js or native Safari.
|
* Play an HLS manifest on a <video> element using hls.js or native Safari.
|
||||||
* Wires up fragmented playback tracking for segment/buffer/quality visibility.
|
* Wires up fragmented playback tracking for segment/buffer/quality visibility.
|
||||||
@@ -575,7 +620,21 @@ function playHls(video, manifestUrl, panel) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Wire all fragmented playback tracking events
|
// Wire all fragmented playback tracking events
|
||||||
wirePlaybackTracking(hls, video, panel);
|
wireHlsEvents(hls, video, panel);
|
||||||
|
|
||||||
|
// Wire video element events (playing, waiting, stalled, timeupdate, etc.)
|
||||||
|
// Store panel ref on state so video event handlers can access it.
|
||||||
|
const state = panelStates.get(video);
|
||||||
|
if (state) state._panel = panel;
|
||||||
|
wireVideoEvents(video);
|
||||||
|
|
||||||
|
// Start periodic buffer health monitor
|
||||||
|
const stopMonitor = startBufferMonitor(video, panel);
|
||||||
|
const origDestroy = hls.destroy.bind(hls);
|
||||||
|
hls.destroy = function () {
|
||||||
|
stopMonitor();
|
||||||
|
origDestroy();
|
||||||
|
};
|
||||||
|
|
||||||
hls.loadSource(manifestUrl);
|
hls.loadSource(manifestUrl);
|
||||||
hls.attachMedia(video);
|
hls.attachMedia(video);
|
||||||
@@ -598,28 +657,6 @@ function loadHlsJs() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ======================================================================== */
|
|
||||||
/* UI helpers */
|
|
||||||
/* ======================================================================== */
|
|
||||||
|
|
||||||
/** Show an error message inside a panel's video placeholder. */
|
|
||||||
function showError(panel, message) {
|
|
||||||
const err = document.createElement('div');
|
|
||||||
err.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#f87171;background:rgba(0,0,0,0.6);font-family:sans-serif;font-size:13px;text-align:center;padding:12px;';
|
|
||||||
err.textContent = message;
|
|
||||||
panel.appendChild(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a video element with standard autoplay attributes. */
|
|
||||||
function createVideoElement() {
|
|
||||||
const video = document.createElement('video');
|
|
||||||
video.autoplay = true;
|
|
||||||
video.muted = true;
|
|
||||||
video.loop = true;
|
|
||||||
video.playsInline = true;
|
|
||||||
return video;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a single panel (video or empty).
|
* Create a single panel (video or empty).
|
||||||
* Returns { panel, hlsEntry } — hlsEntry is set for deferred HLS playback.
|
* Returns { panel, hlsEntry } — hlsEntry is set for deferred HLS playback.
|
||||||
|
|||||||
Reference in new issue
Block a user