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
11 changes: 11 additions & 0 deletions ios/RCTWebRTC/AudioDeviceModuleObserver.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ NS_ASSUME_NONNULL_BEGIN

- (instancetype)initWithWebRTCModule:(WebRTCModule *)module;

// Tracks whether each JS handler is registered. When NO, the observer returns
// immediately without a JS round trip, avoiding the deadlock window entirely.
// Atomic because they are written on the JS thread (handler registration) and
// read on the native audio thread (delegate callbacks).
@property(atomic, assign) BOOL isEngineCreatedActive;
@property(atomic, assign) BOOL isWillEnableEngineActive;
@property(atomic, assign) BOOL isWillStartEngineActive;
@property(atomic, assign) BOOL isDidStopEngineActive;
@property(atomic, assign) BOOL isDidDisableEngineActive;
@property(atomic, assign) BOOL isWillReleaseEngineActive;

// Methods to receive results from JS. requestId echoes the id sent with the
// corresponding event so stale responses from timed-out rounds can be dropped.
- (void)resolveEngineCreatedWithRequestId:(NSInteger)requestId result:(NSInteger)result;
Expand Down
124 changes: 94 additions & 30 deletions ios/RCTWebRTC/AudioDeviceModuleObserver.m
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ - (instancetype)initWithWebRTCModule:(WebRTCModule *)module {
_didStopEngineSemaphore = dispatch_semaphore_create(0);
_didDisableEngineSemaphore = dispatch_semaphore_create(0);
_willReleaseEngineSemaphore = dispatch_semaphore_create(0);

// Default every hook to active so a delegate callback that fires before JS
// has reconciled its handler state (e.g. a recreated observer) still does the
// bounded round trip rather than silently skipping a handler's veto. JS
// reconciles these to the real handler state in setupListeners().
_isEngineCreatedActive = YES;
_isWillEnableEngineActive = YES;
_isWillStartEngineActive = YES;
_isDidStopEngineActive = YES;
_isDidDisableEngineActive = YES;
_isWillReleaseEngineActive = YES;
}
return self;
}
Expand All @@ -87,11 +98,22 @@ - (instancetype)initWithWebRTCModule:(WebRTCModule *)module {
// already given up). No legitimate signal for *this* round can exist at drain
// time because the event has not been sent yet.
//
// Returns the JS-provided result on success, or 0 on timeout.
// If isActive is NO, returns 0 immediately without a JS round trip, avoiding the
// deadlock window when no handler is registered.
//
// Returns the JS-provided result on success, or 0 on timeout or when inactive.
- (NSInteger)sendEventAndWaitWithName:(NSString *)eventName
body:(NSDictionary *)body
semaphore:(dispatch_semaphore_t)semaphore
resultBlock:(NSInteger (^)(void))resultBlock {
resultBlock:(NSInteger (^)(void))resultBlock
isActive:(BOOL)isActive {
if (!isActive) {
// No handler registered, proceed immediately without JS round trip.
// This avoids the deadlock window entirely.
os_log_debug(ADMObserverLog(), "Skipping JS round-trip for %{public}@ (no handler registered)", eventName);
return 0;
}

NSInteger requestId;
@synchronized(self) {
requestId = ++self.requestIdSeq;
Expand Down Expand Up @@ -139,26 +161,37 @@ - (void)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule
}

- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule didCreateEngine:(AVAudioEngine *)engine {
RCTLog(@"[AudioDeviceModuleObserver] Engine created - waiting for JS response");
BOOL isActive = self.isEngineCreatedActive;

if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine created - waiting for JS response");
}

NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineCreated
body:@{}
semaphore:self.engineCreatedSemaphore
resultBlock:^NSInteger {
return self.engineCreatedResult;
}];
}
isActive:isActive];

RCTLog(@"[AudioDeviceModuleObserver] Engine created - JS returned: %ld", (long)result);
if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine created - JS returned: %ld", (long)result);
}
return result;
}

- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule
willEnableEngine:(AVAudioEngine *)engine
isPlayoutEnabled:(BOOL)isPlayoutEnabled
isRecordingEnabled:(BOOL)isRecordingEnabled {
RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - playout: %d, recording: %d - waiting for JS response",
isPlayoutEnabled,
isRecordingEnabled);
BOOL isActive = self.isWillEnableEngineActive;

if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - playout: %d, recording: %d - waiting for JS response",
isPlayoutEnabled,
isRecordingEnabled);
}

NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineWillEnable
body:@{
Expand All @@ -168,12 +201,15 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule
semaphore:self.willEnableEngineSemaphore
resultBlock:^NSInteger {
return self.willEnableEngineResult;
}];
}
isActive:isActive];

RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - JS returned: %ld", (long)result);
if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - JS returned: %ld", (long)result);

AVAudioSession *audioSession = [AVAudioSession sharedInstance];
RCTLog(@"[AudioDeviceModuleObserver] Audio session category: %@", audioSession.category);
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
RCTLog(@"[AudioDeviceModuleObserver] Audio session category: %@", audioSession.category);
}

return result;
}
Expand All @@ -182,9 +218,13 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule
willStartEngine:(AVAudioEngine *)engine
isPlayoutEnabled:(BOOL)isPlayoutEnabled
isRecordingEnabled:(BOOL)isRecordingEnabled {
RCTLog(@"[AudioDeviceModuleObserver] Engine will start - playout: %d, recording: %d - waiting for JS response",
isPlayoutEnabled,
isRecordingEnabled);
BOOL isActive = self.isWillStartEngineActive;

if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine will start - playout: %d, recording: %d - waiting for JS response",
isPlayoutEnabled,
isRecordingEnabled);
}

NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineWillStart
body:@{
Expand All @@ -194,19 +234,26 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule
semaphore:self.willStartEngineSemaphore
resultBlock:^NSInteger {
return self.willStartEngineResult;
}];
}
isActive:isActive];

RCTLog(@"[AudioDeviceModuleObserver] Engine will start - JS returned: %ld", (long)result);
if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine will start - JS returned: %ld", (long)result);
}
return result;
}

- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule
didStopEngine:(AVAudioEngine *)engine
isPlayoutEnabled:(BOOL)isPlayoutEnabled
isRecordingEnabled:(BOOL)isRecordingEnabled {
RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - playout: %d, recording: %d - waiting for JS response",
isPlayoutEnabled,
isRecordingEnabled);
BOOL isActive = self.isDidStopEngineActive;

if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - playout: %d, recording: %d - waiting for JS response",
isPlayoutEnabled,
isRecordingEnabled);
}

NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineDidStop
body:@{
Expand All @@ -216,19 +263,26 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule
semaphore:self.didStopEngineSemaphore
resultBlock:^NSInteger {
return self.didStopEngineResult;
}];
}
isActive:isActive];

RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - JS returned: %ld", (long)result);
if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - JS returned: %ld", (long)result);
}
return result;
}

- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule
didDisableEngine:(AVAudioEngine *)engine
isPlayoutEnabled:(BOOL)isPlayoutEnabled
isRecordingEnabled:(BOOL)isRecordingEnabled {
RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - playout: %d, recording: %d - waiting for JS response",
isPlayoutEnabled,
isRecordingEnabled);
BOOL isActive = self.isDidDisableEngineActive;

if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - playout: %d, recording: %d - waiting for JS response",
isPlayoutEnabled,
isRecordingEnabled);
}

NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineDidDisable
body:@{
Expand All @@ -238,23 +292,33 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule
semaphore:self.didDisableEngineSemaphore
resultBlock:^NSInteger {
return self.didDisableEngineResult;
}];
}
isActive:isActive];

RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - JS returned: %ld", (long)result);
if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - JS returned: %ld", (long)result);
}
return result;
}

- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule willReleaseEngine:(AVAudioEngine *)engine {
RCTLog(@"[AudioDeviceModuleObserver] Engine will release - waiting for JS response");
BOOL isActive = self.isWillReleaseEngineActive;

if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine will release - waiting for JS response");
}

NSInteger result = [self sendEventAndWaitWithName:kEventAudioDeviceModuleEngineWillRelease
body:@{}
semaphore:self.willReleaseEngineSemaphore
resultBlock:^NSInteger {
return self.willReleaseEngineResult;
}];
}
isActive:isActive];

RCTLog(@"[AudioDeviceModuleObserver] Engine will release - JS returned: %ld", (long)result);
if (isActive) {
RCTLog(@"[AudioDeviceModuleObserver] Engine will release - JS returned: %ld", (long)result);
}
return result;
}

Expand Down
32 changes: 32 additions & 0 deletions ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,36 @@ @implementation WebRTCModule (RTCAudioDeviceModule)
return nil;
}

#pragma mark - Handler Active State

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetEngineCreatedActive : (BOOL)isActive) {
self.audioDeviceModuleObserver.isEngineCreatedActive = isActive;
return nil;
}

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetWillEnableEngineActive : (BOOL)isActive) {
self.audioDeviceModuleObserver.isWillEnableEngineActive = isActive;
return nil;
}

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetWillStartEngineActive : (BOOL)isActive) {
self.audioDeviceModuleObserver.isWillStartEngineActive = isActive;
return nil;
}

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetDidStopEngineActive : (BOOL)isActive) {
self.audioDeviceModuleObserver.isDidStopEngineActive = isActive;
return nil;
}

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetDidDisableEngineActive : (BOOL)isActive) {
self.audioDeviceModuleObserver.isDidDisableEngineActive = isActive;
return nil;
}

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetWillReleaseEngineActive : (BOOL)isActive) {
self.audioDeviceModuleObserver.isWillReleaseEngineActive = isActive;
return nil;
}

@end
Loading