@@ -47,6 +47,7 @@ const E2EE_VERSION = 1;
4747const TRAILER_LEN = 12; // 4 frameCounter + 1 keyIndex + 2 clearBytes + 1 version + 4 magic
4848const IV_LEN = 12;
4949const RBSP_FLAG = 0x8000; // bit 15 of the 2-byte clearBytes field signals RBSP escaping
50+ const EMPTY_AAD = new Uint8Array(0);
5051
5152// ---- Key Store ----
5253
@@ -160,14 +161,11 @@ function getLatestKey(userId) {
160161 return null;
161162}
162163
163- // IV = [8-byte prefix from SHA-256(rawKey + userId)][4-byte frame counter big-endian]
164- // The prefix ensures unique IVs across users and key rotations.
165- // The counter ensures unique IVs within a session.
166- function buildIV(prefix, frameCounter) {
167- const iv = new Uint8Array(IV_LEN);
164+ // Fill a pre-allocated IV buffer: [8-byte prefix][4-byte frame counter big-endian].
165+ // Called per-frame; the iv/ivView are allocated once per transform to reduce GC pressure.
166+ function fillIV(iv, ivView, prefix, frameCounter) {
168167 iv.set(prefix, 0);
169- new DataView(iv.buffer).setUint32(8, frameCounter);
170- return iv.buffer;
168+ ivView.setUint32(8, frameCounter);
171169}
172170
173171function nextFrameCounter(userId) {
@@ -299,6 +297,8 @@ let e2eeActive = true;
299297let perfEnabled = false;
300298let perfInterval = null;
301299let encodeFrameCount = 0;
300+ let encodeMaxCryptoMs = 0;
301+ let decodeMaxCryptoMs = 0;
302302// Map<userId, number>
303303const decodeFrameCounts = new Map();
304304
@@ -312,10 +312,13 @@ function startPerfReport() {
312312 }
313313 self.postMessage({
314314 type: 'perf-report',
315- encode: { fps: encodeFrameCount },
315+ encode: { fps: encodeFrameCount, maxCryptoMs: encodeMaxCryptoMs },
316316 decode,
317+ decodeMaxCryptoMs,
317318 });
318319 encodeFrameCount = 0;
320+ encodeMaxCryptoMs = 0;
321+ decodeMaxCryptoMs = 0;
319322 }, 1000);
320323}
321324
@@ -326,16 +329,23 @@ function stopPerfReport() {
326329 perfInterval = null;
327330 }
328331 encodeFrameCount = 0;
332+ encodeMaxCryptoMs = 0;
333+ decodeMaxCryptoMs = 0;
329334 decodeFrameCounts.clear();
330335}
331336
332337// ---- Transforms ----
333338
334- function encodeTransform(userId, codec) {
339+ async function encodeTransform(userId, codec) {
335340 const isNalu = codec === 'h264';
336- // Pre-compute shared key IV prefix at transform creation (not per-frame)
341+ // Await IV prefix computation so the first frames aren't silently dropped
337342 const latestEntry = getLatestKey(userId);
338- if (latestEntry && sharedKey) ensureIVPrefix(userId, latestEntry.keyIndex);
343+ if (latestEntry && sharedKey) await ensureIVPrefix(userId, latestEntry.keyIndex);
344+
345+ // Pre-allocate reusable IV buffer — safe because TransformStream
346+ // processes frames sequentially (next transform call waits for await).
347+ const iv = new Uint8Array(IV_LEN);
348+ const ivView = new DataView(iv.buffer);
339349
340350 return new TransformStream({
341351 async transform(frame, controller) {
@@ -358,16 +368,21 @@ function encodeTransform(userId, codec) {
358368 const src = new Uint8Array(frame.data);
359369 const clearBytes = getClearByteCount(codec, frame.type, src);
360370 const counter = nextFrameCounter(userId);
361- const iv = buildIV( prefix, counter);
362- const aad = clearBytes > 0 ? src.subarray(0, clearBytes) : new Uint8Array(0) ;
363- const plaintext = src.subarray(clearBytes);
371+ fillIV(iv, ivView, prefix, counter);
372+ const aad = clearBytes > 0 ? src.subarray(0, clearBytes) : EMPTY_AAD ;
373+ const plaintext = clearBytes > 0 ? src.subarray(clearBytes) : src ;
364374
365375 try {
376+ const t0 = perfEnabled ? performance.now() : 0;
366377 const encrypted = await crypto.subtle.encrypt(
367378 { name: 'AES-GCM', iv, additionalData: aad },
368379 cryptoKey,
369380 plaintext,
370381 );
382+ if (perfEnabled) {
383+ const dt = performance.now() - t0;
384+ if (dt > encodeMaxCryptoMs) encodeMaxCryptoMs = dt;
385+ }
371386 const ciphertext = new Uint8Array(encrypted);
372387
373388 if (isNalu && clearBytes > 0) {
@@ -394,7 +409,7 @@ function encodeTransform(userId, codec) {
394409 });
395410}
396411
397- function decodeTransform(userId) {
412+ async function decodeTransform(userId) {
398413 // Throttle failure notifications to avoid flooding the main thread.
399414 let lastFailureNotification = 0;
400415 const FAILURE_THROTTLE_MS = 1000;
@@ -407,8 +422,13 @@ function decodeTransform(userId) {
407422 }
408423 }
409424
410- // Pre-compute shared key IV prefix at transform creation (not per-frame)
411- if (sharedKey) ensureIVPrefix(userId, sharedKey.keyIndex);
425+ // Await IV prefix computation so the first frames aren't silently dropped
426+ if (sharedKey) await ensureIVPrefix(userId, sharedKey.keyIndex);
427+
428+ // Pre-allocate reusable IV buffer — safe because TransformStream
429+ // processes frames sequentially (next transform call waits for await).
430+ const iv = new Uint8Array(IV_LEN);
431+ const ivView = new DataView(iv.buffer);
412432
413433 return new TransformStream({
414434 async transform(frame, controller) {
@@ -445,8 +465,8 @@ function decodeTransform(userId) {
445465 }
446466
447467 const bodyEnd = src.length - TRAILER_LEN;
448- const iv = buildIV( prefix, frameCounter);
449- const aad = clearBytes > 0 ? src.subarray(0, clearBytes) : new Uint8Array(0) ;
468+ fillIV(iv, ivView, prefix, frameCounter);
469+ const aad = clearBytes > 0 ? src.subarray(0, clearBytes) : EMPTY_AAD ;
450470
451471 try {
452472 let ciphertext;
@@ -456,11 +476,16 @@ function decodeTransform(userId) {
456476 ciphertext = src.subarray(clearBytes, bodyEnd);
457477 }
458478
479+ const t0 = perfEnabled ? performance.now() : 0;
459480 const decrypted = await crypto.subtle.decrypt(
460481 { name: 'AES-GCM', iv, additionalData: aad },
461482 cryptoKey,
462483 ciphertext,
463484 );
485+ if (perfEnabled) {
486+ const dt = performance.now() - t0;
487+ if (dt > decodeMaxCryptoMs) decodeMaxCryptoMs = dt;
488+ }
464489
465490 const plaintext = new Uint8Array(decrypted);
466491 const dst = new Uint8Array(clearBytes + plaintext.length);
@@ -481,15 +506,22 @@ function decodeTransform(userId) {
481506
482507// ---- Message handling ----
483508
484- function setupTransform({ readable, writable, operation, userId, codec }) {
509+ async function setupTransform({ readable, writable, operation, userId, codec }) {
485510 const transform = operation === 'encode'
486- ? encodeTransform(userId, codec)
487- : decodeTransform(userId);
488- readable.pipeThrough(transform).pipeTo(writable);
511+ ? await encodeTransform(userId, codec)
512+ : await decodeTransform(userId);
513+ readable.pipeThrough(transform).pipeTo(writable).catch((err) => {
514+ self.postMessage({
515+ type: 'error',
516+ message: 'Transform pipeline error (' + operation + ', ' + userId + '): ' + (err && err.message || err),
517+ });
518+ });
489519}
490520
491521addEventListener('rtctransform', ({ transformer: { readable, writable, options } }) => {
492- setupTransform({ readable, writable, ...options });
522+ setupTransform({ readable, writable, ...options }).catch((err) => {
523+ self.postMessage({ type: 'error', message: 'Transform setup failed: ' + (err && err.message || err) });
524+ });
493525});
494526
495527addEventListener('message', ({ data }) => {
@@ -521,7 +553,9 @@ addEventListener('message', ({ data }) => {
521553 break;
522554 default:
523555 // Transform setup (Insertable Streams fallback path)
524- setupTransform(data);
556+ setupTransform(data).catch((err) => {
557+ self.postMessage({ type: 'error', message: 'Transform setup failed: ' + (err && err.message || err) });
558+ });
525559 break;
526560 }
527561});
0 commit comments