diff --git a/sdk/objc/native/src/audio/audio_device_ios.h b/sdk/objc/native/src/audio/audio_device_ios.h index 53645e7a0d..83bfb29aea 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.h +++ b/sdk/objc/native/src/audio/audio_device_ios.h @@ -159,6 +159,7 @@ class AudioDeviceIOS : public AudioDeviceGeneric, UInt32 bus_number, UInt32 num_frames, AudioBufferList* io_data) override; + void OnRunningStateChanged() override; bool IsInterrupted(); @@ -171,6 +172,7 @@ class AudioDeviceIOS : public AudioDeviceGeneric, void HandleSampleRateChange(); void HandlePlayoutGlitchDetected(); void HandleOutputVolumeChange(); + void HandleAudioUnitRunningStateChange(); // Uses current `playout_parameters_` and `record_parameters_` to inform the // audio device buffer (ADB) about our internal audio parameters. diff --git a/sdk/objc/native/src/audio/audio_device_ios.mm b/sdk/objc/native/src/audio/audio_device_ios.mm index aa9a5788ed..091d013c72 100644 --- a/sdk/objc/native/src/audio/audio_device_ios.mm +++ b/sdk/objc/native/src/audio/audio_device_ios.mm @@ -382,6 +382,11 @@ static void LogDeviceInfo() { thread_->PostTask(SafeTask(safety_, [this] { HandleValidRouteChange(); })); } +void AudioDeviceIOS::OnRunningStateChanged() { + RTC_DCHECK(thread_); + thread_->PostTask(SafeTask(safety_, [this] { HandleAudioUnitRunningStateChange(); })); +} + void AudioDeviceIOS::OnCanPlayOrRecordChange(bool can_play_or_record) { RTC_DCHECK(thread_); thread_->PostTask(SafeTask( @@ -549,6 +554,25 @@ static void LogDeviceInfo() { HandleSampleRateChange(); } +void AudioDeviceIOS::HandleAudioUnitRunningStateChange() { + RTC_DCHECK_RUN_ON(thread_); + // The listener fires on every start and stop, including WebRTC's own and the + // restart below; those runs return here. Only a unit marked as started that + // isn't running was stopped by someone else, e.g. other code deactivating the + // audio session. + if (!audio_unit_ || audio_unit_->GetState() != VoiceProcessingAudioUnit::kStarted || + audio_unit_->IsRunning()) { + return; + } + if (!audio_unit_->Stop()) { + RTCLogError(@"Failed to stop the externally stopped audio unit. Not restarting it."); + return; + } + PrepareForNewStart(); + RTCLog(@"Audio unit was stopped externally. Restarting it."); + UpdateAudioUnit([RTC_OBJC_TYPE(RTCAudioSession) sharedInstance].canPlayOrRecord); +} + void AudioDeviceIOS::HandleCanPlayOrRecordChange(bool can_play_or_record) { RTCLog(@"Handling CanPlayOrRecord change to: %d", can_play_or_record); UpdateAudioUnit(can_play_or_record); diff --git a/sdk/objc/native/src/audio/voice_processing_audio_unit.h b/sdk/objc/native/src/audio/voice_processing_audio_unit.h index bbc9f69d9b..d333b83160 100644 --- a/sdk/objc/native/src/audio/voice_processing_audio_unit.h +++ b/sdk/objc/native/src/audio/voice_processing_audio_unit.h @@ -34,6 +34,8 @@ class VoiceProcessingAudioUnitObserver { UInt32 num_frames, AudioBufferList* io_data) = 0; + virtual void OnRunningStateChanged() = 0; + protected: ~VoiceProcessingAudioUnitObserver() {} }; @@ -77,6 +79,8 @@ class VoiceProcessingAudioUnit { VoiceProcessingAudioUnit::State GetState() const; + bool IsRunning() const; + // Initializes the underlying audio unit with the given sample rate. // When enable_input is false, the input bus is disabled so iOS does not // request microphone permission and the orange mic indicator stays off. @@ -115,6 +119,12 @@ class VoiceProcessingAudioUnit { UInt32 num_frames, AudioBufferList* io_data); + static void OnIsRunningChanged(void* in_ref_con, + AudioUnit audio_unit, + AudioUnitPropertyID property_id, + AudioUnitScope scope, + AudioUnitElement element); + // Notifies observer that samples are needed for playback. OSStatus NotifyGetPlayoutData(AudioUnitRenderActionFlags* flags, const AudioTimeStamp* time_stamp, diff --git a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm index 2b19fb30e7..80c147f7b1 100644 --- a/sdk/objc/native/src/audio/voice_processing_audio_unit.mm +++ b/sdk/objc/native/src/audio/voice_processing_audio_unit.mm @@ -112,6 +112,12 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { return false; } + result = AudioUnitAddPropertyListener( + vpio_unit_, kAudioOutputUnitProperty_IsRunning, OnIsRunningChanged, observer_); + if (result != noErr) { + RTCLogError(@"Failed to add the IsRunning listener. Error=%ld.", (long)result); + } + // Enable (or leave disabled) input on the input scope of the input element. // CRITICAL: setting EnableIO=1 on a Voice Processing I/O input bus causes // iOS (via AURemoteIO::SetProperty → AudioSessionRequestRecordPermission) @@ -374,6 +380,18 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { return noErr; } +bool VoiceProcessingAudioUnit::IsRunning() const { + UInt32 is_running = 0; + UInt32 size = sizeof(is_running); + OSStatus result = AudioUnitGetProperty(vpio_unit_, + kAudioOutputUnitProperty_IsRunning, + kAudioUnitScope_Global, + 0, + &is_running, + &size); + return result == noErr && is_running != 0; +} + bool VoiceProcessingAudioUnit::Stop() { RTC_DCHECK_GE(state_, kUninitialized); RTCLog(@"Stopping audio unit."); @@ -447,6 +465,14 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { num_frames, io_data); } +void VoiceProcessingAudioUnit::OnIsRunningChanged(void* in_ref_con, + AudioUnit audio_unit, + AudioUnitPropertyID property_id, + AudioUnitScope scope, + AudioUnitElement element) { + static_cast(in_ref_con)->OnRunningStateChanged(); +} + OSStatus VoiceProcessingAudioUnit::NotifyGetPlayoutData( AudioUnitRenderActionFlags* flags, const AudioTimeStamp* time_stamp, @@ -502,6 +528,9 @@ static OSStatus GetAGCState(AudioUnit audio_unit, UInt32* enabled) { break; } + AudioUnitRemovePropertyListenerWithUserData( + vpio_unit_, kAudioOutputUnitProperty_IsRunning, OnIsRunningChanged, observer_); + RTCLog(@"Disposing audio unit."); OSStatus result = AudioComponentInstanceDispose(vpio_unit_); if (result != noErr) {