import Hls from "hls.js"; import { attachHLSProfile } from "./hls_profile"; import { state } from "./state"; import { absoluteTimeFromDisplay, displayTimeFromAbsolute, invalidateBounds } from "./timeline"; let hls: Hls | null = null; let stopHLSProfile: (() => void) | null = null; const destroyHLS = (): void => { stopHLSProfile?.(); stopHLSProfile = null; hls?.destroy(); hls = null; }; export const destroyVideoSource = (): void => { destroyHLS(); state.elements.video.pause(); state.elements.video.removeAttribute("src"); state.elements.video.load(); }; const shouldUseHLS = (type: string | undefined, url: string): boolean => { if (type === "m3u8") { return true; } try { const parsed = new URL(url, window.location.href); if (parsed.searchParams.get("hls") === "1") { return true; } return parsed.pathname.toLowerCase().endsWith(".m3u8"); } catch (error) { console.error("Failed to parse video URL:", error); return url.toLowerCase().includes(".m3u8"); } }; /** * Force-loads a new video source and preserves playback position. * * Some browsers can be flaky when switching between HLS URLs while playing. Clearing `src` first * ensures the media element fully resets before the new URL is set. */ export const loadVideoSource = (url: string, type?: string, startTimeSeconds?: number): void => { if (!url) { return; } const wasPlaying = !state.elements.video.paused; const prevDisplayTime = displayTimeFromAbsolute(state.elements.video.currentTime); const shouldPreservePosition = prevDisplayTime > 0; // Fully reset the element before setting a new source. destroyVideoSource(); if (shouldUseHLS(type, url) && Hls.isSupported()) { hls = new Hls({ autoStartLoad: false, backBufferLength: 30, maxBufferLength: 18, maxMaxBufferLength: 30, maxBufferSize: 20 * 1000 * 1000, startFragPrefetch: false, }); stopHLSProfile = attachHLSProfile(hls, state.elements.video); hls.loadSource(url); hls.attachMedia(state.elements.video); hls.once(Hls.Events.MEDIA_ATTACHED, () => { const startPosition = startTimeSeconds !== undefined && Number.isFinite(startTimeSeconds) && startTimeSeconds > 0 ? startTimeSeconds : -1; hls?.startLoad(startPosition); }); } else { state.elements.video.src = url; state.elements.video.load(); } // Preserve position only when switching away from an existing playback state. // On initial load, a zero pending seek would overwrite server-provided resume time. state.playback.pendingSeekTime = shouldPreservePosition ? prevDisplayTime : null; if (shouldPreservePosition && state.elements.video.readyState >= HTMLMediaElement.HAVE_METADATA) { invalidateBounds(); state.elements.video.currentTime = absoluteTimeFromDisplay(prevDisplayTime); state.playback.pendingSeekTime = null; } if (wasPlaying) { state.elements.video.play().catch((error) => { console.debug("failed to play video:", error); }); } };