diff --git a/modules/audio_device/audio_engine_device.h b/modules/audio_device/audio_engine_device.h index 74169b31d2..4193f846ed 100644 --- a/modules/audio_device/audio_engine_device.h +++ b/modules/audio_device/audio_engine_device.h @@ -94,7 +94,11 @@ enum AudioEngineErrorCode { // Voice processing errors kAudioEngineVoiceProcessingError = -8000, - kAudioEngineAGCError = -8001 + kAudioEngineAGCError = -8001, + + // Permission and session errors + kAudioEngineErrorInsufficientDevicePermission = -9000, + kAudioEngineErrorAudioSessionInvalidCategory = -9001 }; class FineAudioBuffer; @@ -124,8 +128,6 @@ class AudioEngineDevice : public AudioDeviceModule, public AudioSessionObserver bool output_available = true; bool input_available = true; - // Output will be enabled when input is enabled - bool input_follow_mode = true; bool input_enabled_persistent_mode = false; bool input_muted = true; @@ -150,7 +152,6 @@ class AudioEngineDevice : public AudioDeviceModule, public AudioSessionObserver return input_enabled == rhs.input_enabled && input_running == rhs.input_running && output_enabled == rhs.output_enabled && output_running == rhs.output_running && input_available == rhs.input_available && output_available == rhs.output_available && - input_follow_mode == rhs.input_follow_mode && input_enabled_persistent_mode == rhs.input_enabled_persistent_mode && input_muted == rhs.input_muted && is_interrupted == rhs.is_interrupted && render_mode == rhs.render_mode && mute_mode == rhs.mute_mode && @@ -165,27 +166,64 @@ class AudioEngineDevice : public AudioDeviceModule, public AudioSessionObserver bool operator!=(const EngineState& rhs) const { return !(*this == rhs); } - bool IsOutputInputLinked() const { return input_follow_mode && voice_processing_enabled; } + // AUDIO STATE LOGIC + // + // Device Mode: + // - Output follows input only when voice_processing_enabled=true (for AEC) + // - Input respects mute mode restrictions (RestartEngine + input_muted) + // - Independent operation when voice processing is disabled + // + // Manual Mode: + // - Bidirectional coupling: if ANY component is enabled/running, BOTH are considered + // enabled/running + // - No mute mode restrictions (manual control bypasses automatic muting) + // - Required for AVAudioEngine graph connectivity (input must connect to output) + // + // All modes respect availability flags (input_available/output_available) bool IsOutputEnabled() const { - bool result = IsOutputInputLinked() ? (IsInputEnabled() || output_enabled) : output_enabled; - return output_available && result; + if (!output_available) return false; + + switch (render_mode) { + case RenderMode::Device: + return voice_processing_enabled ? (IsInputEnabled() || output_enabled) : output_enabled; + case RenderMode::Manual: + return output_enabled || input_enabled || input_enabled_persistent_mode; + } } bool IsOutputRunning() const { - bool result = IsOutputInputLinked() ? (IsInputRunning() || output_running) : output_running; - return output_available && result; + if (!output_available) return false; + + switch (render_mode) { + case RenderMode::Device: + return voice_processing_enabled ? (IsInputRunning() || output_running) : output_running; + case RenderMode::Manual: + return output_running || input_running; + } } bool IsInputEnabled() const { - bool result = !(mute_mode == MuteMode::RestartEngine && input_muted) && - (input_enabled || input_enabled_persistent_mode); - return input_available && result; + if (!input_available) return false; + + switch (render_mode) { + case RenderMode::Device: + return !(mute_mode == MuteMode::RestartEngine && input_muted) && + (input_enabled || input_enabled_persistent_mode); + case RenderMode::Manual: + return input_enabled || input_enabled_persistent_mode || output_enabled; + } } bool IsInputRunning() const { - bool result = !(mute_mode == MuteMode::RestartEngine && input_muted) && input_running; - return input_available && result; + if (!input_available) return false; + + switch (render_mode) { + case RenderMode::Device: + return !(mute_mode == MuteMode::RestartEngine && input_muted) && input_running; + case RenderMode::Manual: + return input_running || output_running; + } } bool IsAnyEnabled() const { return IsInputEnabled() || IsOutputEnabled(); } @@ -413,7 +451,6 @@ class AudioEngineDevice : public AudioDeviceModule, public AudioSessionObserver EngineState engine_state_ RTC_GUARDED_BY(thread_); - bool IsMicrophonePermissionGranted(); int32_t ModifyEngineState(std::function state_transform); int32_t ApplyDeviceEngineState(EngineStateUpdate state); @@ -441,6 +478,13 @@ class AudioEngineDevice : public AudioDeviceModule, public AudioSessionObserver std::vector input_device_labels_; #endif + bool IsMicrophonePermissionGranted(); + bool EnsureMicrophonePermissionSync(); + +#if !TARGET_OS_OSX + bool IsAudioSessionCategoryValid(NSString* category, bool is_input_enabled, + bool is_output_enabled); +#endif void DebugAudioEngine(); void StartRenderLoop(); diff --git a/modules/audio_device/audio_engine_device.mm b/modules/audio_device/audio_engine_device.mm index 6f2ed1e81d..85f726a91e 100644 --- a/modules/audio_device/audio_engine_device.mm +++ b/modules/audio_device/audio_engine_device.mm @@ -252,7 +252,7 @@ &runLoop); if (err != noErr) { LOGE() << "AudioObjectSetPropertyData failed with error: " << err; - return -1; + return kAudioEngineInitError; } // Listen for any device changes. @@ -261,7 +261,7 @@ &objectListenerProc, this); if (err != noErr) { LOGE() << "AudioObjectAddPropertyListener failed with error: " << err; - return -1; + return kAudioEngineInitError; } // Listen for default output device change. @@ -270,7 +270,7 @@ &objectListenerProc, this); if (err != noErr) { LOGE() << "AudioObjectAddPropertyListener failed with error: " << err; - return -1; + return kAudioEngineInitError; } // Listen for default input device change. @@ -279,7 +279,7 @@ &objectListenerProc, this); if (err != noErr) { LOGE() << "AudioObjectAddPropertyListener failed with error: " << err; - return -1; + return kAudioEngineInitError; } UpdateAllDeviceIDs(); @@ -309,7 +309,7 @@ &objectListenerProc, this); if (err != noErr) { LOGE() << "AudioObjectRemovePropertyListener failed with error: " << err; - return -1; + return kAudioEngineTerminateError; } propertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; @@ -317,7 +317,7 @@ &objectListenerProc, this); if (err != noErr) { LOGE() << "AudioObjectRemovePropertyListener failed with error: " << err; - return -1; + return kAudioEngineTerminateError; } propertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice; @@ -325,7 +325,7 @@ &objectListenerProc, this); if (err != noErr) { LOGE() << "AudioObjectRemovePropertyListener failed with error: " << err; - return -1; + return kAudioEngineTerminateError; } #endif @@ -1324,11 +1324,6 @@ })); } -bool AudioEngineDevice::IsMicrophonePermissionGranted() { - AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; - return status == AVAuthorizationStatusAuthorized; -} - int32_t AudioEngineDevice::ModifyEngineState( std::function state_transform) { RTC_DCHECK_RUN_ON(thread_); @@ -1441,6 +1436,17 @@ RTC_DCHECK_RUN_ON(thread_); RTC_DCHECK(engine_device_ == nullptr); + std::vector> rollback_actions; + + auto rollback = [&](int32_t result) { + // Execute rollback actions in reverse order (LIFO) + for (auto it = rollback_actions.rbegin(); it != rollback_actions.rend(); ++it) { + (*it)(); + } + + return result; + }; + auto outputNode = [this, state]() { RTC_DCHECK_RUN_ON(thread_); RTC_DCHECK(engine_manual_input_ != nil); @@ -1471,16 +1477,40 @@ engine_manual_input_, state.next.IsOutputEnabled(), state.next.IsInputEnabled()); if (result != 0) { LOGE() << "Call to OnEngineDidStop returned error: " << result; - return result; + return rollback(result); } } } + if (!state.next.IsOutputEnabled() && audio_device_buffer_->IsPlaying()) { + LOGI() << "Stopping playout buffer (Manual)..."; + if (engine_device_ != nullptr) { + // Rendering must be stopped first. + RTC_DCHECK(!engine_device_.running); + } + audio_device_buffer_->StopPlayout(); + } + + if (!state.next.IsInputEnabled() && audio_device_buffer_->IsRecording()) { + LOGI() << "Stopping record buffer (Manual)..."; + if (engine_device_ != nullptr) { + // Rendering must be stopped first. + RTC_DCHECK(!engine_device_.running); + } + audio_device_buffer_->StopRecording(); + } + if (state.next.IsAnyEnabled() && !state.prev.IsAnyEnabled()) { LOGI() << "Creating AVAudioEngine (Manual)..."; RTC_DCHECK(engine_manual_input_ == nullptr); engine_manual_input_ = [[AVAudioEngine alloc] init]; + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back create AVAudioEngine (Manual)..."; + engine_manual_input_ = nil; + }); + NSError* error = nil; BOOL result = [engine_manual_input_ enableManualRenderingMode:AVAudioEngineManualRenderingModeRealtime @@ -1489,35 +1519,18 @@ error:&error]; if (!result) { LOGE() << "Failed to set rendering mode (Manual): " << error.localizedDescription.UTF8String; + return rollback(kAudioEngineManualRenderingError); } if (observer_ != nullptr) { int32_t result = observer_->OnEngineDidCreate(engine_manual_input_); if (result != 0) { LOGE() << "Call to OnEngineDidCreate returned error: " << result; - return result; + return rollback(result); } } } - if (!state.next.IsOutputEnabled() && audio_device_buffer_->IsPlaying()) { - LOGI() << "Stopping playout buffer (Manual)..."; - if (engine_device_ != nullptr) { - // Rendering must be stopped first. - RTC_DCHECK(!engine_device_.running); - } - audio_device_buffer_->StopPlayout(); - } - - if (!state.next.IsInputEnabled() && audio_device_buffer_->IsRecording()) { - LOGI() << "Stopping record buffer (Manual)..."; - if (engine_device_ != nullptr) { - // Rendering must be stopped first. - RTC_DCHECK(!engine_device_.running); - } - audio_device_buffer_->StopRecording(); - } - if (state.DidAnyEnable() && observer_ != nullptr) { // Invoke here before configuring nodes. In iOS, session configuration is required before // enabling AGC, muted talker etc. @@ -1525,7 +1538,7 @@ engine_manual_input_, state.next.IsOutputEnabled(), state.next.IsInputEnabled()); if (result != 0) { LOGE() << "Call to OnEngineWillEnable returned error: " << result; - return result; + return rollback(result); } } @@ -1538,6 +1551,12 @@ RTC_DCHECK(audio_device_buffer_ != nullptr); fine_audio_buffer_.reset(new FineAudioBuffer(audio_device_buffer_.get())); + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back output setup (Manual)..."; + fine_audio_buffer_.reset(); + }); + } else if (state.prev.IsOutputEnabled() && !state.next.IsOutputEnabled()) { LOGI() << "Disabling output for AVAudioEngine (Manual)..."; RTC_DCHECK(!engine_manual_input_.running); @@ -1552,6 +1571,12 @@ RTC_DCHECK(audio_device_buffer_ != nullptr); fine_audio_buffer_.reset(new FineAudioBuffer(audio_device_buffer_.get())); + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back input setup (Manual)..."; + fine_audio_buffer_.reset(); + }); + if (this->observer_ != nullptr) { NSDictionary* context = @{}; int32_t result = this->observer_->OnEngineWillConnectInput( @@ -1559,7 +1584,7 @@ context); if (result != 0) { LOGE() << "Call to OnEngineWillConnectInput returned error: " << result; - return result; + return rollback(result); } } @@ -1577,7 +1602,7 @@ engine_manual_input_, state.next.IsOutputEnabled(), state.next.IsInputEnabled()); if (result != 0) { LOGE() << "Call to OnEngineDidDisable returned error: " << result; - return result; + return rollback(result); } } @@ -1590,6 +1615,14 @@ LOGI() << "Starting playout buffer (Manual)..."; audio_device_buffer_->StartPlayout(); fine_audio_buffer_->ResetPlayout(); + + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back playout buffer start (Manual)..."; + if (audio_device_buffer_->IsPlaying()) { + audio_device_buffer_->StopPlayout(); + } + }); } // Start recording buffer if input is running @@ -1601,6 +1634,14 @@ LOGI() << "Starting record buffer (Manual)..."; audio_device_buffer_->StartRecording(); fine_audio_buffer_->ResetRecord(); + + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back record buffer start (Manual)..."; + if (audio_device_buffer_->IsRecording()) { + audio_device_buffer_->StopRecording(); + } + }); } if (state.next.IsAnyRunning() && !state.prev.IsAnyRunning()) { @@ -1609,7 +1650,7 @@ engine_manual_input_, state.next.IsOutputEnabled(), state.next.IsInputEnabled()); if (result != 0) { LOGE() << "Call to OnEngineWillStart returned error: " << result; - return result; + return rollback(result); } } @@ -1618,11 +1659,23 @@ render_buffer_ = [[AVAudioPCMBuffer alloc] initWithPCMFormat:manual_render_rtc_format_ frameCapacity:kMaximumFramesPerBuffer]; + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back render buffer allocation (Manual)..."; + render_buffer_ = nullptr; + }); + LOGI() << "Allocating read buffer (Manual)..."; RTC_DCHECK(read_buffer_ == nullptr); read_buffer_ = [[AVAudioPCMBuffer alloc] initWithPCMFormat:manual_render_rtc_format_ frameCapacity:kMaximumFramesPerBuffer]; + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back read buffer allocation (Manual)..."; + read_buffer_ = nullptr; + }); + LOGI() << "Starting AVAudioEngine (Manual)..."; NSError* error = nil; @@ -1630,8 +1683,17 @@ if (!start_result) { LOGE() << "Failed to start engine after " << kStartEngineMaxRetries << " attempts"; DebugAudioEngine(); + return rollback(kAudioEnginePlayoutStartError); } + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back engine start (Manual)..."; + if (engine_manual_input_ != nil && engine_manual_input_.running) { + [engine_manual_input_ stop]; + } + }); + // Assign manual rendering block render_block_ = engine_manual_input_.manualRenderingBlock; RTC_DCHECK(render_block_ != nullptr); @@ -1643,6 +1705,15 @@ render_thread_->SetName("render_thread", nullptr); render_thread_->Start(); render_thread_->PostTask([this] { this->StartRenderLoop(); }); + + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back render thread start (Manual)..."; + if (render_thread_ != nullptr) { + render_thread_->Stop(); + render_thread_ = nullptr; + } + }); } if (state.prev.IsAnyEnabled() && !state.next.IsAnyEnabled()) { @@ -1650,7 +1721,7 @@ int32_t result = observer_->OnEngineWillRelease(engine_manual_input_); if (result != 0) { LOGE() << "Call to OnEngineWillRelease returned error: " << result; - return result; + return rollback(result); } } LOGI() << "Releasing AVAudioEngine (Manual)..."; @@ -1667,8 +1738,9 @@ std::vector> rollback_actions; auto rollback = [&](int32_t result) { - for (auto& action : rollback_actions) { - action(); + // Execute rollback actions in reverse order (LIFO) + for (auto it = rollback_actions.rbegin(); it != rollback_actions.rend(); ++it) { + (*it)(); } return result; @@ -1717,6 +1789,30 @@ } } + // -------------------------------------------------------------------------------------------- + // Step: Stop playout buffer + // + if (!state.next.IsOutputEnabled() && audio_device_buffer_->IsPlaying()) { + LOGI() << "Stopping Playout buffer..."; + if (engine_device_ != nullptr) { + // Rendering must be stopped first. + RTC_DCHECK(!engine_device_.running); + } + audio_device_buffer_->StopPlayout(); + } + + // -------------------------------------------------------------------------------------------- + // Step: Stop recording buffer + // + if (!state.next.IsInputEnabled() && audio_device_buffer_->IsRecording()) { + LOGI() << "Stopping Record buffer..."; + if (engine_device_ != nullptr) { + // Rendering must be stopped first. + RTC_DCHECK(!engine_device_.running); + } + audio_device_buffer_->StopRecording(); + } + // -------------------------------------------------------------------------------------------- // Step: Recreate AVAudioEngine // @@ -1742,7 +1838,7 @@ engine_device_ = [[AVAudioEngine alloc] init]; - rollback_actions.push_back([=, this]() { + rollback_actions.push_back([this]() { RTC_DCHECK_RUN_ON(thread_); LOGI() << "Rolling back create AVAudioEngine (device)..."; engine_device_ = nil; @@ -1757,30 +1853,6 @@ } } - // -------------------------------------------------------------------------------------------- - // Step: Stop playout buffer - // - if (!state.next.IsOutputEnabled() && audio_device_buffer_->IsPlaying()) { - LOGI() << "Stopping Playout buffer..."; - if (engine_device_ != nullptr) { - // Rendering must be stopped first. - RTC_DCHECK(!engine_device_.running); - } - audio_device_buffer_->StopPlayout(); - } - - // -------------------------------------------------------------------------------------------- - // Step: Stop recording buffer - // - if (!state.next.IsInputEnabled() && audio_device_buffer_->IsRecording()) { - LOGI() << "Stopping Record buffer..."; - if (engine_device_ != nullptr) { - // Rendering must be stopped first. - RTC_DCHECK(!engine_device_.running); - } - audio_device_buffer_->StopRecording(); - } - // -------------------------------------------------------------------------------------------- // Step: Trigger "engine will enable" event // @@ -1795,6 +1867,35 @@ } } + // -------------------------------------------------------------------------------------------- + // Step: Check microphone permission and audio session category + // + if (state.DidAnyEnable()) { + // Safety checks for device rendering mode with recording enabled + // At this point mic permissions / session should be configured for recording. + if (state.DidEnableInput()) { + LOGI() << "Checking microphone permission..."; + // Attempt to acquire mic permissions at this point to return an erorr early. + bool isAuthorized = EnsureMicrophonePermissionSync(); + LOGI() << "AudioEngine pre-enable check, device permission: " + << (isAuthorized ? "true" : "false"); + if (!isAuthorized) { + return rollback(kAudioEngineErrorInsufficientDevicePermission); + } + } + +#if !TARGET_OS_OSX + NSString* category = [AVAudioSession sharedInstance].category; + bool isCategoryValid = IsAudioSessionCategoryValid(category, state.next.IsInputEnabled(), + state.next.IsOutputEnabled()); + LOGI() << "AudioEngine pre-enable check, audio session category: " << isCategoryValid ? "true" + : "false"; + if (!isCategoryValid) { + return rollback(kAudioEngineErrorAudioSessionInvalidCategory); + } +#endif + } + // -------------------------------------------------------------------------------------------- // Step: Configure Voice-Processing I/O // @@ -1891,6 +1992,12 @@ RTC_DCHECK(audio_device_buffer_ != nullptr); fine_audio_buffer_.reset(new FineAudioBuffer(audio_device_buffer_.get())); + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back output fine audio buffer setup (Device)..."; + fine_audio_buffer_.reset(); + }); + AVAudioFormat* rtc_output_format = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16 sampleRate:engine_output_format.sampleRate @@ -1915,6 +2022,19 @@ renderBlock:source_block]; [engine_device_ attachNode:source_node_]; + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back source node setup (Device)..."; + if (source_node_ != nil && [engine_device_.attachedNodes containsObject:source_node_]) { + @try { + [engine_device_ detachNode:source_node_]; + } @catch (NSException* exception) { + LOGW() << "Failed to detach source node during rollback: " << exception.reason.UTF8String; + } + } + source_node_ = nil; + }); + [engine_device_ connect:source_node_ to:engine_device_.mainMixerNode format:engine_output_format]; @@ -1994,6 +2114,21 @@ input_mixer_node_ = [[AVAudioMixerNode alloc] init]; [engine_device_ attachNode:input_mixer_node_]; + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back input mixer node setup (Device)..."; + if (input_mixer_node_ != nil && + [engine_device_.attachedNodes containsObject:input_mixer_node_]) { + @try { + [engine_device_ detachNode:input_mixer_node_]; + } @catch (NSException* exception) { + LOGW() << "Failed to detach input mixer node during rollback: " + << exception.reason.UTF8String; + } + } + input_mixer_node_ = nil; + }); + // When VoiceProcessingIO is enabled, channels must be reduced from Mac's default 9 channels // to 2 or lower. AVAudioFormat* engine_input_format = [[AVAudioFormat alloc] @@ -2013,17 +2148,41 @@ RTC_DCHECK(audio_device_buffer_ != nullptr); fine_audio_buffer_.reset(new FineAudioBuffer(audio_device_buffer_.get())); + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back input fine audio buffer setup (Device)..."; + fine_audio_buffer_.reset(); + }); + // Prepare Float32 -> Int16 converter. if (converter_ref_ == nullptr) { OSStatus err = AudioConverterNew(engine_input_format.streamDescription, rtc_input_format.streamDescription, &converter_ref_); - RTC_DCHECK(err == noErr); + if (err != noErr) { + LOGE() << "Failed to create audio converter, error: " << err; + return rollback(kAudioEngineDeviceFormatError); + } + + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back audio converter setup (Device)..."; + if (converter_ref_ != nullptr) { + AudioConverterDispose(converter_ref_); + converter_ref_ = nullptr; + } + }); } // Prepare buffer for Int16 converter. if (converter_buffer_ == nil) { converter_buffer_ = [[AVAudioPCMBuffer alloc] initWithPCMFormat:rtc_input_format frameCapacity:kMaximumFramesPerBuffer]; + + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back converter buffer setup (Device)..."; + converter_buffer_ = nil; + }); } // Convert to Int16 buffers within the sink block. @@ -2089,6 +2248,19 @@ sink_node_ = [[AVAudioSinkNode alloc] initWithReceiverBlock:sink_block]; [engine_device_ attachNode:sink_node_]; + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back sink node setup (Device)..."; + if (sink_node_ != nil && [engine_device_.attachedNodes containsObject:sink_node_]) { + @try { + [engine_device_ detachNode:sink_node_]; + } @catch (NSException* exception) { + LOGW() << "Failed to detach sink node during rollback: " << exception.reason.UTF8String; + } + } + sink_node_ = nil; + }); + [engine_device_ connect:input_mixer_node_ to:sink_node_ format:engine_input_format]; } else if ((state.prev.IsInputEnabled() && !state.next.IsInputEnabled()) && @@ -2159,7 +2331,7 @@ if (state.next.mute_mode == MuteMode::VoiceProcessing && state.next.IsInputEnabled() && inputNode().voiceProcessingEnabled && inputNode().voiceProcessingInputMuted != state.next.input_muted) { - LOGI() << "Update mute (voice processing) runtime update" << state.next.input_muted; + LOGI() << "Update mute (voice processing) runtime update: " << state.next.input_muted; inputNode().voiceProcessingInputMuted = state.next.input_muted; } @@ -2171,7 +2343,7 @@ // Only update if the volume has changed. float mixer_volume = state.next.input_muted ? 0.0f : 1.0f; if (input_mixer_node_.outputVolume != mixer_volume) { - LOGI() << "Update mute (input mixer) runtime update" << state.next.input_muted; + LOGI() << "Update mute (input mixer) runtime update: " << state.next.input_muted; input_mixer_node_.outputVolume = mixer_volume; } } @@ -2235,7 +2407,8 @@ kAudioUnitScope_Global, 1, &input_device_id, sizeof(input_device_id)); if (err != noErr) { - LOGE() << "Failed to set input device: " << input_device_id; + LOGE() << "Failed to set input device: " << input_device_id << ", error: " << err; + return rollback(kAudioEngineRecordingDeviceNotAvailableError); } } } @@ -2253,7 +2426,8 @@ kAudioUnitScope_Global, 0, &output_deviceId, sizeof(output_deviceId)); if (err != noErr) { - LOGE() << "Failed to set output device: " << output_deviceId; + LOGE() << "Failed to set output device: " << output_deviceId << ", error: " << err; + return rollback(kAudioEnginePlayoutDeviceNotAvailableError); } } } @@ -2271,6 +2445,14 @@ LOGI() << "Starting Playout buffer..."; audio_device_buffer_->StartPlayout(); fine_audio_buffer_->ResetPlayout(); + + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back playout buffer start (Device)..."; + if (audio_device_buffer_->IsPlaying()) { + audio_device_buffer_->StopPlayout(); + } + }); } // -------------------------------------------------------------------------------------------- @@ -2284,6 +2466,14 @@ LOGI() << "Starting Record buffer..."; audio_device_buffer_->StartRecording(); fine_audio_buffer_->ResetRecord(); + + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back record buffer start (Device)..."; + if (audio_device_buffer_->IsRecording()) { + audio_device_buffer_->StopRecording(); + } + }); } // -------------------------------------------------------------------------------------------- @@ -2346,6 +2536,14 @@ } if (start_result) { + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back engine start (Device)..."; + if (engine_device_ != nil && engine_device_.running) { + [engine_device_ stop]; + } + }); + RTC_DCHECK(configuration_observer_ == nullptr); // Add observer for configuration changes NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; @@ -2362,9 +2560,22 @@ } }]; + rollback_actions.push_back([this]() { + RTC_DCHECK_RUN_ON(thread_); + LOGI() << "Rolling back configuration observer (Device)..."; + if (configuration_observer_ != nullptr) { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center removeObserver:(__bridge_transfer id)configuration_observer_ + name:AVAudioEngineConfigurationChangeNotification + object:engine_device_]; + configuration_observer_ = nil; + } + }); + } else { LOGE() << "Failed to start engine after " << kStartEngineMaxRetries << " attempts"; DebugAudioEngine(); + return rollback(kAudioEnginePlayoutStartError); } } } @@ -2491,6 +2702,74 @@ #endif +// ---------------------------------------------------------------------------------------------------- +// Private - Microphone permission + +bool AudioEngineDevice::IsMicrophonePermissionGranted() { + AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; + return status == AVAuthorizationStatusAuthorized; +} + +bool AudioEngineDevice::EnsureMicrophonePermissionSync() { + AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; + + if (status == AVAuthorizationStatusAuthorized) { + return true; + } + + if (status == AVAuthorizationStatusNotDetermined) { + // Request permission synchronously - this will block WebRTC's worker thread + // but this is acceptable since instantiating AVAudioInputNode would block anyway + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block BOOL granted = NO; + + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio + completionHandler:^(BOOL granted_inner) { + granted = granted_inner; + dispatch_semaphore_signal(semaphore); + }]; + + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + return granted; + } + + // Status is denied or restricted + return false; +} + +// ---------------------------------------------------------------------------------------------------- +// Private - Audio session + +#if !TARGET_OS_OSX +bool AudioEngineDevice::IsAudioSessionCategoryValid(NSString* category, bool is_input_enabled, + bool is_output_enabled) { + // Categories that support both recording and playback + if (is_input_enabled && is_output_enabled) { + return [category isEqualToString:AVAudioSessionCategoryPlayAndRecord] || + [category isEqualToString:AVAudioSessionCategoryMultiRoute]; + } + + // Categories that support recording only + if (is_input_enabled && !is_output_enabled) { + return [category isEqualToString:AVAudioSessionCategoryRecord] || + [category isEqualToString:AVAudioSessionCategoryPlayAndRecord] || + [category isEqualToString:AVAudioSessionCategoryMultiRoute]; + } + + // Categories that support playback only + if (!is_input_enabled && is_output_enabled) { + return [category isEqualToString:AVAudioSessionCategoryAmbient] || + [category isEqualToString:AVAudioSessionCategorySoloAmbient] || + [category isEqualToString:AVAudioSessionCategoryPlayback] || + [category isEqualToString:AVAudioSessionCategoryPlayAndRecord] || + [category isEqualToString:AVAudioSessionCategoryMultiRoute]; + } + + // Neither input nor output enabled - any category is valid + return true; +} +#endif + // ---------------------------------------------------------------------------------------------------- // Private - Debug