diff --git a/webrtc/modules/audio_device/ios/audio_device_ios.mm b/webrtc/modules/audio_device/ios/audio_device_ios.mm index e094ebdc3d..b0d26be6ec 100644 --- a/webrtc/modules/audio_device/ios/audio_device_ios.mm +++ b/webrtc/modules/audio_device/ios/audio_device_ios.mm @@ -86,6 +86,12 @@ const UInt32 kBytesPerSample = 2; // Can most likely be removed. const UInt16 kFixedPlayoutDelayEstimate = 30; const UInt16 kFixedRecordDelayEstimate = 30; +// Calls to AudioUnitInitialize() can fail if called back-to-back on different +// ADM instances. A fall-back solution is to allow multiple sequential calls +// with as small delay between each. This factor sets the max number of allowed +// initialization attempts. +const int kMaxNumberOfAudioUnitInitializeAttempts = 5; + using ios::CheckAndLogError; @@ -741,6 +747,7 @@ bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() { vpio_unit_description.componentManufacturer = kAudioUnitManufacturer_Apple; vpio_unit_description.componentFlags = 0; vpio_unit_description.componentFlagsMask = 0; + // Obtain an audio unit instance given the description. AudioComponent found_vpio_unit_ref = AudioComponentFindNext(nullptr, &vpio_unit_description); @@ -876,17 +883,28 @@ bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() { } // Initialize the Voice-Processing I/O unit instance. + // Calls to AudioUnitInitialize() can fail if called back-to-back on + // different ADM instances. The error message in this case is -66635 which is + // undocumented. Tests have shown that calling AudioUnitInitialize a second + // time, after a short sleep, avoids this issue. + // See webrtc:5166 for details. + int failed_initalize_attempts = 0; result = AudioUnitInitialize(vpio_unit_); - if (result != noErr) { - result = AudioUnitUninitialize(vpio_unit_); - if (result != noErr) { - LOG_F(LS_ERROR) << "AudioUnitUninitialize failed: " << result; - } - DisposeAudioUnit(); + while (result != noErr) { LOG(LS_ERROR) << "Failed to initialize the Voice-Processing I/O unit: " << result; - return false; + ++failed_initalize_attempts; + if (failed_initalize_attempts == kMaxNumberOfAudioUnitInitializeAttempts) { + // Max number of initialization attempts exceeded, hence abort. + LOG(LS_WARNING) << "Too many initialization attempts"; + DisposeAudioUnit(); + return false; + } + LOG(LS_INFO) << "pause 100ms and try audio unit initialization again..."; + [NSThread sleepForTimeInterval:0.1f]; + result = AudioUnitInitialize(vpio_unit_); } + LOG(LS_INFO) << "Voice-Processing I/O unit is now initialized"; return true; } diff --git a/webrtc/modules/audio_device/ios/audio_device_unittest_ios.cc b/webrtc/modules/audio_device/ios/audio_device_unittest_ios.cc index b892f28b1d..8993aceea4 100644 --- a/webrtc/modules/audio_device/ios/audio_device_unittest_ios.cc +++ b/webrtc/modules/audio_device/ios/audio_device_unittest_ios.cc @@ -636,6 +636,55 @@ TEST_F(AudioDeviceTest, StopPlayoutRequiresInitToRestart) { EXPECT_FALSE(audio_device()->PlayoutIsInitialized()); } +// Verify that we can create two ADMs and start playing on the second ADM. +// Only the first active instance shall activate an audio session and the +// last active instace shall deactivate the audio session. +TEST_F(AudioDeviceTest, StartPlayoutOnTwoInstances) { + // Create and initialize a second/extra ADM instance. The default ADM is + // created by the test harness. + rtc::scoped_refptr second_audio_device = + CreateAudioDevice(AudioDeviceModule::kPlatformDefaultAudio); + EXPECT_NE(second_audio_device.get(), nullptr); + EXPECT_EQ(0, second_audio_device->Init()); + + // Start playout for the default ADM. Ignore the callback sequence. + NiceMock mock(kPlayout); + EXPECT_EQ(0, audio_device()->RegisterAudioCallback(&mock)); + StartPlayout(); + + // Initialize playout for the second ADM. If all is OK, the second ADM shall + // reuse the audio session activated when the first ADM started playing. + // This call will also ensure that we avoid a problem related to initializing + // two different audio unit instances back to back (see webrtc:5166 for + // details). + EXPECT_EQ(0, second_audio_device->InitPlayout()); + EXPECT_TRUE(second_audio_device->PlayoutIsInitialized()); + + // Stop playout for the default ADM. The audio session shall not be + // deactivated since it is used by the second ADM. + StopPlayout(); + + // Start playout for the second ADM and verify that it starts as intended. + // Passing this test ensures that initialization of the second audio unit + // has been done successfully. + MockAudioTransport mock2(kPlayout); + mock2.HandleCallbacks(test_is_done_.get(), nullptr, kNumCallbacks); + EXPECT_CALL( + mock2, NeedMorePlayData(playout_frames_per_10ms_buffer(), kBytesPerSample, + playout_channels(), playout_sample_rate(), + NotNull(), _, _, _)) + .Times(AtLeast(kNumCallbacks)); + EXPECT_EQ(0, second_audio_device->RegisterAudioCallback(&mock2)); + EXPECT_EQ(0, second_audio_device->StartPlayout()); + EXPECT_TRUE(second_audio_device->Playing()); + test_is_done_->Wait(kTestTimeOutInMilliseconds); + EXPECT_EQ(0, second_audio_device->StopPlayout()); + EXPECT_FALSE(second_audio_device->Playing()); + EXPECT_FALSE(second_audio_device->PlayoutIsInitialized()); + + EXPECT_EQ(0, second_audio_device->Terminate()); +} + // Start playout and verify that the native audio layer starts asking for real // audio samples to play out using the NeedMorePlayData callback. TEST_F(AudioDeviceTest, StartPlayoutVerifyCallbacks) {