// An adapter of stream audit (wai/extensions/stream-audit.md §13) for a web player whose // configuration takes a loader class: it wraps the player's default loader, so that every // playlist and fragment the player loads reaches the session (./audit.js) as it reaches the // player, and key loads never do (§4.1). The player's own interface is named here, and only // here, because the adapter has to call it. // allow-name: the open-source web player whose loader interface this adapter wraps // // const audit = loaderAudit(Hls, session); // allow-name: the player's constructor // const player = new Hls({ loader: audit.Loader }); // allow-name: the player's constructor // attach(video, session, { programMs: () => audit.programMs(player) }); const enc = new TextEncoder(); /** * The loader class to configure, and the program time of the position (the player's * `playingDate`, from `EXT-X-PROGRAM-DATE-TIME`), for `session`. */ export function loaderAudit(Player, session) { const Base = Player.DefaultConfig.loader; class Loader extends Base { load(context, config, callbacks) { if (context.type === "key" || context.keyInfo) return super.load(context, config, callbacks); const onSuccess = callbacks.onSuccess; const wrapped = { ...callbacks, onSuccess: (response, stats, ctx, details) => { try { record(response, ctx, details); } catch { // The audit never stands in the player's way. } onSuccess(response, stats, ctx, details); }, }; return super.load(context, config, wrapped); } } const header = (details, h) => details?.getResponseHeader?.(h) ?? details?.headers?.get?.(h) ?? null; function record(response, ctx, details) { const url = response.url || ctx.url; const text = typeof response.data === "string" ? response.data : null; const bytes = text !== null ? enc.encode(text) : new Uint8Array(response.data); const media = ctx.type === "media-fragment" || !!ctx.frag; if (text !== null && text.includes("#EXTINF")) session.playlist(text, url); session.object({ url, bytes, media, status: response.code ?? 200, cmsdStatic: header(details, "cmsd-static"), cmsdDynamic: header(details, "cmsd-dynamic") }); } return { Loader, programMs: (player) => player.playingDate?.getTime() ?? null }; }