Train B's merge of #424 (co-performance in the room) with #413 (the browser
sink, 173981739) left `<section id="co">` without its `</section>`: the page
opened five sections and closed four. The parser then nested the binding table
(`#binding-debug`) and the Log inside the hidden `#co`, so in a room without
co-performance neither ever showed.
- index.html: the `</section>` after the `#co-lead` controls.
- web/room/markup.test.mjs (run by e2e/run.sh): every element the page opens is
closed in order; the Log, the binding table and the co-watch section are not
inside `#co`; each mode's controls are inside its own section.
- e2e: in the service scenario (a room without `[co]`), `#log`, `#binding-debug`
and `#cowatch` are not descendants of `#co`, `#co` is hidden and the Log shows;
in the cowatch scenario, the co-watch section and the Log show outside `#co`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
157 lines
9.1 KiB
HTML
157 lines
9.1 KiB
HTML
<!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>
|
||
<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>
|