open-standards/joule-code/joule-code-rs/crates/joule-moq-web/web/room/index.html
Transaction Science 7234c82352 joule-moq-web: the room page shows a host's screen and controls it under its grant (WAI remote-desktop)
web/room/remote.js: input statements, floor statements and latency records written byte for byte,
every statement read, the host's rules and the floor replayed over a room's records, the table
from keyboard codes to HID usages; and RemoteViewer, which draws the host's screen (the page's
WebCodecs decoder, AVC or HEVC in Annex B, every frame verified against its receipt first), shows
what the host captures and who holds the floor, batches pointer, keys, composed text and pasted
text into input statements under the grant's classes, asks for and gives back the floor, and signs
a latency record of each applied input's frame at the display refresh that showed it.
remote.test.mjs checks it against wai/live-conformance/remote/, the host agent's key table among
it. The page draws a room with no composition descriptor, and names a screen track by its
namespace.

e2e: the remote scenario runs wai-remote-host with a pattern source (the platform's hardware
encoder on macOS), two pages, the floor asked for and given, a click and a key applied with their
latency records, an input with no control power refused, and the session the host records
verified by remote_desktop_verify.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 02:47:39 -04:00

168 lines
9.7 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MoQ room client</title>
<link rel="icon" href="data:,">
<style>
:root { --bg: #f6f7f9; --fg: #15171a; --muted: #5d6470; --panel: #ffffff; --line: #d9dde3; --human: #1f6f43; --machine: #8a4b00; }
@media (prefers-color-scheme: dark) { :root { --bg: #111317; --fg: #e8eaee; --muted: #9aa2ae; --panel: #1a1d23; --line: #2c313a; --human: #6fd39b; --machine: #f0a957; } }
* { box-sizing: border-box; }
[hidden] { display: none !important; }
body { margin: 0; padding: 16px; background: var(--bg); color: var(--fg); font: 14px/1.45 system-ui, sans-serif; }
header { display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; margin-bottom: 12px; }
h1 { font-size: 16px; margin: 0; }
#who { color: var(--muted); font-family: ui-monospace, monospace; }
main { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 12px; }
@media (max-width: 760px) { main { grid-template-columns: minmax(0, 1fr); } }
section { background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 10px; min-width: 0; }
canvas { width: 100%; height: auto; display: block; background: #101218; border-radius: 4px; }
#caption { min-height: 2.6em; margin-top: 8px; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
#caption .origin { font-size: 12px; padding: 1px 6px; border-radius: 3px; border: 1px solid currentColor; }
#caption .origin-human { color: var(--human); }
#caption .origin-machine, #caption .origin-machine-reviewed { color: var(--machine); }
#timeline { list-style: none; margin: 0; padding: 0; max-height: 280px; overflow: auto; font-family: ui-monospace, monospace; font-size: 12px; }
#timeline li { padding: 2px 0; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
#log { white-space: pre-wrap; font-family: ui-monospace, monospace; font-size: 11px; color: var(--muted); max-height: 220px; overflow: auto; margin: 0; overflow-wrap: anywhere; }
h2 { font-size: 13px; margin: 0 0 6px; color: var(--muted); font-weight: 600; }
.controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
#status { color: var(--muted); }
/* Co-performance: the floor, what's next, the audience and fidelity. */
.co-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.co-grid h3 { font-size: 12px; margin: 0 0 4px; color: var(--muted); font-weight: 600; }
.co-grid ul, .co-grid ol { margin: 0; padding-left: 18px; font-size: 13px; }
.co-grid li { padding: 1px 0; overflow-wrap: anywhere; }
#co-now { margin: 0 0 6px; font-weight: 600; }
#co-marks, #co-left { color: var(--muted); font-size: 12px; margin: 4px 0 0; }
#co-intents li[data-outcome="upcoming"] { color: var(--machine); }
#co-intents li[data-outcome="kept"] { color: var(--human); }
#co-lead { margin-top: 8px; }
#co-lead input[type="number"] { width: 4.5em; }
#co-input { margin-top: 6px; }
#co-options { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; }
table.binding { border-collapse: collapse; font-family: ui-monospace, monospace; font-size: 12px; width: 100%; }
table.binding th, table.binding td { text-align: left; padding: 2px 8px 2px 0; border-bottom: 1px solid var(--line); white-space: nowrap; }
table.binding tr[data-state="verified"] td:nth-child(2) { color: var(--human); }
table.binding tr[data-state="refused"] td:nth-child(2), table.binding tr[data-state="unbound"] td:nth-child(2) { color: var(--machine); }
</style>
</head>
<body>
<header><h1>MoQ room client</h1><span id="who">loading</span></header>
<!-- An end-to-end encrypted room: its epoch and the members of its group. -->
<div id="e2ee" class="controls" hidden><strong>End-to-end encrypted</strong> <span id="e2ee-state"></span></div>
<!-- With a room service (?service=<url>): enter as a publisher or a viewer. -->
<div id="room-app" class="controls">
<button id="join" type="button">Join with camera and microphone</button>
<button id="watch" type="button">Watch</button>
<button id="leave" type="button" hidden>Leave</button>
<span id="status" role="status" aria-live="polite"></span>
<form id="caption-form" hidden><label for="caption-text">Caption</label> <input id="caption-text" maxlength="200" autocomplete="off"> <button type="submit">Send</button></form>
</div>
<main>
<section>
<h2>The room, composed</h2>
<canvas id="canvas" width="640" height="360"></canvas>
<div id="caption" aria-live="polite"></div>
<div id="dvr" class="controls" style="margin: 8px 0 0">
<label for="seek-s">Seconds back</label>
<input id="seek-s" type="number" min="1" max="900" value="10" style="width: 5em">
<button id="seek" type="button" disabled>Seek back</button>
<button id="live" type="button" hidden>Back to live</button>
<span id="dvr-status" role="status" aria-live="polite"></span>
</div>
<canvas id="dvr-canvas" width="640" height="360" hidden style="margin-top: 8px" aria-label="The participant's earlier video, from the relay's window"></canvas>
</section>
<!-- A watch room (WAI co-watch): the source the room watches, its shared controls and how
closely the room keeps together. -->
<section id="cowatch" hidden style="grid-column: 1 / -1">
<h2>Watching together</h2>
<video id="cowatch-media" playsinline style="width: 100%; max-height: 360px; background: #101218; border-radius: 4px"></video>
<div class="controls" style="margin: 8px 0 0">
<button id="cw-play" type="button">Play</button>
<button id="cw-pause" type="button">Pause</button>
<button id="cw-back" type="button">Back 10 s</button>
<button id="cw-fwd" type="button">Ahead 10 s</button>
<label for="cw-rate">Speed</label>
<select id="cw-rate"><option value="750000">0.75×</option><option value="1000000" selected>1×</option><option value="1250000">1.25×</option><option value="1500000">1.5×</option></select>
<span id="cw-sync" role="status" aria-live="polite"></span>
</div>
</section>
<!-- A remote desktop room (WAI remote-desktop): the host's screen, what it captures and who
holds the floor. -->
<section id="remote" hidden style="grid-column: 1 / -1" aria-label="Remote desktop">
<h2>Remote desktop</h2>
<canvas id="remote-canvas" width="1280" height="720" aria-label="The host's screen"></canvas>
<div class="controls" style="margin: 8px 0 0">
<button id="remote-request" type="button" hidden>Ask for control</button>
<button id="remote-release" type="button" hidden>Give control back</button>
<span id="remote-state" role="status" aria-live="polite"></span>
</div>
</section>
<section>
<h2>Events</h2>
<ul id="timeline"></ul>
</section>
<!-- A room with co-performance (WAI co-performance): see co.js. -->
<section id="co" hidden style="grid-column: 1 / -1" aria-label="Co-performance">
<h2>Co-performance</h2>
<div class="co-grid">
<div>
<h3>The floor</h3>
<p id="co-now" role="status" aria-live="polite"></p>
<ol id="co-turns"></ol>
<p id="co-marks"></p>
<div class="controls" style="margin: 6px 0 0"><button id="co-pass" type="button" hidden>Pass the floor</button></div>
</div>
<div>
<h3>What's next</h3>
<ul id="co-intents" aria-live="polite"></ul>
</div>
<div>
<h3>The audience</h3>
<p id="co-question"></p>
<form id="co-input" hidden>
<div id="co-options"></div>
<div id="co-prompt-row" hidden><label for="co-prompt">Your prompt</label> <input id="co-prompt" maxlength="200" autocomplete="off"> <button type="submit">Send</button></div>
<p id="co-left"></p>
</form>
<ul id="co-selections"></ul>
</div>
<div>
<h3>Fidelity</h3>
<ul id="co-fidelity"></ul>
</div>
</div>
<div id="co-lead" class="controls" hidden>
<label for="co-turn-s">Turn (s)</label> <input id="co-turn-s" type="number" min="1" max="60" value="4">
<label for="co-turns-n">Turns</label> <input id="co-turns-n" type="number" min="1" max="64" value="4">
<label for="co-mode">Out of turn</label> <select id="co-mode"><option value="mark">marked</option><option value="refuse">refused</option></select>
<button id="co-start" type="button">Start turns</button>
<label for="co-q">Ask</label> <input id="co-q" maxlength="200" placeholder="Which way next?">
<label for="co-options-n">Options</label> <input id="co-options-n" type="number" min="2" max="32" value="3">
<label for="co-window-s">Open for (s)</label> <input id="co-window-s" type="number" min="2" max="300" value="8">
<button id="co-vote" type="button">Open a vote</button>
<button id="co-prompts" type="button">Ask for prompts</button>
<button id="co-fid" type="button">State fidelity</button>
</div>
</section>
<!-- With ?debug=binding: the state of the frame each tile shows (WAI browser-sink §10). -->
<section id="binding-debug" style="grid-column: 1 / -1" hidden>
<h2>Frames on screen</h2>
<div style="overflow-x: auto"><table class="binding">
<thead><tr><th>Tile</th><th>State</th><th>Tier</th><th>Digest</th><th>Receipt</th><th>Chain</th><th>Provenance</th><th>Route</th><th>Power efficient</th><th>Energy</th></tr></thead>
<tbody></tbody>
</table></div>
</section>
<section style="grid-column: 1 / -1">
<h2>Log</h2>
<pre id="log"></pre>
</section>
</main>
<script type="module">
// The controls are for a room service alone; a fixture's page has no use for them.
if (!new URLSearchParams(location.search).get("service")) document.getElementById("room-app").hidden = true;
</script>
<script type="module" src="./app.js"></script>
</body>
</html>