feat(demos): sync protocol with Rust codec, add touch/gamepad support
- RLE format: 2-byte LE count (matches Rust rle_encode/rle_decode) - Delta frames: FLAG_COMPRESSED flag set correctly - Signal sync: FLAG_KEYFRAME flag + periodic sync every ~5s for late-join - Source: handle touch, gamepad axis/button, resize inputs from receiver - Receiver: touch event capture for mobile, ping frame handling - Protocol constants: added FRAME_PING, INPUT_TOUCH, INPUT_GAMEPAD_*
This commit is contained in:
parent
69f39746af
commit
968d62d0bb
2 changed files with 87 additions and 14 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue