mirror of
https://github.com/YusufB5/ASCILINE.git
synced 2026-06-17 22:35:13 +02:00
fix: top-3 review findings (decoder ordering, public-by-default, per-session state)
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>
This commit is contained in:
parent
444334cfba
commit
7fa761212e
3 changed files with 266 additions and 38 deletions
31
app.js
31
app.js
|
|
@ -21,6 +21,7 @@ let ws = null;
|
|||
const frameBuffer = [];
|
||||
const BUFFER_SIZE = 4;
|
||||
let codecDecoder = null; // Adaptive codec decoder (codec.js)
|
||||
let decodeChain = Promise.resolve(); // serializes stateful codec decodes in arrival order
|
||||
let targetFps = 24;
|
||||
let frameInterval = 1000 / targetFps;
|
||||
let renderMode = 1;
|
||||
|
|
@ -165,6 +166,9 @@ function connectWebSocket() {
|
|||
frameInterval = 1000 / targetFps;
|
||||
renderMode = parseInt(p[2]);
|
||||
pixelMode = (p.length > 5 && parseInt(p[5]) === 1);
|
||||
// 7th field (added server-side) = current queue index, so /audio
|
||||
// serves THIS client's video even when other clients are connected.
|
||||
const videoIndex = (p.length > 6 && !Number.isNaN(parseInt(p[6]))) ? parseInt(p[6]) : 0;
|
||||
buildCanvas(parseInt(p[3]), parseInt(p[4]));
|
||||
|
||||
// Initialize adaptive codec decoder (pixel=3 bytes, ASCII color=4 bytes)
|
||||
|
|
@ -174,6 +178,13 @@ function connectWebSocket() {
|
|||
codecDecoder = null;
|
||||
}
|
||||
|
||||
// New segment (single video, or the next entry in a playlist):
|
||||
// drop frames still buffered from the previous segment. Their
|
||||
// timestamps belong to the old master clock (which resets when the
|
||||
// audio reloads below), so keeping them stalls A/V sync — and on a
|
||||
// mode change they'd be decoded under the wrong renderer.
|
||||
frameBuffer.length = 0;
|
||||
|
||||
// ── AUDIO READY GATE ──
|
||||
// Buffer video frames but don't render until audio is ready.
|
||||
// This prevents the 0.5s initial stutter.
|
||||
|
|
@ -190,7 +201,7 @@ function connectWebSocket() {
|
|||
|
||||
if (audioEl) {
|
||||
audioEl.pause();
|
||||
audioEl.src = '/audio?' + Date.now();
|
||||
audioEl.src = '/audio?v=' + videoIndex + '&t=' + Date.now();
|
||||
audioEl.volume = volumeSlider ? volumeSlider.value : 1.0;
|
||||
audioEl.load();
|
||||
audioEl.play().catch(() => {});
|
||||
|
|
@ -222,10 +233,20 @@ function connectWebSocket() {
|
|||
} else {
|
||||
// Binary Frames — decoded via adaptive codec (raw/zlib/delta)
|
||||
if (codecDecoder) {
|
||||
codecDecoder.decode(event.data).then(({ frameIndex, frame }) => {
|
||||
const frameTime = frameIndex / targetFps;
|
||||
frameBuffer.push({ data: frame, time: frameTime });
|
||||
});
|
||||
// The codec is STATEFUL: a DELTA frame patches the previously
|
||||
// decoded frame, so decodes MUST run in arrival order. decode()
|
||||
// awaits a real async DecompressionStream, so firing them
|
||||
// concurrently lets a small DELTA resolve before an earlier
|
||||
// keyframe and patch a stale frame. Serialize through a chain.
|
||||
const data = event.data;
|
||||
const dec = codecDecoder;
|
||||
decodeChain = decodeChain.then(async () => {
|
||||
if (dec !== codecDecoder) return; // stream re-INIT'd → drop stale frame
|
||||
const { frameIndex, frame } = await dec.decode(data);
|
||||
frameBuffer.push({ data: frame, time: frameIndex / targetFps });
|
||||
// Cap here (not only in the sync path) since this push is async.
|
||||
while (frameBuffer.length > BUFFER_SIZE * 5) frameBuffer.shift();
|
||||
}).catch((err) => { console.error('ASCILINE decode error:', err); });
|
||||
} else {
|
||||
// Fallback: legacy 4-byte header
|
||||
const buffer = event.data;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue