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 {