feat: profile hls playback

This commit is contained in:
2026-06-16 01:34:42 +02:00
committed by Milas Holsting
parent c70ec383c5
commit c3b3c606db
2 changed files with 120 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
import Hls from "hls.js";
import type { ErrorData } from "hls.js";
interface HLSPlaybackProfile {
sourceLoads: number;
manifestParsed: number;
stalls: number;
totalStallMs: number;
seeks: number;
totalSeekMs: number;
errors: number;
fatalErrors: number;
lastErrorType: string;
}
declare global {
interface Window {
__malHlsProfile?: HLSPlaybackProfile;
}
}
const createProfile = (): HLSPlaybackProfile => ({
sourceLoads: 0,
manifestParsed: 0,
stalls: 0,
totalStallMs: 0,
seeks: 0,
totalSeekMs: 0,
errors: 0,
fatalErrors: 0,
lastErrorType: "",
});
const mark = (name: string): void => {
performance.mark(`mal.hls.${name}`);
};
const measure = (name: string, startMark: string): void => {
try {
performance.measure(`mal.hls.${name}`, `mal.hls.${startMark}`);
} catch {
// Missing marks can happen if HLS.js emits a later event after cleanup.
}
};
export const attachHLSProfile = (hls: Hls, video: HTMLVideoElement): (() => void) => {
const profile = createProfile();
window.__malHlsProfile = profile;
let stallStartedAt: number | null = null;
let seekStartedAt: number | null = null;
const onManifestLoading = (): void => {
profile.sourceLoads += 1;
mark("manifest_loading");
};
const onManifestParsed = (): void => {
profile.manifestParsed += 1;
measure("manifest_load", "manifest_loading");
};
const onHLSError = (_event: string, data: ErrorData): void => {
profile.errors += 1;
if (data.fatal) {
profile.fatalErrors += 1;
}
profile.lastErrorType = data.type;
};
const onWaiting = (): void => {
if (stallStartedAt !== null) return;
stallStartedAt = performance.now();
profile.stalls += 1;
mark("stall_start");
};
const onPlaying = (): void => {
if (stallStartedAt === null) return;
profile.totalStallMs += performance.now() - stallStartedAt;
stallStartedAt = null;
measure("stall", "stall_start");
};
const onSeeking = (): void => {
seekStartedAt = performance.now();
mark("seek_start");
};
const onSeeked = (): void => {
if (seekStartedAt === null) return;
profile.seeks += 1;
profile.totalSeekMs += performance.now() - seekStartedAt;
seekStartedAt = null;
measure("seek", "seek_start");
};
hls.on(Hls.Events.MANIFEST_LOADING, onManifestLoading);
hls.on(Hls.Events.MANIFEST_PARSED, onManifestParsed);
hls.on(Hls.Events.ERROR, onHLSError);
video.addEventListener("waiting", onWaiting);
video.addEventListener("playing", onPlaying);
video.addEventListener("seeking", onSeeking);
video.addEventListener("seeked", onSeeked);
return () => {
hls.off(Hls.Events.MANIFEST_LOADING, onManifestLoading);
hls.off(Hls.Events.MANIFEST_PARSED, onManifestParsed);
hls.off(Hls.Events.ERROR, onHLSError);
video.removeEventListener("waiting", onWaiting);
video.removeEventListener("playing", onPlaying);
video.removeEventListener("seeking", onSeeking);
video.removeEventListener("seeked", onSeeked);
};
};

View File

@@ -1,10 +1,14 @@
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;
};
@@ -44,6 +48,7 @@ export const loadVideoSource = (url: string, type?: string): void => {
if (shouldUseHLS(type, url) && Hls.isSupported()) {
hls = new Hls();
stopHLSProfile = attachHLSProfile(hls, state.video);
hls.loadSource(url);
hls.attachMedia(state.video);
} else {