diff --git a/app/providers/CallProvider/audioE2EE.ts b/app/providers/CallProvider/audioE2EE.ts index fb6ed29..fd8f81f 100644 --- a/app/providers/CallProvider/audioE2EE.ts +++ b/app/providers/CallProvider/audioE2EE.ts @@ -10,93 +10,80 @@ function toUint8Array(input: KeyInput): Uint8Array { return new Uint8Array(u8.slice().buffer); } -function buildNonce(timestamp: unknown): Uint8Array { +/** + * Переиспользуемый процессор фреймов. + * Один экземпляр на sender/receiver — нет аллокаций на каждый фрейм. + */ +function createFrameProcessor(key: Uint8Array) { + // Переиспользуемые буферы — не создаём новые на каждый фрейм const nonce = new Uint8Array(12); - const ts = typeof timestamp === "number" - ? timestamp - : typeof timestamp === "bigint" - ? Number(timestamp) - : 0; - new DataView(nonce.buffer).setUint32(8, ts >>> 0, false); - return nonce; + const nonceView = new DataView(nonce.buffer); + + return function processFrame(data: ArrayBuffer): ArrayBuffer { + // Переиспользуем nonce буфер + nonce.fill(0); + nonceView.setUint32(8, (data.byteLength ^ (data.byteLength << 8)) >>> 0, false); + + const input = new Uint8Array(data); + const output = chacha20(key, nonce, input); + return output.buffer as ArrayBuffer; + }; } -function processFrame(data: ArrayBuffer, key: Uint8Array, timestamp: unknown): ArrayBuffer { - const nonce = buildNonce(timestamp); - const input = new Uint8Array(data); - // ChaCha20 симметричный: encrypt === decrypt, тот же размер - const output = chacha20(key, nonce, input); - return output.buffer as ArrayBuffer; +function createTransform(processFrame: (data: ArrayBuffer) => ArrayBuffer) { + return new TransformStream({ + transform(frame, controller) { + try { + const start = performance.now(); + frame.data = processFrame(frame.data); + const elapsed = performance.now() - start; + if (elapsed > 1) { + console.warn(`E2EE slow frame: ${elapsed.toFixed(2)}ms`); + } + controller.enqueue(frame); + } catch (e) { + // не рвём поток — пропускаем фрейм как есть + console.error("E2EE frame failed:", e); + controller.enqueue(frame); + } + } + }); } export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput): Promise { - if (senderAttached.has(sender)) { - return; - } + if (senderAttached.has(sender)) return; senderAttached.add(sender); const key = toUint8Array(keyInput); - if (key.byteLength !== 32) { - throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); - } + if (key.byteLength !== 32) throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); const anySender = sender as any; - if (!anySender.createEncodedStreams) { - throw new Error("createEncodedStreams is not available on RTCRtpSender"); - } + if (!anySender.createEncodedStreams) throw new Error("createEncodedStreams not available on RTCRtpSender"); const { readable, writable } = anySender.createEncodedStreams(); + const processFrame = createFrameProcessor(key); - const enc = new TransformStream({ - // Синхронный transform — нет async, нет накопления очереди - transform(frame, controller) { - try { - frame.data = processFrame(frame.data, key, frame.timestamp); - controller.enqueue(frame); - } catch (e) { - console.error("Sender E2EE frame failed:", e); - controller.enqueue(frame); - } - } - }); - - readable.pipeThrough(enc).pipeTo(writable).catch((e) => { - console.error("Sender E2EE pipeline failed:", e); - }); + readable + .pipeThrough(createTransform(processFrame)) + .pipeTo(writable) + .catch((e) => console.error("Sender E2EE pipeline failed:", e)); } export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise { - if (receiverAttached.has(receiver)) { - return; - } + if (receiverAttached.has(receiver)) return; receiverAttached.add(receiver); const key = toUint8Array(keyInput); - if (key.byteLength !== 32) { - throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); - } + if (key.byteLength !== 32) throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); const anyReceiver = receiver as any; - if (!anyReceiver.createEncodedStreams) { - throw new Error("createEncodedStreams is not available on RTCRtpReceiver"); - } + if (!anyReceiver.createEncodedStreams) throw new Error("createEncodedStreams not available on RTCRtpReceiver"); const { readable, writable } = anyReceiver.createEncodedStreams(); + const processFrame = createFrameProcessor(key); - const dec = new TransformStream({ - // Синхронный transform — нет async, нет накопления очереди - transform(frame, controller) { - try { - frame.data = processFrame(frame.data, key, frame.timestamp); - controller.enqueue(frame); - } catch (e) { - console.error("Receiver E2EE frame failed:", e); - controller.enqueue(frame); - } - } - }); - - readable.pipeThrough(dec).pipeTo(writable).catch((e) => { - console.error("Receiver E2EE pipeline failed:", e); - }); + readable + .pipeThrough(createTransform(processFrame)) + .pipeTo(writable) + .catch((e) => console.error("Receiver E2EE pipeline failed:", e)); } \ No newline at end of file