wai/wai-audit is the reference collector of stream-audit §12: it verifies each bundle a player posts (wai-rs stream_audit), stores the accepted ones one file per session (a different record of a stored session is refused audit_session_duplicate), takes Event-mode CMCD batches as advisory telemetry, and serves each channel's report signed with its key. Limits per client (an IPv4 address or an IPv6 /64), per route body limits, CORS for the configured page origins. The CLI also verifies a bundle, recomputes a report from a data directory, and checks a served report against its bundles. Its tests post every corpus session and require the corpus's outcome, and require the stored sessions to report exactly the corpus's served reports. web/audit.js is the player integration: it hashes each object with WebCrypto, signs the acknowledgements, claims and session record with WebCrypto's Ed25519, computes the settlement tree root with a one-chunk BLAKE3 of its own, records spans, ads, the log and its CMCD batch. Its Node tests drive it with the corpus's inputs and require the corpus's records byte for byte. adapter-tap.js wraps fetch and XMLHttpRequest for any player; adapter-hlsjs.js wraps a player's loader class. Co-Authored-By: Claude Opus 5.5 <[email protected]>
47 lines
2.3 KiB
JavaScript
47 lines
2.3 KiB
JavaScript
// 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 };
|
|
}
|