diff --git a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java
index 36051cbd3..4c903d8bc 100644
--- a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java
+++ b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java
@@ -156,23 +156,30 @@ public void disposeCallFactory(Promise promise) {
}
/**
- * Disposes the live factory in order — PeerConnections, then tracks, then the factory + its ADM —
- * and returns whether a factory was disposed. A libwebrtc {@code PeerConnectionFactory} must not
- * be disposed while PCs or tracks from it are still alive (use-after-free), so its dependents go
- * first. Shared by {@link #disposeCallFactory} (leave) and {@link #createCallFactory} (replacing a
- * stale bare-fork default at join).
- *
- *
Reference-counted: when the factory is shared by concurrent calls, only the LAST consumer's
- * release actually tears it down. Earlier releases keep the factory (and its PCs/tracks) intact
- * for the remaining call(s) — the leaving call's own PCs were already disposed by its {@code
- * leave}, so its ids are gone from the owned sets before this runs.
+ * Disposes the live factory and its dependents in order: streams → PeerConnections → owned tracks
+ * → factory + ADM. Dependents go before the factory (disposing a libwebrtc factory with live
+ * PCs/tracks is a use-after-free); streams go first so {@code removeTrack()} runs while their
+ * tracks are still alive. Also clears {@code localStreams}, otherwise only released in
+ * {@link #invalidate()} (else it leaks across join/leave). No-op unless this is the last
+ * reference; returns whether it disposed the factory.
*/
private boolean disposeCurrentFactoryOrdered() {
if (!factoryRegistry.releaseReference()) {
return false;
}
- // 1. Dispose the factory's PeerConnections first.
+ for (Map.Entry entry : localStreams.entrySet()) {
+ try {
+ MediaStream stream = entry.getValue();
+ for (AudioTrack t : new ArrayList<>(stream.audioTracks)) stream.removeTrack(t);
+ for (VideoTrack t : new ArrayList<>(stream.videoTracks)) stream.removeTrack(t);
+ stream.dispose();
+ } catch (Exception e) {
+ Log.w(TAG, "disposeCurrentFactoryOrdered(): error disposing stream " + entry.getKey(), e);
+ }
+ }
+ localStreams.clear();
+
for (int pcId : factoryRegistry.currentOwnedPcIds()) {
try {
PeerConnectionObserver pco = mPeerConnectionObservers.get(pcId);
@@ -186,8 +193,6 @@ private boolean disposeCurrentFactoryOrdered() {
}
}
- // 2. Stop + dispose owned tracks (e.g. a camera capturer adopted from the lobby
- // preview) so the camera2 session is fully closed before the VideoSources are freed.
for (String trackId : factoryRegistry.currentOwnedTrackIds()) {
try {
getUserMediaImpl.disposeTrack(trackId);
@@ -196,7 +201,6 @@ private boolean disposeCurrentFactoryOrdered() {
}
}
- // 3. Now it is safe to dispose the factory + its ADM.
return factoryRegistry.disposeCurrent();
}
diff --git a/ios/RCTWebRTC/Utils/PeerConnectionFactory/PeerConnectionFactoryRegistry.swift b/ios/RCTWebRTC/Utils/PeerConnectionFactory/PeerConnectionFactoryRegistry.swift
index 08504b311..7c2d1d00c 100644
--- a/ios/RCTWebRTC/Utils/PeerConnectionFactory/PeerConnectionFactoryRegistry.swift
+++ b/ios/RCTWebRTC/Utils/PeerConnectionFactory/PeerConnectionFactoryRegistry.swift
@@ -65,6 +65,16 @@ public typealias PeerConnectionFactoryBuilder = (_ factoryId: String, _ bypassVo
return currentFactory
}
+ /// True when the live factory is the lazily-built bare-fork default (no per-call factory has
+ /// taken its place). Lets the module tear a stale default down in order before building the
+ /// call factory, matching the Android registry.
+ @objc public func isBareForkDefaultLive() -> Bool {
+ lock.lock()
+ defer { lock.unlock() }
+ guard let currentFactory = currentFactory, !currentFactory.isDisposed() else { return false }
+ return currentIsBareForkDefault
+ }
+
@objc public func create(_ bypassVoiceProcessing: Bool) -> PeerConnectionFactoryProvider? {
lock.lock()
defer { lock.unlock() }
@@ -108,22 +118,35 @@ public typealias PeerConnectionFactoryBuilder = (_ factoryId: String, _ bypassVo
return factory
}
- /// Releases one consumer's reference to the live call factory. Actually disposes only when
- /// the LAST reference is released; when other concurrent-call consumers still
- /// hold it, it decrements and keeps the factory alive. Also false when nothing is live.
- @objc public func disposeCurrent() -> Bool {
+ /// Releases one consumer's reference to the live call factory. Returns true only when the LAST
+ /// reference is released; when other concurrent-call consumers still hold it, it decrements and
+ /// keeps the factory alive. Also false when nothing is live.
+ @objc public func releaseReference() -> Bool {
lock.lock()
defer { lock.unlock() }
guard let factory = currentFactory else {
- NSLog("[PCFactoryRegistry] disposeCurrent(): no live factory (already disposed?)")
return false
}
if currentRefCount > 1 {
currentRefCount -= 1
- NSLog("[PCFactoryRegistry] disposeCurrent(): factory %@ still shared; kept (refCount=%d)",
+ NSLog("[PCFactoryRegistry] releaseReference(): factory %@ still shared; kept (refCount=%d)",
factory.factoryId, currentRefCount)
return false
}
+ currentRefCount = 0
+ return true
+ }
+
+ /// Disposes the live factory + its ADM unconditionally and returns whether one was disposed.
+ /// Reference counting is handled by `releaseReference()`, which must be called first on the
+ /// leave / dispose path; this only performs the final teardown once the last reference is gone.
+ @objc public func disposeCurrent() -> Bool {
+ lock.lock()
+ defer { lock.unlock() }
+ guard let factory = currentFactory else {
+ NSLog("[PCFactoryRegistry] disposeCurrent(): no live factory (already disposed?)")
+ return false
+ }
let wasDefault = currentIsBareForkDefault
let factoryId = factory.factoryId
factory.dispose()
diff --git a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.h b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.h
index 082ea1709..f4f318c22 100644
--- a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.h
+++ b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.h
@@ -14,4 +14,7 @@
- (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack;
- (RTCMediaStreamTrack *)trackForId:(nonnull NSString *)trackId pcId:(nonnull NSNumber *)pcId;
+
+- (void)mediaStreamTrackRelease:(nonnull NSString *)trackID;
+- (void)mediaStreamRelease:(nonnull NSString *)streamID;
@end
\ No newline at end of file
diff --git a/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.h b/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.h
index 6c4b38b96..60099a12d 100644
--- a/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.h
+++ b/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.h
@@ -16,4 +16,7 @@
+ (RTCCertificate *)getCertificate:(NSString *)certId;
+- (void)peerConnectionClose:(nonnull NSNumber *)objectID;
+- (void)peerConnectionDispose:(nonnull NSNumber *)objectID;
+
@end
diff --git a/ios/RCTWebRTC/WebRTCModule.m b/ios/RCTWebRTC/WebRTCModule.m
index f932d85d7..ffde484d5 100644
--- a/ios/RCTWebRTC/WebRTCModule.m
+++ b/ios/RCTWebRTC/WebRTCModule.m
@@ -9,6 +9,7 @@
#import "AudioDeviceModuleObserver.h"
#import "RTCCameraPreviewViewManager.h"
+#import "WebRTCModule+RTCMediaStream.h"
#import "WebRTCModule+RTCPeerConnection.h"
#import "WebRTCModule.h"
#import "WebRTCModuleOptions.h"
@@ -199,6 +200,14 @@ - (dispatch_queue_t)methodQueue {
: (RCTPromiseResolveBlock)resolve rejecter
: (RCTPromiseRejectBlock)reject) {
BOOL bypassVoiceProcessing = [options[@"bypassVoiceProcessing"] boolValue];
+
+ // This makes default factory being disposed in a proper sequence.
+ if ([self.factoryRegistry isBareForkDefaultLive]) {
+ RCTLogInfo(@"createCallFactory(): tearing down stale bare-fork default (ordered) before "
+ "creating the call factory");
+ [self disposeCurrentFactoryOrdered];
+ }
+
PeerConnectionFactoryProvider *factory = [self.factoryRegistry create:bypassVoiceProcessing];
if (factory == nil) {
reject(@"E_FACTORY_CREATE", @"Failed to create call factory: registry is disposed", nil);
@@ -210,7 +219,56 @@ - (dispatch_queue_t)methodQueue {
RCT_EXPORT_METHOD(disposeCallFactory
: (RCTPromiseResolveBlock)resolve rejecter
: (RCTPromiseRejectBlock)reject) {
- resolve(@([self.factoryRegistry disposeCurrent]));
+ resolve(@([self disposeCurrentFactoryOrdered]));
+}
+
+/**
+ * Disposes the live factory and its dependents in order: PeerConnections → local tracks → local
+ * streams → video-effects processor → factory + ADM. Everything is ARC-refcounted, so the factory
+ * is freed only when its LAST reference drops — every dependent that strong-refs it (PCs, tracks,
+ * streams, and the videoEffectProcessor associated object) must be released first or the factory
+ * leaks. No-op unless this is the last reference; returns whether it disposed the factory.
+ */
+- (BOOL)disposeCurrentFactoryOrdered {
+ if (![self.factoryRegistry releaseReference]) {
+ return NO;
+ }
+
+ for (NSNumber *pcId in [self.peerConnections.allKeys copy]) {
+ @try {
+ [self peerConnectionClose:pcId];
+ [self peerConnectionDispose:pcId];
+ } @catch (NSException *e) {
+ RCTLogWarn(@"disposeCurrentFactoryOrdered(): error disposing pc %@: %@", pcId, e.reason);
+ }
+ }
+
+ for (NSString *trackId in [self.localTracks.allKeys copy]) {
+ @try {
+ [self mediaStreamTrackRelease:trackId];
+ } @catch (NSException *e) {
+ RCTLogWarn(@"disposeCurrentFactoryOrdered(): error disposing track %@: %@", trackId, e.reason);
+ }
+ }
+
+ for (NSString *streamId in [self.localStreams.allKeys copy]) {
+ @try {
+ RTCMediaStream *stream = self.localStreams[streamId];
+ for (RTCAudioTrack *t in [stream.audioTracks copy]) {
+ [stream removeAudioTrack:t];
+ }
+ for (RTCVideoTrack *t in [stream.videoTracks copy]) {
+ [stream removeVideoTrack:t];
+ }
+ [self mediaStreamRelease:streamId];
+ } @catch (NSException *e) {
+ RCTLogWarn(@"disposeCurrentFactoryOrdered(): error disposing stream %@: %@", streamId, e.reason);
+ }
+ }
+
+ self.videoEffectProcessor = nil;
+
+ return [self.factoryRegistry disposeCurrent];
}
- (NSArray *)supportedEvents {