From 632ce1a68280e909034948ba1bb33d63cac3747d Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Mon, 28 Sep 2026 13:28:13 +0200 Subject: [PATCH 1/4] fix(android): return the observer-owned remote track from getTrackById getTrackById() looked up remote tracks by enumerating pc.getReceivers() and returning receiver.track(). On Android, PeerConnection.getReceivers() disposes every RtpReceiver wrapper from its previous call before creating new ones, and disposing a receiver disposes its cached VideoTrack, which detaches all of its sinks. So any later getReceivers() call on the same PeerConnection (E2EE decrypt() attaching to another receiver, getStats on a receiver, or getTrackById() itself) silently stopped frames reaching a recorder attached to the returned track. Return the stable wrapper kept in PeerConnectionObserver.remoteTracks instead. It is the same object RTCView renders through and getTrack() already reads, and getReceivers() never touches it. --- .../java/com/oney/WebRTCModule/WebRTCModule.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java index a0b5baf10..50587f472 100644 --- a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java +++ b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java @@ -725,17 +725,13 @@ public MediaStreamTrack getTrackById(String trackId) { if (local != null) { return local; } + // Use the track wrapper the observer keeps. Do not use pc.getReceivers() here: + // it disposes the wrappers from its previous call, which detaches their sinks. for (int i = 0, size = mPeerConnectionObservers.size(); i < size; i++) { PeerConnectionObserver pco = mPeerConnectionObservers.valueAt(i); - PeerConnection pc = pco.getPeerConnection(); - if (pc == null) { - continue; - } - for (RtpReceiver receiver : pc.getReceivers()) { - MediaStreamTrack track = receiver.track(); - if (track != null && trackId.equals(track.id())) { - return track; - } + MediaStreamTrack track = pco.remoteTracks.get(trackId); + if (track != null) { + return track; } } return null; From 4b8d346f0b88dcacda87462e2006e1118882f228 Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Mon, 28 Sep 2026 13:34:06 +0200 Subject: [PATCH 2/4] fix(android): contain exceptions in E2EE event forwarding An exception while converting an encryptionManagerEvent on the crypto worker, or while emitting it on the executor, now gets logged and the event dropped instead of propagating. Keys and transforms are unaffected. --- .../WebRTCModule/EncryptionManagerBridge.java | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/android/src/main/java/com/oney/WebRTCModule/EncryptionManagerBridge.java b/android/src/main/java/com/oney/WebRTCModule/EncryptionManagerBridge.java index 30e692cd7..55c496b90 100644 --- a/android/src/main/java/com/oney/WebRTCModule/EncryptionManagerBridge.java +++ b/android/src/main/java/com/oney/WebRTCModule/EncryptionManagerBridge.java @@ -292,34 +292,46 @@ private class EventObserver implements EncryptionManager.Observer { @Override public void onE2eeEvent(EncryptionManager.E2eeEvent event) { WritableMap params = Arguments.createMap(); - params.putString("managerId", handle); - params.putString("type", event.type.name); - params.putString("userId", event.userId); - - if (event.trackType != null) { - params.putInt("trackType", event.trackType.getValue()); - } - if (event.keyIndex != null) { - params.putInt("keyIndex", event.keyIndex); - } - if (event.version != null) { - params.putInt("version", event.version); - } - if (event.reason != null) { - params.putString("reason", event.reason); - } - if (event.keyState != null) { - params.putMap("keyState", keyStateToMap(event.keyState)); - } - if (event.encode != null) { - params.putArray("encode", trackPerfToArray(event.encode)); - } - if (event.decode != null) { - params.putArray("decode", trackPerfToArray(event.decode)); + try { + params.putString("managerId", handle); + params.putString("type", event.type.name); + params.putString("userId", event.userId); + + if (event.trackType != null) { + params.putInt("trackType", event.trackType.getValue()); + } + if (event.keyIndex != null) { + params.putInt("keyIndex", event.keyIndex); + } + if (event.version != null) { + params.putInt("version", event.version); + } + if (event.reason != null) { + params.putString("reason", event.reason); + } + if (event.keyState != null) { + params.putMap("keyState", keyStateToMap(event.keyState)); + } + if (event.encode != null) { + params.putArray("encode", trackPerfToArray(event.encode)); + } + if (event.decode != null) { + params.putArray("decode", trackPerfToArray(event.decode)); + } + } catch (RuntimeException e) { + // A broken event is dropped; keys and transforms are not affected. + Log.w(TAG, "onE2eeEvent(): dropping event", e); + return; } // This callback runs on the crypto worker; events must be emitted off it. - ThreadUtils.runOnExecutor(() -> webRTCModule.sendEvent("encryptionManagerEvent", params)); + ThreadUtils.runOnExecutor(() -> { + try { + webRTCModule.sendEvent("encryptionManagerEvent", params); + } catch (RuntimeException e) { + Log.w(TAG, "onE2eeEvent(): dropping event", e); + } + }); } } From bf197ddf2ed301659147f7684162ffb4a3b6e33c Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Mon, 28 Sep 2026 13:36:26 +0200 Subject: [PATCH 3/4] docs: clarify E2EE manager teardown comments The comments in Android invalidate() and iOS dealloc read as if disposing the E2EE managers before the peer connections were needed for correctness. They are disposed there because JS is gone and nothing else can release them; the order does not matter for safety. --- .../src/main/java/com/oney/WebRTCModule/WebRTCModule.java | 5 +++-- ios/RCTWebRTC/WebRTCModule.m | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java index 50587f472..5bf9de99e 100644 --- a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java +++ b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java @@ -267,8 +267,9 @@ public void invalidate() { try { ThreadUtils .submitToExecutor(() -> { - // 0. Dispose E2EE managers: their frame transforms are held by the senders and - // receivers of the PeerConnections disposed next. + // 0. Dispose E2EE managers. JS is gone after a reload, so nothing else can + // release them. The order relative to the PeerConnections does not matter + // for safety; on a normal leave the PeerConnections go first. encryptionManagerBridge.disposeAll(); // 1. Dispose PeerConnections (dispose() calls close() internally) diff --git a/ios/RCTWebRTC/WebRTCModule.m b/ios/RCTWebRTC/WebRTCModule.m index f8902de2e..73149496c 100644 --- a/ios/RCTWebRTC/WebRTCModule.m +++ b/ios/RCTWebRTC/WebRTCModule.m @@ -37,8 +37,8 @@ + (BOOL)requiresMainQueueSetup { } - (void)dealloc { - // E2EE managers first: their frame transforms are held by the senders and receivers of the peer - // connections closed below, and nothing in JS survives to dispose them. + // Dispose E2EE managers. JS is gone, so nothing else can release them. The order + // relative to the peer connections does not matter for safety. for (NSString *handle in _encryptionManagers) { RTC_OBJC_TYPE(RTCEncryptionManager) *manager = _encryptionManagers[handle]; manager.delegate = nil; From bf9781ed2acccf5e253056ab850ecdafb2cf1865 Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Mon, 28 Sep 2026 13:38:24 +0200 Subject: [PATCH 4/4] docs: document encrypt() attachment timing Note in the encrypt() JSDoc that the transform should be attached before the sender has a track or is negotiated to send, since frames sent before attachment go out unencrypted. --- src/RTCEncryptionManager.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/RTCEncryptionManager.ts b/src/RTCEncryptionManager.ts index 45f74d17a..541593cdc 100644 --- a/src/RTCEncryptionManager.ts +++ b/src/RTCEncryptionManager.ts @@ -104,6 +104,9 @@ export default class RTCEncryptionManager { * `codec` is an exact lowercase pin (`opus`/`vp8`/`vp9`/`h264`); anything else fails closed. * Omitting it reads the codec from the frame. Omitting `trackType` defaults to audio vs video * from the sender, so screen-share types must be passed explicitly. + * + * Call this before the sender has a track or is negotiated to send. Frames sent + * before the transform is attached go out unencrypted. */ encrypt(sender: RTCRtpSender, codec?: string, trackType?: RTCEncryptionTrackType): void { this._invoke('encryptionManagerEncrypt', {