open-standards/wai/wai-audit/e2e/page/index.html
Transaction Science 4627e02e04 wai-audit: the browser end to end, and what it found
e2e/run.sh builds the collector, installs the automation package and an
open-source web player from e2e/package-lock.json (npm ci, no install
scripts), runs the player integration's unit tests, then
e2e/audit.e2e.mjs: an HLS stream made with the host's media tool (H.264
and AAC in 2 s CMAF segments, a program time, a marked ad over two
segments) served from another origin with CMSD-Static exposed, played in
a real browser twice, once through the loader adapter and once through
the network tap, each session sent to a collector on loopback. It checks
the outcome, the acknowledged objects against the origin's own SHA-256
of what it served, the ad's claim, the CMSD notes, the unmetered energy,
the CMCD batches, the report, its recomputation by `wai-audit check`, a
replay stored once and a tampered record refused.

Three defects it found, fixed here:
- the player hands fragment buffers on (to a worker), emptying them
  after the loader's callback: the session now copies the bytes at once,
  and keeps each object in the order it was given;
- the tap read XMLHttpRequest responses after the player had moved them:
  it now listens from the request's construction, so it reads first;
- a stitched ad is entered between two observations, so its claim began
  past media time 0 and reached quartile 1: play that enters an ad
  continuously now starts its first span at 0 (stream-audit §6.3), as
  leaving it already ran the last span to the ad's end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:50:20 -04:00

60 lines
2.4 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stream audit test page</title>
<style>
:root { color-scheme: light dark; }
body { margin: 0; padding: 16px; font: 14px/1.4 system-ui, sans-serif; background: Canvas; color: CanvasText; }
video { width: 640px; max-width: 100%; aspect-ratio: 16 / 9; background: #000; display: block; }
</style>
</head>
<body>
<video playsinline muted></video>
<pre id="state">starting</pre>
<script src="/vendor/player.js"></script>
<script type="module">
// The end-to-end page of stream audit: a web player plays a stream another origin serves, and the
// audit kit signs the session and sends it to the collector. `?mode=loader` audits through the
// player's loader class (web/adapter-hlsjs.js); `?mode=tap` through the network tap
// (web/adapter-tap.js), using nothing of the player's but what it fetches.
import { AuditSession, attach, CLASS, ROLE } from "/web/audit.js";
import { loaderAudit } from "/web/adapter-hlsjs.js";
import { tap } from "/web/adapter-tap.js";
const q = new URLSearchParams(location.search);
const video = document.querySelector("video");
const state = document.getElementById("state");
const Player = window.Hls; // allow-name: the global the open-source player's script defines
const session = await AuditSession.start({ channel: q.get("channel"), owner: q.get("owner"), collector: q.get("collector"), cmcdEveryMs: 2000 });
// A page has no meter: sink decode and display took part and were not measured (§8.1).
session.energy(ROLE.SINK_DECODE, 0, CLASS.UNMETERED);
session.energy(ROLE.DISPLAY, 0, CLASS.UNMETERED);
let programMs;
let player;
if (q.get("mode") === "tap") {
tap(session, { manifest: q.get("src") });
player = new Player();
programMs = () => session.programAt(video.currentTime);
} else {
const a = loaderAudit(Player, session);
player = new Player({ loader: a.Loader });
programMs = () => a.programMs(player);
}
attach(video, session, {
programMs,
onBundle: (bundle, answer) => {
window.auditResult = { bundle, answer, sid: session.sid };
state.textContent = `sent: ${answer.status}`;
},
});
player.loadSource(q.get("src"));
player.attachMedia(video);
video.muted = true;
video.play().catch((e) => (state.textContent = `play: ${e}`));
state.textContent = "playing";
window.auditSession = session;
</script>
</body>
</html>