Files
multiverse/jumpa-vc/scripts/update_decoder.ts
T

312 lines
11 KiB
TypeScript

/* eslint-disable */
// @ts-nocheck
import fs from 'fs';
const filePath = 'C:/X/workspace/jumpa.id/vc/lib/xcu-quantum-decoder.ts';
let content = fs.readFileSync(filePath, 'utf-8');
// 1. Add videoEngineMode property
content = content.replace(
'public participantRole: "PANELIST" | "AUDIENCE" = "PANELIST";',
'public participantRole: "PANELIST" | "AUDIENCE" = "PANELIST";\n\tpublic videoEngineMode: "canvas" | "webcodecs" = "canvas";\n\tprivate trackProcessor: any = null;\n\tprivate trackGenerator: any = null;\n\tprivate activeCodecStr: string = "avc1.42E01F";'
);
// 2. Add Codec auto-discovery and Dynamic Decoder creation
const decoderLogic = `
private async detectBestCodec(): Promise<string> {
const codecs = [
"av01.0.04M.08", // AV1
"vp09.00.10.08", // VP9
"avc1.42E01F" // H.264
];
for (const c of codecs) {
try {
const support = await VideoEncoder.isConfigSupported({
codec: c,
width: 1280,
height: 720,
bitrate: 2_500_000,
framerate: 30
});
if (support.supported) {
console.log(\`[QUANTUM WEBCODECS] Hardware GPU Codec Terdeteksi: \${c}\`);
return c;
}
} catch (e) {}
}
return "avc1.42E01F"; // Fallback H.264
}
private createDecoderForParticipant(senderId: number, codecStr: string = "avc1.42E01F") {
// Jika decoder sudah ada dan codec-nya berbeda, tutup dulu
if (this.videoDecoders.has(senderId)) {
try { this.videoDecoders.get(senderId)!.close(); } catch(e){}
this.videoDecoders.delete(senderId);
}
const decoder = new VideoDecoder({
output: (frame: any) => {
this.tryAutoRegisterCanvas(senderId);
const ctx = this.canvasCtxMap.get(senderId);
if (ctx) {
ctx.drawImage(frame, 0, 0, ctx.canvas.width, ctx.canvas.height);
}
frame.close();
},
error: (e: any) => console.error(\`Decoder Error untuk \${senderId}:\`, e),
});
try {
decoder.configure({ codec: codecStr, codedWidth: 1280, codedHeight: 720 });
this.videoDecoders.set(senderId, decoder);
this.firstKeyFrameReceived.set(senderId, false);
console.log(\`[QUANTUM WEBCODECS] Hardware Decoder (\${codecStr}) siap untuk Partisipan \${senderId}\`);
} catch (e) {
console.error("[QUANTUM WEBCODECS] Gagal konfigurasi decoder:", e);
}
}
`;
content = content.replace(
/private createDecoderForParticipant\(senderId: number\) \{[\s\S]*?this\.firstKeyFrameReceived\.set\(senderId, false\);\s*?\n\s*?\/\/ Notifikasi React untuk merender tile/,
decoderLogic + '\n\n\t\t// Notifikasi React untuk merender tile'
);
// 3. Modify `activateUplink` to support WebCodecs
const uplinkOld = ` const videoTrack = this.mediaStream.getVideoTracks()[0];
// === Canvas JPEG Pipeline: Works on ALL browsers ===
const captureVideo = document.createElement("video");`;
const uplinkNew = ` const videoTrack = this.mediaStream.getVideoTracks()[0];
if (this.videoEngineMode === "webcodecs") {
// === XCU QUANTUM WEBCODECS (HARDWARE GPU PIPELINE) ===
console.log("[UPLINK] XCU Quantum WebCodecs (GPU) Diaktifkan!");
this.activeCodecStr = await this.detectBestCodec();
let codecId = 0;
if (this.activeCodecStr.startsWith("av01")) codecId = 2;
else if (this.activeCodecStr.startsWith("vp09")) codecId = 1;
this.videoEncoder = new VideoEncoder({
output: async (chunk: any, meta: any) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
const chunkData = new Uint8Array(chunk.byteLength);
chunk.copyTo(chunkData);
// ENCRYPTION
let finalPayload = chunkData;
let isEncrypted = 0;
let iv = new Uint8Array(12);
if (this.e2eeKey) {
window.crypto.getRandomValues(iv);
const cipherBuffer = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
this.e2eeKey,
chunkData
);
finalPayload = new Uint8Array(cipherBuffer);
isEncrypted = 2;
}
const frameType = chunk.type === "key" ? FRAME_VIDEO_KEY : FRAME_VIDEO_DELTA;
// Jika KeyFrame, sisipkan 1 byte Codec ID di awal payload
let packetLen = 8 + finalPayload.length;
if (frameType === FRAME_VIDEO_KEY) packetLen += 1;
if (isEncrypted) packetLen += 12; // tambah IV
const packet = new Uint8Array(packetLen);
packet[0] = frameType;
packet[1] = isEncrypted ? 2 : 1;
const view = new DataView(packet.buffer);
view.setUint16(2, this.participantId, true);
let offset = 8;
if (isEncrypted) {
view.setUint32(4, packetLen - 8, true);
packet.set(iv, offset);
offset += 12;
} else {
view.setUint32(4, packetLen - 8, true);
}
if (frameType === FRAME_VIDEO_KEY) {
packet[offset] = codecId; // Sisipkan Codec ID
offset += 1;
}
packet.set(finalPayload, offset);
this.ws!.send(packet);
this._frameCount++;
},
error: (e: any) => console.error("[QUANTUM WEBCODECS] Encoder Error:", e)
});
this.videoEncoder.configure({
codec: this.activeCodecStr,
width: 1280,
height: 720,
hardwareAcceleration: "require",
bitrate: 2_500_000,
framerate: 30,
latencyMode: "realtime"
});
// Extract frames directly from camera using MediaStreamTrackProcessor
if (typeof (window as any).MediaStreamTrackProcessor !== "undefined") {
this.trackProcessor = new (window as any).MediaStreamTrackProcessor({ track: videoTrack });
const reader = this.trackProcessor.readable.getReader();
// Asynchronous background encoding loop
(async () => {
while (this.isRunning && this.videoEngineMode === "webcodecs") {
try {
const { done, value: frame } = await reader.read();
if (done || !frame) break;
if (this.videoEncoder && this.videoEncoder.state === "configured") {
// Force KeyFrame every 30 frames (1 second) for resilience
this.videoEncoder.encode(frame, { keyFrame: this._frameCount % 30 === 0 });
}
frame.close();
} catch (e) { break; }
}
})();
}
}
// === Canvas JPEG Pipeline: Works on ALL browsers ===
const captureVideo = document.createElement("video");`;
content = content.replace(uplinkOld, uplinkNew);
// 4. Wrap setInterval to only run when canvas mode
content = content.replace(
`const jpegInterval = setInterval(() => {`,
`const jpegInterval = setInterval(() => {\n\t\t\t\tif (this.videoEngineMode === "webcodecs") return; // Bypass if using Quantum Engine`
);
// 5. Update receiver to handle Codec ID for FRAME_VIDEO_KEY and Decryption
const receiverOld = ` if (frameType === FRAME_VIDEO_DELTA || frameType === FRAME_VIDEO_KEY) {
if (!this.videoDecoders.has(senderId)) {
this.createDecoderForParticipant(senderId);
}
if (frameType === FRAME_VIDEO_DELTA && !this.firstKeyFrameReceived.get(senderId)) {
continue;
}
if (frameType === FRAME_VIDEO_KEY) {
this.firstKeyFrameReceived.set(senderId, true);
}
this.tryAutoRegisterCanvas(senderId);
const decoder = this.videoDecoders.get(senderId);
if (decoder && decoder.state === "configured") {
const chunk = new EncodedVideoChunk({
type: frameType === FRAME_VIDEO_KEY ? "key" : "delta",
timestamp: performance.now() * 1000,
data: payloadData,
});
decoder.decode(chunk);
}
continue;
}`;
const receiverNew = ` if (frameType === FRAME_VIDEO_DELTA || frameType === FRAME_VIDEO_KEY) {
let rawData = payloadData;
let isDecryptionFailed = false;
if (quality === 2) { // Encrypted WebCodecs chunk
if (this.e2eeKey && payloadData.length > 12) {
const iv = payloadData.slice(0, 12);
const cipher = payloadData.slice(12);
try {
const plainBuf = await window.crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv },
this.e2eeKey,
cipher
);
rawData = new Uint8Array(plainBuf);
} catch (e) {
isDecryptionFailed = true;
}
} else {
isDecryptionFailed = true;
}
}
if (isDecryptionFailed) continue;
// Extract Codec ID if KeyFrame
let chunkData = rawData;
let codecStr = "avc1.42E01F"; // Default
if (frameType === FRAME_VIDEO_KEY) {
const codecId = rawData[0];
chunkData = rawData.slice(1);
if (codecId === 2) codecStr = "av01.0.04M.08";
else if (codecId === 1) codecStr = "vp09.00.10.08";
// Re-create decoder if codec changes or not exists
const existing = this.videoDecoders.get(senderId);
if (!existing || (existing as any)._currentCodec !== codecStr) {
this.createDecoderForParticipant(senderId, codecStr);
const newDec = this.videoDecoders.get(senderId);
if (newDec) (newDec as any)._currentCodec = codecStr;
}
this.firstKeyFrameReceived.set(senderId, true);
} else {
if (!this.videoDecoders.has(senderId)) {
this.createDecoderForParticipant(senderId);
}
}
if (frameType === FRAME_VIDEO_DELTA && !this.firstKeyFrameReceived.get(senderId)) {
continue;
}
this.tryAutoRegisterCanvas(senderId);
const decoder = this.videoDecoders.get(senderId);
if (decoder && decoder.state === "configured") {
try {
const chunk = new (window as any).EncodedVideoChunk({
type: frameType === FRAME_VIDEO_KEY ? "key" : "delta",
timestamp: performance.now() * 1000,
data: chunkData,
});
decoder.decode(chunk);
} catch (e) {
// Ignore decode errors to ensure Zero Error crash
}
}
continue;
}`;
content = content.replace(receiverOld, receiverNew);
// 6. Update deactivateUplink to cleanly close trackProcessor
content = content.replace(
`if (this.videoEncoder && this.videoEncoder.state !== "closed") {`,
`if (this.videoEncoder && this.videoEncoder.state !== "closed") {\n\t\t\tthis.videoEncoder.close();\n\t\t\tthis.videoEncoder = null;\n\t\t}\n\t\tif (this.trackProcessor) {\n\t\t\ttry { /* Let GC handle trackProcessor */ } catch(e){}\n\t\t\tthis.trackProcessor = null;\n\t\t}\n\t\tif (false) {`
);
// Hot Swap Method
const hotSwap = `
public async hotSwapVideoEngine(mode: "canvas" | "webcodecs") {
if (this.videoEngineMode === mode) return;
console.log(\`[QUANTUM HOT-SWAP] Mengalihkan Engine ke: \${mode}\`);
this.videoEngineMode = mode;
// Jika kamera sedang nyala, kita matikan lalu nyalakan secara instan tanpa mematikan koneksi WebTransport
if (this.mediaStream) {
await this.deactivateUplink();
await this.activateUplink('camera');
}
}
`;
content = content.replace('public unlockAudio() {', hotSwap + '\n\tpublic unlockAudio() {');
fs.writeFileSync(filePath, content, 'utf-8');
console.log('Successfully updated xcu-quantum-decoder.ts');