All checks were successful
WAI operation catalogue / does this change touch it (push) Successful in 17s
wai-fleet service / does this change touch it (push) Successful in 16s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 25s
wai-app and Create / wai-app · does this change touch it (push) Successful in 23s
wai-ledger service / does this change touch it (push) Successful in 19s
wai-sink-swift native sink package / does this change touch it (push) Successful in 22s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 53s
WAI Studio / does this change touch it (push) Successful in 18s
wai-quantum for JavaScript / does this change touch it (push) Successful in 54s
wai-audit collector and player integration / does this change touch it (push) Successful in 57s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 1m7s
wai-key-proxy / does this change touch it (push) Successful in 57s
wai-quantum studio and desktop / does this change touch it (push) Successful in 55s
wai-sink-android native sink library / does this change touch it (push) Successful in 55s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m46s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 5m2s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 5m5s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 6m41s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 16s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 15s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 1m2s
wai-fleet service / service · linux (push) Successful in 3m16s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m14s
wai-ledger service / service · linux (push) Successful in 4m28s
wai-audit collector and player integration / collector · linux (push) Successful in 5m1s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 8m15s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m28s
wai-quantum studio and desktop / studio · browser (push) Successful in 5m18s
wai-key-proxy / proxy · linux (push) Successful in 3m1s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 10m1s
wai-quantum for JavaScript / javascript · linux (push) Successful in 7m14s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 12m23s
wai-fleet service / service · macos (push) Successful in 38s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 7m35s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 9m15s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 9m29s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 12m24s
WAI Studio / Studio in WebKit · linux (push) Successful in 20m25s
wai-ledger service / service · macos (push) Successful in 1m28s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 7m54s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 20m38s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 15m54s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m9s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 49s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m34s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m34s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 15m59s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 6m42s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 7m0s
browser-sink §14.9: where a device has no decoder of wai.graphics.vector, a composing page presents each graphics piece's floor (graphics-vector §6.3), the piece's transcript as wai.text.zstd, as text: the blocks active at the clock's tick, in block order, each with its language, direction and writing mode. A piece presents its overlay or its floor, never both. Before presenting, the page holds the floor object to its piece: its derived_from names the piece's base (floor_base), its payload is the raw frame and no other Zstandard frame (floor_frame), its target states the transcript's length and a timebase (floor_target), and its content is a transcript in graphics-vector §5.4's one encoding (floor_transcript). A caption or a subtitle block is never presented from the floor (caption_track): the text component of its language carries it, and the text layer presents it or not as the viewer set it. A forced_narrative block is presented from the floor unless the text layer shows a cue of its language with its string at that millisecond (forced_in_cue), so the words are on the page once. The text layer presents one text component of a package, or none. wai-rs browser_sink::floor is the reference (read, present), which wai-web's compose bundle calls as TextFloor. Corpus live-conformance/browser-floor/ (49 floors, 25 presentations), written by wai_browser_floor_vectors and regenerating byte for byte on x86_64 and aarch64; tests/browser_floor.rs also reads the floors wai-pack wrote for pack-conformance/multi/multi-captions-01, and wai-web's compose::floor_tests holds them to wai-vector's transcripts. browser_floor_verify.py, the second implementation, refuses 72 mutated floors each with its defect's reason, fails 23 mutated copies of the corpus, and each of its 32 checks, dropped, is caught. compose/run.mjs gains case 10. Not here: a base that is routed and then refused when loaded still refuses the package, and the overlay path does not yet leave out caption items (graphics-vector §5.7). Co-Authored-By: Claude <[email protected]>
228 lines
11 KiB
JavaScript
228 lines
11 KiB
JavaScript
// 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 <pkg>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);
|
||
}
|