Files
multiverse/jumpa-vc/lib/xcu-pulsar-codec.ts
T

108 lines
3.1 KiB
TypeScript

/**
* XCU PulsarCodec — TypeScript Port
* Delta-based pixel compression codec ported from Rust (xcu-wasm-sdk/pulsar.rs)
*
* Protocol: 7 bytes per changed pixel [X_hi, X_lo, Y_hi, Y_lo, R, G, B]
*
* This is the PRIMARY video codec for XCU. H.264 WebCodecs is the fallback.
* PulsarCodec sends ONLY pixels that changed since the last frame,
* achieving near-zero bandwidth when the scene is static.
*/
const NEUROMORPHIC_THRESHOLD = 25;
export class XCUPulsarCodec {
private lastFrame: Uint8Array;
private canvasBuffer: Uint8Array;
private width: number;
private height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
this.lastFrame = new Uint8Array(width * height * 4);
this.canvasBuffer = new Uint8Array(width * height * 4);
// Initialize canvas buffer to black with full alpha
for (let i = 3; i < this.canvasBuffer.length; i += 4) {
this.canvasBuffer[i] = 255;
}
}
/**
* Encode delta: Compare current RGBA frame to last frame.
* Output only changed pixels as 7-byte chunks [X_hi, X_lo, Y_hi, Y_lo, R, G, B]
*/
encodeDelta(currentFrame: Uint8Array): Uint8Array {
const deltaChunks: number[] = [];
for (let i = 0; i < currentFrame.length; i += 4) {
const currR = currentFrame[i];
const currG = currentFrame[i + 1];
const currB = currentFrame[i + 2];
const lastR = this.lastFrame[i];
const lastG = this.lastFrame[i + 1];
const lastB = this.lastFrame[i + 2];
const diffTotal = Math.abs(currR - lastR) + Math.abs(currG - lastG) + Math.abs(currB - lastB);
if (diffTotal > NEUROMORPHIC_THRESHOLD) {
const pixelIndex = (i / 4) | 0;
const x = pixelIndex % this.width;
const y = (pixelIndex / this.width) | 0;
// 7-byte format: [X_hi, X_lo, Y_hi, Y_lo, R, G, B]
deltaChunks.push(
(x >> 8) & 0xFF, x & 0xFF,
(y >> 8) & 0xFF, y & 0xFF,
currR, currG, currB,
);
// Update last frame (only changed pixels)
this.lastFrame[i] = currR;
this.lastFrame[i + 1] = currG;
this.lastFrame[i + 2] = currB;
}
}
return new Uint8Array(deltaChunks);
}
/**
* Decode truecolor: Render RGB asli dari delta payload ke canvas buffer.
* Berbeda dengan decode_xray di Rust yang render hijau neon.
*/
decodeTruecolor(payload: Uint8Array): Uint8Array {
for (let i = 0; i + 6 < payload.length; i += 7) {
const x = (payload[i] << 8) | payload[i + 1];
const y = (payload[i + 2] << 8) | payload[i + 3];
const r = payload[i + 4];
const g = payload[i + 5];
const b = payload[i + 6];
const bufIdx = (y * this.width + x) * 4;
if (bufIdx >= 0 && bufIdx + 3 < this.canvasBuffer.length) {
this.canvasBuffer[bufIdx] = r;
this.canvasBuffer[bufIdx + 1] = g;
this.canvasBuffer[bufIdx + 2] = b;
this.canvasBuffer[bufIdx + 3] = 255; // Full alpha
}
}
return this.canvasBuffer;
}
/**
* Reset codec state (e.g., when switching video source)
*/
reset(): void {
this.lastFrame.fill(0);
this.canvasBuffer.fill(0);
for (let i = 3; i < this.canvasBuffer.length; i += 4) {
this.canvasBuffer[i] = 255;
}
}
get bufferWidth(): number { return this.width; }
get bufferHeight(): number { return this.height; }
}