mirror of
https://github.com/YusufB5/ASCILINE.git
synced 2026-06-14 22:25:13 +02:00
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.)
74 lines
2.9 KiB
JavaScript
74 lines
2.9 KiB
JavaScript
/**
|
|
* codec.js — Adaptive frame decoder for ASCILINE.
|
|
*
|
|
* Mirrors codec.py. Runs in the browser (attaches window.AscilineCodec) and in
|
|
* Node (module.exports) so the end-to-end test exercises the exact shipped path.
|
|
*
|
|
* Wire format per binary frame:
|
|
* [4B frame_index big-endian][1B tag][payload]
|
|
* tag 0 RAW : payload is the framebuffer bytes
|
|
* tag 1 ZLIB : payload is zlib(framebuffer bytes) -> 'deflate'
|
|
* tag 2 DELTA : payload is zlib(indices[uint32 LE] ++ changed values)
|
|
*
|
|
* Decoding MUST stay in arrival order (deltas patch the previous frame), so
|
|
* callers feed messages through a sequential queue (see makeDecoder).
|
|
*/
|
|
(function (root, factory) {
|
|
const api = factory();
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
else root.AscilineCodec = api;
|
|
})(typeof self !== 'undefined' ? self : this, function () {
|
|
const TAG_RAW = 0, TAG_ZLIB = 1, TAG_DELTA = 2;
|
|
|
|
async function inflate(bytes) {
|
|
// Python zlib.compress -> RFC1950 zlib wrapper -> 'deflate' here.
|
|
const ds = new DecompressionStream('deflate');
|
|
const stream = new Blob([bytes]).stream().pipeThrough(ds);
|
|
const buf = await new Response(stream).arrayBuffer();
|
|
return new Uint8Array(buf);
|
|
}
|
|
|
|
/**
|
|
* Create a stateful decoder. `cellBytes` = channels per cell (4 ASCII color,
|
|
* 3 pixel). Returns { decode(message) -> {frameIndex, frame}, reset() }.
|
|
* `frame` is a Uint8Array of the full framebuffer for that frame.
|
|
*/
|
|
function makeDecoder(cellBytes) {
|
|
let prev = null; // Uint8Array of last full frame
|
|
|
|
async function decode(message) {
|
|
const bytes = new Uint8Array(message);
|
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
const frameIndex = view.getUint32(0, false); // big-endian
|
|
const tag = bytes[4];
|
|
const payload = bytes.subarray(5);
|
|
|
|
let frame;
|
|
if (tag === TAG_RAW) {
|
|
frame = payload.slice(); // own copy; becomes next prev
|
|
} else if (tag === TAG_ZLIB) {
|
|
frame = await inflate(payload);
|
|
} else if (tag === TAG_DELTA) {
|
|
const body = await inflate(payload);
|
|
const k = body.length / (4 + cellBytes);
|
|
const idx = new DataView(body.buffer, body.byteOffset, body.byteLength);
|
|
frame = prev.slice(); // patch onto a copy of previous frame
|
|
const valuesOffset = k * 4;
|
|
for (let j = 0; j < k; j++) {
|
|
const cell = idx.getUint32(j * 4, true); // little-endian indices
|
|
const dst = cell * cellBytes;
|
|
const src = valuesOffset + j * cellBytes;
|
|
for (let c = 0; c < cellBytes; c++) frame[dst + c] = body[src + c];
|
|
}
|
|
} else {
|
|
throw new Error('Unknown ASCILINE codec tag: ' + tag);
|
|
}
|
|
prev = frame;
|
|
return { frameIndex, frame };
|
|
}
|
|
|
|
return { decode, reset() { prev = null; } };
|
|
}
|
|
|
|
return { makeDecoder, inflate, TAG_RAW, TAG_ZLIB, TAG_DELTA };
|
|
});
|