// The player integration of stream audit (wai/extensions/stream-audit.md §13) against the corpus // wai/live-conformance/audit/ (`node --test web/audit.test.mjs`): a session driven with the // corpus's inputs signs the corpus's records byte for byte, and writes its log and its CMCD batch // byte for byte; and the playlist reader, the tree root and the network tap do what §4 to §7 say. import test from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import * as au from "./audit.js"; import { blake3 } from "./blake3.js"; import { tap } from "./adapter-tap.js"; import { loaderAudit } from "./adapter-hlsjs.js"; const doc = JSON.parse(readFileSync(new URL("../../live-conformance/audit/vectors.json", import.meta.url), "utf8")); const vector = (name) => doc.sessions.find((v) => v.name === name); const enc = new TextEncoder(); /** Session key `i` of the corpus: SHA-256 of its label and `i` (its README). */ const key = async (i) => au.keyFromSeed(await au.sha256(enc.encode("wai:stream-audit/corpus/key\x00"), Uint8Array.of(i))); const T0 = 1_792_886_400_000_000; const ms = (t) => T0 / 1000 + t; /** The playlist the ads of `live_two_origins` are marked in: its creatives' segments and program times. */ const PLAYLIST = [ "#EXTM3U", "#EXT-X-TARGETDURATION:2", "#EXT-X-PROGRAM-DATE-TIME:2026-10-25T00:00:06.000Z", '#EXT-X-DATERANGE:ID="ad-1",START-DATE="2026-10-25T00:00:06.000Z",DURATION=4.000,SCTE35-OUT=0xFC302000', "#EXTINF:2.000,", "https://cdn-b.example/a0", "#EXTINF:2.000,", "https://cdn-b.example/a1", "#EXT-X-PROGRAM-DATE-TIME:2026-10-25T00:00:14.000Z", '#EXT-X-DATERANGE:ID="ad-2",START-DATE="2026-10-25T00:00:14.000Z",DURATION=4.000,SCTE35-OUT=0xFC302000', "#EXTINF:2.000,", "https://cdn-b.example/a2", "#EXTINF:2.000,", "https://cdn-b.example/a3", "", ].join("\n"); test("BLAKE3 of one chunk: the specification's empty input, and the corpus's tree roots", () => { assert.equal(au.hex(blake3(new Uint8Array(0))), "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"); assert.throws(() => blake3(new Uint8Array(1025))); }); test("a live session from two origins: every record, the log and the CMCD batch are the corpus's", async () => { const v = vector("live_two_origins"); const sc = v.script; let clock = T0; const s = await au.AuditSession.start({ channel: "news-east", owner: doc.owner, presentation: au.LIVE, windowUs: sc.window_us, key: await key(sc.key), now: () => clock, cmcdEveryMs: 0 }); s.playlist(PLAYLIST, "https://cdn-b.example/live.m3u8"); const notes = { s0: { cmsdStatic: "ot=v,sf=h,st=l" }, s1: { cmsdDynamic: '"cdn-b";etp=1200;rtt=40' } }; const steps = [...sc.received.map((r) => ({ at: r.at, r })), ...sc.events.map((e) => ({ at: e.at, e }))].sort((a, b) => a.at - b.at); // Playback, observed every 100 ms from 1 s to 29.5 s: stalled from 20 s to 21.5 s, the second ad // 30 % in view, everything audible. const ticks = []; for (let t = 1000; t <= 29_500; t += 100) ticks.push({ at: T0 + t * 1000, t }); const all = [...steps, ...ticks.map((k) => ({ at: k.at, tick: k.t }))].sort((a, b) => a.at - b.at || (a.tick === undefined ? -1 : 1)); for (const x of all) { clock = x.at; if (x.e) s.event(x.e.event, x.at); else if (x.r) { const url = `${x.r.origin}/${x.r.object}`; const n = notes[x.r.object]; if (n) delete notes[x.r.object]; await s.object({ url, bytes: au.unhex(doc.objects[x.r.object]), media: x.r.media, at: x.at, ...(n ?? {}) }); } else { const t = x.tick; const playing = t < 20_000 || (t >= 21_500 && t < 29_500); s.observe({ at: x.at, playing, visibleBp: t >= 14_000 && t < 18_000 ? 3000 : 10_000, audible: true, programMs: ms(t) }); } } s.energy(au.ROLE.SINK_DECODE, 1_250_000, au.CLASS.ESTIMATOR); s.energy(au.ROLE.DISPLAY, 0, au.CLASS.UNMETERED); const bundle = await s.close("user", T0 + 30_000_000); assert.equal(bundle.log, v.bundle.log, "the log"); assert.equal(bundle.notes, v.bundle.notes, "the notes"); assert.deepEqual(bundle.acks, v.bundle.acks, "the acknowledgements"); assert.deepEqual(bundle.claims, v.bundle.claims, "the claims"); assert.equal(bundle.session, v.bundle.session, "the session record"); assert.deepEqual(bundle, v.bundle); assert.equal(au.cmcdBatch(s.log), v.cmcd, "the CMCD batch"); }); test("the playlist reader: segments, program times, marked ads, keys and on demand", () => { const p = au.parsePlaylist(PLAYLIST + '#EXT-X-KEY:METHOD=AES-128,URI="k/1.key"\n#EXT-X-DATERANGE:ID="n",START-DATE="2026-10-25T00:01:00.000Z",DURATION=1.0,CLASS="x"\n#EXT-X-ENDLIST\n', "https://cdn-b.example/v/live.m3u8"); assert.deepEqual(p.segments.map((x) => [x.uri, x.durationMs, x.programMs]), [ ["https://cdn-b.example/a0", 2000, ms(6000)], ["https://cdn-b.example/a1", 2000, ms(8000)], ["https://cdn-b.example/a2", 2000, ms(14_000)], ["https://cdn-b.example/a3", 2000, ms(16_000)], ]); assert.deepEqual(p.ads.map((a) => [a.id, a.startMs, a.durationMs]), [["ad-1", ms(6000), 4000], ["ad-2", ms(14_000), 4000]], "a CLASS not configured is not an ad"); assert.deepEqual(p.keys, ["https://cdn-b.example/v/k/1.key"]); assert.equal(p.onDemand, true); assert.equal(au.parsePlaylist(PLAYLIST + '#EXT-X-DATERANGE:ID="n",START-DATE="2026-10-25T00:01:00.000Z",DURATION=1.0,CLASS="x"\n', "https://a.example/", { adClasses: ["x"] }).ads.length, 3); }); test("a key a playlist names is never acknowledged, and a repeat in a window is counted once", async () => { let clock = T0; const s = await au.AuditSession.start({ channel: "news-east", owner: doc.owner, key: await key(10), now: () => clock, cmcdEveryMs: 0 }); s.playlist('#EXTM3U\n#EXT-X-KEY:METHOD=AES-128,URI="https://k.example/1.key"\n#EXTINF:2,\ns.ts\n#EXT-X-ENDLIST\n', "https://o.example/p.m3u8"); assert.equal(await s.object({ url: "https://k.example/1.key", bytes: new Uint8Array(16), media: false }), null); await s.object({ url: "https://o.example/s.ts", bytes: enc.encode("segment"), media: true, at: T0 + 1000 }); await s.object({ url: "https://o.example/s.ts", bytes: enc.encode("segment"), media: true, at: T0 + 2000 }); const b = await s.close("ended", T0 + 3_000_000); assert.equal(b.acks.length, 1); assert.equal(b.acks[0].items[0].length, 96, "one item of 48 bytes, hexadecimal"); const rec = au.unhex(b.session); assert.equal(rec[4 + 1 + 32 * 3 + 16], au.ON_DEMAND, "on demand, from EXT-X-ENDLIST"); }); /** A window with only what the tap reads: `fetch`, `XMLHttpRequest`, `location`. */ function fakeWindow(responses) { const win = { location: { href: "https://page.example/" } }; win.fetch = async (input) => { const url = new URL(input, win.location.href).href; const r = responses[url]; return new Response(r.body, { status: 200, headers: r.headers ?? {} }); }; // Response.url is read-only; a real fetch sets it. Stand in with a subclass that has it. const plain = win.fetch; win.fetch = async (input, init) => { const res = await plain(input, init); const url = new URL(input, win.location.href).href; Object.defineProperty(res, "url", { value: url }); const clone = res.clone.bind(res); res.clone = () => { const c = clone(); Object.defineProperty(c, "url", { value: url }); return c; }; return res; }; return win; } test("the tap hands the session what the presentation names, and nothing else", async () => { const pl = '#EXTM3U\n#EXT-X-MAP:URI="init.mp4"\n#EXT-X-KEY:METHOD=AES-128,URI="k.key"\n#EXTINF:2,\nseg0.m4s\n#EXT-X-ENDLIST\n'; const win = fakeWindow({ "https://cdn.example/v/p.m3u8": { body: pl }, "https://cdn.example/v/init.mp4": { body: "init", headers: { "content-type": "video/mp4" } }, "https://cdn.example/v/seg0.m4s": { body: "seg0", headers: { "content-type": "video/iso.segment", "cmsd-static": "ot=v" } }, "https://cdn.example/v/k.key": { body: "0123456789abcdef" }, "https://analytics.example/x": { body: "{}" }, }); const s = await au.AuditSession.start({ channel: "film-library", owner: doc.owner, key: await key(11), cmcdEveryMs: 0 }); const t = tap(s, { manifest: "https://cdn.example/v/p.m3u8", win }); for (const u of ["https://cdn.example/v/p.m3u8", "https://cdn.example/v/init.mp4", "https://cdn.example/v/seg0.m4s", "https://cdn.example/v/k.key", "https://analytics.example/x"]) { await (await win.fetch(u)).arrayBuffer(); await new Promise((r) => setTimeout(r, 20)); } t.restore(); await Promise.all(s.pending); const h = async (t) => au.hex(await au.sha256(enc.encode(t))); assert.deepEqual(s.received.map((r) => [r.origin, r.media, au.hex(r.hash)]), [ ["https://cdn.example", false, await h(pl)], ["https://cdn.example", true, await h("init")], ["https://cdn.example", true, await h("seg0")], ]); assert.deepEqual(s.notes, [{ content_hash: au.hex(await au.sha256(enc.encode("seg0"))), cmsd_static: "ot=v", cmsd_dynamic: null }]); assert.equal(s.presentation, au.ON_DEMAND); }); test("the loader adapter wraps the player's loader, and passes key loads through untouched", async () => { const seen = []; class Base { load(context, config, callbacks) { seen.push(context.url); const data = context.type === "media-fragment" ? new TextEncoder().encode("frag").buffer : "#EXTM3U\n#EXTINF:2,\nf.ts\n"; callbacks.onSuccess({ url: context.url, data, code: 200 }, {}, context, { getResponseHeader: (h) => (h === "cmsd-dynamic" ? '"e";etp=9' : null) }); } } const Player = { DefaultConfig: { loader: Base } }; const s = await au.AuditSession.start({ channel: "news-east", owner: doc.owner, key: await key(12), cmcdEveryMs: 0 }); const a = loaderAudit(Player, s); const l = new a.Loader(); const got = []; const cb = { onSuccess: (r) => got.push(r.url), onError() {}, onTimeout() {} }; l.load({ type: "level", url: "https://o.example/l.m3u8" }, {}, cb); l.load({ type: "media-fragment", url: "https://o.example/f.ts", frag: {} }, {}, cb); l.load({ type: "key", url: "https://o.example/k.key", keyInfo: {} }, {}, cb); await new Promise((r) => setTimeout(r, 20)); assert.deepEqual(got, ["https://o.example/l.m3u8", "https://o.example/f.ts", "https://o.example/k.key"], "the player gets every answer"); assert.deepEqual(s.received.map((r) => [r.media, r.length]), [[false, 24], [true, 4]], "the key is not the session's"); assert.equal(s.segments.has("https://o.example/f.ts"), true); assert.equal(s.notes.length, 2); assert.equal(a.programMs({ playingDate: new Date(5000) }), 5000); }); test("an ad entered between two observations is stated from its start, and to its end", async () => { let clock = T0; const s = await au.AuditSession.start({ channel: "news-east", owner: doc.owner, key: await key(13), now: () => clock, cmcdEveryMs: 0 }); s.playlist(PLAYLIST, "https://cdn-b.example/live.m3u8"); for (const n of ["a0", "a1"]) await s.object({ url: `https://cdn-b.example/${n}`, bytes: au.unhex(doc.objects[n]), media: true, at: T0 + 1000 }); // Every 200 ms from 1.03 s: the first observation within the ad is 30 ms into it, the last 170 ms before its end. for (let t = 1030; t <= 12_000; t += 200) { clock = T0 + t * 1000; s.observe({ at: clock, playing: true, visibleBp: 10_000, audible: true, programMs: ms(t) }); } const ad = [...s.ads.values()][0]; assert.deepEqual(ad.recorder.spans(), [{ startMs: 0, lengthMs: 4000, visibleBp: 10_000, flags: au.AUDIBLE }]); }); test("the log's stalls are those begun after the first presentation (delivery-telemetry §7.2)", () => { const o = (sn, t, event) => au.observation("00".repeat(16), sn, t * 1000, event, "l", { present: event === "present" ? { unit: 0, layers: 1, first: true, late_ms: null, decode_us: null } : null }); const log = [o(0, 0, "start"), o(1, 100, "stall-start"), o(2, 300, "stall-end"), o(3, 400, "present"), o(4, 900, "stall-start"), o(5, 1200, "stall-end"), o(6, 2000, "end")]; assert.deepEqual(au.sessionMetrics(log), { vst: 400, stalls: 1, stallMs: 300 }); });