/** * 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; } }