Improved error handling in iOS ADM to avoid race during init

BUG=webrtc:5166
R=pbos@webrtc.org, tkchin@webrtc.org

Review URL: https://codereview.webrtc.org/1435293003 .

Cr-Commit-Position: refs/heads/master@{#10728}
This commit is contained in:
henrika 2015-11-20 15:47:09 +01:00
parent 76a31ca3d4
commit 34911ad55c
2 changed files with 227 additions and 73 deletions

View File

@ -182,7 +182,10 @@ class AudioDeviceIOS : public AudioDeviceGeneric {
bool InitPlayOrRecord();
// Closes and deletes the voice-processing I/O unit.
bool ShutdownPlayOrRecord();
void ShutdownPlayOrRecord();
// Helper method for destroying the existing audio unit.
void DisposeAudioUnit();
// Callback function called on a real-time priority I/O thread from the audio
// unit. This method is used to signal that recorded audio is available.

View File

@ -19,12 +19,24 @@
#include "webrtc/base/atomicops.h"
#include "webrtc/base/checks.h"
#include "webrtc/base/criticalsection.h"
#include "webrtc/base/logging.h"
#include "webrtc/base/thread_annotations.h"
#include "webrtc/modules/audio_device/fine_audio_buffer.h"
#include "webrtc/modules/utility/include/helpers_ios.h"
namespace webrtc {
// Protects |g_audio_session_users|.
static rtc::GlobalLockPod g_lock;
// Counts number of users (=instances of this object) who needs an active
// audio session. This variable is used to ensure that we only activate an audio
// session for the first user and deactivate it for the last.
// Member is static to ensure that the value is counted for all instances
// and not per instance.
static int g_audio_session_users GUARDED_BY(g_lock) = 0;
#define LOGI() LOG(LS_INFO) << "AudioDeviceIOS::"
#define LOG_AND_RETURN_IF_ERROR(error, message) \
@ -77,10 +89,34 @@ const UInt16 kFixedRecordDelayEstimate = 30;
using ios::CheckAndLogError;
// Verifies that the current audio session supports input audio and that the
// required category and mode are enabled.
static bool VerifyAudioSession(AVAudioSession* session) {
LOG(LS_INFO) << "VerifyAudioSession";
// Ensure that the device currently supports audio input.
if (!session.isInputAvailable) {
LOG(LS_ERROR) << "No audio input path is available!";
return false;
}
// Ensure that the required category and mode are actually activated.
if (![session.category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) {
LOG(LS_ERROR)
<< "Failed to set category to AVAudioSessionCategoryPlayAndRecord";
return false;
}
if (![session.mode isEqualToString:AVAudioSessionModeVoiceChat]) {
LOG(LS_ERROR) << "Failed to set mode to AVAudioSessionModeVoiceChat";
return false;
}
return true;
}
// Activates an audio session suitable for full duplex VoIP sessions when
// |activate| is true. Also sets the preferred sample rate and IO buffer
// duration. Deactivates an active audio session if |activate| is set to false.
static void ActivateAudioSession(AVAudioSession* session, bool activate) {
static bool ActivateAudioSession(AVAudioSession* session, bool activate)
EXCLUSIVE_LOCKS_REQUIRED(g_lock) {
LOG(LS_INFO) << "ActivateAudioSession(" << activate << ")";
@autoreleasepool {
NSError* error = nil;
@ -96,8 +132,7 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) {
setActive:NO
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
error:&error];
RTC_DCHECK(CheckAndLogError(success, error));
return;
return CheckAndLogError(success, error);
}
// Go ahead and active our own audio session since |activate| is true.
@ -129,7 +164,6 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) {
RTC_DCHECK(CheckAndLogError(success, error));
// Set the preferred audio I/O buffer duration, in seconds.
// TODO(henrika): add more comments here.
error = nil;
success = [session setPreferredIOBufferDuration:kPreferredIOBufferDuration
error:&error];
@ -139,13 +173,15 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) {
// session (e.g. phone call) has higher priority than ours.
error = nil;
success = [session setActive:YES error:&error];
RTC_DCHECK(CheckAndLogError(success, error));
RTC_CHECK(session.isInputAvailable) << "No input path is available!";
if (!CheckAndLogError(success, error)) {
return false;
}
// Ensure that category and mode are actually activated.
RTC_DCHECK(
[session.category isEqualToString:AVAudioSessionCategoryPlayAndRecord]);
RTC_DCHECK([session.mode isEqualToString:AVAudioSessionModeVoiceChat]);
// Ensure that the active audio session has the correct category and mode.
if (!VerifyAudioSession(session)) {
LOG(LS_ERROR) << "Failed to verify audio session category and mode";
return false;
}
// Try to set the preferred number of hardware audio channels. These calls
// must be done after setting the audio sessions category and mode and
@ -164,9 +200,54 @@ static void ActivateAudioSession(AVAudioSession* session, bool activate) {
[session setPreferredOutputNumberOfChannels:kPreferredNumberOfChannels
error:&error];
RTC_DCHECK(CheckAndLogError(success, error));
return true;
}
}
// An application can create more than one ADM and start audio streaming
// for all of them. It is essential that we only activate the app's audio
// session once (for the first one) and deactivate it once (for the last).
static bool ActivateAudioSession() {
LOGI() << "ActivateAudioSession";
rtc::GlobalLockScope ls(&g_lock);
if (g_audio_session_users == 0) {
// The system provides an audio session object upon launch of an
// application. However, we must initialize the session in order to
// handle interruptions. Implicit initialization occurs when obtaining
// a reference to the AVAudioSession object.
AVAudioSession* session = [AVAudioSession sharedInstance];
// Try to activate the audio session and ask for a set of preferred audio
// parameters.
if (!ActivateAudioSession(session, true)) {
LOG(LS_ERROR) << "Failed to activate the audio session";
return false;
}
LOG(LS_INFO) << "The audio session is now activated";
}
++g_audio_session_users;
LOG(LS_INFO) << "Number of audio session users: " << g_audio_session_users;
return true;
}
// If more than one object is using the audio session, ensure that only the
// last object deactivates. Apple recommends: "activate your audio session
// only as needed and deactivate it when you are not using audio".
static bool DeactivateAudioSession() {
LOGI() << "DeactivateAudioSession";
rtc::GlobalLockScope ls(&g_lock);
if (g_audio_session_users == 1) {
AVAudioSession* session = [AVAudioSession sharedInstance];
if (!ActivateAudioSession(session, false)) {
LOG(LS_ERROR) << "Failed to deactivate the audio session";
return false;
}
LOG(LS_INFO) << "Our audio session is now deactivated";
}
--g_audio_session_users;
LOG(LS_INFO) << "Number of audio session users: " << g_audio_session_users;
return true;
}
#if !defined(NDEBUG)
// Helper method for printing out an AudioStreamBasicDescription structure.
static void LogABSD(AudioStreamBasicDescription absd) {
@ -212,7 +293,7 @@ AudioDeviceIOS::AudioDeviceIOS()
}
AudioDeviceIOS::~AudioDeviceIOS() {
LOGI() << "~dtor";
LOGI() << "~dtor" << ios::GetCurrentThreadDescription();
RTC_DCHECK(thread_checker_.CalledOnValidThread());
Terminate();
}
@ -254,8 +335,16 @@ int32_t AudioDeviceIOS::Terminate() {
if (!initialized_) {
return 0;
}
ShutdownPlayOrRecord();
StopPlayout();
StopRecording();
initialized_ = false;
{
rtc::GlobalLockScope ls(&g_lock);
if (g_audio_session_users != 0) {
LOG(LS_WARNING) << "Object is destructed with an active audio session";
}
RTC_DCHECK_GE(g_audio_session_users, 0);
}
return 0;
}
@ -267,7 +356,7 @@ int32_t AudioDeviceIOS::InitPlayout() {
RTC_DCHECK(!playing_);
if (!rec_is_initialized_) {
if (!InitPlayOrRecord()) {
LOG_F(LS_ERROR) << "InitPlayOrRecord failed!";
LOG_F(LS_ERROR) << "InitPlayOrRecord failed for InitPlayout!";
return -1;
}
}
@ -283,7 +372,7 @@ int32_t AudioDeviceIOS::InitRecording() {
RTC_DCHECK(!recording_);
if (!play_is_initialized_) {
if (!InitPlayOrRecord()) {
LOG_F(LS_ERROR) << "InitPlayOrRecord failed!";
LOG_F(LS_ERROR) << "InitPlayOrRecord failed for InitRecording!";
return -1;
}
}
@ -300,9 +389,11 @@ int32_t AudioDeviceIOS::StartPlayout() {
if (!recording_) {
OSStatus result = AudioOutputUnitStart(vpio_unit_);
if (result != noErr) {
LOG_F(LS_ERROR) << "AudioOutputUnitStart failed: " << result;
LOG_F(LS_ERROR) << "AudioOutputUnitStart failed for StartPlayout: "
<< result;
return -1;
}
LOG(LS_INFO) << "Voice-Processing I/O audio unit is now started";
}
rtc::AtomicOps::ReleaseStore(&playing_, 1);
return 0;
@ -331,9 +422,11 @@ int32_t AudioDeviceIOS::StartRecording() {
if (!playing_) {
OSStatus result = AudioOutputUnitStart(vpio_unit_);
if (result != noErr) {
LOG_F(LS_ERROR) << "AudioOutputUnitStart failed: " << result;
LOG_F(LS_ERROR) << "AudioOutputUnitStart failed for StartRecording: "
<< result;
return -1;
}
LOG(LS_INFO) << "Voice-Processing I/O audio unit is now started";
}
rtc::AtomicOps::ReleaseStore(&recording_, 1);
return 0;
@ -639,7 +732,7 @@ void AudioDeviceIOS::SetupAudioBuffersForActiveAudioSession() {
bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() {
LOGI() << "SetupAndInitializeVoiceProcessingAudioUnit";
RTC_DCHECK(!vpio_unit_);
RTC_DCHECK(!vpio_unit_) << "VoiceProcessingIO audio unit already exists";
// Create an audio component description to identify the Voice-Processing
// I/O audio unit.
AudioComponentDescription vpio_unit_description;
@ -653,29 +746,42 @@ bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() {
AudioComponentFindNext(nullptr, &vpio_unit_description);
// Create a Voice-Processing IO audio unit.
LOG_AND_RETURN_IF_ERROR(
AudioComponentInstanceNew(found_vpio_unit_ref, &vpio_unit_),
"Failed to create a VoiceProcessingIO audio unit");
OSStatus result = noErr;
result = AudioComponentInstanceNew(found_vpio_unit_ref, &vpio_unit_);
if (result != noErr) {
vpio_unit_ = nullptr;
LOG(LS_ERROR) << "AudioComponentInstanceNew failed: " << result;
return false;
}
// A VP I/O unit's bus 1 connects to input hardware (microphone). Enable
// input on the input scope of the input element.
AudioUnitElement input_bus = 1;
UInt32 enable_input = 1;
LOG_AND_RETURN_IF_ERROR(
AudioUnitSetProperty(vpio_unit_, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input, input_bus, &enable_input,
sizeof(enable_input)),
"Failed to enable input on input scope of input element");
result = AudioUnitSetProperty(vpio_unit_, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input, input_bus, &enable_input,
sizeof(enable_input));
if (result != noErr) {
DisposeAudioUnit();
LOG(LS_ERROR) << "Failed to enable input on input scope of input element: "
<< result;
return false;
}
// A VP I/O unit's bus 0 connects to output hardware (speaker). Enable
// output on the output scope of the output element.
AudioUnitElement output_bus = 0;
UInt32 enable_output = 1;
LOG_AND_RETURN_IF_ERROR(
AudioUnitSetProperty(vpio_unit_, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output, output_bus, &enable_output,
sizeof(enable_output)),
"Failed to enable output on output scope of output element");
result = AudioUnitSetProperty(vpio_unit_, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output, output_bus,
&enable_output, sizeof(enable_output));
if (result != noErr) {
DisposeAudioUnit();
LOG(LS_ERROR)
<< "Failed to enable output on output scope of output element: "
<< result;
return false;
}
// Set the application formats for input and output:
// - use same format in both directions
@ -703,38 +809,55 @@ bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() {
#endif
// Set the application format on the output scope of the input element/bus.
LOG_AND_RETURN_IF_ERROR(
AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output, input_bus,
&application_format, size),
"Failed to set application format on output scope of input element");
result = AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output, input_bus,
&application_format, size);
if (result != noErr) {
DisposeAudioUnit();
LOG(LS_ERROR)
<< "Failed to set application format on output scope of input bus: "
<< result;
return false;
}
// Set the application format on the input scope of the output element/bus.
LOG_AND_RETURN_IF_ERROR(
AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input, output_bus,
&application_format, size),
"Failed to set application format on input scope of output element");
result = AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input, output_bus,
&application_format, size);
if (result != noErr) {
DisposeAudioUnit();
LOG(LS_ERROR)
<< "Failed to set application format on input scope of output bus: "
<< result;
return false;
}
// Specify the callback function that provides audio samples to the audio
// unit.
AURenderCallbackStruct render_callback;
render_callback.inputProc = GetPlayoutData;
render_callback.inputProcRefCon = this;
LOG_AND_RETURN_IF_ERROR(
AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input, output_bus, &render_callback,
sizeof(render_callback)),
"Failed to specify the render callback on the output element");
result = AudioUnitSetProperty(
vpio_unit_, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input,
output_bus, &render_callback, sizeof(render_callback));
if (result != noErr) {
DisposeAudioUnit();
LOG(LS_ERROR) << "Failed to specify the render callback on the output bus: "
<< result;
return false;
}
// Disable AU buffer allocation for the recorder, we allocate our own.
// TODO(henrika): not sure that it actually saves resource to make this call.
UInt32 flag = 0;
LOG_AND_RETURN_IF_ERROR(
AudioUnitSetProperty(vpio_unit_, kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output, input_bus, &flag,
sizeof(flag)),
"Failed to disable buffer allocation on the input element");
result = AudioUnitSetProperty(
vpio_unit_, kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output, input_bus, &flag, sizeof(flag));
if (result != noErr) {
DisposeAudioUnit();
LOG(LS_ERROR) << "Failed to disable buffer allocation on the input bus: "
<< result;
}
// Specify the callback to be called by the I/O thread to us when input audio
// is available. The recorded samples can then be obtained by calling the
@ -742,16 +865,28 @@ bool AudioDeviceIOS::SetupAndInitializeVoiceProcessingAudioUnit() {
AURenderCallbackStruct input_callback;
input_callback.inputProc = RecordedDataIsAvailable;
input_callback.inputProcRefCon = this;
LOG_AND_RETURN_IF_ERROR(
AudioUnitSetProperty(vpio_unit_,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, input_bus, &input_callback,
sizeof(input_callback)),
"Failed to specify the input callback on the input element");
result = AudioUnitSetProperty(vpio_unit_,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, input_bus,
&input_callback, sizeof(input_callback));
if (result != noErr) {
DisposeAudioUnit();
LOG(LS_ERROR) << "Failed to specify the input callback on the input bus: "
<< result;
}
// Initialize the Voice-Processing I/O unit instance.
LOG_AND_RETURN_IF_ERROR(AudioUnitInitialize(vpio_unit_),
"Failed to initialize the Voice-Processing I/O unit");
result = AudioUnitInitialize(vpio_unit_);
if (result != noErr) {
result = AudioUnitUninitialize(vpio_unit_);
if (result != noErr) {
LOG_F(LS_ERROR) << "AudioUnitUninitialize failed: " << result;
}
DisposeAudioUnit();
LOG(LS_ERROR) << "Failed to initialize the Voice-Processing I/O unit: "
<< result;
return false;
}
return true;
}
@ -790,9 +925,18 @@ bool AudioDeviceIOS::RestartAudioUnitWithNewFormat(float sample_rate) {
bool AudioDeviceIOS::InitPlayOrRecord() {
LOGI() << "InitPlayOrRecord";
// Activate the audio session if not already activated.
if (!ActivateAudioSession()) {
return false;
}
// Ensure that the active audio session has the correct category and mode.
AVAudioSession* session = [AVAudioSession sharedInstance];
// Activate the audio session and ask for a set of preferred audio parameters.
ActivateAudioSession(session, true);
if (!VerifyAudioSession(session)) {
DeactivateAudioSession();
LOG(LS_ERROR) << "Failed to verify audio session category and mode";
return false;
}
// Start observing audio session interruptions and route changes.
RegisterNotificationObservers();
@ -802,16 +946,16 @@ bool AudioDeviceIOS::InitPlayOrRecord() {
// Create, setup and initialize a new Voice-Processing I/O unit.
if (!SetupAndInitializeVoiceProcessingAudioUnit()) {
// Reduce usage count for the audio session and possibly deactivate it if
// this object is the only user.
DeactivateAudioSession();
return false;
}
return true;
}
bool AudioDeviceIOS::ShutdownPlayOrRecord() {
void AudioDeviceIOS::ShutdownPlayOrRecord() {
LOGI() << "ShutdownPlayOrRecord";
// Remove audio session notification observers.
UnregisterNotificationObservers();
// Close and delete the voice-processing I/O unit.
OSStatus result = -1;
if (nullptr != vpio_unit_) {
@ -823,18 +967,25 @@ bool AudioDeviceIOS::ShutdownPlayOrRecord() {
if (result != noErr) {
LOG_F(LS_ERROR) << "AudioUnitUninitialize failed: " << result;
}
result = AudioComponentInstanceDispose(vpio_unit_);
if (result != noErr) {
LOG_F(LS_ERROR) << "AudioComponentInstanceDispose failed: " << result;
}
vpio_unit_ = nullptr;
DisposeAudioUnit();
}
// Remove audio session notification observers.
UnregisterNotificationObservers();
// All I/O should be stopped or paused prior to deactivating the audio
// session, hence we deactivate as last action.
AVAudioSession* session = [AVAudioSession sharedInstance];
ActivateAudioSession(session, false);
return true;
DeactivateAudioSession();
}
void AudioDeviceIOS::DisposeAudioUnit() {
if (nullptr == vpio_unit_)
return;
OSStatus result = AudioComponentInstanceDispose(vpio_unit_);
if (result != noErr) {
LOG(LS_ERROR) << "AudioComponentInstanceDispose failed:" << result;
}
vpio_unit_ = nullptr;
}
OSStatus AudioDeviceIOS::RecordedDataIsAvailable(