feat: profile hls playback
This commit is contained in:
115
static/player/hls_profile.ts
Normal file
115
static/player/hls_profile.ts
Normal 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);
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
|
import { attachHLSProfile } from "./hls_profile";
|
||||||
import { state } from "./state";
|
import { state } from "./state";
|
||||||
import { absoluteTimeFromDisplay, displayTimeFromAbsolute, invalidateBounds } from "./timeline";
|
import { absoluteTimeFromDisplay, displayTimeFromAbsolute, invalidateBounds } from "./timeline";
|
||||||
|
|
||||||
let hls: Hls | null = null;
|
let hls: Hls | null = null;
|
||||||
|
let stopHLSProfile: (() => void) | null = null;
|
||||||
|
|
||||||
const destroyHLS = (): void => {
|
const destroyHLS = (): void => {
|
||||||
|
stopHLSProfile?.();
|
||||||
|
stopHLSProfile = null;
|
||||||
hls?.destroy();
|
hls?.destroy();
|
||||||
hls = null;
|
hls = null;
|
||||||
};
|
};
|
||||||
@@ -44,6 +48,7 @@ export const loadVideoSource = (url: string, type?: string): void => {
|
|||||||
|
|
||||||
if (shouldUseHLS(type, url) && Hls.isSupported()) {
|
if (shouldUseHLS(type, url) && Hls.isSupported()) {
|
||||||
hls = new Hls();
|
hls = new Hls();
|
||||||
|
stopHLSProfile = attachHLSProfile(hls, state.video);
|
||||||
hls.loadSource(url);
|
hls.loadSource(url);
|
||||||
hls.attachMedia(state.video);
|
hls.attachMedia(state.video);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user