520 lines
16 KiB
TypeScript
520 lines
16 KiB
TypeScript
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
|
export class XCUQuantumMatrix {
|
|
private transport: WebTransport | null = null;
|
|
private stream: WebTransportBidirectionalStream | null = null;
|
|
private streamWriter: WritableStreamDefaultWriter | null = null;
|
|
private streamReader: ReadableStreamDefaultReader | null = null;
|
|
private videoDecoders: Map<number, VideoDecoder> = new Map();
|
|
private canvasCtxMap: Map<number, CanvasRenderingContext2D> = new Map();
|
|
private videoEncoder: VideoEncoder | null = null;
|
|
private isRunning: boolean = false;
|
|
private mediaStream: MediaStream | null = null;
|
|
public participantId: number = 0;
|
|
public participantRole: "PANELIST" | "AUDIENCE" = "PANELIST";
|
|
|
|
// Callbacks
|
|
public onParticipantJoined: ((id: number) => void) | null = null;
|
|
public onParticipantLeft: ((id: number) => void) | null = null;
|
|
public onQuantumDataReceived:
|
|
| ((senderId: number, payload: string) => void)
|
|
| null = null;
|
|
public onLocalStream: ((stream: MediaStream) => void) | null = null;
|
|
public onActiveSpeakerChanged: ((speakerId: number) => void) | null = null;
|
|
public onAudioLevel: ((level: number) => void) | null = null;
|
|
|
|
private vadAudioCtx: AudioContext | null = null;
|
|
|
|
public registerCanvas(participantId: number, canvasId: string) {
|
|
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
|
|
if (canvas) {
|
|
const ctx = canvas.getContext("2d");
|
|
if (ctx) this.canvasCtxMap.set(participantId, ctx);
|
|
console.log(
|
|
`[QUANTUM MATRIX] Canvas didaftarkan untuk Partisipan ${participantId}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
public async ignite() {
|
|
this.isRunning = true;
|
|
console.log(
|
|
"[QUANTUM MATRIX] Menginisialisasi Pipa WebTransport & WebCodecs...",
|
|
);
|
|
|
|
// 2. Setup WebTransport ke Rust Backend
|
|
try {
|
|
// Menggunakan port Standar Kuantum 4433 (UDP) dengan Sertifikat Valid Let's Encrypt
|
|
this.transport = new WebTransport(
|
|
typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:8443` : "/xcu-engine",
|
|
);
|
|
await this.transport.ready;
|
|
console.log("[QUANTUM MATRIX] Terhubung ke Server Zero-Copy QUIC!");
|
|
|
|
// Setup awal dihapus, decoder akan dibuat per-partisipan
|
|
this.startReceiver();
|
|
} catch (e: unknown) {
|
|
console.error("[QUANTUM MATRIX] Gagal menembus matriks QUIC:", e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
public toggleMic(enabled: boolean) {
|
|
if (this.mediaStream) {
|
|
this.mediaStream.getAudioTracks().forEach((track) => {
|
|
track.enabled = enabled;
|
|
});
|
|
console.log(`[QUANTUM UPLINK] Mikrofon ${enabled ? "Aktif" : "Mati"}`);
|
|
}
|
|
}
|
|
|
|
public resumeAudioContext() {
|
|
if (this.vadAudioCtx && this.vadAudioCtx.state === "suspended") {
|
|
this.vadAudioCtx
|
|
.resume()
|
|
.catch((e) => console.error("Force resume failed", e));
|
|
}
|
|
}
|
|
|
|
private createDecoderForParticipant(senderId: number) {
|
|
const decoder = new VideoDecoder({
|
|
output: (frame: unknown) => {
|
|
const ctx = this.canvasCtxMap.get(senderId);
|
|
if (ctx) {
|
|
ctx.drawImage(
|
|
frame as unknown as CanvasImageSource,
|
|
0,
|
|
0,
|
|
ctx.canvas.width,
|
|
ctx.canvas.height,
|
|
);
|
|
}
|
|
(frame as { close(): void }).close();
|
|
},
|
|
error: (e: unknown) =>
|
|
console.error(`Decoder Error untuk ${senderId}:`, e),
|
|
});
|
|
|
|
decoder.configure({
|
|
codec: "avc1.42E01E", // H.264 Baseline
|
|
codedWidth: 1280,
|
|
codedHeight: 720,
|
|
});
|
|
|
|
this.videoDecoders.set(senderId, decoder);
|
|
|
|
// Notifikasi React untuk merender tile
|
|
if (this.onParticipantJoined) {
|
|
this.onParticipantJoined(senderId);
|
|
}
|
|
}
|
|
|
|
public async activateUplink(source: "camera" | "screen" = "camera") {
|
|
if (this.mediaStream) {
|
|
console.warn(
|
|
"[QUANTUM UPLINK] Kamera/Layar sudah aktif! Mengabaikan perintah ganda untuk mencegah kebocoran memori.",
|
|
);
|
|
return;
|
|
}
|
|
console.log(
|
|
`[QUANTUM UPLINK] Memulai Injeksi ${source === "screen" ? "Layar" : "Kamera"} ke WebCodecs...`,
|
|
);
|
|
try {
|
|
if (source === "screen") {
|
|
this.mediaStream = await navigator.mediaDevices.getDisplayMedia({
|
|
video: { width: 1280, height: 720 },
|
|
audio: false,
|
|
});
|
|
} else {
|
|
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
video: { width: 1280, height: 720 },
|
|
audio: true,
|
|
});
|
|
this.startVadLoop(this.mediaStream); // Phase 37: Auto-Start VAD
|
|
}
|
|
|
|
if (this.onLocalStream) {
|
|
this.onLocalStream(this.mediaStream);
|
|
}
|
|
|
|
// Jika Klien adalah AUDIENCE, abaikan penyalaan WebCodecs untuk menghemat RAM 100%
|
|
if (this.participantRole === "AUDIENCE") {
|
|
console.log(
|
|
"[QUANTUM MATRIX] Tersambung sebagai AUDIENCE. Menutup jalur Uplink Video.",
|
|
);
|
|
await this.sendRoleSignal();
|
|
return;
|
|
}
|
|
|
|
const videoTrack = this.mediaStream.getVideoTracks()[0];
|
|
|
|
const processor = new MediaStreamTrackProcessor({
|
|
track: videoTrack,
|
|
});
|
|
const reader = processor.readable.getReader();
|
|
|
|
// Setup Hardware Encoder
|
|
this.videoEncoder = new VideoEncoder({
|
|
output: async (chunk: unknown) => {
|
|
if (this.streamWriter && this.isRunning) {
|
|
// Quantum Protocol v3: 8-Byte Header
|
|
// Byte 0: Type (0=Delta, 1=Key, 2=Audio, 3=Control)
|
|
// Byte 1: Quality (0=Low, 1=High, 2=VAD Active)
|
|
// Byte 2-3: Participant ID (u16)
|
|
// Byte 4-7: Length (4 bytes)
|
|
const packet = new Uint8Array(
|
|
8 + (chunk as { byteLength: number }).byteLength,
|
|
);
|
|
packet[0] = (chunk as { type: string }).type === "key" ? 1 : 0;
|
|
packet[1] = 1; // High Quality
|
|
|
|
const view = new DataView(packet.buffer);
|
|
view.setUint16(2, this.participantId, true);
|
|
view.setUint32(
|
|
4,
|
|
(chunk as { byteLength: number }).byteLength,
|
|
true,
|
|
); // Little-endian length
|
|
|
|
(chunk as { copyTo(buf: ArrayBuffer): void }).copyTo(
|
|
packet.buffer.slice(8),
|
|
);
|
|
|
|
// Tembakkan via QUIC Stream (Otomatis Fragmentasi MTU)
|
|
await this.streamWriter.write(packet);
|
|
}
|
|
},
|
|
error: (e: unknown) => console.error("Encoder Error:", e),
|
|
});
|
|
|
|
this.videoEncoder.configure({
|
|
codec: "avc1.42E01E", // H.264
|
|
width: 1280,
|
|
height: 720,
|
|
bitrate: 2_000_000, // 2 Mbps
|
|
framerate: 30,
|
|
latencyMode: "realtime",
|
|
});
|
|
|
|
// Loop pengiriman Frame ke GPU Encoder
|
|
this.encodeLoop(reader);
|
|
} catch (e: unknown) {
|
|
console.error("[QUANTUM UPLINK] Gagal mengaktifkan kamera:", e);
|
|
}
|
|
}
|
|
|
|
public async deactivateUplink() {
|
|
console.log("[QUANTUM UPLINK] Mematikan Kamera (Downlink tetap aktif)...");
|
|
if (this.mediaStream) {
|
|
this.mediaStream.getTracks().forEach((t) => {
|
|
t.stop();
|
|
});
|
|
this.mediaStream = null;
|
|
}
|
|
if (this.videoEncoder && this.videoEncoder.state !== "closed") {
|
|
this.videoEncoder.close();
|
|
this.videoEncoder = null;
|
|
}
|
|
}
|
|
|
|
private async encodeLoop(reader: ReadableStreamDefaultReader) {
|
|
while (this.isRunning) {
|
|
try {
|
|
const { done, value: frame } = await reader.read();
|
|
if (done || !frame) break;
|
|
|
|
if (this.videoEncoder && this.videoEncoder.state === "configured") {
|
|
this.videoEncoder.encode(frame, { keyFrame: false });
|
|
}
|
|
if (frame) frame.close();
|
|
} catch {
|
|
// Stream terputus atau kamera dimatikan
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async startReceiver() {
|
|
if (!this.transport) return;
|
|
|
|
try {
|
|
console.log("[QUANTUM MATRIX] Menciptakan QUIC Bidirectional Stream...");
|
|
// Klien yang berinisiatif membuka Bi-Directional Stream pertama kali
|
|
this.stream = await this.transport.createBidirectionalStream();
|
|
this.streamWriter = this.stream.writable.getWriter();
|
|
this.streamReader = this.stream.readable.getReader();
|
|
|
|
let buffer = new Uint8Array(0);
|
|
|
|
while (this.isRunning) {
|
|
const { value, done } = await this.streamReader.read();
|
|
if (done) {
|
|
console.log("QUIC Stream Ditutup oleh Server.");
|
|
break;
|
|
}
|
|
|
|
if (value) {
|
|
// Gabungkan chunk yang baru datang ke dalam buffer
|
|
const newBuffer = new Uint8Array(buffer.length + value.length);
|
|
newBuffer.set(buffer);
|
|
newBuffer.set(value, buffer.length);
|
|
buffer = newBuffer;
|
|
|
|
// Ekstrak Frame berdasarkan 8-byte header
|
|
while (buffer.length >= 8) {
|
|
const frameType = buffer[0];
|
|
const quality = buffer[1];
|
|
const view = new DataView(
|
|
buffer.buffer,
|
|
buffer.byteOffset,
|
|
buffer.byteLength,
|
|
);
|
|
const senderId = view.getUint16(2, true);
|
|
const frameLength = view.getUint32(4, true);
|
|
|
|
if (buffer.length >= 8 + frameLength) {
|
|
const payloadData = buffer.slice(8, 8 + frameLength);
|
|
buffer = buffer.slice(8 + frameLength);
|
|
|
|
// [FASE 40] Eksekusi Quantum Ledger (Telepati Data)
|
|
if (frameType === 6) {
|
|
if (this.onQuantumDataReceived) {
|
|
const textDecoder = new TextDecoder();
|
|
const payloadStr = textDecoder.decode(payloadData);
|
|
this.onQuantumDataReceived(senderId, payloadStr);
|
|
}
|
|
continue; // Lanjut ke paket berikutnya, ini bukan Video
|
|
}
|
|
|
|
// [FASE 37] Tangkap Sinyal Active Speaker Broadcast dari Core
|
|
if (frameType === 3 && quality === 2) {
|
|
if (this.onActiveSpeakerChanged) {
|
|
this.onActiveSpeakerChanged(senderId);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Injeksi Frame Video langsung ke Hardware Decoder Klien
|
|
if (frameType === 0 || frameType === 1) {
|
|
if (!this.videoDecoders.has(senderId)) {
|
|
this.createDecoderForParticipant(senderId);
|
|
}
|
|
|
|
const decoder = this.videoDecoders.get(senderId);
|
|
if (decoder && decoder.state === "configured") {
|
|
const chunk = new EncodedVideoChunk({
|
|
type: frameType === 1 ? "key" : "delta",
|
|
timestamp: performance.now() * 1000,
|
|
data: payloadData,
|
|
});
|
|
decoder.decode(chunk);
|
|
}
|
|
}
|
|
} else {
|
|
// Butuh lebih banyak byte untuk membentuk frame utuh
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e: unknown) {
|
|
console.error("[QUANTUM MATRIX] Kegagalan Aliran (Stream Failure):", e);
|
|
}
|
|
}
|
|
|
|
// Fase 37: Voice Activity Detection (VAD) Loop
|
|
private startVadLoop(stream: MediaStream) {
|
|
try {
|
|
this.vadAudioCtx = new (
|
|
window.AudioContext ||
|
|
(window as unknown as { webkitAudioContext: typeof AudioContext })
|
|
.webkitAudioContext
|
|
)();
|
|
if (this.vadAudioCtx.state === "suspended") {
|
|
this.vadAudioCtx
|
|
.resume()
|
|
.catch((e) => console.warn("[VAD] Cannot resume AudioContext", e));
|
|
}
|
|
const source = this.vadAudioCtx.createMediaStreamSource(stream);
|
|
const analyser = this.vadAudioCtx.createAnalyser();
|
|
analyser.fftSize = 512;
|
|
|
|
// Trik khusus untuk browser modern agar benar-benar memproses FFT stream
|
|
const dummyGain = this.vadAudioCtx.createGain();
|
|
dummyGain.gain.value = 0; // Bisukan (Mute) agar tidak terjadi echo
|
|
|
|
source.connect(analyser);
|
|
analyser.connect(dummyGain);
|
|
dummyGain.connect(this.vadAudioCtx.destination);
|
|
|
|
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
let speakingFrames = 0;
|
|
|
|
setInterval(() => {
|
|
analyser.getByteFrequencyData(dataArray);
|
|
const sum = dataArray.reduce((a, b) => a + b, 0);
|
|
const avg = sum / dataArray.length;
|
|
|
|
if (this.onAudioLevel) {
|
|
this.onAudioLevel(avg);
|
|
}
|
|
|
|
if (avg > 5) {
|
|
// Ambang batas diturunkan drastis ke 5
|
|
speakingFrames++;
|
|
if (speakingFrames === 3) {
|
|
// Hanya kirim jika uplink siap
|
|
if (this.streamWriter && this.isRunning) {
|
|
this.sendDirectorSignal(2);
|
|
}
|
|
}
|
|
} else {
|
|
speakingFrames = 0;
|
|
}
|
|
}, 100);
|
|
} catch (e: unknown) {
|
|
console.warn(
|
|
"[VAD] Audio Context tidak didukung atau dicekal browser.",
|
|
e,
|
|
);
|
|
}
|
|
}
|
|
|
|
private async sendDirectorSignal(actionQuality: number) {
|
|
if (!this.streamWriter) return;
|
|
const packet = new Uint8Array(8);
|
|
packet[0] = 3; // Control
|
|
packet[1] = actionQuality;
|
|
const view = new DataView(packet.buffer);
|
|
view.setUint16(2, this.participantId, true);
|
|
view.setUint32(4, 0, true);
|
|
await this.streamWriter.write(packet);
|
|
}
|
|
|
|
// Fase 34: Zero-CPU Omniscient Recording
|
|
public async triggerQuantumRecording() {
|
|
if (!this.streamWriter) return;
|
|
const packet = new Uint8Array(8);
|
|
packet[0] = 3; // Control
|
|
packet[1] = 6; // Set Action: Record
|
|
const view = new DataView(packet.buffer);
|
|
view.setUint16(2, this.participantId, true);
|
|
view.setUint32(4, 0, true);
|
|
await this.streamWriter.write(packet);
|
|
console.log(
|
|
"[VAULT MATRIX] Sinyal Perekaman Kuantum Langsung Ke SSD Server Dikirim.",
|
|
);
|
|
}
|
|
|
|
// Fase 35: SVC Downgrade (Anti-Lag Manual/Otomatis)
|
|
public async activateAntiLagDowngrade() {
|
|
if (this.streamWriter) {
|
|
console.log("[SVC MATRIX] Mengirim sinyal Downgrade ke Server Rust...");
|
|
const controlPacket = new Uint8Array(8);
|
|
controlPacket[0] = 3; // Type: Control
|
|
controlPacket[1] = 0; // Quality: Low (Drop paket 1080p)
|
|
const view = new DataView(controlPacket.buffer);
|
|
view.setUint16(2, this.participantId, true);
|
|
view.setUint32(4, 0, true); // Payload Length: 0
|
|
|
|
await this.streamWriter.write(controlPacket);
|
|
}
|
|
}
|
|
|
|
// Fase 39: Webinar Asimetris (Audience)
|
|
public async joinAsAudience() {
|
|
this.participantRole = "AUDIENCE";
|
|
await this.sendRoleSignal();
|
|
console.log(
|
|
"[WEBINAR MATRIX] Beralih ke Mode AUDIENCE (Uplink Dimatikan).",
|
|
);
|
|
}
|
|
|
|
private async sendRoleSignal() {
|
|
if (!this.streamWriter) return;
|
|
const packet = new Uint8Array(8);
|
|
packet[0] = 3; // Control
|
|
packet[1] = 4; // Set Role
|
|
const view = new DataView(packet.buffer);
|
|
view.setUint16(2, this.participantId, true);
|
|
packet[4] = this.participantRole === "AUDIENCE" ? 1 : 0; // 1=Audience, 0=Panelist
|
|
await this.streamWriter.write(packet);
|
|
}
|
|
|
|
// Fase 39: Podcast Mode (Matikan Video di Routing Level)
|
|
public async setPodcastMode(isPodcast: boolean) {
|
|
if (!this.streamWriter) return;
|
|
const packet = new Uint8Array(8);
|
|
packet[0] = 3; // Control
|
|
packet[1] = 5; // Set Mode
|
|
const view = new DataView(packet.buffer);
|
|
view.setUint16(2, this.participantId, true);
|
|
packet[4] = isPodcast ? 1 : 0; // 1=Podcast, 0=Webinar
|
|
await this.streamWriter.write(packet);
|
|
console.log(
|
|
`[PODCAST MATRIX] Sinyal ${isPodcast ? "PODCAST" : "WEBINAR"} dikirim ke Core.`,
|
|
);
|
|
}
|
|
|
|
// Fase 40: The Quantum Ledger (Pengganti WebSocket Node.js)
|
|
// Menembakkan Chat, JSON, Emoji, Koordinat langsung via QUIC
|
|
public async sendQuantumData(payload: string) {
|
|
if (!this.streamWriter) return;
|
|
|
|
const textEncoder = new TextEncoder();
|
|
const payloadBytes = textEncoder.encode(payload);
|
|
|
|
const packet = new Uint8Array(8 + payloadBytes.byteLength);
|
|
packet[0] = 6; // Type: Ledger Data
|
|
packet[1] = 0; // Quality: N/A
|
|
const view = new DataView(packet.buffer);
|
|
view.setUint16(2, this.participantId, true);
|
|
view.setUint32(4, payloadBytes.byteLength, true);
|
|
|
|
packet.set(payloadBytes, 8);
|
|
|
|
await this.streamWriter.write(packet);
|
|
}
|
|
|
|
public disconnect() {
|
|
this.isRunning = false;
|
|
if (this.transport) {
|
|
this.transport.close();
|
|
}
|
|
this.videoDecoders.forEach((decoder) => {
|
|
if (decoder.state !== "closed") decoder.close();
|
|
});
|
|
this.videoDecoders.clear();
|
|
this.canvasCtxMap.clear();
|
|
if (this.videoEncoder && this.videoEncoder.state !== "closed") {
|
|
this.videoEncoder.close();
|
|
}
|
|
if (this.mediaStream) {
|
|
this.mediaStream.getTracks().forEach((t) => {
|
|
t.stop();
|
|
});
|
|
}
|
|
console.log("[QUANTUM MATRIX] Sistem Terputus.");
|
|
}
|
|
|
|
public shutdown() {
|
|
this.isRunning = false;
|
|
if (this.transport) {
|
|
this.transport.close();
|
|
}
|
|
this.videoDecoders.forEach((decoder) => {
|
|
if (decoder.state !== "closed") decoder.close();
|
|
});
|
|
this.videoDecoders.clear();
|
|
this.canvasCtxMap.clear();
|
|
if (this.videoEncoder && this.videoEncoder.state !== "closed") {
|
|
this.videoEncoder.close();
|
|
}
|
|
if (this.mediaStream) {
|
|
this.mediaStream.getTracks().forEach((t) => {
|
|
t.stop();
|
|
});
|
|
}
|
|
console.log("[QUANTUM MATRIX] Sistem Terputus.");
|
|
}
|
|
}
|