WASM для ускорения шифрования звонков, тест
This commit is contained in:
@@ -98,6 +98,67 @@ export function CallProvider(props : CallProviderProps) {
|
||||
}
|
||||
}, [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(() => {
|
||||
/**
|
||||
* Нам нужно получить ICE серверы для установки соединения из разных сетей
|
||||
|
||||
@@ -1,51 +1,80 @@
|
||||
import { chacha20 } from "@noble/ciphers/chacha";
|
||||
import _sodium from "libsodium-wrappers";
|
||||
|
||||
type KeyInput = Buffer | Uint8Array;
|
||||
|
||||
const senderAttached = new WeakSet<RTCRtpSender>();
|
||||
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 {
|
||||
const u8 = input instanceof Uint8Array ? input : new Uint8Array(input);
|
||||
return new Uint8Array(u8.slice().buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Переиспользуемый процессор фреймов.
|
||||
* Один экземпляр на sender/receiver — нет аллокаций на каждый фрейм.
|
||||
*/
|
||||
function createFrameProcessor(key: Uint8Array) {
|
||||
// Переиспользуемые буферы — не создаём новые на каждый фрейм
|
||||
const nonce = new Uint8Array(12);
|
||||
const nonceView = new DataView(nonce.buffer);
|
||||
// Переиспользуемый nonce буфер — нет аллокаций на каждый фрейм
|
||||
const nonce = new Uint8Array(sodium.crypto_stream_chacha20_NONCEBYTES); // 8 bytes
|
||||
|
||||
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);
|
||||
|
||||
// Обновляем 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;
|
||||
};
|
||||
}
|
||||
|
||||
function createTransform(processFrame: (data: ArrayBuffer) => ArrayBuffer) {
|
||||
let frames = 0;
|
||||
let slowFrames = 0;
|
||||
let total = 0;
|
||||
let max = 0;
|
||||
|
||||
return new TransformStream<any, any>({
|
||||
transform(frame, controller) {
|
||||
const started = performance.now();
|
||||
|
||||
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);
|
||||
console.error("[E2EE] frame error:", e);
|
||||
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;
|
||||
senderAttached.add(sender);
|
||||
|
||||
await initE2EE();
|
||||
|
||||
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;
|
||||
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 processFrame = createFrameProcessor(key);
|
||||
const processFrame = createFrameProcessor(key.slice(0, sodium.crypto_stream_chacha20_KEYBYTES));
|
||||
|
||||
readable
|
||||
.pipeThrough(createTransform(processFrame))
|
||||
.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> {
|
||||
if (receiverAttached.has(receiver)) return;
|
||||
receiverAttached.add(receiver);
|
||||
|
||||
await initE2EE();
|
||||
|
||||
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;
|
||||
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 processFrame = createFrameProcessor(key);
|
||||
const processFrame = createFrameProcessor(key.slice(0, sodium.crypto_stream_chacha20_KEYBYTES));
|
||||
|
||||
readable
|
||||
.pipeThrough(createTransform(processFrame))
|
||||
.pipeTo(writable)
|
||||
.catch((e) => console.error("Receiver E2EE pipeline failed:", e));
|
||||
.catch((e) => console.error("[E2EE] Receiver pipeline failed:", e));
|
||||
}
|
||||
Reference in New Issue
Block a user