open-standards/joule-code/joule-code-rs/crates/joule-moq-web/web/ads/stitched.test.mjs
Transaction Science c70aedef60 wai: stitched presentation — player-signed claims of stitched and interstitial ads, the signed pod-input record, ad frames in live-compose
wai.video.stitched_presentation (extensions/stitched-presentation.md): a WPS1 claim names an ad
by the marker the stream carries for it (an EXT-X-DATERANGE tag, an interstitial's asset, a DASH
event) in the manifest it states the SHA-256 of; a WPI1 pod-input record, signed by the
stitcher or a proxy in front of the ad server, binds the response digest, the chosen ads, their
creatives and prices, the avail, the frame and the measurement terms; a verifier recomputes
each ad's presentations, quartiles and the spend that counts by currency. A form-1
wai.video.ssai receipt gains a JSON carriage (its signed bytes unchanged) so it can be a
record's response. live-compose gains the squeeze and double-box ad frames (§5.9). Corpus
live-conformance/stitched/, its independent verifier stitched_presentation_verify.py, and the
browser producer web/ads/stitched.js with tests against the corpus.

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

137 lines
7.3 KiB
JavaScript

// The reference producer of stitched presentation claims (wai/extensions/stitched-presentation.md
// §8), against the corpus wai/live-conformance/stitched/ (`node --test web/ads/stitched.test.mjs`):
// markers read from the corpus's own manifests, observation sequences recorded into spans, the
// ad's region measured in a composited frame, and the claims written and signed from them, byte
// for byte as the corpus holds them.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import * as ap from "./presentation.js";
import * as st from "./stitched.js";
const DIR = new URL("../../../../../../wai/live-conformance/stitched/", import.meta.url);
const file = (n) => readFileSync(new URL(n, DIR), "utf8");
const doc = JSON.parse(file("vectors.json"));
const vector = (name) => doc.vectors.find((v) => v.name === name);
const hex = (s) => Uint8Array.from(s.match(/../g) ?? [], (h) => parseInt(h, 16));
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
const creative = (k) => doc.creatives[k].objects.map((o) => ({ contentHash: hex(o.content_hash), durationMs: o.duration_ms }));
/** Session key `i` of the corpus: SHA-256 of its label and `i` (its README). */
const key = async (i) => ap.sessionKeyFromSeed(await ap.sha256(new TextEncoder().encode("wai:stitched-presentation/corpus/player\x00"), Uint8Array.of(i)));
/** Observations every 100 ms of the ad's media time from `from` to `to`, at normal speed. */
function play(rec, from, to, visibleBp, audible = true) {
for (let p = from; p <= to; p += 100) rec.observe({ positionMs: p, playing: true, visibleBp, audible });
}
function ended(rec, at, visibleBp) {
rec.observe({ positionMs: at, playing: false, visibleBp, audible: true });
return rec.spans();
}
async function claim({ player, found, manifest, objects, spans, geometry = ap.GEOMETRY.AD, shareBp = 10_000, attestation }) {
const k = await key(player);
const c = {
marker: found.marker,
adId: found.adId,
manifest: await st.manifestDigest(manifest),
session: await st.sessionOf(k.publicKey),
creativeHash: await st.creativeDigest(objects),
player: k.publicKey,
geometry,
shareBp,
frames: st.NOT_EXPOSED,
objects: st.touched(objects, spans),
spans,
attestation,
};
return { hex: toHex(await st.signClaim(c, k.privateKey)), claim: c };
}
test("a stitched ad under a date range: the marker read from the playlist, the corpus's claim", async () => {
const playlist = file("hls_stitched.viewer1.m3u8");
const ads = st.hlsAds(playlist, "X-AD-ID");
assert.deepEqual(ads.map((a) => a.adId), ["srv-v1-0", "srv-v1-1"], "the date ranges that carry an ad identity, and not the break's own");
const rec = new ap.SpanRecorder();
play(rec, 0, 15_000, 10_000);
const spans = ended(rec, 15_000, 10_000);
const got = await claim({ player: 1, found: ads[0], manifest: playlist, objects: creative(0), spans });
assert.equal(got.hex, vector("hls_stitched").claims[0]);
});
test("scrolled partly out of view to the third quartile: a span at each change", async () => {
const playlist = file("hls_stitched.viewer1.m3u8");
const rec = new ap.SpanRecorder();
play(rec, 0, 3_900, 10_000);
play(rec, 4_000, 7_900, 6_000);
play(rec, 8_000, 11_250, 3_000);
rec.observe({ positionMs: 11_250, playing: true, visibleBp: 3_000, audible: true });
const spans = ended(rec, 11_250, 3_000);
assert.deepEqual(spans.map((s) => [s.startMs, s.lengthMs, s.visibleBp]), [[0, 4_000, 10_000], [4_000, 4_000, 6_000], [8_000, 3_250, 3_000]]);
const got = await claim({ player: 1, found: st.hlsAds(playlist, "X-AD-ID")[1], manifest: playlist, objects: creative(1), spans });
assert.equal(got.hex, vector("hls_stitched").claims[1]);
});
test("an interstitial's asset: its date range from the playlist, its identity from the asset list", async () => {
const v = vector("hls_interstitial");
const [dr] = st.interstitials(file("hls_interstitial.m3u8"));
assert.equal(dr.id, "break-300");
const list = file("hls_interstitial.assets.json");
const ads = st.interstitialAds(dr, list);
const want = hex(v.claims[0]);
// The token is the issuer's (the corpus signs it with RFC 9578's test key); the claim carries
// it as the long field before its signature.
const tokenLen = 98 + 256;
const token = want.subarray(want.length - 64 - tokenLen, want.length - 64);
const rec = new ap.SpanRecorder();
play(rec, 0, 15_000, 10_000);
const spans = ended(rec, 15_000, 10_000);
const got = await claim({ player: 11, found: ads[0], manifest: list, objects: creative(3), spans, geometry: ap.GEOMETRY.SCREEN, attestation: token });
assert.equal(got.hex, v.claims[0]);
const challenge = ap.tokenChallenge(2, doc.issuers[0].name, await st.redemptionContext(got.claim));
assert.equal(toHex(token.subarray(34, 66)), toHex(await ap.sha256(challenge)), "the token answers this presentation's own challenge");
});
test("a squeeze: the ad's region from the frame, 70% of it in view, the corpus's claim", async () => {
const mpd = file("squeezeback.mpd");
const [found] = st.mpdAds(mpd, "messageData");
assert.equal(found.adId, "srv-sq-1");
const frame = { kind: 3, width: 1920, height: 1080, corner: 4, windowW: 1440, windowH: 810, gap: 0, aspect: 0 };
const { region } = st.adFrame(frame);
assert.deepEqual(region, [[0, 0, 1920, 270], [0, 270, 480, 810]]);
assert.equal(st.shareBp(frame), 4375);
// The picture drawn at its own size, and the viewport cut 513 pixels down: the whole top band
// and 243 of the side band's 810 rows are in view, 635 040 of 907 200 pixels.
const displayed = { x: 0, y: 0, width: 1920, height: 1080 };
const vis = st.regionVisibleBp(region, 1920, 1080, displayed, { x: 0, y: 0, width: 1920, height: 513 });
assert.equal(vis, 7_000);
assert.equal(st.regionVisibleBp(null, 1920, 1080, displayed, { x: 0, y: 0, width: 1920, height: 513 }), 4_750, "the whole picture, not the region");
const rec = new ap.SpanRecorder();
play(rec, 0, 20_000, vis);
const spans = ended(rec, 20_000, vis);
const got = await claim({ player: 22, found, manifest: mpd, objects: creative(5), spans, shareBp: st.shareBp(frame) });
assert.equal(got.hex, vector("squeezeback").claims[1]);
});
test("a double box: the ad's box, and its share", async () => {
const frame = { kind: 4, width: 1920, height: 1080, corner: 1, windowW: 0, windowH: 0, gap: 16, aspect: 1 };
assert.deepEqual(st.adFrame(frame).ad, [968, 272, 952, 535]);
assert.equal(st.shareBp(frame), 2456);
const playlist = file("double_box.m3u8");
const [found] = st.hlsAds(playlist, "X-AD-ID");
const rec = new ap.SpanRecorder();
play(rec, 0, 10_000, 10_000);
const got = await claim({ player: 31, found, manifest: playlist, objects: creative(6), spans: ended(rec, 10_000, 10_000), shareBp: st.shareBp(frame) });
assert.equal(got.hex, vector("double_box").claims[0]);
});
test("the readers refuse what the subset and the attribute list leave out", () => {
assert.equal(st.readXml("<!DOCTYPE x><x/>"), null);
assert.equal(st.readXml('<x a="1" a="2"/>'), null);
assert.equal(st.readXml("<x>&nbsp;</x>"), null);
assert.deepEqual(st.readXml("<a><![CDATA[ b ]]></a>").children, [" b "]);
assert.equal(st.attributeList('ID="a",,X=1'), null);
assert.equal(st.attributeList("ID=a b"), null);
assert.equal(st.hlsAds("not a playlist", "X-AD-ID"), null);
});