Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
* <p>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<String, MediaStream> 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);
Expand All @@ -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);
Expand All @@ -196,7 +201,6 @@ private boolean disposeCurrentFactoryOrdered() {
}
}

// 3. Now it is safe to dispose the factory + its ADM.
return factoryRegistry.disposeCurrent();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions ios/RCTWebRTC/WebRTCModule+RTCMediaStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@

+ (RTCCertificate *)getCertificate:(NSString *)certId;

- (void)peerConnectionClose:(nonnull NSNumber *)objectID;
- (void)peerConnectionDispose:(nonnull NSNumber *)objectID;

@end
60 changes: 59 additions & 1 deletion ios/RCTWebRTC/WebRTCModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#import "AudioDeviceModuleObserver.h"
#import "RTCCameraPreviewViewManager.h"
#import "WebRTCModule+RTCMediaStream.h"
#import "WebRTCModule+RTCPeerConnection.h"
#import "WebRTCModule.h"
#import "WebRTCModuleOptions.h"
Expand Down Expand Up @@ -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);
Expand All @@ -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<NSString *> *)supportedEvents {
Expand Down
Loading