// The reference page of the composing sink: opens a composed package with compose.js, // shows what this device dispatched and why, lists every check made before presenting, // and plays the components on one timeline. // // Query parameters: // pkg the package's directory, default "package/"; // receipt the receipt's URL, default package.receipt.json; // without capabilities to force the device profile without, comma separated; // captions the text component the text layer presents: a language tag, or "off" for none; // default the first the receipt lists (browser-sink §14.9); // bundle the wai_compose module, default "./pkg/wai_compose.js"; // set the outline set the page holds for wai.graphics.vector, default the default // stroke set of wai/vector-conformance; // gs the parameter set the page holds for wai.splat.int_codec (`g_s.whm`), default the // pinned synthesis of wai-rs/tests/vectors/int_splat. // // window.__wai_compose is the hook a test reads: {state, refusal, dispatch, checks, stats, // audio, floor, staged, play(), overlayDigests(), pictureSha256(), presentAt(t), shownAt(t)}. // `floor` is null unless a graphics piece presents its text floor; otherwise its pieces, each // with its transcript. `shownAt(t)` presents at `t` and returns what is then on the page: the // overlay's frame or null, the floor's result (§14.9) or null, the text and attributes of each // block the floor's region holds, and the text of each cue in the text layer. `staged` is null for a // package with no layered piece; otherwise {state: "refining" | "done", stages}, every stage // presented with the SHA-256 the page computed and the SHA-256 of the picture read back from the // canvas once it was drawn. import { openPackage, Refused } from "./compose.js"; const q = new URLSearchParams(location.search); const base = new URL(q.get("pkg") || "package/", location.href).href; const without = (q.get("without") || "").split(",").filter(Boolean); const bundle = q.get("bundle") || "./pkg/wai_compose.js"; const sets = [q.get("set") || "../../vector-conformance/sets/stroke-ascii.wos1"]; const gs = q.get("gs") || "../../wai-rs/tests/vectors/int_splat/splat_gs.bin"; const $ = (id) => document.getElementById(id); const hook = { state: "opening", refusal: null, dispatch: null, checks: [], stats: null, staged: null }; window.__wai_compose = hook; const status = (text, bad) => { $("status").textContent = text; $("status").className = bad ? "bad" : ""; }; function cell(tr, text, cls) { const td = document.createElement("td"); td.textContent = text; if (cls) td.className = cls; tr.appendChild(td); return td; } function showChecks(checks) { const body = $("checks").tBodies[0]; body.replaceChildren(); for (const c of checks) { const tr = document.createElement("tr"); cell(tr, c.ok ? "✓" : "✗", c.ok ? "ok" : "bad").setAttribute("aria-label", c.ok ? "holds" : "refused"); cell(tr, c.what); const extra = Object.entries(c).filter(([k, v]) => !["what", "ok"].includes(k) && v !== null && v !== undefined); cell(tr, extra.map(([k, v]) => `${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`).join(" · "), "meta"); body.appendChild(tr); } } function showDispatch(d) { const body = $("dispatch").tBodies[0]; body.replaceChildren(); for (const c of d.components) { c.pieces.forEach((p, i) => { const tr = document.createElement("tr"); cell(tr, i === 0 ? `${c.id} (${c.layer || "not presented"})` : ""); cell(tr, String(p.piece)); cell(tr, p.capability ? `${p.capability} (${p.role || "record"})` : "nothing", p.capability ? "" : "bad"); cell(tr, p.route); cell(tr, p.why, "meta"); body.appendChild(tr); // Each layer rides on the base it refines: its outcome, and the side planes it read. for (const L of p.layers || []) { const lr = document.createElement("tr"); cell(lr, ""); cell(lr, ""); cell(lr, `layer ${L.layer}: ${L.capability}`, L.outcome === "refused" ? "bad" : ""); cell(lr, L.route); const sides = L.sides.map((x) => `${x.entry || x.object.slice(0, 12)}${x.render ? " rendered here" : ""}: ${x.held ? (x.exact ? "held, exact" : "held, not exact") : "not held" + (x.why ? ` (${x.why})` : "")}`); cell(lr, `${L.outcome}${L.stage !== null ? `, stage ${L.stage}` : ""}${L.reason ? ` (${L.reason}${L.detail ? ": " + L.detail : ""})` : ""}${sides.length ? `; side planes: ${sides.join("; ")}` : ""}`, "meta"); body.appendChild(lr); } }); } const dev = d.device; $("device").textContent = `Device: ${dev.user_agent}. WebAssembly decoders: ${dev.wasm.join(", ")}. ` + `User agent answers: ${Object.entries(dev.ua).map(([k, v]) => `${k} supported=${v.supported} smooth=${v.smooth} powerEfficient=${v.power_efficient}`).join("; ") || "none asked"}.` + (dev.forced_without.length ? ` Forced profile: without ${dev.forced_without.join(", ")}.` : ""); } function showState(tick, records, hash) { const box = $("state"); const t = document.createElement("table"); for (const r of records) { const tr = document.createElement("tr"); for (const c of r.cells) cell(tr, c.t === "fixed" ? c.v.toFixed(2) : String(c.v)); t.appendChild(tr); } const m = document.createElement("div"); m.className = "meta"; m.textContent = `tick ${tick} · state ${hash.slice(0, 16)}…`; box.replaceChildren(t, m); } $("profile").value = without.join(","); if ($("profile").value !== without.join(",")) { const o = document.createElement("option"); o.value = o.textContent = without.join(","); $("profile").appendChild(o); $("profile").value = o.value; } $("profile").addEventListener("change", () => { const u = new URL(location.href); if ($("profile").value) u.searchParams.set("without", $("profile").value); else u.searchParams.delete("without"); location.href = u.href; }); try { const mod = await import(bundle); await mod.default(); const dpr = globalThis.devicePixelRatio || 1; const r = $("overlay").getBoundingClientRect(); const comp = await openPackage(base, { wai: mod, receipt: q.get("receipt") ? new URL(q.get("receipt"), location.href).href : undefined, sets: sets.map((s) => new URL(s, location.href).href), paramSets: { "g_s.whm": new URL(gs, location.href).href }, without, captions: q.get("captions") || undefined, overlayPx: [Math.round(r.width * dpr), Math.round(r.height * dpr)], }); hook.dispatch = comp.dispatch; hook.checks = comp.checks; hook.stats = comp.stats; const au = comp.layers.audio; hook.audio = au ? { sample_rate: au.sampleRate, channels: au.channels, samples: au.frames, pcm_sha256: au.pcmSha256, pieces: au.pieces.map((p) => ({ start: p.start, samples: p.samples.length / au.channels, pcm_sha256: p.pcmSha256 })) } : null; hook.overlayDigests = () => comp.overlayDigests(mod); // The picture as drawn: the video canvas read back, its RGB8 hashed in the page. const pictureSha256 = () => { const c = $("video"); const px = c.getContext("2d").getImageData(0, 0, c.width, c.height).data; const rgb = new Uint8Array((px.length / 4) * 3); for (let i = 0, j = 0; i < px.length; i += 4, j += 3) { rgb[j] = px[i]; rgb[j + 1] = px[i + 1]; rgb[j + 2] = px[i + 2]; } return mod.sha256Hex(rgb); }; const view = { video: $("video"), overlay: $("overlay"), floor: $("floor"), floorAnnounce: $("floor-announce"), text: $("text"), state: showState, readback: pictureSha256 }; const fl = comp.layers.floor; hook.floor = fl ? { component: fl.component, pieces: fl.pieces.map((p) => ({ piece: p.piece, start: p.start, duration: p.duration, tb: p.tb, transcript: p.transcript })) } : null; hook.text = comp.dispatch.text; hook.shownAt = (t) => { comp.present(t, view); return { overlay: comp.now.overlay, floor: comp.now.floor, blocks: [...$("floor").children].map((el) => ({ text: el.textContent, lang: el.lang, dir: el.dir, index: Number(el.dataset.index), markup: el.children.length })), announced: [...$("floor-announce").children].map((el) => el.textContent), cues: comp.now.cues, }; }; hook.pictureSha256 = pictureSha256; hook.presentAt = (t) => { comp._n = undefined; comp.present(t, view); return pictureSha256(); }; hook.play = async () => { hook.state = "playing"; $("play").disabled = true; await comp.play(view); hook.state = "ended"; $("play").disabled = false; }; showDispatch(comp.dispatch); showChecks(comp.checks); comp.present(0, view); const floors = comp.dispatch.components.flatMap((c) => c.pieces.filter((p) => p.role === "floor").map((p) => `${c.id}/${p.piece}`)); const missing = comp.dispatch.components.filter((c) => !c.presented).map((c) => c.id); const retried = comp.dispatch.components.flatMap((c) => c.pieces.filter((p) => p.tried).map((p) => `${c.id}/${p.piece}`)); const opened = `Every check holds (${comp.checks.length}). ` + (floors.length ? `The floor plays for ${floors.join(", ")}. ` : "") + (fl ? `The titles of ${fl.component} are presented as text from their floor. ` : "") + (missing.length ? `Not presented on this device: ${missing.join(", ")}. ` : "") + (retried.length ? `The user agent's decoder did not decode ${retried.join(", ")}; the next alternative plays. ` : ""); status(opened + (comp.staged.length ? "Each layered piece presents its base; its layers follow as they arrive." : "Ready.")); const tick = () => { $("clock").textContent = `clock ${comp.stats.clock_s.toFixed(2)} s of ${comp.duration.toFixed(2)} s · video frames ${comp.stats.video_frames} · overlay frames ${comp.stats.overlay_frames} · state updates ${comp.stats.state_updates} · audio ${comp.stats.audio_started ? "playing, re-placed " + comp.stats.audio_resyncs + "×" : "not started"}`; requestAnimationFrame(tick); }; tick(); $("play").disabled = false; $("play").addEventListener("click", () => hook.play()); hook.state = "ready"; if (comp.staged.length) { // The layers, as they arrive: each stage presented once it holds its signed digest. hook.staged = { state: "refining", stages: comp.stages }; comp.present(0, view); if (comp.stages.length) comp.stages[0].presented_sha256 = pictureSha256(); await comp.refine(view, () => { showDispatch(comp.dispatch); showChecks(comp.checks); }); showDispatch(comp.dispatch); showChecks(comp.checks); comp.present(0, view); const ls = comp.dispatch.components.flatMap((c) => c.pieces.flatMap((p) => (p.layers || []).map((L) => ({ ...L, at: `${c.id}/${p.piece}` })))); const refused = ls.filter((L) => L.outcome !== "applied"); const top = comp.staged.map((it) => `${it.c.id}/${it.pi} at stage ${it.vp.stage}`); status(opened + `Presented: ${top.join(", ")}. ` + refused.map((L) => `The layer ${L.name} was not applied (${L.reason}); ${L.at} presents the stage below. `).join("") + "Ready.", refused.length > 0); hook.staged.state = "done"; } } catch (e) { const r = e instanceof Refused ? { stage: e.stage, code: e.code, detail: e.detail, where: e.where } : { stage: "page", code: "error", detail: String(e && e.stack || e), where: null }; hook.refusal = r; hook.checks = (e && e.checks) || []; hook.state = "refused"; status(`Refused, nothing presented: ${r.stage} ${r.code}${r.where ? " at " + r.where : ""}${r.detail ? " (" + r.detail + ")" : ""}`, true); if (e && e.checks) showChecks(e.checks); }