From 227533d7839d81c52cbd3676bcc831742a83fd2b Mon Sep 17 00:00:00 2001 From: Dmytro Date: Wed, 1 Jul 2026 16:58:54 +0300 Subject: [PATCH] fix: register ADM observer after the peer connection factory is created (iOS) The observer was assigned in the plugin init, but _peerConnectionFactory is created lazily in initialize:, so the assignment ran on nil and was a silent no-op - no ADM callback (device updates via audioDeviceModuleDidUpdateDevices, engine lifecycle) was ever delivered. The observer is now set right after the factory is created. The full RTCAudioDeviceModuleDelegate protocol is implemented with no-op stubs for the engine-lifecycle methods: the ADM invokes every method without respondsToSelector checks (all are required), so a partial adoption would crash once the observer is live. --- common/darwin/Classes/FlutterWebRTCPlugin.m | 76 ++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/common/darwin/Classes/FlutterWebRTCPlugin.m b/common/darwin/Classes/FlutterWebRTCPlugin.m index e8e4efbad0..e6c57e2f8d 100644 --- a/common/darwin/Classes/FlutterWebRTCPlugin.m +++ b/common/darwin/Classes/FlutterWebRTCPlugin.m @@ -215,8 +215,9 @@ - (instancetype)initWithChannel:(FlutterMethodChannel*)channel object:session]; #endif - // Observe audio device module events. - _peerConnectionFactory.audioDeviceModule.observer = self; + // NOTE: do not set the ADM observer here - _peerConnectionFactory is created lazily in + // initialize:, so at this point it is nil and the assignment would be a silent no-op. + // The observer is set right after the factory is created. return self; } @@ -327,6 +328,11 @@ - (void)initialize:(NSArray*)networkIgnoreMask decoderFactory:decoderFactory audioProcessingModule:_audioManager.audioProcessingModule]; + // Observe audio device module events (device changes + AudioEngine lifecycle). + // Must happen after the factory exists; the property is weak, but the plugin + // instance is retained by the Flutter registrar for the app's lifetime. + _peerConnectionFactory.audioDeviceModule.observer = self; + #if TARGET_OS_OSX // CoreAudio ADM requires explicit device initialization on macOS RTCAudioDeviceModule* audioDeviceModule = [_peerConnectionFactory audioDeviceModule]; @@ -2681,4 +2687,70 @@ - (void)audioDeviceModuleDidUpdateDevices:(RTCAudioDeviceModule *)audioDeviceMod } } +#if TARGET_OS_IPHONE +// AudioEngine ADM delegate. Only device updates are handled; the engine-lifecycle methods +// are no-op stubs, implemented because the ADM invokes every protocol method without +// respondsToSelector checks (all methods are required) - a partial adoption would crash +// with unrecognized selector once the observer is registered. +- (void)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + didReceiveSpeechActivityEvent:(RTCSpeechActivityEvent)speechActivityEvent { +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + didCreateEngine:(AVAudioEngine *)engine { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + willEnableEngine:(AVAudioEngine *)engine + isPlayoutEnabled:(BOOL)isPlayoutEnabled + isRecordingEnabled:(BOOL)isRecordingEnabled { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + willStartEngine:(AVAudioEngine *)engine + isPlayoutEnabled:(BOOL)isPlayoutEnabled + isRecordingEnabled:(BOOL)isRecordingEnabled { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + didStopEngine:(AVAudioEngine *)engine + isPlayoutEnabled:(BOOL)isPlayoutEnabled + isRecordingEnabled:(BOOL)isRecordingEnabled { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + didDisableEngine:(AVAudioEngine *)engine + isPlayoutEnabled:(BOOL)isPlayoutEnabled + isRecordingEnabled:(BOOL)isRecordingEnabled { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + willReleaseEngine:(AVAudioEngine *)engine { + return 0; +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + engine:(AVAudioEngine *)engine + configureInputFromSource:(AVAudioNode *)source + toDestination:(AVAudioNode *)destination + withFormat:(AVAudioFormat *)format + context:(NSDictionary *)context { + return 0; // no input-graph changes; the ADM applies its default wiring +} + +- (NSInteger)audioDeviceModule:(RTCAudioDeviceModule *)audioDeviceModule + engine:(AVAudioEngine *)engine + configureOutputFromSource:(AVAudioNode *)source + toDestination:(AVAudioNode *)destination + withFormat:(AVAudioFormat *)format + context:(NSDictionary *)context { + return 0; // no output-graph changes; the ADM applies its default wiring +} +#endif + @end