diff --git a/examples/stream-receiver.html b/examples/stream-receiver.html
index 83d398f..12aaa19 100644
--- a/examples/stream-receiver.html
+++ b/examples/stream-receiver.html
@@ -361,9 +361,12 @@
const HEADER_SIZE = 16;
const FRAME_PIXELS = 0x01, FRAME_DELTA = 0x03, FRAME_AUDIO = 0x10;
const FRAME_SIGNAL_SYNC = 0x30, FRAME_SIGNAL_DIFF = 0x31, FRAME_NEURAL = 0x40;
+ const FRAME_PING = 0xFE;
const INPUT_POINTER = 0x01, INPUT_PTR_DOWN = 0x02, INPUT_PTR_UP = 0x03;
- const INPUT_KEY_DOWN = 0x10, INPUT_KEY_UP = 0x11, INPUT_SCROLL = 0x50;
- const FLAG_INPUT = 0x01;
+ const INPUT_KEY_DOWN = 0x10, INPUT_KEY_UP = 0x11;
+ const INPUT_TOUCH = 0x20, INPUT_TOUCH_END = 0x21;
+ const INPUT_SCROLL = 0x50;
+ const FLAG_INPUT = 0x01, FLAG_KEYFRAME = 0x02, FLAG_COMPRESSED = 0x04;
const canvas = document.getElementById('display');
const ctx = canvas.getContext('2d');
@@ -434,14 +437,15 @@
}
// ── Feature 1: Delta decompression ──
+ // RLE decompression matching Rust codec: 0x00 + 2-byte LE count
function rleDecompress(compressed, expectedLen) {
const out = new Uint8Array(expectedLen);
let ci = 0, oi = 0;
while (ci < compressed.length && oi < expectedLen) {
- if (compressed[ci] === 0 && ci + 1 < compressed.length) {
- const run = compressed[ci + 1];
+ if (compressed[ci] === 0 && ci + 2 < compressed.length) {
+ const run = compressed[ci + 1] | (compressed[ci + 2] << 8);
for (let j = 0; j < run && oi < expectedLen; j++) out[oi++] = 0;
- ci += 2;
+ ci += 3;
} else {
out[oi++] = compressed[ci++];
}
@@ -561,6 +565,10 @@
busBytes.audio += e.data.byteLength;
break;
}
+
+ case FRAME_PING:
+ // Keepalive — ignore
+ return;
}
framesRecv++;
@@ -628,6 +636,37 @@
busBytes.scroll += HEADER_SIZE + 4;
}, { passive: false });
+ // Touch — for mobile receivers
+ function touchPayload(id, x, y, phase) {
+ const b = new ArrayBuffer(6), v = new DataView(b);
+ v.setUint8(0, id); v.setUint16(1, x, true); v.setUint16(3, y, true); v.setUint8(5, phase);
+ return b;
+ }
+ canvas.addEventListener('touchstart', e => {
+ e.preventDefault();
+ const r = canvas.getBoundingClientRect();
+ for (const t of e.changedTouches) {
+ const x = Math.round(t.clientX - r.left), y = Math.round(t.clientY - r.top);
+ sendInput(encodeInput(INPUT_TOUCH, touchPayload(t.identifier & 0xFF, x, y, 0)));
+ busBytes.pointer += HEADER_SIZE + 6;
+ }
+ }, { passive: false });
+ canvas.addEventListener('touchmove', e => {
+ e.preventDefault();
+ const r = canvas.getBoundingClientRect();
+ for (const t of e.changedTouches) {
+ const x = Math.round(t.clientX - r.left), y = Math.round(t.clientY - r.top);
+ sendInput(encodeInput(INPUT_TOUCH, touchPayload(t.identifier & 0xFF, x, y, 0)));
+ busBytes.pointer += HEADER_SIZE + 6;
+ }
+ }, { passive: false });
+ canvas.addEventListener('touchend', e => {
+ for (const t of e.changedTouches) {
+ sendInput(encodeInput(INPUT_TOUCH_END, touchPayload(t.identifier & 0xFF, 0, 0, 1)));
+ busBytes.pointer += HEADER_SIZE + 6;
+ }
+ });
+
// ── Stats ──
setInterval(() => {
const now = performance.now(), elapsed = (now - lastSecTime) / 1000;
diff --git a/examples/stream-source.html b/examples/stream-source.html
index f592c87..24c8e47 100644
--- a/examples/stream-source.html
+++ b/examples/stream-source.html
@@ -371,9 +371,13 @@
const HEADER_SIZE = 16;
const FRAME_PIXELS = 0x01, FRAME_DELTA = 0x03, FRAME_AUDIO = 0x10;
const FRAME_SIGNAL_SYNC = 0x30, FRAME_SIGNAL_DIFF = 0x31, FRAME_NEURAL = 0x40;
+ const FRAME_PING = 0xFE, FRAME_END = 0xFF;
const INPUT_POINTER = 0x01, INPUT_PTR_DOWN = 0x02, INPUT_PTR_UP = 0x03;
- const INPUT_KEY_DOWN = 0x10, INPUT_KEY_UP = 0x11, INPUT_SCROLL = 0x50;
- const FLAG_INPUT = 0x01, FLAG_KEYFRAME = 0x02;
+ const INPUT_KEY_DOWN = 0x10, INPUT_KEY_UP = 0x11;
+ const INPUT_TOUCH = 0x20, INPUT_TOUCH_END = 0x21;
+ const INPUT_GAMEPAD_AXIS = 0x30, INPUT_GAMEPAD_BUTTON = 0x31;
+ const INPUT_SCROLL = 0x50, INPUT_RESIZE = 0x60;
+ const FLAG_INPUT = 0x01, FLAG_KEYFRAME = 0x02, FLAG_COMPRESSED = 0x04;
function encodeHeader(type, flags, seq, ts, w, h, len) {
const b = new ArrayBuffer(HEADER_SIZE), v = new DataView(b);
@@ -624,6 +628,29 @@
ballR.value = Math.max(10, Math.min(80, ballR._signal._value + dy * 0.1));
}
break;
+ case INPUT_TOUCH:
+ if (payload.length >= 6) {
+ const tx = view.getUint16(1, true), ty = view.getUint16(3, true);
+ const phase = payload[5];
+ if (phase === 0) handlePointer(tx, ty, 'down');
+ else handlePointer(tx, ty, 'move');
+ }
+ break;
+ case INPUT_TOUCH_END:
+ handlePointer(0, 0, 'up');
+ break;
+ case INPUT_GAMEPAD_AXIS:
+ if (payload.length >= 3) {
+ const axis = payload[0], val = view.getInt16(1, true) / 32768;
+ if (axis === 0) { targetX.value = 300 + val * 250; ballX.value = targetX._value; }
+ if (axis === 1) { targetY.value = 200 + val * 150; ballY.value = targetY._value; }
+ }
+ break;
+ case INPUT_GAMEPAD_BUTTON:
+ if (payload.length >= 3 && payload[1] === 1) {
+ ballR.value = 70; setTimeout(() => ballR.value = 25, 250);
+ }
+ break;
}
}
@@ -690,7 +717,7 @@
// Delta is worthwhile — compress by RLE-encoding the zero runs
const compressed = rleCompress(delta);
prevFrame = new Uint8Array(pixels);
- const header = encodeHeader(FRAME_DELTA, 0, seq & 0xFFFF, ts, W, H, compressed.length);
+ const header = encodeHeader(FRAME_DELTA, FLAG_COMPRESSED, seq & 0xFFFF, ts, W, H, compressed.length);
const msg = new Uint8Array(HEADER_SIZE + compressed.length);
msg.set(header, 0); msg.set(compressed, HEADER_SIZE);
return msg;
@@ -705,15 +732,20 @@
return msg;
}
- // Simple RLE for delta frames: encode runs of zeros compactly
+ // RLE compression matching Rust codec: 0x00 + 2-byte LE count for zero runs
function rleCompress(data) {
const out = [];
let i = 0;
while (i < data.length) {
if (data[i] === 0) {
let run = 0;
- while (i < data.length && data[i] === 0 && run < 255) { run++; i++; }
- out.push(0, run); // 0x00 followed by run length
+ while (i < data.length && data[i] === 0) { run++; i++; }
+ // Emit run in chunks of max 65535
+ while (run > 0) {
+ const chunk = Math.min(run, 65535);
+ out.push(0, chunk & 0xFF, (chunk >> 8) & 0xFF);
+ run -= chunk;
+ }
} else {
out.push(data[i]); i++;
}
@@ -737,11 +769,13 @@
const json = JSON.stringify(state);
const payload = new TextEncoder().encode(json);
- // Send full sync or diff
- const frameType = prevSignalState ? FRAME_SIGNAL_DIFF : FRAME_SIGNAL_SYNC;
+ // Send full sync (with FLAG_KEYFRAME) or diff
+ const isSync = !prevSignalState || (framesSent % 150 === 0); // sync every ~5s at 30fps
+ const frameType = isSync ? FRAME_SIGNAL_SYNC : FRAME_SIGNAL_DIFF;
+ const flags = isSync ? FLAG_KEYFRAME : 0;
prevSignalState = state;
- const header = encodeHeader(frameType, 0, seq & 0xFFFF, ts, W, H, payload.length);
+ const header = encodeHeader(frameType, flags, seq & 0xFFFF, ts, W, H, payload.length);
const msg = new Uint8Array(HEADER_SIZE + payload.length);
msg.set(header, 0); msg.set(payload, HEADER_SIZE);
return msg;