Frontend (app.js):
- Serialize the stateful adaptive-codec decoder through a promise chain.
decode() awaits a real async DecompressionStream, so the previous
concurrent .then() let a small DELTA resolve before its keyframe and
patch a stale/null prev -> corrupt frames. Adds .catch + stale-decoder
guard so a re-INIT drops in-flight frames from the previous segment.
- Flush frameBuffer on INIT so playlist transitions don't stall the reset
master clock on the previous video's tail frames (or render them under
the new renderer on a mode change).
- Request /audio?v=<idx> using the new INIT queue-index field so audio is
correct when multiple clients are connected.
Server (stream_server.py):
- Bind 127.0.0.1 by default (--host to opt into LAN); same-origin Origin
check before streaming (CSWSH defense that still allows LAN same-origin).
- Scope /static to an app.js/style.css/codec.js whitelist (was serving the
whole repo: source, playlist, any local .env/notes).
- Per-session audio: INIT carries the queue index; /audio?v= reads it
(bounds-checked) instead of the shared global current_index.
- Validate/clamp playlist+CLI mode/vol/pixel/cols/rows; guard malformed
playlist JSON. ffmpeg gets -nostdin + terminate/kill-with-timeout.
- Re-enable WS keepalive (reap dead clients); release VideoCapture on the
isOpened()-false path.
Adds experiments/test_decode_order.js: dependency-free regression proving
serialized decode is bit-exact + in-order and that delta-before-keyframe
throws (no video fixtures needed).
Server fixes built by Codex from a Claude spec; Claude integrated + reviewed
(tightened the Origin check to same-origin so --host 0.0.0.0 LAN mode works).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The binary protocol re-sent the full grid every frame. This adds an opt-in
per-frame codec that picks the smallest of three encodings and tags it in a
1-byte header, without changing the rendered output:
0 RAW framebuffer as-is (legacy)
1 ZLIB zlib(framebuffer)
2 DELTA only the cells changed since the previous frame, patched on top
Clients opt in via /ws?codec=adaptive; omitting it yields the original protocol
byte-for-byte, so existing clients are unaffected. A keyframe is forced
periodically for resync. codec.js is shared by the browser and the Node test,
so the shipped decode path is the tested one.
Optional --quality {lossless,high,balanced,low} enables lossy temporal delta
(conditional replenishment): a colour cell is only re-sent once it drifts past a
tolerance from what the viewer already sees; the character plane stays exact.
Default lossless = bit-exact.
Measured wire savings (mode 5, 200x80): static screen 0.3% of legacy (~375x),
pixel mode 11.6%, high-motion 63% (never worse). Encoder tuned (zlib level 3,
smart candidate selection) to stay well under the frame budget.
Verified bit-exact two independent ways: Python->Node vectors and a live
adaptive-vs-legacy WebSocket diff. (A fuller mutation + Autobahn conformance
harness exists on request.)