WASM для ускорения шифрования звонков, тест

This commit is contained in:
RoyceDa
2026-03-21 18:51:39 +02:00
parent f269046c46
commit 4df39cb83d
3 changed files with 133 additions and 29 deletions

View File

@@ -98,6 +98,67 @@ export function CallProvider(props : CallProviderProps) {
} }
}, [callState]); }, [callState]);
// ...existing code...
const checkWebRTCStats = async () => {
if (!peerConnectionRef.current) return;
const stats = await peerConnectionRef.current.getStats();
stats.forEach(report => {
// Исходящий аудио
if (report.type === "outbound-rtp" && report.mediaType === "audio") {
console.info("[WebRTC OUT]", {
bytesSent: report.bytesSent,
packetsSent: report.packetsSent,
timestamp: report.timestamp
});
}
// Входящий аудио
if (report.type === "inbound-rtp" && report.mediaType === "audio") {
console.info("[WebRTC IN]", {
bytesReceived: report.bytesReceived,
packetsReceived: report.packetsReceived,
jitter: report.jitter,
packetsLost: report.packetsLost,
timestamp: report.timestamp
});
}
// RTT и задержка
if (report.type === "candidate-pair" && report.state === "succeeded") {
console.info("[WebRTC RTT]", {
currentRoundTripTime: (report.currentRoundTripTime * 1000).toFixed(2) + "ms",
availableOutgoingBitrate: (report.availableOutgoingBitrate / 1024 / 1024).toFixed(2) + " Mbps",
availableIncomingBitrate: (report.availableIncomingBitrate / 1024 / 1024).toFixed(2) + " Mbps"
});
}
// Codec info
if (report.type === "codec") {
if (report.mediaType === "audio") {
console.info("[WebRTC Codec]", {
mimeType: report.mimeType,
channels: report.channels,
clockRate: report.clockRate
});
}
}
});
};
// Вызываем каждые 2 секунды при активном звонке
useEffect(() => {
if (callState !== CallState.ACTIVE) return;
const interval = setInterval(() => {
void checkWebRTCStats();
}, 2000);
return () => clearInterval(interval);
}, [callState]);
// ...existing code...
useEffect(() => { useEffect(() => {
/** /**
* Нам нужно получить ICE серверы для установки соединения из разных сетей * Нам нужно получить ICE серверы для установки соединения из разных сетей

View File

@@ -1,51 +1,80 @@
import { chacha20 } from "@noble/ciphers/chacha"; import _sodium from "libsodium-wrappers";
type KeyInput = Buffer | Uint8Array; type KeyInput = Buffer | Uint8Array;
const senderAttached = new WeakSet<RTCRtpSender>(); const senderAttached = new WeakSet<RTCRtpSender>();
const receiverAttached = new WeakSet<RTCRtpReceiver>(); const receiverAttached = new WeakSet<RTCRtpReceiver>();
let sodiumReady = false;
let sodium: typeof _sodium;
export async function initE2EE(): Promise<void> {
if (sodiumReady) return;
await _sodium.ready;
sodium = _sodium;
sodiumReady = true;
}
function toUint8Array(input: KeyInput): Uint8Array { function toUint8Array(input: KeyInput): Uint8Array {
const u8 = input instanceof Uint8Array ? input : new Uint8Array(input); const u8 = input instanceof Uint8Array ? input : new Uint8Array(input);
return new Uint8Array(u8.slice().buffer); return new Uint8Array(u8.slice().buffer);
} }
/**
* Переиспользуемый процессор фреймов.
* Один экземпляр на sender/receiver — нет аллокаций на каждый фрейм.
*/
function createFrameProcessor(key: Uint8Array) { function createFrameProcessor(key: Uint8Array) {
// Переиспользуемые буферы — не создаём новые на каждый фрейм // Переиспользуемый nonce буфер — нет аллокаций на каждый фрейм
const nonce = new Uint8Array(12); const nonce = new Uint8Array(sodium.crypto_stream_chacha20_NONCEBYTES); // 8 bytes
const nonceView = new DataView(nonce.buffer);
return function processFrame(data: ArrayBuffer): ArrayBuffer { 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 input = new Uint8Array(data);
const output = chacha20(key, nonce, input);
// Обновляем nonce из длины и byteOffset фрейма — уникально и без аллокаций
nonce.fill(0);
new DataView(nonce.buffer).setUint32(0, input.byteLength ^ 0xdeadbeef, false);
new DataView(nonce.buffer).setUint32(4, input[0] ^ input[input.byteLength - 1], false);
// WASM ChaCha20 — синхронный, нативная скорость
const output = sodium.crypto_stream_chacha20_xor(input, nonce, key);
return output.buffer as ArrayBuffer; return output.buffer as ArrayBuffer;
}; };
} }
function createTransform(processFrame: (data: ArrayBuffer) => ArrayBuffer) { function createTransform(processFrame: (data: ArrayBuffer) => ArrayBuffer) {
let frames = 0;
let slowFrames = 0;
let total = 0;
let max = 0;
return new TransformStream<any, any>({ return new TransformStream<any, any>({
transform(frame, controller) { transform(frame, controller) {
const started = performance.now();
try { try {
const start = performance.now();
frame.data = processFrame(frame.data); 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) { } catch (e) {
// не рвём поток — пропускаем фрейм как есть console.error("[E2EE] frame error:", e);
console.error("E2EE frame failed:", e);
controller.enqueue(frame); controller.enqueue(frame);
return;
} }
const elapsed = performance.now() - started;
frames++;
total += elapsed;
if (elapsed > max) max = elapsed;
if (elapsed > 2) slowFrames++;
if (frames >= 200) {
console.info("[E2EE stats]", {
avgMs: +(total / frames).toFixed(3),
maxMs: +max.toFixed(3),
slowFrames,
});
frames = 0;
slowFrames = 0;
total = 0;
max = 0;
}
controller.enqueue(frame);
} }
}); });
} }
@@ -54,36 +83,48 @@ export async function attachSenderE2EE(sender: RTCRtpSender, keyInput: KeyInput)
if (senderAttached.has(sender)) return; if (senderAttached.has(sender)) return;
senderAttached.add(sender); senderAttached.add(sender);
await initE2EE();
const key = toUint8Array(keyInput); const key = toUint8Array(keyInput);
if (key.byteLength !== 32) throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); if (key.byteLength < sodium.crypto_stream_chacha20_KEYBYTES) {
throw new Error(`Key must be at least ${sodium.crypto_stream_chacha20_KEYBYTES} bytes`);
}
const anySender = sender as any; const anySender = sender as any;
if (!anySender.createEncodedStreams) throw new Error("createEncodedStreams not available on RTCRtpSender"); if (!anySender.createEncodedStreams) {
throw new Error("createEncodedStreams not available on RTCRtpSender");
}
const { readable, writable } = anySender.createEncodedStreams(); const { readable, writable } = anySender.createEncodedStreams();
const processFrame = createFrameProcessor(key); const processFrame = createFrameProcessor(key.slice(0, sodium.crypto_stream_chacha20_KEYBYTES));
readable readable
.pipeThrough(createTransform(processFrame)) .pipeThrough(createTransform(processFrame))
.pipeTo(writable) .pipeTo(writable)
.catch((e) => console.error("Sender E2EE pipeline failed:", e)); .catch((e) => console.error("[E2EE] Sender pipeline failed:", e));
} }
export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise<void> { export async function attachReceiverE2EE(receiver: RTCRtpReceiver, keyInput: KeyInput): Promise<void> {
if (receiverAttached.has(receiver)) return; if (receiverAttached.has(receiver)) return;
receiverAttached.add(receiver); receiverAttached.add(receiver);
await initE2EE();
const key = toUint8Array(keyInput); const key = toUint8Array(keyInput);
if (key.byteLength !== 32) throw new Error(`E2EE key must be 32 bytes, got ${key.byteLength}`); if (key.byteLength < sodium.crypto_stream_chacha20_KEYBYTES) {
throw new Error(`Key must be at least ${sodium.crypto_stream_chacha20_KEYBYTES} bytes`);
}
const anyReceiver = receiver as any; const anyReceiver = receiver as any;
if (!anyReceiver.createEncodedStreams) throw new Error("createEncodedStreams not available on RTCRtpReceiver"); if (!anyReceiver.createEncodedStreams) {
throw new Error("createEncodedStreams not available on RTCRtpReceiver");
}
const { readable, writable } = anyReceiver.createEncodedStreams(); const { readable, writable } = anyReceiver.createEncodedStreams();
const processFrame = createFrameProcessor(key); const processFrame = createFrameProcessor(key.slice(0, sodium.crypto_stream_chacha20_KEYBYTES));
readable readable
.pipeThrough(createTransform(processFrame)) .pipeThrough(createTransform(processFrame))
.pipeTo(writable) .pipeTo(writable)
.catch((e) => console.error("Receiver E2EE pipeline failed:", e)); .catch((e) => console.error("[E2EE] Receiver pipeline failed:", e));
} }

View File

@@ -111,6 +111,7 @@
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"libsodium": "^0.8.2", "libsodium": "^0.8.2",
"libsodium-wrappers": "^0.8.2",
"lottie-react": "^2.4.1", "lottie-react": "^2.4.1",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"node-machine-id": "^1.1.12", "node-machine-id": "^1.1.12",
@@ -136,6 +137,7 @@
"@electron/rebuild": "^4.0.3", "@electron/rebuild": "^4.0.3",
"@rushstack/eslint-patch": "^1.10.5", "@rushstack/eslint-patch": "^1.10.5",
"@tailwindcss/vite": "^4.0.9", "@tailwindcss/vite": "^4.0.9",
"@types/libsodium-wrappers": "^0.7.14",
"@types/node": "^22.13.5", "@types/node": "^22.13.5",
"@types/react": "^19.0.10", "@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",