open-standards/wai/wai-audit/web/adapter-hlsjs.js
Transaction Science e04d96eba2 wai-audit: the stream audit collector and the player integration
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]>
2026-10-25 21:41:51 -04:00

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 };
}