97 lines
3.0 KiB
TypeScript
97 lines
3.0 KiB
TypeScript
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);
|
|
});
|
|
}
|
|
};
|