312 lines
11 KiB
TypeScript
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');
|