From 857c741db52d5fba95b028bbdb97fe8710973b55 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Wed, 28 Jan 2026 14:26:58 +0100 Subject: [PATCH 01/15] chore: adjusted audio track disabling logic --- ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m index e64b32218..27c51980c 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m @@ -18,6 +18,12 @@ #import "TrackCapturerEventsEmitter.h" #import "VideoCaptureController.h" +#if __has_include() +#import +#elif __has_include("stream_react_native_webrtc-Swift.h") +#import "stream_react_native_webrtc-Swift.h" +#endif + @implementation WebRTCModule (RTCMediaStream) - (VideoEffectProcessor *)videoEffectProcessor @@ -535,7 +541,12 @@ - (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack { return; } - track.isEnabled = enabled; + if ([track.kind isEqual:@"audio"]) { + [[self audioDeviceModule] setRecording:enabled error:nil]; + } else { + track.isEnabled = enabled; + } + #if !TARGET_OS_TV if (track.captureController) { // It could be a remote track! if (enabled) { From cea4426d81275822f25963dbee5e535a1e832c63 Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Wed, 28 Jan 2026 15:43:52 +0100 Subject: [PATCH 02/15] fix: only do ADM config for local tracks --- ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m index 27c51980c..cb62a9437 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m @@ -481,9 +481,12 @@ - (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack { // Clean up dimension detection for local video tracks if ([track.kind isEqualToString:@"video"]) { [self removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)track]; + } else { + // disable recording for local audio tracks + [[self audioDeviceModule] setRecording:false error:nil]; } - track.isEnabled = NO; +// track.isEnabled = NO; [track.captureController stopCapture]; [self.localTracks removeObjectForKey:trackID]; } @@ -542,10 +545,15 @@ - (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack { } if ([track.kind isEqual:@"audio"]) { - [[self audioDeviceModule] setRecording:enabled error:nil]; - } else { - track.isEnabled = enabled; + RTCMediaStreamTrack *track = self.localTracks[trackID]; + if (track) { + [[self audioDeviceModule] setRecording:enabled error:nil]; + } + return; } + + track.isEnabled = enabled; + #if !TARGET_OS_TV if (track.captureController) { // It could be a remote track! From fa9b7579fdab58c520ddceaedc9544e0fc62b2cf Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Wed, 28 Jan 2026 16:24:23 +0100 Subject: [PATCH 03/15] reenable isEnabled --- ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m index cb62a9437..6633d23d6 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m @@ -147,7 +147,6 @@ - (NSArray *)createMediaStream:(NSArray *)tracks { return @[ mediaStreamId, trackInfos ]; #endif } - /** * Initializes a new {@link RTCVideoTrack} which satisfies the given constraints. */ @@ -486,7 +485,9 @@ - (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack { [[self audioDeviceModule] setRecording:false error:nil]; } -// track.isEnabled = NO; + if (track.isEnabled) { + track.isEnabled = NO; + } [track.captureController stopCapture]; [self.localTracks removeObjectForKey:trackID]; } @@ -549,7 +550,6 @@ - (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack { if (track) { [[self audioDeviceModule] setRecording:enabled error:nil]; } - return; } track.isEnabled = enabled; From 8c2ef04dd7a982e3160c71c7eb0d3025a7e4747b Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sat, 4 Oct 2025 21:23:31 +0800 Subject: [PATCH 04/15] wip - new swm # Conflicts: # ios/RCTWebRTC/WebRTCModule.h # ios/RCTWebRTC/WebRTCModule.m # src/index.ts --- ios/RCTWebRTC/AudioDeviceModuleObserver.h | 20 ++ ios/RCTWebRTC/AudioDeviceModuleObserver.m | 219 ++++++++++++++++ .../WebRTCModule+RTCAudioDeviceModule.h | 5 + .../WebRTCModule+RTCAudioDeviceModule.m | 215 +++++++++++++++ ios/RCTWebRTC/WebRTCModule.h | 18 ++ ios/RCTWebRTC/WebRTCModule.m | 20 +- src/AudioDeviceModule.ts | 246 ++++++++++++++++++ src/AudioDeviceModuleEvents.ts | 199 ++++++++++++++ src/index.ts | 7 +- 9 files changed, 947 insertions(+), 2 deletions(-) create mode 100644 ios/RCTWebRTC/AudioDeviceModuleObserver.h create mode 100644 ios/RCTWebRTC/AudioDeviceModuleObserver.m create mode 100644 ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.h create mode 100644 ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m create mode 100644 src/AudioDeviceModule.ts create mode 100644 src/AudioDeviceModuleEvents.ts diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.h b/ios/RCTWebRTC/AudioDeviceModuleObserver.h new file mode 100644 index 000000000..c2c0e2500 --- /dev/null +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.h @@ -0,0 +1,20 @@ +#import +#import "WebRTCModule.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface AudioDeviceModuleObserver : NSObject + +- (instancetype)initWithWebRTCModule:(WebRTCModule *)module; + +// Methods to receive results from JS +- (void)resolveEngineCreatedWithResult:(NSInteger)result; +- (void)resolveWillEnableEngineWithResult:(NSInteger)result; +- (void)resolveWillStartEngineWithResult:(NSInteger)result; +- (void)resolveDidStopEngineWithResult:(NSInteger)result; +- (void)resolveDidDisableEngineWithResult:(NSInteger)result; +- (void)resolveWillReleaseEngineWithResult:(NSInteger)result; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.m b/ios/RCTWebRTC/AudioDeviceModuleObserver.m new file mode 100644 index 000000000..9a28d8cff --- /dev/null +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.m @@ -0,0 +1,219 @@ +#import "AudioDeviceModuleObserver.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface AudioDeviceModuleObserver () + +@property(weak, nonatomic) WebRTCModule *module; +@property(nonatomic, strong) dispatch_semaphore_t engineCreatedSemaphore; +@property(nonatomic, strong) dispatch_semaphore_t willEnableEngineSemaphore; +@property(nonatomic, strong) dispatch_semaphore_t willStartEngineSemaphore; +@property(nonatomic, strong) dispatch_semaphore_t didStopEngineSemaphore; +@property(nonatomic, strong) dispatch_semaphore_t didDisableEngineSemaphore; +@property(nonatomic, strong) dispatch_semaphore_t willReleaseEngineSemaphore; + +@property(nonatomic, assign) NSInteger engineCreatedResult; +@property(nonatomic, assign) NSInteger willEnableEngineResult; +@property(nonatomic, assign) NSInteger willStartEngineResult; +@property(nonatomic, assign) NSInteger didStopEngineResult; +@property(nonatomic, assign) NSInteger didDisableEngineResult; +@property(nonatomic, assign) NSInteger willReleaseEngineResult; + +@end + +@implementation AudioDeviceModuleObserver + +- (instancetype)initWithWebRTCModule:(WebRTCModule *)module { + self = [super init]; + if (self) { + self.module = module; + _engineCreatedSemaphore = dispatch_semaphore_create(0); + _willEnableEngineSemaphore = dispatch_semaphore_create(0); + _willStartEngineSemaphore = dispatch_semaphore_create(0); + _didStopEngineSemaphore = dispatch_semaphore_create(0); + _didDisableEngineSemaphore = dispatch_semaphore_create(0); + _willReleaseEngineSemaphore = dispatch_semaphore_create(0); + } + return self; +} + +#pragma mark - RTCAudioDeviceModuleDelegate + +- (void)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + didReceiveSpeechActivityEvent:(RTCSpeechActivityEvent)speechActivityEvent { + NSString *eventType = speechActivityEvent == RTCSpeechActivityEventStarted ? @"started" : @"ended"; + + [self.module sendEventWithName:kEventAudioDeviceModuleSpeechActivity + body:@{ + @"event" : eventType, + }]; + + RCTLog(@"[AudioDeviceModuleObserver] Speech activity event: %@", eventType); +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule didCreateEngine:(AVAudioEngine *)engine { + RCTLog(@"[AudioDeviceModuleObserver] Engine created - waiting for JS response"); + + [self.module sendEventWithName:kEventAudioDeviceModuleEngineCreated body:@{}]; + + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.engineCreatedSemaphore, DISPATCH_TIME_FOREVER); + + RCTLog(@"[AudioDeviceModuleObserver] Engine created - JS returned: %ld", (long)self.engineCreatedResult); + return self.engineCreatedResult; +} + +- (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); + + [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillEnable + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; + + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.willEnableEngineSemaphore, DISPATCH_TIME_FOREVER); + + RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - JS returned: %ld", (long)self.willEnableEngineResult); + return self.willEnableEngineResult; +} + +- (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); + + [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillStart + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; + + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.willStartEngineSemaphore, DISPATCH_TIME_FOREVER); + + RCTLog(@"[AudioDeviceModuleObserver] Engine will start - JS returned: %ld", (long)self.willStartEngineResult); + return self.willStartEngineResult; +} + +- (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); + + [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidStop + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; + + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.didStopEngineSemaphore, DISPATCH_TIME_FOREVER); + + RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - JS returned: %ld", (long)self.didStopEngineResult); + return self.didStopEngineResult; +} + +- (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); + + [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidDisable + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; + + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.didDisableEngineSemaphore, DISPATCH_TIME_FOREVER); + + RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - JS returned: %ld", (long)self.didDisableEngineResult); + return self.didDisableEngineResult; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule willReleaseEngine:(AVAudioEngine *)engine { + RCTLog(@"[AudioDeviceModuleObserver] Engine will release - waiting for JS response"); + + [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillRelease body:@{}]; + + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.willReleaseEngineSemaphore, DISPATCH_TIME_FOREVER); + + RCTLog(@"[AudioDeviceModuleObserver] Engine will release - JS returned: %ld", (long)self.willReleaseEngineResult); + return self.willReleaseEngineResult; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + engine:(AVAudioEngine *)engine + configureInputFromSource:(nullable AVAudioNode *)source + toDestination:(AVAudioNode *)destination + withFormat:(AVAudioFormat *)format + context:(NSDictionary *)context { + RCTLog(@"[AudioDeviceModuleObserver] Configure input - format: %@", format); + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + engine:(AVAudioEngine *)engine + configureOutputFromSource:(AVAudioNode *)source + toDestination:(nullable AVAudioNode *)destination + withFormat:(AVAudioFormat *)format + context:(NSDictionary *)context { + RCTLog(@"[AudioDeviceModuleObserver] Configure output - format: %@", format); + return 0; +} + +- (void)audioDeviceModuleDidUpdateDevices:(RTCAudioDeviceModule *)audioDeviceModule { + [self.module sendEventWithName:kEventAudioDeviceModuleDevicesUpdated body:@{}]; + + RCTLog(@"[AudioDeviceModuleObserver] Devices updated"); +} + +#pragma mark - Resolve methods from JS + +- (void)resolveEngineCreatedWithResult:(NSInteger)result { + self.engineCreatedResult = result; + dispatch_semaphore_signal(self.engineCreatedSemaphore); +} + +- (void)resolveWillEnableEngineWithResult:(NSInteger)result { + self.willEnableEngineResult = result; + dispatch_semaphore_signal(self.willEnableEngineSemaphore); +} + +- (void)resolveWillStartEngineWithResult:(NSInteger)result { + self.willStartEngineResult = result; + dispatch_semaphore_signal(self.willStartEngineSemaphore); +} + +- (void)resolveDidStopEngineWithResult:(NSInteger)result { + self.didStopEngineResult = result; + dispatch_semaphore_signal(self.didStopEngineSemaphore); +} + +- (void)resolveDidDisableEngineWithResult:(NSInteger)result { + self.didDisableEngineResult = result; + dispatch_semaphore_signal(self.didDisableEngineSemaphore); +} + +- (void)resolveWillReleaseEngineWithResult:(NSInteger)result { + self.willReleaseEngineResult = result; + dispatch_semaphore_signal(self.willReleaseEngineSemaphore); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.h b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.h new file mode 100644 index 000000000..32fcd47f5 --- /dev/null +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.h @@ -0,0 +1,5 @@ +#import "WebRTCModule.h" + +@interface WebRTCModule (RTCAudioDeviceModule) + +@end diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m new file mode 100644 index 000000000..9870253bd --- /dev/null +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m @@ -0,0 +1,215 @@ +#import + +#import +#import + +#import "AudioDeviceModuleObserver.h" +#import "WebRTCModule.h" + +@implementation WebRTCModule (RTCAudioDeviceModule) + +#pragma mark - Recording & Playback Control + +RCT_EXPORT_METHOD(audioDeviceModuleStartPlayout : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) + reject) { + NSInteger result = [self.audioDeviceModule startPlayout]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject(@"playout_error", [NSString stringWithFormat:@"Failed to start playout: %ld", (long)result], nil); + } +} + +RCT_EXPORT_METHOD(audioDeviceModuleStopPlayout : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) + reject) { + NSInteger result = [self.audioDeviceModule stopPlayout]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject(@"playout_error", [NSString stringWithFormat:@"Failed to stop playout: %ld", (long)result], nil); + } +} + +RCT_EXPORT_METHOD(audioDeviceModuleStartRecording : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) + reject) { + NSInteger result = [self.audioDeviceModule startRecording]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject(@"recording_error", [NSString stringWithFormat:@"Failed to start recording: %ld", (long)result], nil); + } +} + +RCT_EXPORT_METHOD(audioDeviceModuleStopRecording : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) + reject) { + NSInteger result = [self.audioDeviceModule stopRecording]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject(@"recording_error", [NSString stringWithFormat:@"Failed to stop recording: %ld", (long)result], nil); + } +} + +RCT_EXPORT_METHOD(audioDeviceModuleStartLocalRecording : (RCTPromiseResolveBlock) + resolve rejecter : (RCTPromiseRejectBlock)reject) { + NSError *error = nil; + AVAudioSession *session = [AVAudioSession sharedInstance]; + + // Set category to PlayAndRecord with some options + [session setCategory:AVAudioSessionCategoryPlayAndRecord + withOptions:(AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetooth) + error:&error]; + if (error) { + NSLog(@"Error setting category: %@", error); + } + + // Activate the session + [session setActive:YES error:&error]; + if (error) { + NSLog(@"Error activating session: %@", error); + } + + NSInteger result = [self.audioDeviceModule initAndStartRecording]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject(@"recording_error", [NSString stringWithFormat:@"Failed to start local recording: %ld", (long)result], nil); + } +} + +RCT_EXPORT_METHOD(audioDeviceModuleStopLocalRecording : (RCTPromiseResolveBlock) + resolve rejecter : (RCTPromiseRejectBlock)reject) { + NSInteger result = [self.audioDeviceModule stopRecording]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject(@"recording_error", [NSString stringWithFormat:@"Failed to stop local recording: %ld", (long)result], nil); + } +} + +#pragma mark - Microphone Control + +RCT_EXPORT_METHOD(audioDeviceModuleSetMicrophoneMuted : (BOOL)muted resolver : (RCTPromiseResolveBlock) + resolve rejecter : (RCTPromiseRejectBlock)reject) { + NSInteger result = [self.audioDeviceModule setMicrophoneMuted:muted]; + if (result == 0) { + resolve(@{@"success" : @YES, @"muted" : @(muted)}); + } else { + reject(@"mute_error", [NSString stringWithFormat:@"Failed to set microphone mute: %ld", (long)result], nil); + } +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsMicrophoneMuted) { + return @(self.audioDeviceModule.isMicrophoneMuted); +} + +#pragma mark - Voice Processing + +RCT_EXPORT_METHOD(audioDeviceModuleSetVoiceProcessingEnabled : (BOOL)enabled resolver : (RCTPromiseResolveBlock) + resolve rejecter : (RCTPromiseRejectBlock)reject) { + NSInteger result = [self.audioDeviceModule setVoiceProcessingEnabled:enabled]; + if (result == 0) { + resolve(@{@"success" : @YES, @"enabled" : @(enabled)}); + } else { + reject(@"voice_processing_error", [NSString stringWithFormat:@"Failed to set voice processing: %ld", (long)result], + nil); + } +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsVoiceProcessingEnabled) { + return @(self.audioDeviceModule.isVoiceProcessingEnabled); +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetVoiceProcessingBypassed : (BOOL)bypassed) { + self.audioDeviceModule.voiceProcessingBypassed = bypassed; + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsVoiceProcessingBypassed) { + return @(self.audioDeviceModule.isVoiceProcessingBypassed); +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetVoiceProcessingAGCEnabled : (BOOL)enabled) { + self.audioDeviceModule.voiceProcessingAGCEnabled = enabled; + return @{@"success" : @YES, @"enabled" : @(enabled)}; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsVoiceProcessingAGCEnabled) { + return @(self.audioDeviceModule.isVoiceProcessingAGCEnabled); +} + +#pragma mark - Status + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsPlaying) { return @(self.audioDeviceModule.isPlaying); } + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsRecording) { return @(self.audioDeviceModule.isRecording); } + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsEngineRunning) { + return @(self.audioDeviceModule.isEngineRunning); +} + +#pragma mark - Advanced Features + +RCT_EXPORT_METHOD(audioDeviceModuleSetMuteMode : (NSInteger)mode resolver : (RCTPromiseResolveBlock) + resolve rejecter : (RCTPromiseRejectBlock)reject) { + NSInteger result = [self.audioDeviceModule setMuteMode:(RTCAudioEngineMuteMode)mode]; + if (result == 0) { + resolve(@{@"success" : @YES, @"mode" : @(mode)}); + } else { + reject(@"mute_mode_error", [NSString stringWithFormat:@"Failed to set mute mode: %ld", (long)result], nil); + } +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleGetMuteMode) { return @(self.audioDeviceModule.muteMode); } + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetAdvancedDuckingEnabled : (BOOL)enabled) { + self.audioDeviceModule.advancedDuckingEnabled = enabled; + return @{@"success" : @YES, @"enabled" : @(enabled)}; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsAdvancedDuckingEnabled) { + return @(self.audioDeviceModule.isAdvancedDuckingEnabled); +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetDuckingLevel : (NSInteger)level) { + self.audioDeviceModule.duckingLevel = level; + return @{@"success" : @YES, @"level" : @(level)}; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleGetDuckingLevel) { + return @(self.audioDeviceModule.duckingLevel); +} + +#pragma mark - Observer Delegate Response Methods + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveEngineCreated : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveEngineCreatedWithResult:result]; + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillEnableEngine : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveWillEnableEngineWithResult:result]; + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillStartEngine : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveWillStartEngineWithResult:result]; + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveDidStopEngine : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveDidStopEngineWithResult:result]; + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveDidDisableEngine : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveDidDisableEngineWithResult:result]; + return nil; +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillReleaseEngine : (NSInteger)result) { + [self.audioDeviceModuleObserver resolveWillReleaseEngineWithResult:result]; + return nil; +} + +@end diff --git a/ios/RCTWebRTC/WebRTCModule.h b/ios/RCTWebRTC/WebRTCModule.h index 5f20e3fb7..989f97d70 100644 --- a/ios/RCTWebRTC/WebRTCModule.h +++ b/ios/RCTWebRTC/WebRTCModule.h @@ -22,6 +22,17 @@ static NSString *const kEventVideoTrackDimensionChanged = @"videoTrackDimensionC static NSString *const kEventMediaStreamTrackEnded = @"mediaStreamTrackEnded"; static NSString *const kEventPeerConnectionOnRemoveTrack = @"peerConnectionOnRemoveTrack"; static NSString *const kEventPeerConnectionOnTrack = @"peerConnectionOnTrack"; +static NSString *const kEventFrameCryptionStateChanged = @"frameCryptionStateChanged"; +static NSString *const kEventAudioDeviceModuleSpeechActivity = @"audioDeviceModuleSpeechActivity"; +static NSString *const kEventAudioDeviceModuleEngineCreated = @"audioDeviceModuleEngineCreated"; +static NSString *const kEventAudioDeviceModuleEngineWillEnable = @"audioDeviceModuleEngineWillEnable"; +static NSString *const kEventAudioDeviceModuleEngineWillStart = @"audioDeviceModuleEngineWillStart"; +static NSString *const kEventAudioDeviceModuleEngineDidStop = @"audioDeviceModuleEngineDidStop"; +static NSString *const kEventAudioDeviceModuleEngineDidDisable = @"audioDeviceModuleEngineDidDisable"; +static NSString *const kEventAudioDeviceModuleEngineWillRelease = @"audioDeviceModuleEngineWillRelease"; +static NSString *const kEventAudioDeviceModuleDevicesUpdated = @"audioDeviceModuleDevicesUpdated"; + +@class AudioDeviceModuleObserver; @class AudioDeviceModule; @@ -38,6 +49,13 @@ static NSString *const kEventPeerConnectionOnTrack = @"peerConnectionOnTrack"; @property(nonatomic, strong) NSMutableDictionary *localStreams; @property(nonatomic, strong) NSMutableDictionary *localTracks; +@property(nonatomic, strong) NSMutableDictionary *frameCryptors; +@property(nonatomic, strong) NSMutableDictionary *keyProviders; +@property(nonatomic, strong) NSMutableDictionary *dataPacketCryptors; + +@property(nonatomic, readonly) RTCAudioDeviceModule *audioDeviceModule; +@property(nonatomic, strong) AudioDeviceModuleObserver *audioDeviceModuleObserver; + - (RTCMediaStream *)streamForReactTag:(NSString *)reactTag; @end diff --git a/ios/RCTWebRTC/WebRTCModule.m b/ios/RCTWebRTC/WebRTCModule.m index 3d6f74d38..7c4d3da14 100644 --- a/ios/RCTWebRTC/WebRTCModule.m +++ b/ios/RCTWebRTC/WebRTCModule.m @@ -10,6 +10,7 @@ #import "WebRTCModule+RTCPeerConnection.h" #import "WebRTCModule.h" #import "WebRTCModuleOptions.h" +#import "AudioDeviceModuleObserver.h" // Import Swift classes // We need the following if and elif directives to properly import the generated Swift header for the module, @@ -113,6 +114,14 @@ - (instancetype)init { _localStreams = [NSMutableDictionary new]; _localTracks = [NSMutableDictionary new]; + _frameCryptors = [NSMutableDictionary new]; + _keyProviders = [NSMutableDictionary new]; + _dataPacketCryptors = [NSMutableDictionary new]; + + _audioDeviceModule = _peerConnectionFactory.audioDeviceModule; + _audioDeviceModuleObserver = [[AudioDeviceModuleObserver alloc] initWithWebRTCModule:self]; + _audioDeviceModule.observer = _audioDeviceModuleObserver; + dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1); _workerQueue = dispatch_queue_create("WebRTCModule.queue", attributes); @@ -157,7 +166,16 @@ - (dispatch_queue_t)methodQueue { kEventVideoTrackDimensionChanged, kEventMediaStreamTrackEnded, kEventPeerConnectionOnRemoveTrack, - kEventPeerConnectionOnTrack + kEventPeerConnectionOnTrack, + kEventFrameCryptionStateChanged, + kEventAudioDeviceModuleSpeechActivity, + kEventAudioDeviceModuleEngineCreated, + kEventAudioDeviceModuleEngineWillEnable, + kEventAudioDeviceModuleEngineWillStart, + kEventAudioDeviceModuleEngineDidStop, + kEventAudioDeviceModuleEngineDidDisable, + kEventAudioDeviceModuleEngineWillRelease, + kEventAudioDeviceModuleDevicesUpdated ]; } diff --git a/src/AudioDeviceModule.ts b/src/AudioDeviceModule.ts new file mode 100644 index 000000000..2e5a8c973 --- /dev/null +++ b/src/AudioDeviceModule.ts @@ -0,0 +1,246 @@ +import { NativeModules, Platform } from 'react-native'; + +const { WebRTCModule } = NativeModules; + +export enum AudioEngineMuteMode { + Unknown = -1, + VoiceProcessing = 0, + RestartEngine = 1, + InputMixer = 2, +} + +/** + * Audio Device Module API for controlling audio devices and settings. + * iOS/macOS only - will throw on Android. + */ +export class AudioDeviceModule { + /** + * Start audio playback + */ + static async startPlayout(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleStartPlayout(); + } + + /** + * Stop audio playback + */ + static async stopPlayout(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleStopPlayout(); + } + + /** + * Start audio recording + */ + static async startRecording(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleStartRecording(); + } + + /** + * Stop audio recording + */ + static async stopRecording(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleStopRecording(); + } + + /** + * Initialize and start local audio recording (calls initAndStartRecording) + */ + static async startLocalRecording(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleStartLocalRecording(); + } + + /** + * Stop local audio recording + */ + static async stopLocalRecording(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleStopLocalRecording(); + } + + /** + * Mute or unmute the microphone + */ + static async setMicrophoneMuted(muted: boolean): Promise<{ success: boolean; muted: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleSetMicrophoneMuted(muted); + } + + /** + * Check if microphone is currently muted + */ + static isMicrophoneMuted(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleIsMicrophoneMuted(); + } + + /** + * Enable or disable voice processing (requires engine restart) + */ + static async setVoiceProcessingEnabled(enabled: boolean): Promise<{ success: boolean; enabled: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleSetVoiceProcessingEnabled(enabled); + } + + /** + * Check if voice processing is enabled + */ + static isVoiceProcessingEnabled(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleIsVoiceProcessingEnabled(); + } + + /** + * Temporarily bypass voice processing without restarting the engine + */ + static setVoiceProcessingBypassed(bypassed: boolean): void { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + WebRTCModule.audioDeviceModuleSetVoiceProcessingBypassed(bypassed); + } + + /** + * Check if voice processing is currently bypassed + */ + static isVoiceProcessingBypassed(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleIsVoiceProcessingBypassed(); + } + + /** + * Enable or disable Automatic Gain Control (AGC) + */ + static setVoiceProcessingAGCEnabled(enabled: boolean): { success: boolean; enabled: boolean } { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleSetVoiceProcessingAGCEnabled(enabled); + } + + /** + * Check if AGC is enabled + */ + static isVoiceProcessingAGCEnabled(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleIsVoiceProcessingAGCEnabled(); + } + + /** + * Check if audio is currently playing + */ + static isPlaying(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleIsPlaying(); + } + + /** + * Check if audio is currently recording + */ + static isRecording(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleIsRecording(); + } + + /** + * Check if the audio engine is running + */ + static isEngineRunning(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleIsEngineRunning(); + } + + /** + * Set the microphone mute mode + */ + static async setMuteMode(mode: AudioEngineMuteMode): Promise<{ success: boolean; mode: AudioEngineMuteMode }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleSetMuteMode(mode); + } + + /** + * Get the current mute mode + */ + static getMuteMode(): AudioEngineMuteMode { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleGetMuteMode(); + } + + /** + * Enable or disable advanced audio ducking + */ + static setAdvancedDuckingEnabled(enabled: boolean): { success: boolean; enabled: boolean } { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleSetAdvancedDuckingEnabled(enabled); + } + + /** + * Check if advanced ducking is enabled + */ + static isAdvancedDuckingEnabled(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleIsAdvancedDuckingEnabled(); + } + + /** + * Set the audio ducking level (0-100) + */ + static setDuckingLevel(level: number): { success: boolean; level: number } { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleSetDuckingLevel(level); + } + + /** + * Get the current ducking level + */ + static getDuckingLevel(): number { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + return WebRTCModule.audioDeviceModuleGetDuckingLevel(); + } +} diff --git a/src/AudioDeviceModuleEvents.ts b/src/AudioDeviceModuleEvents.ts new file mode 100644 index 000000000..a695f95ce --- /dev/null +++ b/src/AudioDeviceModuleEvents.ts @@ -0,0 +1,199 @@ +import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; + +const { WebRTCModule } = NativeModules; + +export type SpeechActivityEvent = 'started' | 'ended'; + +export interface SpeechActivityEventData { + event: SpeechActivityEvent; +} + +export interface EngineStateEventData { + isPlayoutEnabled: boolean; + isRecordingEnabled: boolean; +} + +export type AudioDeviceModuleEventType = + | 'speechActivity' + | 'devicesUpdated'; + +export type AudioDeviceModuleEventData = + | SpeechActivityEventData + | EngineStateEventData + | Record; // Empty object for events with no data + +export type AudioDeviceModuleEventListener = (data: AudioDeviceModuleEventData) => void; + +/** + * Handler function that must return a number (0 for success, non-zero for error) + */ +export type AudioEngineEventNoParamsHandler = () => Promise; +export type AudioEngineEventHandler = ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => Promise; + +/** + * Event emitter for RTCAudioDeviceModule delegate callbacks. + * iOS/macOS only. + */ +class AudioDeviceModuleEventEmitter { + private eventEmitter: NativeEventEmitter | null = null; + private engineCreatedHandler: AudioEngineEventNoParamsHandler | null = null; + private willEnableEngineHandler: AudioEngineEventHandler | null = null; + private willStartEngineHandler: AudioEngineEventHandler | null = null; + private didStopEngineHandler: AudioEngineEventHandler | null = null; + private didDisableEngineHandler: AudioEngineEventHandler | null = null; + private willReleaseEngineHandler: AudioEngineEventNoParamsHandler | null = null; + + constructor() { + if (Platform.OS !== 'android' && WebRTCModule) { + this.eventEmitter = new NativeEventEmitter(WebRTCModule); + + // Setup handlers for blocking delegate methods + this.eventEmitter.addListener('audioDeviceModuleEngineCreated', async () => { + let result = 0; + if (this.engineCreatedHandler) { + try { + await this.engineCreatedHandler(); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + WebRTCModule.audioDeviceModuleResolveEngineCreated(result); + }); + + this.eventEmitter.addListener('audioDeviceModuleEngineWillEnable', async ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => { + let result = 0; + if (this.willEnableEngineHandler) { + try { + await this.willEnableEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + WebRTCModule.audioDeviceModuleResolveWillEnableEngine(result); + }); + + this.eventEmitter.addListener('audioDeviceModuleEngineWillStart', async ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => { + let result = 0; + if (this.willStartEngineHandler) { + try { + await this.willStartEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + WebRTCModule.audioDeviceModuleResolveWillStartEngine(result); + }); + + this.eventEmitter.addListener('audioDeviceModuleEngineDidStop', async ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => { + let result = 0; + if (this.didStopEngineHandler) { + try { + await this.didStopEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + WebRTCModule.audioDeviceModuleResolveDidStopEngine(result); + }); + + this.eventEmitter.addListener('audioDeviceModuleEngineDidDisable', async ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => { + let result = 0; + if (this.didDisableEngineHandler) { + try { + await this.didDisableEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + WebRTCModule.audioDeviceModuleResolveDidDisableEngine(result); + }); + + this.eventEmitter.addListener('audioDeviceModuleEngineWillRelease', async () => { + let result = 0; + if (this.willReleaseEngineHandler) { + try { + await this.willReleaseEngineHandler(); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + WebRTCModule.audioDeviceModuleResolveWillReleaseEngine(result); + }); + } + } + /** + * Subscribe to speech activity events (started/ended) + */ + addSpeechActivityListener(listener: (data: SpeechActivityEventData) => void) { + if (!this.eventEmitter) { + throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + } + return this.eventEmitter.addListener('audioDeviceModuleSpeechActivity', listener); + } + + /** + * Subscribe to devices updated event (input/output devices changed) + */ + addDevicesUpdatedListener(listener: () => void) { + if (!this.eventEmitter) { + throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + } + return this.eventEmitter.addListener('audioDeviceModuleDevicesUpdated', listener); + } + + /** + * Set handler for engine created delegate - MUST return 0 for success or error code + * This handler blocks the native thread until it returns + */ + setEngineCreatedHandler(handler: AudioEngineEventNoParamsHandler | null) { + this.engineCreatedHandler = handler; + } + + /** + * Set handler for will enable engine delegate - MUST return 0 for success or error code + * This handler blocks the native thread until it returns + */ + setWillEnableEngineHandler(handler: AudioEngineEventHandler | null) { + this.willEnableEngineHandler = handler; + } + + /** + * Set handler for will start engine delegate - MUST return 0 for success or error code + * This handler blocks the native thread until it returns + */ + setWillStartEngineHandler(handler: AudioEngineEventHandler | null) { + this.willStartEngineHandler = handler; + } + + /** + * Set handler for did stop engine delegate - MUST return 0 for success or error code + * This handler blocks the native thread until it returns + */ + setDidStopEngineHandler(handler: AudioEngineEventHandler | null) { + this.didStopEngineHandler = handler; + } + + /** + * Set handler for did disable engine delegate - MUST return 0 for success or error code + * This handler blocks the native thread until it returns + */ + setDidDisableEngineHandler(handler: AudioEngineEventHandler | null) { + this.didDisableEngineHandler = handler; + } + + /** + * Set handler for will release engine delegate - MUST return 0 for success or error code + * This handler blocks the native thread until it returns + */ + setWillReleaseEngineHandler(handler: AudioEngineEventNoParamsHandler | null) { + this.willReleaseEngineHandler = handler; + } +} + +export const audioDeviceModuleEvents = new AudioDeviceModuleEventEmitter(); diff --git a/src/index.ts b/src/index.ts index bda35462d..7e19ed1e3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,8 @@ if (WebRTCModule === null) { }`); } +import { AudioDeviceModule, AudioEngineMuteMode } from './AudioDeviceModule'; +import { audioDeviceModuleEvents } from './AudioDeviceModuleEvents'; import { setupNativeEvents } from './EventEmitter'; import Logger from './Logger'; import mediaDevices from './MediaDevices'; @@ -47,7 +49,10 @@ export { type MediaTrackSettings, mediaDevices, permissions, - registerGlobals + registerGlobals, + AudioDeviceModule, + AudioEngineMuteMode, + audioDeviceModuleEvents, }; declare const global: any; From 52c29cba23c33b58a082157c432616bc897a98dd Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:26:03 +0800 Subject: [PATCH 05/15] Remove test code --- .../WebRTCModule+RTCAudioDeviceModule.m | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m index 9870253bd..ad7bf7bd8 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m @@ -52,23 +52,6 @@ @implementation WebRTCModule (RTCAudioDeviceModule) RCT_EXPORT_METHOD(audioDeviceModuleStartLocalRecording : (RCTPromiseResolveBlock) resolve rejecter : (RCTPromiseRejectBlock)reject) { - NSError *error = nil; - AVAudioSession *session = [AVAudioSession sharedInstance]; - - // Set category to PlayAndRecord with some options - [session setCategory:AVAudioSessionCategoryPlayAndRecord - withOptions:(AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetooth) - error:&error]; - if (error) { - NSLog(@"Error setting category: %@", error); - } - - // Activate the session - [session setActive:YES error:&error]; - if (error) { - NSLog(@"Error activating session: %@", error); - } - NSInteger result = [self.audioDeviceModule initAndStartRecording]; if (result == 0) { resolve(@{@"success" : @YES}); From 24a5a4ae7e40f23447134899b56922d9108e92de Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:39:25 +0800 Subject: [PATCH 06/15] Update format --- ios/RCTWebRTC/AudioDeviceModuleObserver.m | 190 +++++------ .../WebRTCModule+RTCAudioDeviceModule.m | 179 ++++++----- src/AudioDeviceModule.ts | 299 ++++++++++-------- src/AudioDeviceModuleEvents.ts | 287 +++++++++-------- 4 files changed, 514 insertions(+), 441 deletions(-) diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.m b/ios/RCTWebRTC/AudioDeviceModuleObserver.m index 9a28d8cff..b619a5c37 100644 --- a/ios/RCTWebRTC/AudioDeviceModuleObserver.m +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.m @@ -25,135 +25,143 @@ @interface AudioDeviceModuleObserver () @implementation AudioDeviceModuleObserver - (instancetype)initWithWebRTCModule:(WebRTCModule *)module { - self = [super init]; - if (self) { - self.module = module; - _engineCreatedSemaphore = dispatch_semaphore_create(0); - _willEnableEngineSemaphore = dispatch_semaphore_create(0); - _willStartEngineSemaphore = dispatch_semaphore_create(0); - _didStopEngineSemaphore = dispatch_semaphore_create(0); - _didDisableEngineSemaphore = dispatch_semaphore_create(0); - _willReleaseEngineSemaphore = dispatch_semaphore_create(0); - } - return self; + self = [super init]; + if (self) { + self.module = module; + _engineCreatedSemaphore = dispatch_semaphore_create(0); + _willEnableEngineSemaphore = dispatch_semaphore_create(0); + _willStartEngineSemaphore = dispatch_semaphore_create(0); + _didStopEngineSemaphore = dispatch_semaphore_create(0); + _didDisableEngineSemaphore = dispatch_semaphore_create(0); + _willReleaseEngineSemaphore = dispatch_semaphore_create(0); + } + return self; } #pragma mark - RTCAudioDeviceModuleDelegate - (void)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule didReceiveSpeechActivityEvent:(RTCSpeechActivityEvent)speechActivityEvent { - NSString *eventType = speechActivityEvent == RTCSpeechActivityEventStarted ? @"started" : @"ended"; + NSString *eventType = speechActivityEvent == RTCSpeechActivityEventStarted ? @"started" : @"ended"; - [self.module sendEventWithName:kEventAudioDeviceModuleSpeechActivity - body:@{ - @"event" : eventType, - }]; + [self.module sendEventWithName:kEventAudioDeviceModuleSpeechActivity + body:@{ + @"event" : eventType, + }]; - RCTLog(@"[AudioDeviceModuleObserver] Speech activity event: %@", eventType); + RCTLog(@"[AudioDeviceModuleObserver] Speech activity event: %@", eventType); } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule didCreateEngine:(AVAudioEngine *)engine { - RCTLog(@"[AudioDeviceModuleObserver] Engine created - waiting for JS response"); + RCTLog(@"[AudioDeviceModuleObserver] Engine created - waiting for JS response"); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineCreated body:@{}]; + [self.module sendEventWithName:kEventAudioDeviceModuleEngineCreated body:@{}]; - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.engineCreatedSemaphore, DISPATCH_TIME_FOREVER); + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.engineCreatedSemaphore, DISPATCH_TIME_FOREVER); - RCTLog(@"[AudioDeviceModuleObserver] Engine created - JS returned: %ld", (long)self.engineCreatedResult); - return self.engineCreatedResult; + RCTLog(@"[AudioDeviceModuleObserver] Engine created - JS returned: %ld", (long)self.engineCreatedResult); + return self.engineCreatedResult; } - (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); + RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - playout: %d, recording: %d - waiting for JS response", + isPlayoutEnabled, + isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillEnable - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; + [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillEnable + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.willEnableEngineSemaphore, DISPATCH_TIME_FOREVER); + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.willEnableEngineSemaphore, DISPATCH_TIME_FOREVER); - RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - JS returned: %ld", (long)self.willEnableEngineResult); - return self.willEnableEngineResult; + RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - JS returned: %ld", (long)self.willEnableEngineResult); + + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + RCTLog(@"[AudioDeviceModuleObserver] Audio session category: %@", audioSession.category); + + return self.willEnableEngineResult; } - (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); + RCTLog(@"[AudioDeviceModuleObserver] Engine will start - playout: %d, recording: %d - waiting for JS response", + isPlayoutEnabled, + isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillStart - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; + [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillStart + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.willStartEngineSemaphore, DISPATCH_TIME_FOREVER); + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.willStartEngineSemaphore, DISPATCH_TIME_FOREVER); - RCTLog(@"[AudioDeviceModuleObserver] Engine will start - JS returned: %ld", (long)self.willStartEngineResult); - return self.willStartEngineResult; + RCTLog(@"[AudioDeviceModuleObserver] Engine will start - JS returned: %ld", (long)self.willStartEngineResult); + return self.willStartEngineResult; } - (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); + RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - playout: %d, recording: %d - waiting for JS response", + isPlayoutEnabled, + isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidStop - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; + [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidStop + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.didStopEngineSemaphore, DISPATCH_TIME_FOREVER); + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.didStopEngineSemaphore, DISPATCH_TIME_FOREVER); - RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - JS returned: %ld", (long)self.didStopEngineResult); - return self.didStopEngineResult; + RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - JS returned: %ld", (long)self.didStopEngineResult); + return self.didStopEngineResult; } - (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); + RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - playout: %d, recording: %d - waiting for JS response", + isPlayoutEnabled, + isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidDisable - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; + [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidDisable + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.didDisableEngineSemaphore, DISPATCH_TIME_FOREVER); + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.didDisableEngineSemaphore, DISPATCH_TIME_FOREVER); - RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - JS returned: %ld", (long)self.didDisableEngineResult); - return self.didDisableEngineResult; + RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - JS returned: %ld", (long)self.didDisableEngineResult); + return self.didDisableEngineResult; } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule willReleaseEngine:(AVAudioEngine *)engine { - RCTLog(@"[AudioDeviceModuleObserver] Engine will release - waiting for JS response"); + RCTLog(@"[AudioDeviceModuleObserver] Engine will release - waiting for JS response"); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillRelease body:@{}]; + [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillRelease body:@{}]; - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.willReleaseEngineSemaphore, DISPATCH_TIME_FOREVER); + // Wait indefinitely for JS to respond + dispatch_semaphore_wait(self.willReleaseEngineSemaphore, DISPATCH_TIME_FOREVER); - RCTLog(@"[AudioDeviceModuleObserver] Engine will release - JS returned: %ld", (long)self.willReleaseEngineResult); - return self.willReleaseEngineResult; + RCTLog(@"[AudioDeviceModuleObserver] Engine will release - JS returned: %ld", (long)self.willReleaseEngineResult); + return self.willReleaseEngineResult; } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule @@ -162,8 +170,8 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule toDestination:(AVAudioNode *)destination withFormat:(AVAudioFormat *)format context:(NSDictionary *)context { - RCTLog(@"[AudioDeviceModuleObserver] Configure input - format: %@", format); - return 0; + RCTLog(@"[AudioDeviceModuleObserver] Configure input - format: %@", format); + return 0; } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule @@ -172,46 +180,46 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule toDestination:(nullable AVAudioNode *)destination withFormat:(AVAudioFormat *)format context:(NSDictionary *)context { - RCTLog(@"[AudioDeviceModuleObserver] Configure output - format: %@", format); - return 0; + RCTLog(@"[AudioDeviceModuleObserver] Configure output - format: %@", format); + return 0; } - (void)audioDeviceModuleDidUpdateDevices:(RTCAudioDeviceModule *)audioDeviceModule { - [self.module sendEventWithName:kEventAudioDeviceModuleDevicesUpdated body:@{}]; + [self.module sendEventWithName:kEventAudioDeviceModuleDevicesUpdated body:@{}]; - RCTLog(@"[AudioDeviceModuleObserver] Devices updated"); + RCTLog(@"[AudioDeviceModuleObserver] Devices updated"); } #pragma mark - Resolve methods from JS - (void)resolveEngineCreatedWithResult:(NSInteger)result { - self.engineCreatedResult = result; - dispatch_semaphore_signal(self.engineCreatedSemaphore); + self.engineCreatedResult = result; + dispatch_semaphore_signal(self.engineCreatedSemaphore); } - (void)resolveWillEnableEngineWithResult:(NSInteger)result { - self.willEnableEngineResult = result; - dispatch_semaphore_signal(self.willEnableEngineSemaphore); + self.willEnableEngineResult = result; + dispatch_semaphore_signal(self.willEnableEngineSemaphore); } - (void)resolveWillStartEngineWithResult:(NSInteger)result { - self.willStartEngineResult = result; - dispatch_semaphore_signal(self.willStartEngineSemaphore); + self.willStartEngineResult = result; + dispatch_semaphore_signal(self.willStartEngineSemaphore); } - (void)resolveDidStopEngineWithResult:(NSInteger)result { - self.didStopEngineResult = result; - dispatch_semaphore_signal(self.didStopEngineSemaphore); + self.didStopEngineResult = result; + dispatch_semaphore_signal(self.didStopEngineSemaphore); } - (void)resolveDidDisableEngineWithResult:(NSInteger)result { - self.didDisableEngineResult = result; - dispatch_semaphore_signal(self.didDisableEngineSemaphore); + self.didDisableEngineResult = result; + dispatch_semaphore_signal(self.didDisableEngineSemaphore); } - (void)resolveWillReleaseEngineWithResult:(NSInteger)result { - self.willReleaseEngineResult = result; - dispatch_semaphore_signal(self.willReleaseEngineSemaphore); + self.willReleaseEngineResult = result; + dispatch_semaphore_signal(self.willReleaseEngineSemaphore); } @end diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m index ad7bf7bd8..dc5b308f7 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m @@ -12,187 +12,196 @@ @implementation WebRTCModule (RTCAudioDeviceModule) RCT_EXPORT_METHOD(audioDeviceModuleStartPlayout : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) reject) { - NSInteger result = [self.audioDeviceModule startPlayout]; - if (result == 0) { - resolve(@{@"success" : @YES}); - } else { - reject(@"playout_error", [NSString stringWithFormat:@"Failed to start playout: %ld", (long)result], nil); - } + NSInteger result = [self.audioDeviceModule startPlayout]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject(@"playout_error", [NSString stringWithFormat:@"Failed to start playout: %ld", (long)result], nil); + } } RCT_EXPORT_METHOD(audioDeviceModuleStopPlayout : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) reject) { - NSInteger result = [self.audioDeviceModule stopPlayout]; - if (result == 0) { - resolve(@{@"success" : @YES}); - } else { - reject(@"playout_error", [NSString stringWithFormat:@"Failed to stop playout: %ld", (long)result], nil); - } + NSInteger result = [self.audioDeviceModule stopPlayout]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject(@"playout_error", [NSString stringWithFormat:@"Failed to stop playout: %ld", (long)result], nil); + } } RCT_EXPORT_METHOD(audioDeviceModuleStartRecording : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) reject) { - NSInteger result = [self.audioDeviceModule startRecording]; - if (result == 0) { - resolve(@{@"success" : @YES}); - } else { - reject(@"recording_error", [NSString stringWithFormat:@"Failed to start recording: %ld", (long)result], nil); - } + NSInteger result = [self.audioDeviceModule startRecording]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject(@"recording_error", [NSString stringWithFormat:@"Failed to start recording: %ld", (long)result], nil); + } } RCT_EXPORT_METHOD(audioDeviceModuleStopRecording : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) reject) { - NSInteger result = [self.audioDeviceModule stopRecording]; - if (result == 0) { - resolve(@{@"success" : @YES}); - } else { - reject(@"recording_error", [NSString stringWithFormat:@"Failed to stop recording: %ld", (long)result], nil); - } + NSInteger result = [self.audioDeviceModule stopRecording]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject(@"recording_error", [NSString stringWithFormat:@"Failed to stop recording: %ld", (long)result], nil); + } } RCT_EXPORT_METHOD(audioDeviceModuleStartLocalRecording : (RCTPromiseResolveBlock) resolve rejecter : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule initAndStartRecording]; - if (result == 0) { - resolve(@{@"success" : @YES}); - } else { - reject(@"recording_error", [NSString stringWithFormat:@"Failed to start local recording: %ld", (long)result], nil); - } + NSInteger result = [self.audioDeviceModule initAndStartRecording]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject( + @"recording_error", [NSString stringWithFormat:@"Failed to start local recording: %ld", (long)result], nil); + } } RCT_EXPORT_METHOD(audioDeviceModuleStopLocalRecording : (RCTPromiseResolveBlock) resolve rejecter : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule stopRecording]; - if (result == 0) { - resolve(@{@"success" : @YES}); - } else { - reject(@"recording_error", [NSString stringWithFormat:@"Failed to stop local recording: %ld", (long)result], nil); - } + NSInteger result = [self.audioDeviceModule stopRecording]; + if (result == 0) { + resolve(@{@"success" : @YES}); + } else { + reject( + @"recording_error", [NSString stringWithFormat:@"Failed to stop local recording: %ld", (long)result], nil); + } } #pragma mark - Microphone Control RCT_EXPORT_METHOD(audioDeviceModuleSetMicrophoneMuted : (BOOL)muted resolver : (RCTPromiseResolveBlock) resolve rejecter : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule setMicrophoneMuted:muted]; - if (result == 0) { - resolve(@{@"success" : @YES, @"muted" : @(muted)}); - } else { - reject(@"mute_error", [NSString stringWithFormat:@"Failed to set microphone mute: %ld", (long)result], nil); - } + NSInteger result = [self.audioDeviceModule setMicrophoneMuted:muted]; + if (result == 0) { + resolve(@{@"success" : @YES, @"muted" : @(muted)}); + } else { + reject(@"mute_error", [NSString stringWithFormat:@"Failed to set microphone mute: %ld", (long)result], nil); + } } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsMicrophoneMuted) { - return @(self.audioDeviceModule.isMicrophoneMuted); + return @(self.audioDeviceModule.isMicrophoneMuted); } #pragma mark - Voice Processing RCT_EXPORT_METHOD(audioDeviceModuleSetVoiceProcessingEnabled : (BOOL)enabled resolver : (RCTPromiseResolveBlock) resolve rejecter : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule setVoiceProcessingEnabled:enabled]; - if (result == 0) { - resolve(@{@"success" : @YES, @"enabled" : @(enabled)}); - } else { - reject(@"voice_processing_error", [NSString stringWithFormat:@"Failed to set voice processing: %ld", (long)result], - nil); - } + NSInteger result = [self.audioDeviceModule setVoiceProcessingEnabled:enabled]; + if (result == 0) { + resolve(@{@"success" : @YES, @"enabled" : @(enabled)}); + } else { + reject(@"voice_processing_error", + [NSString stringWithFormat:@"Failed to set voice processing: %ld", (long)result], + nil); + } } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsVoiceProcessingEnabled) { - return @(self.audioDeviceModule.isVoiceProcessingEnabled); + return @(self.audioDeviceModule.isVoiceProcessingEnabled); } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetVoiceProcessingBypassed : (BOOL)bypassed) { - self.audioDeviceModule.voiceProcessingBypassed = bypassed; - return nil; + self.audioDeviceModule.voiceProcessingBypassed = bypassed; + return nil; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsVoiceProcessingBypassed) { - return @(self.audioDeviceModule.isVoiceProcessingBypassed); + return @(self.audioDeviceModule.isVoiceProcessingBypassed); } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetVoiceProcessingAGCEnabled : (BOOL)enabled) { - self.audioDeviceModule.voiceProcessingAGCEnabled = enabled; - return @{@"success" : @YES, @"enabled" : @(enabled)}; + self.audioDeviceModule.voiceProcessingAGCEnabled = enabled; + return @{@"success" : @YES, @"enabled" : @(enabled)}; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsVoiceProcessingAGCEnabled) { - return @(self.audioDeviceModule.isVoiceProcessingAGCEnabled); + return @(self.audioDeviceModule.isVoiceProcessingAGCEnabled); } #pragma mark - Status -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsPlaying) { return @(self.audioDeviceModule.isPlaying); } +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsPlaying) { + return @(self.audioDeviceModule.isPlaying); +} -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsRecording) { return @(self.audioDeviceModule.isRecording); } +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsRecording) { + return @(self.audioDeviceModule.isRecording); +} RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsEngineRunning) { - return @(self.audioDeviceModule.isEngineRunning); + return @(self.audioDeviceModule.isEngineRunning); } #pragma mark - Advanced Features RCT_EXPORT_METHOD(audioDeviceModuleSetMuteMode : (NSInteger)mode resolver : (RCTPromiseResolveBlock) resolve rejecter : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule setMuteMode:(RTCAudioEngineMuteMode)mode]; - if (result == 0) { - resolve(@{@"success" : @YES, @"mode" : @(mode)}); - } else { - reject(@"mute_mode_error", [NSString stringWithFormat:@"Failed to set mute mode: %ld", (long)result], nil); - } + NSInteger result = [self.audioDeviceModule setMuteMode:(RTCAudioEngineMuteMode)mode]; + if (result == 0) { + resolve(@{@"success" : @YES, @"mode" : @(mode)}); + } else { + reject(@"mute_mode_error", [NSString stringWithFormat:@"Failed to set mute mode: %ld", (long)result], nil); + } } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleGetMuteMode) { return @(self.audioDeviceModule.muteMode); } +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleGetMuteMode) { + return @(self.audioDeviceModule.muteMode); +} RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetAdvancedDuckingEnabled : (BOOL)enabled) { - self.audioDeviceModule.advancedDuckingEnabled = enabled; - return @{@"success" : @YES, @"enabled" : @(enabled)}; + self.audioDeviceModule.advancedDuckingEnabled = enabled; + return @{@"success" : @YES, @"enabled" : @(enabled)}; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsAdvancedDuckingEnabled) { - return @(self.audioDeviceModule.isAdvancedDuckingEnabled); + return @(self.audioDeviceModule.isAdvancedDuckingEnabled); } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetDuckingLevel : (NSInteger)level) { - self.audioDeviceModule.duckingLevel = level; - return @{@"success" : @YES, @"level" : @(level)}; + self.audioDeviceModule.duckingLevel = level; + return @{@"success" : @YES, @"level" : @(level)}; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleGetDuckingLevel) { - return @(self.audioDeviceModule.duckingLevel); + return @(self.audioDeviceModule.duckingLevel); } #pragma mark - Observer Delegate Response Methods RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveEngineCreated : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveEngineCreatedWithResult:result]; - return nil; + [self.audioDeviceModuleObserver resolveEngineCreatedWithResult:result]; + return nil; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillEnableEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveWillEnableEngineWithResult:result]; - return nil; + [self.audioDeviceModuleObserver resolveWillEnableEngineWithResult:result]; + return nil; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillStartEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveWillStartEngineWithResult:result]; - return nil; + [self.audioDeviceModuleObserver resolveWillStartEngineWithResult:result]; + return nil; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveDidStopEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveDidStopEngineWithResult:result]; - return nil; + [self.audioDeviceModuleObserver resolveDidStopEngineWithResult:result]; + return nil; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveDidDisableEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveDidDisableEngineWithResult:result]; - return nil; + [self.audioDeviceModuleObserver resolveDidDisableEngineWithResult:result]; + return nil; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillReleaseEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveWillReleaseEngineWithResult:result]; - return nil; + [self.audioDeviceModuleObserver resolveWillReleaseEngineWithResult:result]; + return nil; } @end diff --git a/src/AudioDeviceModule.ts b/src/AudioDeviceModule.ts index 2e5a8c973..4c2073186 100644 --- a/src/AudioDeviceModule.ts +++ b/src/AudioDeviceModule.ts @@ -14,233 +14,256 @@ export enum AudioEngineMuteMode { * iOS/macOS only - will throw on Android. */ export class AudioDeviceModule { - /** + /** * Start audio playback */ - static async startPlayout(): Promise<{ success: boolean }> { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static async startPlayout(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleStartPlayout(); } - return WebRTCModule.audioDeviceModuleStartPlayout(); - } - /** + /** * Stop audio playback */ - static async stopPlayout(): Promise<{ success: boolean }> { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static async stopPlayout(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleStopPlayout(); } - return WebRTCModule.audioDeviceModuleStopPlayout(); - } - /** + /** * Start audio recording */ - static async startRecording(): Promise<{ success: boolean }> { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static async startRecording(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleStartRecording(); } - return WebRTCModule.audioDeviceModuleStartRecording(); - } - /** + /** * Stop audio recording */ - static async stopRecording(): Promise<{ success: boolean }> { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static async stopRecording(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleStopRecording(); } - return WebRTCModule.audioDeviceModuleStopRecording(); - } - /** + /** * Initialize and start local audio recording (calls initAndStartRecording) */ - static async startLocalRecording(): Promise<{ success: boolean }> { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static async startLocalRecording(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleStartLocalRecording(); } - return WebRTCModule.audioDeviceModuleStartLocalRecording(); - } - /** + /** * Stop local audio recording */ - static async stopLocalRecording(): Promise<{ success: boolean }> { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static async stopLocalRecording(): Promise<{ success: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleStopLocalRecording(); } - return WebRTCModule.audioDeviceModuleStopLocalRecording(); - } - /** + /** * Mute or unmute the microphone */ - static async setMicrophoneMuted(muted: boolean): Promise<{ success: boolean; muted: boolean }> { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static async setMicrophoneMuted(muted: boolean): Promise<{ success: boolean; muted: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleSetMicrophoneMuted(muted); } - return WebRTCModule.audioDeviceModuleSetMicrophoneMuted(muted); - } - /** + /** * Check if microphone is currently muted */ - static isMicrophoneMuted(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static isMicrophoneMuted(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleIsMicrophoneMuted(); } - return WebRTCModule.audioDeviceModuleIsMicrophoneMuted(); - } - /** + /** * Enable or disable voice processing (requires engine restart) */ - static async setVoiceProcessingEnabled(enabled: boolean): Promise<{ success: boolean; enabled: boolean }> { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static async setVoiceProcessingEnabled(enabled: boolean): Promise<{ success: boolean; enabled: boolean }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleSetVoiceProcessingEnabled(enabled); } - return WebRTCModule.audioDeviceModuleSetVoiceProcessingEnabled(enabled); - } - /** + /** * Check if voice processing is enabled */ - static isVoiceProcessingEnabled(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static isVoiceProcessingEnabled(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleIsVoiceProcessingEnabled(); } - return WebRTCModule.audioDeviceModuleIsVoiceProcessingEnabled(); - } - /** + /** * Temporarily bypass voice processing without restarting the engine */ - static setVoiceProcessingBypassed(bypassed: boolean): void { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static setVoiceProcessingBypassed(bypassed: boolean): void { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + WebRTCModule.audioDeviceModuleSetVoiceProcessingBypassed(bypassed); } - WebRTCModule.audioDeviceModuleSetVoiceProcessingBypassed(bypassed); - } - /** + /** * Check if voice processing is currently bypassed */ - static isVoiceProcessingBypassed(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static isVoiceProcessingBypassed(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleIsVoiceProcessingBypassed(); } - return WebRTCModule.audioDeviceModuleIsVoiceProcessingBypassed(); - } - /** + /** * Enable or disable Automatic Gain Control (AGC) */ - static setVoiceProcessingAGCEnabled(enabled: boolean): { success: boolean; enabled: boolean } { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static setVoiceProcessingAGCEnabled(enabled: boolean): { success: boolean; enabled: boolean } { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleSetVoiceProcessingAGCEnabled(enabled); } - return WebRTCModule.audioDeviceModuleSetVoiceProcessingAGCEnabled(enabled); - } - /** + /** * Check if AGC is enabled */ - static isVoiceProcessingAGCEnabled(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static isVoiceProcessingAGCEnabled(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleIsVoiceProcessingAGCEnabled(); } - return WebRTCModule.audioDeviceModuleIsVoiceProcessingAGCEnabled(); - } - /** + /** * Check if audio is currently playing */ - static isPlaying(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static isPlaying(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleIsPlaying(); } - return WebRTCModule.audioDeviceModuleIsPlaying(); - } - /** + /** * Check if audio is currently recording */ - static isRecording(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static isRecording(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleIsRecording(); } - return WebRTCModule.audioDeviceModuleIsRecording(); - } - /** + /** * Check if the audio engine is running */ - static isEngineRunning(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static isEngineRunning(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleIsEngineRunning(); } - return WebRTCModule.audioDeviceModuleIsEngineRunning(); - } - /** + /** * Set the microphone mute mode */ - static async setMuteMode(mode: AudioEngineMuteMode): Promise<{ success: boolean; mode: AudioEngineMuteMode }> { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static async setMuteMode(mode: AudioEngineMuteMode): Promise<{ success: boolean; mode: AudioEngineMuteMode }> { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleSetMuteMode(mode); } - return WebRTCModule.audioDeviceModuleSetMuteMode(mode); - } - /** + /** * Get the current mute mode */ - static getMuteMode(): AudioEngineMuteMode { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static getMuteMode(): AudioEngineMuteMode { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleGetMuteMode(); } - return WebRTCModule.audioDeviceModuleGetMuteMode(); - } - /** + /** * Enable or disable advanced audio ducking */ - static setAdvancedDuckingEnabled(enabled: boolean): { success: boolean; enabled: boolean } { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static setAdvancedDuckingEnabled(enabled: boolean): { success: boolean; enabled: boolean } { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleSetAdvancedDuckingEnabled(enabled); } - return WebRTCModule.audioDeviceModuleSetAdvancedDuckingEnabled(enabled); - } - /** + /** * Check if advanced ducking is enabled */ - static isAdvancedDuckingEnabled(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static isAdvancedDuckingEnabled(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleIsAdvancedDuckingEnabled(); } - return WebRTCModule.audioDeviceModuleIsAdvancedDuckingEnabled(); - } - /** + /** * Set the audio ducking level (0-100) */ - static setDuckingLevel(level: number): { success: boolean; level: number } { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static setDuckingLevel(level: number): { success: boolean; level: number } { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleSetDuckingLevel(level); } - return WebRTCModule.audioDeviceModuleSetDuckingLevel(level); - } - /** + /** * Get the current ducking level */ - static getDuckingLevel(): number { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); + static getDuckingLevel(): number { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleGetDuckingLevel(); } - return WebRTCModule.audioDeviceModuleGetDuckingLevel(); - } } diff --git a/src/AudioDeviceModuleEvents.ts b/src/AudioDeviceModuleEvents.ts index a695f95ce..9f81dddcb 100644 --- a/src/AudioDeviceModuleEvents.ts +++ b/src/AudioDeviceModuleEvents.ts @@ -28,172 +28,205 @@ export type AudioDeviceModuleEventListener = (data: AudioDeviceModuleEventData) * Handler function that must return a number (0 for success, non-zero for error) */ export type AudioEngineEventNoParamsHandler = () => Promise; -export type AudioEngineEventHandler = ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => Promise; +export type AudioEngineEventHandler = (params: { + isPlayoutEnabled: boolean; + isRecordingEnabled: boolean; +}) => Promise; /** * Event emitter for RTCAudioDeviceModule delegate callbacks. * iOS/macOS only. */ class AudioDeviceModuleEventEmitter { - private eventEmitter: NativeEventEmitter | null = null; - private engineCreatedHandler: AudioEngineEventNoParamsHandler | null = null; - private willEnableEngineHandler: AudioEngineEventHandler | null = null; - private willStartEngineHandler: AudioEngineEventHandler | null = null; - private didStopEngineHandler: AudioEngineEventHandler | null = null; - private didDisableEngineHandler: AudioEngineEventHandler | null = null; - private willReleaseEngineHandler: AudioEngineEventNoParamsHandler | null = null; - - constructor() { - if (Platform.OS !== 'android' && WebRTCModule) { - this.eventEmitter = new NativeEventEmitter(WebRTCModule); - - // Setup handlers for blocking delegate methods - this.eventEmitter.addListener('audioDeviceModuleEngineCreated', async () => { - let result = 0; - if (this.engineCreatedHandler) { - try { - await this.engineCreatedHandler(); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } + private eventEmitter: NativeEventEmitter | null = null; + private engineCreatedHandler: AudioEngineEventNoParamsHandler | null = null; + private willEnableEngineHandler: AudioEngineEventHandler | null = null; + private willStartEngineHandler: AudioEngineEventHandler | null = null; + private didStopEngineHandler: AudioEngineEventHandler | null = null; + private didDisableEngineHandler: AudioEngineEventHandler | null = null; + private willReleaseEngineHandler: AudioEngineEventNoParamsHandler | null = null; + + constructor() { + if (Platform.OS !== 'android' && WebRTCModule) { + this.eventEmitter = new NativeEventEmitter(WebRTCModule); + + // Setup handlers for blocking delegate methods + this.eventEmitter.addListener('audioDeviceModuleEngineCreated', async () => { + let result = 0; + + if (this.engineCreatedHandler) { + try { + await this.engineCreatedHandler(); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + + WebRTCModule.audioDeviceModuleResolveEngineCreated(result); + }); + + this.eventEmitter.addListener( + 'audioDeviceModuleEngineWillEnable', + async (params: { isPlayoutEnabled: boolean; isRecordingEnabled: boolean }) => { + const { isPlayoutEnabled, isRecordingEnabled } = params; + let result = 0; + + if (this.willEnableEngineHandler) { + try { + await this.willEnableEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + + WebRTCModule.audioDeviceModuleResolveWillEnableEngine(result); + }, + ); + + this.eventEmitter.addListener( + 'audioDeviceModuleEngineWillStart', + async (params: { isPlayoutEnabled: boolean; isRecordingEnabled: boolean }) => { + const { isPlayoutEnabled, isRecordingEnabled } = params; + let result = 0; + + if (this.willStartEngineHandler) { + try { + await this.willStartEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + + WebRTCModule.audioDeviceModuleResolveWillStartEngine(result); + }, + ); + + this.eventEmitter.addListener( + 'audioDeviceModuleEngineDidStop', + async (params: { isPlayoutEnabled: boolean; isRecordingEnabled: boolean }) => { + const { isPlayoutEnabled, isRecordingEnabled } = params; + let result = 0; + + if (this.didStopEngineHandler) { + try { + await this.didStopEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + + WebRTCModule.audioDeviceModuleResolveDidStopEngine(result); + }, + ); + + this.eventEmitter.addListener( + 'audioDeviceModuleEngineDidDisable', + async (params: { isPlayoutEnabled: boolean; isRecordingEnabled: boolean }) => { + const { isPlayoutEnabled, isRecordingEnabled } = params; + let result = 0; + + if (this.didDisableEngineHandler) { + try { + await this.didDisableEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + + WebRTCModule.audioDeviceModuleResolveDidDisableEngine(result); + }, + ); + + this.eventEmitter.addListener('audioDeviceModuleEngineWillRelease', async () => { + let result = 0; + + if (this.willReleaseEngineHandler) { + try { + await this.willReleaseEngineHandler(); + } catch (error) { + // If error is a number, use it as the error code, otherwise use -1 + result = typeof error === 'number' ? error : -1; + } + } + + WebRTCModule.audioDeviceModuleResolveWillReleaseEngine(result); + }); } - WebRTCModule.audioDeviceModuleResolveEngineCreated(result); - }); - - this.eventEmitter.addListener('audioDeviceModuleEngineWillEnable', async ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => { - let result = 0; - if (this.willEnableEngineHandler) { - try { - await this.willEnableEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } - } - WebRTCModule.audioDeviceModuleResolveWillEnableEngine(result); - }); - - this.eventEmitter.addListener('audioDeviceModuleEngineWillStart', async ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => { - let result = 0; - if (this.willStartEngineHandler) { - try { - await this.willStartEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } - } - WebRTCModule.audioDeviceModuleResolveWillStartEngine(result); - }); - - this.eventEmitter.addListener('audioDeviceModuleEngineDidStop', async ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => { - let result = 0; - if (this.didStopEngineHandler) { - try { - await this.didStopEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } - } - WebRTCModule.audioDeviceModuleResolveDidStopEngine(result); - }); - - this.eventEmitter.addListener('audioDeviceModuleEngineDidDisable', async ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => { - let result = 0; - if (this.didDisableEngineHandler) { - try { - await this.didDisableEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } - } - WebRTCModule.audioDeviceModuleResolveDidDisableEngine(result); - }); - - this.eventEmitter.addListener('audioDeviceModuleEngineWillRelease', async () => { - let result = 0; - if (this.willReleaseEngineHandler) { - try { - await this.willReleaseEngineHandler(); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } - } - WebRTCModule.audioDeviceModuleResolveWillReleaseEngine(result); - }); } - } - /** + /** * Subscribe to speech activity events (started/ended) */ - addSpeechActivityListener(listener: (data: SpeechActivityEventData) => void) { - if (!this.eventEmitter) { - throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + addSpeechActivityListener(listener: (data: SpeechActivityEventData) => void) { + if (!this.eventEmitter) { + throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + } + + return this.eventEmitter.addListener('audioDeviceModuleSpeechActivity', listener); } - return this.eventEmitter.addListener('audioDeviceModuleSpeechActivity', listener); - } - /** + /** * Subscribe to devices updated event (input/output devices changed) */ - addDevicesUpdatedListener(listener: () => void) { - if (!this.eventEmitter) { - throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + addDevicesUpdatedListener(listener: () => void) { + if (!this.eventEmitter) { + throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + } + + return this.eventEmitter.addListener('audioDeviceModuleDevicesUpdated', listener); } - return this.eventEmitter.addListener('audioDeviceModuleDevicesUpdated', listener); - } - /** + /** * Set handler for engine created delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns */ - setEngineCreatedHandler(handler: AudioEngineEventNoParamsHandler | null) { - this.engineCreatedHandler = handler; - } + setEngineCreatedHandler(handler: AudioEngineEventNoParamsHandler | null) { + this.engineCreatedHandler = handler; + } - /** + /** * Set handler for will enable engine delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns */ - setWillEnableEngineHandler(handler: AudioEngineEventHandler | null) { - this.willEnableEngineHandler = handler; - } + setWillEnableEngineHandler(handler: AudioEngineEventHandler | null) { + this.willEnableEngineHandler = handler; + } - /** + /** * Set handler for will start engine delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns */ - setWillStartEngineHandler(handler: AudioEngineEventHandler | null) { - this.willStartEngineHandler = handler; - } + setWillStartEngineHandler(handler: AudioEngineEventHandler | null) { + this.willStartEngineHandler = handler; + } - /** + /** * Set handler for did stop engine delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns */ - setDidStopEngineHandler(handler: AudioEngineEventHandler | null) { - this.didStopEngineHandler = handler; - } + setDidStopEngineHandler(handler: AudioEngineEventHandler | null) { + this.didStopEngineHandler = handler; + } - /** + /** * Set handler for did disable engine delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns */ - setDidDisableEngineHandler(handler: AudioEngineEventHandler | null) { - this.didDisableEngineHandler = handler; - } + setDidDisableEngineHandler(handler: AudioEngineEventHandler | null) { + this.didDisableEngineHandler = handler; + } - /** + /** * Set handler for will release engine delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns */ - setWillReleaseEngineHandler(handler: AudioEngineEventNoParamsHandler | null) { - this.willReleaseEngineHandler = handler; - } + setWillReleaseEngineHandler(handler: AudioEngineEventNoParamsHandler | null) { + this.willReleaseEngineHandler = handler; + } } export const audioDeviceModuleEvents = new AudioDeviceModuleEventEmitter(); From 4f2290417fdb31060e660adec6ab6517cc5946b6 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:16:09 +0800 Subject: [PATCH 07/15] Update format # Conflicts: # ios/RCTWebRTC/WebRTCModule.m --- ios/RCTWebRTC/AudioDeviceModuleObserver.h | 2 +- .../WebRTCModule+RTCAudioDeviceModule.m | 48 ++++++++------ ios/RCTWebRTC/WebRTCModule.m | 2 +- src/AudioDeviceModule.ts | 64 +++++++++---------- 4 files changed, 64 insertions(+), 52 deletions(-) diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.h b/ios/RCTWebRTC/AudioDeviceModuleObserver.h index c2c0e2500..a1f415909 100644 --- a/ios/RCTWebRTC/AudioDeviceModuleObserver.h +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.h @@ -3,7 +3,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface AudioDeviceModuleObserver : NSObject +@interface AudioDeviceModuleObserver : NSObject - (instancetype)initWithWebRTCModule:(WebRTCModule *)module; diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m index dc5b308f7..154f91aeb 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m @@ -10,8 +10,9 @@ @implementation WebRTCModule (RTCAudioDeviceModule) #pragma mark - Recording & Playback Control -RCT_EXPORT_METHOD(audioDeviceModuleStartPlayout : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) - reject) { +RCT_EXPORT_METHOD(audioDeviceModuleStartPlayout + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule startPlayout]; if (result == 0) { resolve(@{@"success" : @YES}); @@ -20,8 +21,9 @@ @implementation WebRTCModule (RTCAudioDeviceModule) } } -RCT_EXPORT_METHOD(audioDeviceModuleStopPlayout : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) - reject) { +RCT_EXPORT_METHOD(audioDeviceModuleStopPlayout + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule stopPlayout]; if (result == 0) { resolve(@{@"success" : @YES}); @@ -30,8 +32,9 @@ @implementation WebRTCModule (RTCAudioDeviceModule) } } -RCT_EXPORT_METHOD(audioDeviceModuleStartRecording : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) - reject) { +RCT_EXPORT_METHOD(audioDeviceModuleStartRecording + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule startRecording]; if (result == 0) { resolve(@{@"success" : @YES}); @@ -40,8 +43,9 @@ @implementation WebRTCModule (RTCAudioDeviceModule) } } -RCT_EXPORT_METHOD(audioDeviceModuleStopRecording : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock) - reject) { +RCT_EXPORT_METHOD(audioDeviceModuleStopRecording + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule stopRecording]; if (result == 0) { resolve(@{@"success" : @YES}); @@ -50,8 +54,9 @@ @implementation WebRTCModule (RTCAudioDeviceModule) } } -RCT_EXPORT_METHOD(audioDeviceModuleStartLocalRecording : (RCTPromiseResolveBlock) - resolve rejecter : (RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(audioDeviceModuleStartLocalRecording + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule initAndStartRecording]; if (result == 0) { resolve(@{@"success" : @YES}); @@ -61,8 +66,9 @@ @implementation WebRTCModule (RTCAudioDeviceModule) } } -RCT_EXPORT_METHOD(audioDeviceModuleStopLocalRecording : (RCTPromiseResolveBlock) - resolve rejecter : (RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(audioDeviceModuleStopLocalRecording + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule stopRecording]; if (result == 0) { resolve(@{@"success" : @YES}); @@ -74,8 +80,10 @@ @implementation WebRTCModule (RTCAudioDeviceModule) #pragma mark - Microphone Control -RCT_EXPORT_METHOD(audioDeviceModuleSetMicrophoneMuted : (BOOL)muted resolver : (RCTPromiseResolveBlock) - resolve rejecter : (RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(audioDeviceModuleSetMicrophoneMuted + : (BOOL)muted resolver + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule setMicrophoneMuted:muted]; if (result == 0) { resolve(@{@"success" : @YES, @"muted" : @(muted)}); @@ -90,8 +98,10 @@ @implementation WebRTCModule (RTCAudioDeviceModule) #pragma mark - Voice Processing -RCT_EXPORT_METHOD(audioDeviceModuleSetVoiceProcessingEnabled : (BOOL)enabled resolver : (RCTPromiseResolveBlock) - resolve rejecter : (RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(audioDeviceModuleSetVoiceProcessingEnabled + : (BOOL)enabled resolver + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule setVoiceProcessingEnabled:enabled]; if (result == 0) { resolve(@{@"success" : @YES, @"enabled" : @(enabled)}); @@ -140,8 +150,10 @@ @implementation WebRTCModule (RTCAudioDeviceModule) #pragma mark - Advanced Features -RCT_EXPORT_METHOD(audioDeviceModuleSetMuteMode : (NSInteger)mode resolver : (RCTPromiseResolveBlock) - resolve rejecter : (RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(audioDeviceModuleSetMuteMode + : (NSInteger)mode resolver + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule setMuteMode:(RTCAudioEngineMuteMode)mode]; if (result == 0) { resolve(@{@"success" : @YES, @"mode" : @(mode)}); diff --git a/ios/RCTWebRTC/WebRTCModule.m b/ios/RCTWebRTC/WebRTCModule.m index 7c4d3da14..54c0942af 100644 --- a/ios/RCTWebRTC/WebRTCModule.m +++ b/ios/RCTWebRTC/WebRTCModule.m @@ -7,10 +7,10 @@ #import #import +#import "AudioDeviceModuleObserver.h" #import "WebRTCModule+RTCPeerConnection.h" #import "WebRTCModule.h" #import "WebRTCModuleOptions.h" -#import "AudioDeviceModuleObserver.h" // Import Swift classes // We need the following if and elif directives to properly import the generated Swift header for the module, diff --git a/src/AudioDeviceModule.ts b/src/AudioDeviceModule.ts index 4c2073186..90fa280b6 100644 --- a/src/AudioDeviceModule.ts +++ b/src/AudioDeviceModule.ts @@ -15,8 +15,8 @@ export enum AudioEngineMuteMode { */ export class AudioDeviceModule { /** - * Start audio playback - */ + * Start audio playback + */ static async startPlayout(): Promise<{ success: boolean }> { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -26,8 +26,8 @@ export class AudioDeviceModule { } /** - * Stop audio playback - */ + * Stop audio playback + */ static async stopPlayout(): Promise<{ success: boolean }> { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -37,8 +37,8 @@ export class AudioDeviceModule { } /** - * Start audio recording - */ + * Start audio recording + */ static async startRecording(): Promise<{ success: boolean }> { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -48,8 +48,8 @@ export class AudioDeviceModule { } /** - * Stop audio recording - */ + * Stop audio recording + */ static async stopRecording(): Promise<{ success: boolean }> { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -59,8 +59,8 @@ export class AudioDeviceModule { } /** - * Initialize and start local audio recording (calls initAndStartRecording) - */ + * Initialize and start local audio recording (calls initAndStartRecording) + */ static async startLocalRecording(): Promise<{ success: boolean }> { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -70,8 +70,8 @@ export class AudioDeviceModule { } /** - * Stop local audio recording - */ + * Stop local audio recording + */ static async stopLocalRecording(): Promise<{ success: boolean }> { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -81,8 +81,8 @@ export class AudioDeviceModule { } /** - * Mute or unmute the microphone - */ + * Mute or unmute the microphone + */ static async setMicrophoneMuted(muted: boolean): Promise<{ success: boolean; muted: boolean }> { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -92,8 +92,8 @@ export class AudioDeviceModule { } /** - * Check if microphone is currently muted - */ + * Check if microphone is currently muted + */ static isMicrophoneMuted(): boolean { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -103,8 +103,8 @@ export class AudioDeviceModule { } /** - * Enable or disable voice processing (requires engine restart) - */ + * Enable or disable voice processing (requires engine restart) + */ static async setVoiceProcessingEnabled(enabled: boolean): Promise<{ success: boolean; enabled: boolean }> { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -114,8 +114,8 @@ export class AudioDeviceModule { } /** - * Check if voice processing is enabled - */ + * Check if voice processing is enabled + */ static isVoiceProcessingEnabled(): boolean { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -125,8 +125,8 @@ export class AudioDeviceModule { } /** - * Temporarily bypass voice processing without restarting the engine - */ + * Temporarily bypass voice processing without restarting the engine + */ static setVoiceProcessingBypassed(bypassed: boolean): void { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -136,8 +136,8 @@ export class AudioDeviceModule { } /** - * Check if voice processing is currently bypassed - */ + * Check if voice processing is currently bypassed + */ static isVoiceProcessingBypassed(): boolean { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -147,8 +147,8 @@ export class AudioDeviceModule { } /** - * Enable or disable Automatic Gain Control (AGC) - */ + * Enable or disable Automatic Gain Control (AGC) + */ static setVoiceProcessingAGCEnabled(enabled: boolean): { success: boolean; enabled: boolean } { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -158,8 +158,8 @@ export class AudioDeviceModule { } /** - * Check if AGC is enabled - */ + * Check if AGC is enabled + */ static isVoiceProcessingAGCEnabled(): boolean { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -169,8 +169,8 @@ export class AudioDeviceModule { } /** - * Check if audio is currently playing - */ + * Check if audio is currently playing + */ static isPlaying(): boolean { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -180,8 +180,8 @@ export class AudioDeviceModule { } /** - * Check if audio is currently recording - */ + * Check if audio is currently recording + */ static isRecording(): boolean { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); From ed4cb3ad09ededcd370ddcbe6f8b6c9a649a662f Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:49:04 +0800 Subject: [PATCH 08/15] Setup listeners early --- src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/index.ts b/src/index.ts index 7e19ed1e3..c6146594e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,4 +83,10 @@ function registerGlobals(): void { global.RTCRtpReceiver = RTCRtpReceiver; global.RTCRtpSender = RTCRtpSender; global.RTCErrorEvent = RTCErrorEvent; + + // Ensure audioDeviceModuleEvents is initialized and event listeners are registered + // This forces the constructor to run and set up native event listeners. + // We use void operator to explicitly indicate we're intentionally evaluating the expression + // without using its value, which prevents tree-shaking from removing this reference. + void audioDeviceModuleEvents; } From d97b493851ad7c61ae887268de3a805660d5b9cb Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:57:06 +0800 Subject: [PATCH 09/15] Fixes --- .../WebRTCModule+RTCAudioDeviceModule.m | 24 +++++++++---------- src/AudioDeviceModule.ts | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m index 154f91aeb..cda1a4774 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m @@ -15,7 +15,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule startPlayout]; if (result == 0) { - resolve(@{@"success" : @YES}); + resolve(nil); } else { reject(@"playout_error", [NSString stringWithFormat:@"Failed to start playout: %ld", (long)result], nil); } @@ -26,7 +26,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule stopPlayout]; if (result == 0) { - resolve(@{@"success" : @YES}); + resolve(nil); } else { reject(@"playout_error", [NSString stringWithFormat:@"Failed to stop playout: %ld", (long)result], nil); } @@ -37,7 +37,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule startRecording]; if (result == 0) { - resolve(@{@"success" : @YES}); + resolve(nil); } else { reject(@"recording_error", [NSString stringWithFormat:@"Failed to start recording: %ld", (long)result], nil); } @@ -48,7 +48,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule stopRecording]; if (result == 0) { - resolve(@{@"success" : @YES}); + resolve(nil); } else { reject(@"recording_error", [NSString stringWithFormat:@"Failed to stop recording: %ld", (long)result], nil); } @@ -59,7 +59,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule initAndStartRecording]; if (result == 0) { - resolve(@{@"success" : @YES}); + resolve(nil); } else { reject( @"recording_error", [NSString stringWithFormat:@"Failed to start local recording: %ld", (long)result], nil); @@ -71,7 +71,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule stopRecording]; if (result == 0) { - resolve(@{@"success" : @YES}); + resolve(nil); } else { reject( @"recording_error", [NSString stringWithFormat:@"Failed to stop local recording: %ld", (long)result], nil); @@ -86,7 +86,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule setMicrophoneMuted:muted]; if (result == 0) { - resolve(@{@"success" : @YES, @"muted" : @(muted)}); + resolve(nil); } else { reject(@"mute_error", [NSString stringWithFormat:@"Failed to set microphone mute: %ld", (long)result], nil); } @@ -104,7 +104,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule setVoiceProcessingEnabled:enabled]; if (result == 0) { - resolve(@{@"success" : @YES, @"enabled" : @(enabled)}); + resolve(nil); } else { reject(@"voice_processing_error", [NSString stringWithFormat:@"Failed to set voice processing: %ld", (long)result], @@ -127,7 +127,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetVoiceProcessingAGCEnabled : (BOOL)enabled) { self.audioDeviceModule.voiceProcessingAGCEnabled = enabled; - return @{@"success" : @YES, @"enabled" : @(enabled)}; + return nil; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsVoiceProcessingAGCEnabled) { @@ -156,7 +156,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) : (RCTPromiseRejectBlock)reject) { NSInteger result = [self.audioDeviceModule setMuteMode:(RTCAudioEngineMuteMode)mode]; if (result == 0) { - resolve(@{@"success" : @YES, @"mode" : @(mode)}); + resolve(nil); } else { reject(@"mute_mode_error", [NSString stringWithFormat:@"Failed to set mute mode: %ld", (long)result], nil); } @@ -168,7 +168,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetAdvancedDuckingEnabled : (BOOL)enabled) { self.audioDeviceModule.advancedDuckingEnabled = enabled; - return @{@"success" : @YES, @"enabled" : @(enabled)}; + return nil; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsAdvancedDuckingEnabled) { @@ -177,7 +177,7 @@ @implementation WebRTCModule (RTCAudioDeviceModule) RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetDuckingLevel : (NSInteger)level) { self.audioDeviceModule.duckingLevel = level; - return @{@"success" : @YES, @"level" : @(level)}; + return nil; } RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleGetDuckingLevel) { diff --git a/src/AudioDeviceModule.ts b/src/AudioDeviceModule.ts index 90fa280b6..1507bb084 100644 --- a/src/AudioDeviceModule.ts +++ b/src/AudioDeviceModule.ts @@ -17,7 +17,7 @@ export class AudioDeviceModule { /** * Start audio playback */ - static async startPlayout(): Promise<{ success: boolean }> { + static async startPlayout(): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -28,7 +28,7 @@ export class AudioDeviceModule { /** * Stop audio playback */ - static async stopPlayout(): Promise<{ success: boolean }> { + static async stopPlayout(): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -39,7 +39,7 @@ export class AudioDeviceModule { /** * Start audio recording */ - static async startRecording(): Promise<{ success: boolean }> { + static async startRecording(): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -50,7 +50,7 @@ export class AudioDeviceModule { /** * Stop audio recording */ - static async stopRecording(): Promise<{ success: boolean }> { + static async stopRecording(): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -61,7 +61,7 @@ export class AudioDeviceModule { /** * Initialize and start local audio recording (calls initAndStartRecording) */ - static async startLocalRecording(): Promise<{ success: boolean }> { + static async startLocalRecording(): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -72,7 +72,7 @@ export class AudioDeviceModule { /** * Stop local audio recording */ - static async stopLocalRecording(): Promise<{ success: boolean }> { + static async stopLocalRecording(): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -83,7 +83,7 @@ export class AudioDeviceModule { /** * Mute or unmute the microphone */ - static async setMicrophoneMuted(muted: boolean): Promise<{ success: boolean; muted: boolean }> { + static async setMicrophoneMuted(muted: boolean): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -105,7 +105,7 @@ export class AudioDeviceModule { /** * Enable or disable voice processing (requires engine restart) */ - static async setVoiceProcessingEnabled(enabled: boolean): Promise<{ success: boolean; enabled: boolean }> { + static async setVoiceProcessingEnabled(enabled: boolean): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -149,7 +149,7 @@ export class AudioDeviceModule { /** * Enable or disable Automatic Gain Control (AGC) */ - static setVoiceProcessingAGCEnabled(enabled: boolean): { success: boolean; enabled: boolean } { + static setVoiceProcessingAGCEnabled(enabled: boolean): void { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -204,7 +204,7 @@ export class AudioDeviceModule { /** * Set the microphone mute mode */ - static async setMuteMode(mode: AudioEngineMuteMode): Promise<{ success: boolean; mode: AudioEngineMuteMode }> { + static async setMuteMode(mode: AudioEngineMuteMode): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -226,7 +226,7 @@ export class AudioDeviceModule { /** * Enable or disable advanced audio ducking */ - static setAdvancedDuckingEnabled(enabled: boolean): { success: boolean; enabled: boolean } { + static setAdvancedDuckingEnabled(enabled: boolean): void { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -248,7 +248,7 @@ export class AudioDeviceModule { /** * Set the audio ducking level (0-100) */ - static setDuckingLevel(level: number): { success: boolean; level: number } { + static setDuckingLevel(level: number): void { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } From 05a8e48fb39e9fdd3421a2b9f1aa56a1e2894697 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 6 Oct 2025 20:50:36 +0800 Subject: [PATCH 10/15] Availability APIs --- .../WebRTCModule+RTCAudioDeviceModule.m | 44 ++++++++++ src/AudioDeviceModule.ts | 88 ++++++++++++++++--- src/index.ts | 3 +- 3 files changed, 120 insertions(+), 15 deletions(-) diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m index cda1a4774..24a0199ba 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m @@ -184,6 +184,50 @@ @implementation WebRTCModule (RTCAudioDeviceModule) return @(self.audioDeviceModule.duckingLevel); } +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsRecordingAlwaysPreparedMode) { + return @(self.audioDeviceModule.recordingAlwaysPreparedMode); +} + +RCT_EXPORT_METHOD(audioDeviceModuleSetRecordingAlwaysPreparedMode + : (BOOL)enabled resolver + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { + NSInteger result = [self.audioDeviceModule setRecordingAlwaysPreparedMode:enabled]; + if (result == 0) { + resolve(nil); + } else { + reject(@"recording_always_prepared_mode_error", + [NSString stringWithFormat:@"Failed to set recording always prepared mode: %ld", (long)result], + nil); + } +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleGetEngineAvailability) { + RTCAudioEngineAvailability availability = self.audioDeviceModule.engineAvailability; + return @{ + @"isInputAvailable" : @(availability.isInputAvailable), + @"isOutputAvailable" : @(availability.isOutputAvailable) + }; +} + +RCT_EXPORT_METHOD(audioDeviceModuleSetEngineAvailability + : (NSDictionary *)availabilityDict resolver + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { + RTCAudioEngineAvailability availability; + availability.isInputAvailable = [availabilityDict[@"isInputAvailable"] boolValue]; + availability.isOutputAvailable = [availabilityDict[@"isOutputAvailable"] boolValue]; + [self.audioDeviceModule setEngineAvailability:availability]; + NSInteger result = [self.audioDeviceModule setEngineAvailability:availability]; + if (result == 0) { + resolve(nil); + } else { + reject(@"engine_availability_error", + [NSString stringWithFormat:@"Failed to set engine availability: %ld", (long)result], + nil); + } +} + #pragma mark - Observer Delegate Response Methods RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveEngineCreated : (NSInteger)result) { diff --git a/src/AudioDeviceModule.ts b/src/AudioDeviceModule.ts index 1507bb084..a00fdce13 100644 --- a/src/AudioDeviceModule.ts +++ b/src/AudioDeviceModule.ts @@ -9,6 +9,22 @@ export enum AudioEngineMuteMode { InputMixer = 2, } +export interface AudioEngineAvailability { + isInputAvailable: boolean; + isOutputAvailable: boolean; +} + +export const AudioEngineAvailability = { + default: { + isInputAvailable: true, + isOutputAvailable: true, + }, + none: { + isInputAvailable: false, + isOutputAvailable: false, + }, +} as const; + /** * Audio Device Module API for controlling audio devices and settings. * iOS/macOS only - will throw on Android. @@ -191,8 +207,8 @@ export class AudioDeviceModule { } /** - * Check if the audio engine is running - */ + * Check if the audio engine is running + */ static isEngineRunning(): boolean { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -202,8 +218,8 @@ export class AudioDeviceModule { } /** - * Set the microphone mute mode - */ + * Set the microphone mute mode + */ static async setMuteMode(mode: AudioEngineMuteMode): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -213,8 +229,8 @@ export class AudioDeviceModule { } /** - * Get the current mute mode - */ + * Get the current mute mode + */ static getMuteMode(): AudioEngineMuteMode { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -224,8 +240,8 @@ export class AudioDeviceModule { } /** - * Enable or disable advanced audio ducking - */ + * Enable or disable advanced audio ducking + */ static setAdvancedDuckingEnabled(enabled: boolean): void { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -235,8 +251,8 @@ export class AudioDeviceModule { } /** - * Check if advanced ducking is enabled - */ + * Check if advanced ducking is enabled + */ static isAdvancedDuckingEnabled(): boolean { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -246,8 +262,8 @@ export class AudioDeviceModule { } /** - * Set the audio ducking level (0-100) - */ + * Set the audio ducking level (0-100) + */ static setDuckingLevel(level: number): void { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -257,8 +273,8 @@ export class AudioDeviceModule { } /** - * Get the current ducking level - */ + * Get the current ducking level + */ static getDuckingLevel(): number { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); @@ -266,4 +282,48 @@ export class AudioDeviceModule { return WebRTCModule.audioDeviceModuleGetDuckingLevel(); } + + /** + * Check if recording always prepared mode is enabled + */ + static isRecordingAlwaysPreparedMode(): boolean { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleIsRecordingAlwaysPreparedMode(); + } + + /** + * Enable or disable recording always prepared mode + */ + static setRecordingAlwaysPreparedMode(enabled: boolean): void { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleSetRecordingAlwaysPreparedMode(enabled); + } + + /** + * Get the current engine availability (input/output availability) + */ + static getEngineAvailability(): AudioEngineAvailability { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleGetEngineAvailability(); + } + + /** + * Set the engine availability (input/output availability) + */ + static setEngineAvailability(availability: AudioEngineAvailability): void { + if (Platform.OS === 'android') { + throw new Error('AudioDeviceModule is only available on iOS/macOS'); + } + + return WebRTCModule.audioDeviceModuleSetEngineAvailability(availability); + } } diff --git a/src/index.ts b/src/index.ts index c6146594e..c661efca6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ if (WebRTCModule === null) { }`); } -import { AudioDeviceModule, AudioEngineMuteMode } from './AudioDeviceModule'; +import { AudioDeviceModule, AudioEngineMuteMode, type AudioEngineAvailability } from './AudioDeviceModule'; import { audioDeviceModuleEvents } from './AudioDeviceModuleEvents'; import { setupNativeEvents } from './EventEmitter'; import Logger from './Logger'; @@ -52,6 +52,7 @@ export { registerGlobals, AudioDeviceModule, AudioEngineMuteMode, + type AudioEngineAvailability, audioDeviceModuleEvents, }; From 73649eb688da7eaccd373fc7be4bad1648c1d6cd Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 6 Oct 2025 21:29:17 +0800 Subject: [PATCH 11/15] Minor fixes --- src/AudioDeviceModuleEvents.ts | 47 +++++++++++++++++----------------- src/index.ts | 5 +--- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/AudioDeviceModuleEvents.ts b/src/AudioDeviceModuleEvents.ts index 9f81dddcb..e0997d87e 100644 --- a/src/AudioDeviceModuleEvents.ts +++ b/src/AudioDeviceModuleEvents.ts @@ -46,7 +46,7 @@ class AudioDeviceModuleEventEmitter { private didDisableEngineHandler: AudioEngineEventHandler | null = null; private willReleaseEngineHandler: AudioEngineEventNoParamsHandler | null = null; - constructor() { + public setupListeners() { if (Platform.OS !== 'android' && WebRTCModule) { this.eventEmitter = new NativeEventEmitter(WebRTCModule); @@ -158,9 +158,10 @@ class AudioDeviceModuleEventEmitter { }); } } + /** - * Subscribe to speech activity events (started/ended) - */ + * Subscribe to speech activity events (started/ended) + */ addSpeechActivityListener(listener: (data: SpeechActivityEventData) => void) { if (!this.eventEmitter) { throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); @@ -170,8 +171,8 @@ class AudioDeviceModuleEventEmitter { } /** - * Subscribe to devices updated event (input/output devices changed) - */ + * Subscribe to devices updated event (input/output devices changed) + */ addDevicesUpdatedListener(listener: () => void) { if (!this.eventEmitter) { throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); @@ -181,49 +182,49 @@ class AudioDeviceModuleEventEmitter { } /** - * Set handler for engine created delegate - MUST return 0 for success or error code - * This handler blocks the native thread until it returns - */ + * Set handler for engine created delegate - MUST return 0 for success or error code + * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + */ setEngineCreatedHandler(handler: AudioEngineEventNoParamsHandler | null) { this.engineCreatedHandler = handler; } /** - * Set handler for will enable engine delegate - MUST return 0 for success or error code - * This handler blocks the native thread until it returns - */ + * Set handler for will enable engine delegate - MUST return 0 for success or error code + * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + */ setWillEnableEngineHandler(handler: AudioEngineEventHandler | null) { this.willEnableEngineHandler = handler; } /** - * Set handler for will start engine delegate - MUST return 0 for success or error code - * This handler blocks the native thread until it returns - */ + * Set handler for will start engine delegate - MUST return 0 for success or error code + * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + */ setWillStartEngineHandler(handler: AudioEngineEventHandler | null) { this.willStartEngineHandler = handler; } /** - * Set handler for did stop engine delegate - MUST return 0 for success or error code - * This handler blocks the native thread until it returns - */ + * Set handler for did stop engine delegate - MUST return 0 for success or error code + * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + */ setDidStopEngineHandler(handler: AudioEngineEventHandler | null) { this.didStopEngineHandler = handler; } /** - * Set handler for did disable engine delegate - MUST return 0 for success or error code - * This handler blocks the native thread until it returns - */ + * Set handler for did disable engine delegate - MUST return 0 for success or error code + * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + */ setDidDisableEngineHandler(handler: AudioEngineEventHandler | null) { this.didDisableEngineHandler = handler; } /** - * Set handler for will release engine delegate - MUST return 0 for success or error code - * This handler blocks the native thread until it returns - */ + * Set handler for will release engine delegate + * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + */ setWillReleaseEngineHandler(handler: AudioEngineEventNoParamsHandler | null) { this.willReleaseEngineHandler = handler; } diff --git a/src/index.ts b/src/index.ts index c661efca6..262949014 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,8 +86,5 @@ function registerGlobals(): void { global.RTCErrorEvent = RTCErrorEvent; // Ensure audioDeviceModuleEvents is initialized and event listeners are registered - // This forces the constructor to run and set up native event listeners. - // We use void operator to explicitly indicate we're intentionally evaluating the expression - // without using its value, which prevents tree-shaking from removing this reference. - void audioDeviceModuleEvents; + audioDeviceModuleEvents.setupListeners(); } From dfb98ed176ce2b6f6f1e0f87b83ebdc92587f40c Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:54:37 +0900 Subject: [PATCH 12/15] fix duplicate call --- ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m index 24a0199ba..fad0f7c3f 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m @@ -217,7 +217,6 @@ @implementation WebRTCModule (RTCAudioDeviceModule) RTCAudioEngineAvailability availability; availability.isInputAvailable = [availabilityDict[@"isInputAvailable"] boolValue]; availability.isOutputAvailable = [availabilityDict[@"isOutputAvailable"] boolValue]; - [self.audioDeviceModule setEngineAvailability:availability]; NSInteger result = [self.audioDeviceModule setEngineAvailability:availability]; if (result == 0) { resolve(nil); From d5191aa05b4774947d0484cb1f39288147302306 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:54:43 +0900 Subject: [PATCH 13/15] fix async call --- src/AudioDeviceModule.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AudioDeviceModule.ts b/src/AudioDeviceModule.ts index a00fdce13..8d2ab676d 100644 --- a/src/AudioDeviceModule.ts +++ b/src/AudioDeviceModule.ts @@ -297,7 +297,7 @@ export class AudioDeviceModule { /** * Enable or disable recording always prepared mode */ - static setRecordingAlwaysPreparedMode(enabled: boolean): void { + static async setRecordingAlwaysPreparedMode(enabled: boolean): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } @@ -319,7 +319,7 @@ export class AudioDeviceModule { /** * Set the engine availability (input/output availability) */ - static setEngineAvailability(availability: AudioEngineAvailability): void { + static async setEngineAvailability(availability: AudioEngineAvailability): Promise { if (Platform.OS === 'android') { throw new Error('AudioDeviceModule is only available on iOS/macOS'); } From 8d830dce26b9672c0300b122dcbf493cb60220fb Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Tue, 3 Feb 2026 10:39:57 +0100 Subject: [PATCH 14/15] wip - speaking while muted --- ios/RCTWebRTC/AudioDeviceModuleObserver.h | 8 - ios/RCTWebRTC/AudioDeviceModuleObserver.m | 187 ++++------ .../AudioDeviceModule/AudioDeviceModule.swift | 93 ++++- .../WebRTCModule+RTCAudioDeviceModule.h | 5 - .../WebRTCModule+RTCAudioDeviceModule.m | 262 -------------- ios/RCTWebRTC/WebRTCModule.h | 6 +- ios/RCTWebRTC/WebRTCModule.m | 16 +- package.json | 2 +- src/AudioDeviceModule.ts | 329 ------------------ src/AudioDeviceModuleEvents.ts | 216 ++++-------- src/index.ts | 10 +- 11 files changed, 239 insertions(+), 895 deletions(-) delete mode 100644 ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.h delete mode 100644 ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m delete mode 100644 src/AudioDeviceModule.ts diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.h b/ios/RCTWebRTC/AudioDeviceModuleObserver.h index a1f415909..b163fb6c4 100644 --- a/ios/RCTWebRTC/AudioDeviceModuleObserver.h +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.h @@ -7,14 +7,6 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithWebRTCModule:(WebRTCModule *)module; -// Methods to receive results from JS -- (void)resolveEngineCreatedWithResult:(NSInteger)result; -- (void)resolveWillEnableEngineWithResult:(NSInteger)result; -- (void)resolveWillStartEngineWithResult:(NSInteger)result; -- (void)resolveDidStopEngineWithResult:(NSInteger)result; -- (void)resolveDidDisableEngineWithResult:(NSInteger)result; -- (void)resolveWillReleaseEngineWithResult:(NSInteger)result; - @end NS_ASSUME_NONNULL_END diff --git a/ios/RCTWebRTC/AudioDeviceModuleObserver.m b/ios/RCTWebRTC/AudioDeviceModuleObserver.m index b619a5c37..53318726d 100644 --- a/ios/RCTWebRTC/AudioDeviceModuleObserver.m +++ b/ios/RCTWebRTC/AudioDeviceModuleObserver.m @@ -6,19 +6,6 @@ @interface AudioDeviceModuleObserver () @property(weak, nonatomic) WebRTCModule *module; -@property(nonatomic, strong) dispatch_semaphore_t engineCreatedSemaphore; -@property(nonatomic, strong) dispatch_semaphore_t willEnableEngineSemaphore; -@property(nonatomic, strong) dispatch_semaphore_t willStartEngineSemaphore; -@property(nonatomic, strong) dispatch_semaphore_t didStopEngineSemaphore; -@property(nonatomic, strong) dispatch_semaphore_t didDisableEngineSemaphore; -@property(nonatomic, strong) dispatch_semaphore_t willReleaseEngineSemaphore; - -@property(nonatomic, assign) NSInteger engineCreatedResult; -@property(nonatomic, assign) NSInteger willEnableEngineResult; -@property(nonatomic, assign) NSInteger willStartEngineResult; -@property(nonatomic, assign) NSInteger didStopEngineResult; -@property(nonatomic, assign) NSInteger didDisableEngineResult; -@property(nonatomic, assign) NSInteger willReleaseEngineResult; @end @@ -28,12 +15,7 @@ - (instancetype)initWithWebRTCModule:(WebRTCModule *)module { self = [super init]; if (self) { self.module = module; - _engineCreatedSemaphore = dispatch_semaphore_create(0); - _willEnableEngineSemaphore = dispatch_semaphore_create(0); - _willStartEngineSemaphore = dispatch_semaphore_create(0); - _didStopEngineSemaphore = dispatch_semaphore_create(0); - _didDisableEngineSemaphore = dispatch_semaphore_create(0); - _willReleaseEngineSemaphore = dispatch_semaphore_create(0); + RCTLog(@"[AudioDeviceModuleObserver] Initialized observer: %@ for module: %@", self, module); } return self; } @@ -44,124 +26,110 @@ - (void)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule didReceiveSpeechActivityEvent:(RTCSpeechActivityEvent)speechActivityEvent { NSString *eventType = speechActivityEvent == RTCSpeechActivityEventStarted ? @"started" : @"ended"; - [self.module sendEventWithName:kEventAudioDeviceModuleSpeechActivity - body:@{ - @"event" : eventType, - }]; + if (self.module.bridge != nil) { + [self.module sendEventWithName:kEventAudioDeviceModuleSpeechActivity + body:@{ + @"event" : eventType, + }]; + } RCTLog(@"[AudioDeviceModuleObserver] Speech activity event: %@", eventType); } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule didCreateEngine:(AVAudioEngine *)engine { - RCTLog(@"[AudioDeviceModuleObserver] Engine created - waiting for JS response"); - - [self.module sendEventWithName:kEventAudioDeviceModuleEngineCreated body:@{}]; + RCTLog(@"[AudioDeviceModuleObserver] Engine created"); - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.engineCreatedSemaphore, DISPATCH_TIME_FOREVER); + if (self.module.bridge != nil) { + [self.module sendEventWithName:kEventAudioDeviceModuleEngineCreated body:@{}]; + } - RCTLog(@"[AudioDeviceModuleObserver] Engine created - JS returned: %ld", (long)self.engineCreatedResult); - return self.engineCreatedResult; + return 0; // Success } - (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", + RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - playout: %d, recording: %d", isPlayoutEnabled, isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillEnable - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; - - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.willEnableEngineSemaphore, DISPATCH_TIME_FOREVER); - - RCTLog(@"[AudioDeviceModuleObserver] Engine will enable - JS returned: %ld", (long)self.willEnableEngineResult); - - AVAudioSession *audioSession = [AVAudioSession sharedInstance]; - RCTLog(@"[AudioDeviceModuleObserver] Audio session category: %@", audioSession.category); + if (self.module.bridge != nil) { + [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillEnable + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; + } - return self.willEnableEngineResult; + return 0; // Success } - (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", + RCTLog(@"[AudioDeviceModuleObserver] Engine will start - playout: %d, recording: %d", isPlayoutEnabled, isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillStart - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; - - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.willStartEngineSemaphore, DISPATCH_TIME_FOREVER); + if (self.module.bridge != nil) { + [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillStart + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; + } - RCTLog(@"[AudioDeviceModuleObserver] Engine will start - JS returned: %ld", (long)self.willStartEngineResult); - return self.willStartEngineResult; + return 0; // Success } - (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", + RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - playout: %d, recording: %d", isPlayoutEnabled, isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidStop - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; - - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.didStopEngineSemaphore, DISPATCH_TIME_FOREVER); + if (self.module.bridge != nil) { + [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidStop + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; + } - RCTLog(@"[AudioDeviceModuleObserver] Engine did stop - JS returned: %ld", (long)self.didStopEngineResult); - return self.didStopEngineResult; + return 0; // Success } - (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", + RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - playout: %d, recording: %d", isPlayoutEnabled, isRecordingEnabled); - [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidDisable - body:@{ - @"isPlayoutEnabled" : @(isPlayoutEnabled), - @"isRecordingEnabled" : @(isRecordingEnabled), - }]; - - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.didDisableEngineSemaphore, DISPATCH_TIME_FOREVER); + if (self.module.bridge != nil) { + [self.module sendEventWithName:kEventAudioDeviceModuleEngineDidDisable + body:@{ + @"isPlayoutEnabled" : @(isPlayoutEnabled), + @"isRecordingEnabled" : @(isRecordingEnabled), + }]; + } - RCTLog(@"[AudioDeviceModuleObserver] Engine did disable - JS returned: %ld", (long)self.didDisableEngineResult); - return self.didDisableEngineResult; + return 0; // Success } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule willReleaseEngine:(AVAudioEngine *)engine { - RCTLog(@"[AudioDeviceModuleObserver] Engine will release - waiting for JS response"); - - [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillRelease body:@{}]; + RCTLog(@"[AudioDeviceModuleObserver] Engine will release"); - // Wait indefinitely for JS to respond - dispatch_semaphore_wait(self.willReleaseEngineSemaphore, DISPATCH_TIME_FOREVER); + if (self.module.bridge != nil) { + [self.module sendEventWithName:kEventAudioDeviceModuleEngineWillRelease body:@{}]; + } - RCTLog(@"[AudioDeviceModuleObserver] Engine will release - JS returned: %ld", (long)self.willReleaseEngineResult); - return self.willReleaseEngineResult; + return 0; // Success } - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule @@ -185,41 +153,30 @@ - (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule } - (void)audioDeviceModuleDidUpdateDevices:(RTCAudioDeviceModule *)audioDeviceModule { - [self.module sendEventWithName:kEventAudioDeviceModuleDevicesUpdated body:@{}]; + if (self.module.bridge != nil) { + [self.module sendEventWithName:kEventAudioDeviceModuleDevicesUpdated body:@{}]; + } RCTLog(@"[AudioDeviceModuleObserver] Devices updated"); } -#pragma mark - Resolve methods from JS - -- (void)resolveEngineCreatedWithResult:(NSInteger)result { - self.engineCreatedResult = result; - dispatch_semaphore_signal(self.engineCreatedSemaphore); -} - -- (void)resolveWillEnableEngineWithResult:(NSInteger)result { - self.willEnableEngineResult = result; - dispatch_semaphore_signal(self.willEnableEngineSemaphore); -} - -- (void)resolveWillStartEngineWithResult:(NSInteger)result { - self.willStartEngineResult = result; - dispatch_semaphore_signal(self.willStartEngineSemaphore); -} - -- (void)resolveDidStopEngineWithResult:(NSInteger)result { - self.didStopEngineResult = result; - dispatch_semaphore_signal(self.didStopEngineSemaphore); -} - -- (void)resolveDidDisableEngineWithResult:(NSInteger)result { - self.didDisableEngineResult = result; - dispatch_semaphore_signal(self.didDisableEngineSemaphore); -} +- (void)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + didUpdateAudioProcessingState:(RTCAudioProcessingState)state { + if (self.module.bridge != nil) { + [self.module sendEventWithName:kEventAudioDeviceModuleAudioProcessingStateUpdated + body:@{ + @"voiceProcessingEnabled" : @(state.voiceProcessingEnabled), + @"voiceProcessingBypassed" : @(state.voiceProcessingBypassed), + @"voiceProcessingAGCEnabled" : @(state.voiceProcessingAGCEnabled), + @"stereoPlayoutEnabled" : @(state.stereoPlayoutEnabled), + }]; + } -- (void)resolveWillReleaseEngineWithResult:(NSInteger)result { - self.willReleaseEngineResult = result; - dispatch_semaphore_signal(self.willReleaseEngineSemaphore); + RCTLog(@"[AudioDeviceModuleObserver] Audio processing state updated - VP enabled: %d, VP bypassed: %d, AGC enabled: %d, stereo: %d", + state.voiceProcessingEnabled, + state.voiceProcessingBypassed, + state.voiceProcessingAGCEnabled, + state.stereoPlayoutEnabled); } @end diff --git a/ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift b/ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift index 64113fcf3..eb6ae7bd4 100644 --- a/ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift +++ b/ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift @@ -179,6 +179,10 @@ import WebRTC /// Strong reference to the current engine so we can introspect it if needed. @objc public var engine: AVAudioEngine? + /// Secondary observer that receives forwarded delegate callbacks. + /// This allows the AudioDeviceModuleObserver to receive events and forward them to JS. + private let delegateObserver: RTCAudioDeviceModuleDelegate + /// Textual diagnostics for logging and debugging. @objc public override var description: String { "{ " + @@ -195,12 +199,17 @@ import WebRTC } /// Creates a module that mirrors the provided WebRTC audio device module. - /// - Parameter source: The audio device module implementation to observe. + /// - Parameters: + /// - source: The audio device module implementation to observe. + /// - delegateObserver: The observer that receives forwarded delegate callbacks. + /// - audioLevelsNodeAdapter: Adapter for audio level monitoring. init( _ source: any RTCAudioDeviceModuleControlling, + delegateObserver: RTCAudioDeviceModuleDelegate, audioLevelsNodeAdapter: AudioEngineNodeAdapting = AudioEngineLevelNodeAdapter() ) { self.source = source + self.delegateObserver = delegateObserver self.isPlayingSubject = .init(source.isPlaying) self.isRecordingSubject = .init(source.isRecording) self.isMicrophoneMutedSubject = .init(source.isMicrophoneMuted) @@ -227,10 +236,12 @@ import WebRTC } /// Objective-C compatible convenience initializer. - /// - Parameter source: The RTCAudioDeviceModule to wrap. + /// - Parameters: + /// - source: The RTCAudioDeviceModule to wrap. + /// - delegateObserver: The observer that receives forwarded delegate callbacks. @objc public - convenience init(source: RTCAudioDeviceModule) { - self.init(source as any RTCAudioDeviceModuleControlling, audioLevelsNodeAdapter: AudioEngineLevelNodeAdapter()) + convenience init(source: RTCAudioDeviceModule, delegateObserver: RTCAudioDeviceModuleDelegate) { + self.init(source as any RTCAudioDeviceModuleControlling, delegateObserver: delegateObserver, audioLevelsNodeAdapter: AudioEngineLevelNodeAdapter()) } // MARK: - Recording @@ -346,6 +357,9 @@ import WebRTC @unknown default: break } + + // Forward to observer + delegateObserver.audioDeviceModule(audioDeviceModule, didReceiveSpeechActivityEvent: speechActivityEvent) } /// Stores the created engine reference and emits an event so observers can @@ -356,6 +370,10 @@ import WebRTC ) -> Int { self.engine = engine subject.send(.didCreateAudioEngine(engine)) + + // Forward to observer + delegateObserver.audioDeviceModule(audioDeviceModule, didCreateEngine: engine) + return Constant.successResult } @@ -376,6 +394,15 @@ import WebRTC ) isPlayingSubject.send(isPlayoutEnabled) isRecordingSubject.send(isRecordingEnabled) + + // Forward to observer + delegateObserver.audioDeviceModule( + audioDeviceModule, + willEnableEngine: engine, + isPlayoutEnabled: isPlayoutEnabled, + isRecordingEnabled: isRecordingEnabled + ) + return Constant.successResult } @@ -397,6 +424,14 @@ import WebRTC isPlayingSubject.send(isPlayoutEnabled) isRecordingSubject.send(isRecordingEnabled) + // Forward to observer + delegateObserver.audioDeviceModule( + audioDeviceModule, + willStartEngine: engine, + isPlayoutEnabled: isPlayoutEnabled, + isRecordingEnabled: isRecordingEnabled + ) + return Constant.successResult } @@ -417,6 +452,15 @@ import WebRTC ) isPlayingSubject.send(isPlayoutEnabled) isRecordingSubject.send(isRecordingEnabled) + + // Forward to observer + delegateObserver.audioDeviceModule( + audioDeviceModule, + didStopEngine: engine, + isPlayoutEnabled: isPlayoutEnabled, + isRecordingEnabled: isRecordingEnabled + ) + return Constant.successResult } @@ -437,6 +481,15 @@ import WebRTC ) isPlayingSubject.send(isPlayoutEnabled) isRecordingSubject.send(isRecordingEnabled) + + // Forward to observer + delegateObserver.audioDeviceModule( + audioDeviceModule, + didDisableEngine: engine, + isPlayoutEnabled: isPlayoutEnabled, + isRecordingEnabled: isRecordingEnabled + ) + return Constant.successResult } @@ -448,6 +501,10 @@ import WebRTC self.engine = nil subject.send(.willReleaseAudioEngine(engine)) audioLevelsAdapter.uninstall(on: 0) + + // Forward to observer + delegateObserver.audioDeviceModule(audioDeviceModule, willReleaseEngine: engine) + return Constant.successResult } @@ -475,6 +532,17 @@ import WebRTC bus: 0, bufferSize: 1024 ) + + // Forward to observer + delegateObserver.audioDeviceModule( + audioDeviceModule, + engine: engine, + configureInputFromSource: source, + toDestination: destination, + format: format, + context: context + ) + return Constant.successResult } @@ -495,6 +563,17 @@ import WebRTC format: format ) ) + + // Forward to observer + delegateObserver.audioDeviceModule( + audioDeviceModule, + engine: engine, + configureOutputFromSource: source, + toDestination: destination, + format: format, + context: context + ) + return Constant.successResult } @@ -502,7 +581,8 @@ import WebRTC public func audioDeviceModuleDidUpdateDevices( _ audioDeviceModule: RTCAudioDeviceModule ) { - // No-op + // Forward to observer + delegateObserver.audioDeviceModuleDidUpdateDevices(audioDeviceModule) } /// Mirrors state changes coming from CallKit/WebRTC voice-processing @@ -523,6 +603,9 @@ import WebRTC isVoiceProcessingBypassedSubject.send(state.voiceProcessingBypassed) isVoiceProcessingAGCEnabledSubject.send(state.voiceProcessingAGCEnabled) isStereoPlayoutEnabledSubject.send(state.stereoPlayoutEnabled) + + // Forward to observer + delegateObserver.audioDeviceModule(module, didUpdateAudioProcessingState: state) } /// Mirrors the subset of properties that can be encoded for debugging. diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.h b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.h deleted file mode 100644 index 32fcd47f5..000000000 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.h +++ /dev/null @@ -1,5 +0,0 @@ -#import "WebRTCModule.h" - -@interface WebRTCModule (RTCAudioDeviceModule) - -@end diff --git a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m b/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m deleted file mode 100644 index fad0f7c3f..000000000 --- a/ios/RCTWebRTC/WebRTCModule+RTCAudioDeviceModule.m +++ /dev/null @@ -1,262 +0,0 @@ -#import - -#import -#import - -#import "AudioDeviceModuleObserver.h" -#import "WebRTCModule.h" - -@implementation WebRTCModule (RTCAudioDeviceModule) - -#pragma mark - Recording & Playback Control - -RCT_EXPORT_METHOD(audioDeviceModuleStartPlayout - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule startPlayout]; - if (result == 0) { - resolve(nil); - } else { - reject(@"playout_error", [NSString stringWithFormat:@"Failed to start playout: %ld", (long)result], nil); - } -} - -RCT_EXPORT_METHOD(audioDeviceModuleStopPlayout - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule stopPlayout]; - if (result == 0) { - resolve(nil); - } else { - reject(@"playout_error", [NSString stringWithFormat:@"Failed to stop playout: %ld", (long)result], nil); - } -} - -RCT_EXPORT_METHOD(audioDeviceModuleStartRecording - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule startRecording]; - if (result == 0) { - resolve(nil); - } else { - reject(@"recording_error", [NSString stringWithFormat:@"Failed to start recording: %ld", (long)result], nil); - } -} - -RCT_EXPORT_METHOD(audioDeviceModuleStopRecording - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule stopRecording]; - if (result == 0) { - resolve(nil); - } else { - reject(@"recording_error", [NSString stringWithFormat:@"Failed to stop recording: %ld", (long)result], nil); - } -} - -RCT_EXPORT_METHOD(audioDeviceModuleStartLocalRecording - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule initAndStartRecording]; - if (result == 0) { - resolve(nil); - } else { - reject( - @"recording_error", [NSString stringWithFormat:@"Failed to start local recording: %ld", (long)result], nil); - } -} - -RCT_EXPORT_METHOD(audioDeviceModuleStopLocalRecording - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule stopRecording]; - if (result == 0) { - resolve(nil); - } else { - reject( - @"recording_error", [NSString stringWithFormat:@"Failed to stop local recording: %ld", (long)result], nil); - } -} - -#pragma mark - Microphone Control - -RCT_EXPORT_METHOD(audioDeviceModuleSetMicrophoneMuted - : (BOOL)muted resolver - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule setMicrophoneMuted:muted]; - if (result == 0) { - resolve(nil); - } else { - reject(@"mute_error", [NSString stringWithFormat:@"Failed to set microphone mute: %ld", (long)result], nil); - } -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsMicrophoneMuted) { - return @(self.audioDeviceModule.isMicrophoneMuted); -} - -#pragma mark - Voice Processing - -RCT_EXPORT_METHOD(audioDeviceModuleSetVoiceProcessingEnabled - : (BOOL)enabled resolver - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule setVoiceProcessingEnabled:enabled]; - if (result == 0) { - resolve(nil); - } else { - reject(@"voice_processing_error", - [NSString stringWithFormat:@"Failed to set voice processing: %ld", (long)result], - nil); - } -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsVoiceProcessingEnabled) { - return @(self.audioDeviceModule.isVoiceProcessingEnabled); -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetVoiceProcessingBypassed : (BOOL)bypassed) { - self.audioDeviceModule.voiceProcessingBypassed = bypassed; - return nil; -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsVoiceProcessingBypassed) { - return @(self.audioDeviceModule.isVoiceProcessingBypassed); -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetVoiceProcessingAGCEnabled : (BOOL)enabled) { - self.audioDeviceModule.voiceProcessingAGCEnabled = enabled; - return nil; -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsVoiceProcessingAGCEnabled) { - return @(self.audioDeviceModule.isVoiceProcessingAGCEnabled); -} - -#pragma mark - Status - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsPlaying) { - return @(self.audioDeviceModule.isPlaying); -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsRecording) { - return @(self.audioDeviceModule.isRecording); -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsEngineRunning) { - return @(self.audioDeviceModule.isEngineRunning); -} - -#pragma mark - Advanced Features - -RCT_EXPORT_METHOD(audioDeviceModuleSetMuteMode - : (NSInteger)mode resolver - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule setMuteMode:(RTCAudioEngineMuteMode)mode]; - if (result == 0) { - resolve(nil); - } else { - reject(@"mute_mode_error", [NSString stringWithFormat:@"Failed to set mute mode: %ld", (long)result], nil); - } -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleGetMuteMode) { - return @(self.audioDeviceModule.muteMode); -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetAdvancedDuckingEnabled : (BOOL)enabled) { - self.audioDeviceModule.advancedDuckingEnabled = enabled; - return nil; -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsAdvancedDuckingEnabled) { - return @(self.audioDeviceModule.isAdvancedDuckingEnabled); -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleSetDuckingLevel : (NSInteger)level) { - self.audioDeviceModule.duckingLevel = level; - return nil; -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleGetDuckingLevel) { - return @(self.audioDeviceModule.duckingLevel); -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleIsRecordingAlwaysPreparedMode) { - return @(self.audioDeviceModule.recordingAlwaysPreparedMode); -} - -RCT_EXPORT_METHOD(audioDeviceModuleSetRecordingAlwaysPreparedMode - : (BOOL)enabled resolver - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - NSInteger result = [self.audioDeviceModule setRecordingAlwaysPreparedMode:enabled]; - if (result == 0) { - resolve(nil); - } else { - reject(@"recording_always_prepared_mode_error", - [NSString stringWithFormat:@"Failed to set recording always prepared mode: %ld", (long)result], - nil); - } -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleGetEngineAvailability) { - RTCAudioEngineAvailability availability = self.audioDeviceModule.engineAvailability; - return @{ - @"isInputAvailable" : @(availability.isInputAvailable), - @"isOutputAvailable" : @(availability.isOutputAvailable) - }; -} - -RCT_EXPORT_METHOD(audioDeviceModuleSetEngineAvailability - : (NSDictionary *)availabilityDict resolver - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - RTCAudioEngineAvailability availability; - availability.isInputAvailable = [availabilityDict[@"isInputAvailable"] boolValue]; - availability.isOutputAvailable = [availabilityDict[@"isOutputAvailable"] boolValue]; - NSInteger result = [self.audioDeviceModule setEngineAvailability:availability]; - if (result == 0) { - resolve(nil); - } else { - reject(@"engine_availability_error", - [NSString stringWithFormat:@"Failed to set engine availability: %ld", (long)result], - nil); - } -} - -#pragma mark - Observer Delegate Response Methods - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveEngineCreated : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveEngineCreatedWithResult:result]; - return nil; -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillEnableEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveWillEnableEngineWithResult:result]; - return nil; -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillStartEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveWillStartEngineWithResult:result]; - return nil; -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveDidStopEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveDidStopEngineWithResult:result]; - return nil; -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveDidDisableEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveDidDisableEngineWithResult:result]; - return nil; -} - -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(audioDeviceModuleResolveWillReleaseEngine : (NSInteger)result) { - [self.audioDeviceModuleObserver resolveWillReleaseEngineWithResult:result]; - return nil; -} - -@end diff --git a/ios/RCTWebRTC/WebRTCModule.h b/ios/RCTWebRTC/WebRTCModule.h index 989f97d70..538240911 100644 --- a/ios/RCTWebRTC/WebRTCModule.h +++ b/ios/RCTWebRTC/WebRTCModule.h @@ -31,8 +31,7 @@ static NSString *const kEventAudioDeviceModuleEngineDidStop = @"audioDeviceModul static NSString *const kEventAudioDeviceModuleEngineDidDisable = @"audioDeviceModuleEngineDidDisable"; static NSString *const kEventAudioDeviceModuleEngineWillRelease = @"audioDeviceModuleEngineWillRelease"; static NSString *const kEventAudioDeviceModuleDevicesUpdated = @"audioDeviceModuleDevicesUpdated"; - -@class AudioDeviceModuleObserver; +static NSString *const kEventAudioDeviceModuleAudioProcessingStateUpdated = @"audioDeviceModuleAudioProcessingStateUpdated"; @class AudioDeviceModule; @@ -53,9 +52,6 @@ static NSString *const kEventAudioDeviceModuleDevicesUpdated = @"audioDeviceModu @property(nonatomic, strong) NSMutableDictionary *keyProviders; @property(nonatomic, strong) NSMutableDictionary *dataPacketCryptors; -@property(nonatomic, readonly) RTCAudioDeviceModule *audioDeviceModule; -@property(nonatomic, strong) AudioDeviceModuleObserver *audioDeviceModuleObserver; - - (RTCMediaStream *)streamForReactTag:(NSString *)reactTag; @end diff --git a/ios/RCTWebRTC/WebRTCModule.m b/ios/RCTWebRTC/WebRTCModule.m index 54c0942af..da9a335a2 100644 --- a/ios/RCTWebRTC/WebRTCModule.m +++ b/ios/RCTWebRTC/WebRTCModule.m @@ -23,6 +23,9 @@ #endif @interface WebRTCModule () + +@property(nonatomic, strong) AudioDeviceModuleObserver *rtcAudioDeviceModuleObserver; + @end @implementation WebRTCModule @@ -107,8 +110,10 @@ - (instancetype)init { decoderFactory:decoderFactory audioProcessingModule:nil]; } - - _audioDeviceModule = [[AudioDeviceModule alloc] initWithSource:_peerConnectionFactory.audioDeviceModule]; + + _rtcAudioDeviceModuleObserver = [[AudioDeviceModuleObserver alloc] initWithWebRTCModule:self]; + _audioDeviceModule = [[AudioDeviceModule alloc] initWithSource:_peerConnectionFactory.audioDeviceModule + delegateObserver:_rtcAudioDeviceModuleObserver]; _peerConnections = [NSMutableDictionary new]; _localStreams = [NSMutableDictionary new]; @@ -118,10 +123,6 @@ - (instancetype)init { _keyProviders = [NSMutableDictionary new]; _dataPacketCryptors = [NSMutableDictionary new]; - _audioDeviceModule = _peerConnectionFactory.audioDeviceModule; - _audioDeviceModuleObserver = [[AudioDeviceModuleObserver alloc] initWithWebRTCModule:self]; - _audioDeviceModule.observer = _audioDeviceModuleObserver; - dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1); _workerQueue = dispatch_queue_create("WebRTCModule.queue", attributes); @@ -175,7 +176,8 @@ - (dispatch_queue_t)methodQueue { kEventAudioDeviceModuleEngineDidStop, kEventAudioDeviceModuleEngineDidDisable, kEventAudioDeviceModuleEngineWillRelease, - kEventAudioDeviceModuleDevicesUpdated + kEventAudioDeviceModuleDevicesUpdated, + kEventAudioDeviceModuleAudioProcessingStateUpdated ]; } diff --git a/package.json b/package.json index 7f604dc7a..94dfd02c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/react-native-webrtc", - "version": "137.1.0", + "version": "137.1.1-alpha.16", "repository": { "type": "git", "url": "git+https://github.com/GetStream/react-native-webrtc.git" diff --git a/src/AudioDeviceModule.ts b/src/AudioDeviceModule.ts deleted file mode 100644 index 8d2ab676d..000000000 --- a/src/AudioDeviceModule.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { NativeModules, Platform } from 'react-native'; - -const { WebRTCModule } = NativeModules; - -export enum AudioEngineMuteMode { - Unknown = -1, - VoiceProcessing = 0, - RestartEngine = 1, - InputMixer = 2, -} - -export interface AudioEngineAvailability { - isInputAvailable: boolean; - isOutputAvailable: boolean; -} - -export const AudioEngineAvailability = { - default: { - isInputAvailable: true, - isOutputAvailable: true, - }, - none: { - isInputAvailable: false, - isOutputAvailable: false, - }, -} as const; - -/** - * Audio Device Module API for controlling audio devices and settings. - * iOS/macOS only - will throw on Android. - */ -export class AudioDeviceModule { - /** - * Start audio playback - */ - static async startPlayout(): Promise { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleStartPlayout(); - } - - /** - * Stop audio playback - */ - static async stopPlayout(): Promise { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleStopPlayout(); - } - - /** - * Start audio recording - */ - static async startRecording(): Promise { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleStartRecording(); - } - - /** - * Stop audio recording - */ - static async stopRecording(): Promise { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleStopRecording(); - } - - /** - * Initialize and start local audio recording (calls initAndStartRecording) - */ - static async startLocalRecording(): Promise { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleStartLocalRecording(); - } - - /** - * Stop local audio recording - */ - static async stopLocalRecording(): Promise { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleStopLocalRecording(); - } - - /** - * Mute or unmute the microphone - */ - static async setMicrophoneMuted(muted: boolean): Promise { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleSetMicrophoneMuted(muted); - } - - /** - * Check if microphone is currently muted - */ - static isMicrophoneMuted(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleIsMicrophoneMuted(); - } - - /** - * Enable or disable voice processing (requires engine restart) - */ - static async setVoiceProcessingEnabled(enabled: boolean): Promise { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleSetVoiceProcessingEnabled(enabled); - } - - /** - * Check if voice processing is enabled - */ - static isVoiceProcessingEnabled(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleIsVoiceProcessingEnabled(); - } - - /** - * Temporarily bypass voice processing without restarting the engine - */ - static setVoiceProcessingBypassed(bypassed: boolean): void { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - WebRTCModule.audioDeviceModuleSetVoiceProcessingBypassed(bypassed); - } - - /** - * Check if voice processing is currently bypassed - */ - static isVoiceProcessingBypassed(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleIsVoiceProcessingBypassed(); - } - - /** - * Enable or disable Automatic Gain Control (AGC) - */ - static setVoiceProcessingAGCEnabled(enabled: boolean): void { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleSetVoiceProcessingAGCEnabled(enabled); - } - - /** - * Check if AGC is enabled - */ - static isVoiceProcessingAGCEnabled(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleIsVoiceProcessingAGCEnabled(); - } - - /** - * Check if audio is currently playing - */ - static isPlaying(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleIsPlaying(); - } - - /** - * Check if audio is currently recording - */ - static isRecording(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleIsRecording(); - } - - /** - * Check if the audio engine is running - */ - static isEngineRunning(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleIsEngineRunning(); - } - - /** - * Set the microphone mute mode - */ - static async setMuteMode(mode: AudioEngineMuteMode): Promise { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleSetMuteMode(mode); - } - - /** - * Get the current mute mode - */ - static getMuteMode(): AudioEngineMuteMode { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleGetMuteMode(); - } - - /** - * Enable or disable advanced audio ducking - */ - static setAdvancedDuckingEnabled(enabled: boolean): void { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleSetAdvancedDuckingEnabled(enabled); - } - - /** - * Check if advanced ducking is enabled - */ - static isAdvancedDuckingEnabled(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleIsAdvancedDuckingEnabled(); - } - - /** - * Set the audio ducking level (0-100) - */ - static setDuckingLevel(level: number): void { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleSetDuckingLevel(level); - } - - /** - * Get the current ducking level - */ - static getDuckingLevel(): number { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleGetDuckingLevel(); - } - - /** - * Check if recording always prepared mode is enabled - */ - static isRecordingAlwaysPreparedMode(): boolean { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleIsRecordingAlwaysPreparedMode(); - } - - /** - * Enable or disable recording always prepared mode - */ - static async setRecordingAlwaysPreparedMode(enabled: boolean): Promise { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleSetRecordingAlwaysPreparedMode(enabled); - } - - /** - * Get the current engine availability (input/output availability) - */ - static getEngineAvailability(): AudioEngineAvailability { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleGetEngineAvailability(); - } - - /** - * Set the engine availability (input/output availability) - */ - static async setEngineAvailability(availability: AudioEngineAvailability): Promise { - if (Platform.OS === 'android') { - throw new Error('AudioDeviceModule is only available on iOS/macOS'); - } - - return WebRTCModule.audioDeviceModuleSetEngineAvailability(availability); - } -} diff --git a/src/AudioDeviceModuleEvents.ts b/src/AudioDeviceModuleEvents.ts index e0997d87e..190a62cde 100644 --- a/src/AudioDeviceModuleEvents.ts +++ b/src/AudioDeviceModuleEvents.ts @@ -13,149 +13,34 @@ export interface EngineStateEventData { isRecordingEnabled: boolean; } -export type AudioDeviceModuleEventType = - | 'speechActivity' - | 'devicesUpdated'; +export interface AudioProcessingStateEventData { + voiceProcessingEnabled: boolean; + voiceProcessingBypassed: boolean; + voiceProcessingAGCEnabled: boolean; + stereoPlayoutEnabled: boolean; +} export type AudioDeviceModuleEventData = | SpeechActivityEventData | EngineStateEventData + | AudioProcessingStateEventData | Record; // Empty object for events with no data -export type AudioDeviceModuleEventListener = (data: AudioDeviceModuleEventData) => void; - -/** - * Handler function that must return a number (0 for success, non-zero for error) - */ -export type AudioEngineEventNoParamsHandler = () => Promise; -export type AudioEngineEventHandler = (params: { - isPlayoutEnabled: boolean; - isRecordingEnabled: boolean; -}) => Promise; - /** * Event emitter for RTCAudioDeviceModule delegate callbacks. * iOS/macOS only. */ class AudioDeviceModuleEventEmitter { private eventEmitter: NativeEventEmitter | null = null; - private engineCreatedHandler: AudioEngineEventNoParamsHandler | null = null; - private willEnableEngineHandler: AudioEngineEventHandler | null = null; - private willStartEngineHandler: AudioEngineEventHandler | null = null; - private didStopEngineHandler: AudioEngineEventHandler | null = null; - private didDisableEngineHandler: AudioEngineEventHandler | null = null; - private willReleaseEngineHandler: AudioEngineEventNoParamsHandler | null = null; public setupListeners() { + // Only setup once (idempotent) + if (this.eventEmitter !== null) { + return; + } + if (Platform.OS !== 'android' && WebRTCModule) { this.eventEmitter = new NativeEventEmitter(WebRTCModule); - - // Setup handlers for blocking delegate methods - this.eventEmitter.addListener('audioDeviceModuleEngineCreated', async () => { - let result = 0; - - if (this.engineCreatedHandler) { - try { - await this.engineCreatedHandler(); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } - } - - WebRTCModule.audioDeviceModuleResolveEngineCreated(result); - }); - - this.eventEmitter.addListener( - 'audioDeviceModuleEngineWillEnable', - async (params: { isPlayoutEnabled: boolean; isRecordingEnabled: boolean }) => { - const { isPlayoutEnabled, isRecordingEnabled } = params; - let result = 0; - - if (this.willEnableEngineHandler) { - try { - await this.willEnableEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } - } - - WebRTCModule.audioDeviceModuleResolveWillEnableEngine(result); - }, - ); - - this.eventEmitter.addListener( - 'audioDeviceModuleEngineWillStart', - async (params: { isPlayoutEnabled: boolean; isRecordingEnabled: boolean }) => { - const { isPlayoutEnabled, isRecordingEnabled } = params; - let result = 0; - - if (this.willStartEngineHandler) { - try { - await this.willStartEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } - } - - WebRTCModule.audioDeviceModuleResolveWillStartEngine(result); - }, - ); - - this.eventEmitter.addListener( - 'audioDeviceModuleEngineDidStop', - async (params: { isPlayoutEnabled: boolean; isRecordingEnabled: boolean }) => { - const { isPlayoutEnabled, isRecordingEnabled } = params; - let result = 0; - - if (this.didStopEngineHandler) { - try { - await this.didStopEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } - } - - WebRTCModule.audioDeviceModuleResolveDidStopEngine(result); - }, - ); - - this.eventEmitter.addListener( - 'audioDeviceModuleEngineDidDisable', - async (params: { isPlayoutEnabled: boolean; isRecordingEnabled: boolean }) => { - const { isPlayoutEnabled, isRecordingEnabled } = params; - let result = 0; - - if (this.didDisableEngineHandler) { - try { - await this.didDisableEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } - } - - WebRTCModule.audioDeviceModuleResolveDidDisableEngine(result); - }, - ); - - this.eventEmitter.addListener('audioDeviceModuleEngineWillRelease', async () => { - let result = 0; - - if (this.willReleaseEngineHandler) { - try { - await this.willReleaseEngineHandler(); - } catch (error) { - // If error is a number, use it as the error code, otherwise use -1 - result = typeof error === 'number' ? error : -1; - } - } - - WebRTCModule.audioDeviceModuleResolveWillReleaseEngine(result); - }); } } @@ -182,51 +67,80 @@ class AudioDeviceModuleEventEmitter { } /** - * Set handler for engine created delegate - MUST return 0 for success or error code - * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + * Subscribe to audio processing state updated event + */ + addAudioProcessingStateUpdatedListener(listener: (data: AudioProcessingStateEventData) => void) { + if (!this.eventEmitter) { + throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + } + + return this.eventEmitter.addListener('audioDeviceModuleAudioProcessingStateUpdated', listener); + } + + /** + * Subscribe to engine created event */ - setEngineCreatedHandler(handler: AudioEngineEventNoParamsHandler | null) { - this.engineCreatedHandler = handler; + addEngineCreatedListener(listener: () => void) { + if (!this.eventEmitter) { + throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + } + + return this.eventEmitter.addListener('audioDeviceModuleEngineCreated', listener); } /** - * Set handler for will enable engine delegate - MUST return 0 for success or error code - * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + * Subscribe to engine will enable event */ - setWillEnableEngineHandler(handler: AudioEngineEventHandler | null) { - this.willEnableEngineHandler = handler; + addEngineWillEnableListener(listener: (data: EngineStateEventData) => void) { + if (!this.eventEmitter) { + throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + } + + return this.eventEmitter.addListener('audioDeviceModuleEngineWillEnable', listener); } /** - * Set handler for will start engine delegate - MUST return 0 for success or error code - * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + * Subscribe to engine will start event */ - setWillStartEngineHandler(handler: AudioEngineEventHandler | null) { - this.willStartEngineHandler = handler; + addEngineWillStartListener(listener: (data: EngineStateEventData) => void) { + if (!this.eventEmitter) { + throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + } + + return this.eventEmitter.addListener('audioDeviceModuleEngineWillStart', listener); } /** - * Set handler for did stop engine delegate - MUST return 0 for success or error code - * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + * Subscribe to engine did stop event */ - setDidStopEngineHandler(handler: AudioEngineEventHandler | null) { - this.didStopEngineHandler = handler; + addEngineDidStopListener(listener: (data: EngineStateEventData) => void) { + if (!this.eventEmitter) { + throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + } + + return this.eventEmitter.addListener('audioDeviceModuleEngineDidStop', listener); } /** - * Set handler for did disable engine delegate - MUST return 0 for success or error code - * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + * Subscribe to engine did disable event */ - setDidDisableEngineHandler(handler: AudioEngineEventHandler | null) { - this.didDisableEngineHandler = handler; + addEngineDidDisableListener(listener: (data: EngineStateEventData) => void) { + if (!this.eventEmitter) { + throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + } + + return this.eventEmitter.addListener('audioDeviceModuleEngineDidDisable', listener); } /** - * Set handler for will release engine delegate - * This handler blocks the native thread until it returns, throw to cancel audio engine's operation + * Subscribe to engine will release event */ - setWillReleaseEngineHandler(handler: AudioEngineEventNoParamsHandler | null) { - this.willReleaseEngineHandler = handler; + addEngineWillReleaseListener(listener: () => void) { + if (!this.eventEmitter) { + throw new Error('AudioDeviceModuleEvents is only available on iOS/macOS'); + } + + return this.eventEmitter.addListener('audioDeviceModuleEngineWillRelease', listener); } } diff --git a/src/index.ts b/src/index.ts index 262949014..496c83b96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,6 @@ if (WebRTCModule === null) { }`); } -import { AudioDeviceModule, AudioEngineMuteMode, type AudioEngineAvailability } from './AudioDeviceModule'; import { audioDeviceModuleEvents } from './AudioDeviceModuleEvents'; import { setupNativeEvents } from './EventEmitter'; import Logger from './Logger'; @@ -33,6 +32,9 @@ Logger.enable(`${Logger.ROOT_PREFIX}:*`); // Add listeners for the native events early, since they are added asynchronously. setupNativeEvents(); +// Ensure audioDeviceModuleEvents is initialized and event listeners are registered +audioDeviceModuleEvents.setupListeners(); + export { RTCIceCandidate, RTCPeerConnection, @@ -50,9 +52,6 @@ export { mediaDevices, permissions, registerGlobals, - AudioDeviceModule, - AudioEngineMuteMode, - type AudioEngineAvailability, audioDeviceModuleEvents, }; @@ -84,7 +83,4 @@ function registerGlobals(): void { global.RTCRtpReceiver = RTCRtpReceiver; global.RTCRtpSender = RTCRtpSender; global.RTCErrorEvent = RTCErrorEvent; - - // Ensure audioDeviceModuleEvents is initialized and event listeners are registered - audioDeviceModuleEvents.setupListeners(); } From fac1e1e5091772fc1e6b11686276e4a607edc3d3 Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Fri, 6 Feb 2026 14:59:57 +0100 Subject: [PATCH 15/15] change muteMode --- .../AudioDeviceModule/AudioDeviceModule.swift | 6 +++++- ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m | 20 +------------------ 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift b/ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift index 7863a2695..f46c2c911 100644 --- a/ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift +++ b/ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift @@ -228,6 +228,7 @@ import WebRTC .eraseToAnyPublisher() super.init() + _ = source.setMuteMode(.inputMixer) audioLevelsAdapter.subject = audioLevelSubject source.observer = self } @@ -246,6 +247,7 @@ import WebRTC /// Reinitializes the ADM, clearing its internal audio graph state. @objc public func reset() { _ = source.reset() + _ = source.setMuteMode(.inputMixer) } /// Switches between stereo and mono playout while keeping the recording @@ -258,7 +260,7 @@ import WebRTC /// means that for outputs where VP is disabled (e.g. stereo) we cannot mute/unmute. /// - `.restartEngine`: rebuilds the whole graph and requires explicit calling of /// `initAndStartRecording` . - _ = source.setMuteMode(isPreferred ? .inputMixer : .voiceProcessing) + // _ = source.setMuteMode(isPreferred ? .inputMixer : .voiceProcessing) /// - Important: We can probably set this one to false when the user doesn't have /// sendAudio capability. _ = source.setRecordingAlwaysPreparedMode(false) @@ -349,8 +351,10 @@ import WebRTC ) { switch speechActivityEvent { case .started: + NSLog("[Callingx | AudioDeviceModule] speechActivityStarted") subject.send(.speechActivityStarted) case .ended: + NSLog("[Callingx | AudioDeviceModule] speechActivityEnded") subject.send(.speechActivityEnded) @unknown default: break diff --git a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m index d4cec4ca8..682fc3473 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m @@ -18,12 +18,6 @@ #import "TrackCapturerEventsEmitter.h" #import "VideoCaptureController.h" -#if __has_include() -#import -#elif __has_include("stream_react_native_webrtc-Swift.h") -#import "stream_react_native_webrtc-Swift.h" -#endif - @implementation WebRTCModule (RTCMediaStream) - (VideoEffectProcessor *)videoEffectProcessor @@ -500,14 +494,10 @@ - (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack { RTCMediaStreamTrack *track = self.localTracks[trackID]; if (track) { - // Clean up dimension detection for local video tracks if ([track.kind isEqualToString:@"video"]) { + // Clean up dimension detection for local video tracks [self removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)track]; - } else { - // disable recording for local audio tracks - [[self audioDeviceModule] setRecording:false error:nil]; } - if (track.isEnabled) { track.isEnabled = NO; } @@ -568,15 +558,7 @@ - (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack { return; } - if ([track.kind isEqual:@"audio"]) { - RTCMediaStreamTrack *track = self.localTracks[trackID]; - if (track) { - [[self audioDeviceModule] setRecording:enabled error:nil]; - } - } - track.isEnabled = enabled; - #if !TARGET_OS_TV if (track.captureController) { // It could be a remote track!