From e54467f73e2630bbaf751a4db4326d1278f704e5 Mon Sep 17 00:00:00 2001 From: tkchin Date: Tue, 15 Mar 2016 16:54:03 -0700 Subject: [PATCH] Use RTCAudioSessionDelegateAdapter in AudioDeviceIOS. Part 3 of refactor. Also: - better weak pointer delegate storage + tests - we now ignore route changes when we're interrupted - fixed bug where preferred sample rate wasn't set if audio session wasn't active BUG= Review URL: https://codereview.webrtc.org/1796983004 Cr-Commit-Position: refs/heads/master@{#12007} --- webrtc/base/objc/RTCMacros.h | 15 +- .../examples/objc/AppRTCDemo/ios/Info.plist | 1 + webrtc/modules/audio_device/BUILD.gn | 3 + webrtc/modules/audio_device/audio_device.gypi | 3 + .../audio_device/ios/audio_device_ios.h | 34 ++- .../audio_device/ios/audio_device_ios.mm | 265 +++++++----------- .../audio_device/ios/audio_session_observer.h | 37 +++ .../ios/objc/RTCAudioSession+Configuration.mm | 7 +- .../ios/objc/RTCAudioSession+Private.h | 22 +- .../audio_device/ios/objc/RTCAudioSession.h | 5 +- .../audio_device/ios/objc/RTCAudioSession.mm | 51 +++- .../ios/objc/RTCAudioSessionDelegateAdapter.h | 30 ++ .../objc/RTCAudioSessionDelegateAdapter.mm | 79 ++++++ .../ios/objc/RTCAudioSessionTest.mm | 137 ++++++++- webrtc/modules/modules.gyp | 3 + 15 files changed, 499 insertions(+), 193 deletions(-) create mode 100644 webrtc/modules/audio_device/ios/audio_session_observer.h create mode 100644 webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.h create mode 100644 webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.mm diff --git a/webrtc/base/objc/RTCMacros.h b/webrtc/base/objc/RTCMacros.h index 9d4646bd21..71fa0967ac 100644 --- a/webrtc/base/objc/RTCMacros.h +++ b/webrtc/base/objc/RTCMacros.h @@ -8,8 +8,19 @@ * be found in the AUTHORS file in the root of the source tree. */ +#ifndef WEBRTC_BASE_OBJC_RTC_MACROS_H_ +#define WEBRTC_BASE_OBJC_RTC_MACROS_H_ + #if defined(__cplusplus) - #define RTC_EXPORT extern "C" +#define RTC_EXPORT extern "C" #else - #define RTC_EXPORT extern +#define RTC_EXPORT extern #endif + +#ifdef __OBJC__ +#define RTC_FWD_DECL_OBJC_CLASS(classname) @class classname +#else +#define RTC_FWD_DECL_OBJC_CLASS(classname) typedef struct objc_object classname +#endif + +#endif // WEBRTC_BASE_OBJC_RTC_MACROS_H_ diff --git a/webrtc/examples/objc/AppRTCDemo/ios/Info.plist b/webrtc/examples/objc/AppRTCDemo/ios/Info.plist index fd1e26f8a5..5cde6f52c4 100644 --- a/webrtc/examples/objc/AppRTCDemo/ios/Info.plist +++ b/webrtc/examples/objc/AppRTCDemo/ios/Info.plist @@ -58,6 +58,7 @@ UIBackgroundModes + audio voip UILaunchImages diff --git a/webrtc/modules/audio_device/BUILD.gn b/webrtc/modules/audio_device/BUILD.gn index ea83f96d5e..6c6db28a00 100644 --- a/webrtc/modules/audio_device/BUILD.gn +++ b/webrtc/modules/audio_device/BUILD.gn @@ -135,12 +135,15 @@ source_set("audio_device") { "ios/audio_device_ios.h", "ios/audio_device_ios.mm", "ios/audio_device_not_implemented_ios.mm", + "ios/audio_session_observer.h", "ios/objc/RTCAudioSession+Configuration.mm", "ios/objc/RTCAudioSession+Private.h", "ios/objc/RTCAudioSession.h", "ios/objc/RTCAudioSession.mm", "ios/objc/RTCAudioSessionConfiguration.h", "ios/objc/RTCAudioSessionConfiguration.m", + "ios/objc/RTCAudioSessionDelegateAdapter.h", + "ios/objc/RTCAudioSessionDelegateAdapter.mm", ] cflags += [ "-fobjc-arc" ] # CLANG_ENABLE_OBJC_ARC = YES. libs = [ diff --git a/webrtc/modules/audio_device/audio_device.gypi b/webrtc/modules/audio_device/audio_device.gypi index 927ecdfec2..36dd788a8a 100644 --- a/webrtc/modules/audio_device/audio_device.gypi +++ b/webrtc/modules/audio_device/audio_device.gypi @@ -173,12 +173,15 @@ 'ios/audio_device_ios.h', 'ios/audio_device_ios.mm', 'ios/audio_device_not_implemented_ios.mm', + 'ios/audio_session_observer.h', 'ios/objc/RTCAudioSession+Configuration.mm', 'ios/objc/RTCAudioSession+Private.h', 'ios/objc/RTCAudioSession.h', 'ios/objc/RTCAudioSession.mm', 'ios/objc/RTCAudioSessionConfiguration.h', 'ios/objc/RTCAudioSessionConfiguration.m', + 'ios/objc/RTCAudioSessionDelegateAdapter.h', + 'ios/objc/RTCAudioSessionDelegateAdapter.mm', ], 'xcode_settings': { 'CLANG_ENABLE_OBJC_ARC': 'YES', diff --git a/webrtc/modules/audio_device/ios/audio_device_ios.h b/webrtc/modules/audio_device/ios/audio_device_ios.h index 73208864d2..fe6af65205 100644 --- a/webrtc/modules/audio_device/ios/audio_device_ios.h +++ b/webrtc/modules/audio_device/ios/audio_device_ios.h @@ -15,8 +15,14 @@ #include +#include "webrtc/base/asyncinvoker.h" +#include "webrtc/base/objc/RTCMacros.h" +#include "webrtc/base/thread.h" #include "webrtc/base/thread_checker.h" #include "webrtc/modules/audio_device/audio_device_generic.h" +#include "webrtc/modules/audio_device/ios/audio_session_observer.h" + +RTC_FWD_DECL_OBJC_CLASS(RTCAudioSessionDelegateAdapter); namespace webrtc { @@ -35,7 +41,8 @@ class FineAudioBuffer; // Recorded audio will be delivered on a real-time internal I/O thread in the // audio unit. The audio unit will also ask for audio data to play out on this // same thread. -class AudioDeviceIOS : public AudioDeviceGeneric { +class AudioDeviceIOS : public AudioDeviceGeneric, + public AudioSessionObserver { public: AudioDeviceIOS(); ~AudioDeviceIOS(); @@ -151,16 +158,21 @@ class AudioDeviceIOS : public AudioDeviceGeneric { void ClearRecordingWarning() override {} void ClearRecordingError() override {} + // AudioSessionObserver methods. May be called from any thread. + void OnInterruptionBegin() override; + void OnInterruptionEnd() override; + void OnValidRouteChange() override; + private: + // Called by the relevant AudioSessionObserver methods on |thread_|. + void HandleInterruptionBegin(); + void HandleInterruptionEnd(); + void HandleValidRouteChange(); + // Uses current |playout_parameters_| and |record_parameters_| to inform the // audio device buffer (ADB) about our internal audio parameters. void UpdateAudioDeviceBuffer(); - // Registers observers for the AVAudioSessionRouteChangeNotification and - // AVAudioSessionInterruptionNotification notifications. - void RegisterNotificationObservers(); - void UnregisterNotificationObservers(); - // Since the preferred audio parameters are only hints to the OS, the actual // values may be different once the AVAudioSession has been activated. // This method asks for the current hardware parameters and takes actions @@ -218,6 +230,10 @@ class AudioDeviceIOS : public AudioDeviceGeneric { // Ensures that methods are called from the same thread as this object is // created on. rtc::ThreadChecker thread_checker_; + // Thread that this object is created on. + rtc::Thread* thread_; + // Invoker used to execute methods on thread_. + std::unique_ptr async_invoker_; // Raw pointer handle provided to us in AttachAudioBuffer(). Owned by the // AudioDeviceModuleImpl class and called by AudioDeviceModuleImpl::Create(). @@ -286,9 +302,11 @@ class AudioDeviceIOS : public AudioDeviceGeneric { // Set to true after successful call to InitPlayout(), false otherwise. bool play_is_initialized_; + // Set to true if audio session is interrupted, false otherwise. + bool is_interrupted_; + // Audio interruption observer instance. - void* audio_interruption_observer_; - void* route_change_observer_; + RTCAudioSessionDelegateAdapter* audio_session_observer_; // Contains the audio data format specification for a stream of audio. AudioStreamBasicDescription application_format_; diff --git a/webrtc/modules/audio_device/ios/audio_device_ios.mm b/webrtc/modules/audio_device/ios/audio_device_ios.mm index 1c2fe829e6..6f20c6a8b7 100644 --- a/webrtc/modules/audio_device/ios/audio_device_ios.mm +++ b/webrtc/modules/audio_device/ios/audio_device_ios.mm @@ -18,16 +18,20 @@ #include "webrtc/modules/audio_device/ios/audio_device_ios.h" #include "webrtc/base/atomicops.h" +#include "webrtc/base/bind.h" #include "webrtc/base/checks.h" #include "webrtc/base/criticalsection.h" #include "webrtc/base/logging.h" +#include "webrtc/base/thread.h" #include "webrtc/base/thread_annotations.h" #include "webrtc/modules/audio_device/fine_audio_buffer.h" #include "webrtc/modules/utility/include/helpers_ios.h" #import "webrtc/base/objc/RTCLogging.h" #import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h" +#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h" #import "webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.h" +#import "webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.h" namespace webrtc { @@ -106,20 +110,24 @@ static void LogDeviceInfo() { #endif // !defined(NDEBUG) AudioDeviceIOS::AudioDeviceIOS() - : audio_device_buffer_(nullptr), - vpio_unit_(nullptr), - recording_(0), - playing_(0), - initialized_(false), - rec_is_initialized_(false), - play_is_initialized_(false), - audio_interruption_observer_(nullptr), - route_change_observer_(nullptr) { + : async_invoker_(new rtc::AsyncInvoker()), + audio_device_buffer_(nullptr), + vpio_unit_(nullptr), + recording_(0), + playing_(0), + initialized_(false), + rec_is_initialized_(false), + play_is_initialized_(false), + is_interrupted_(false) { LOGI() << "ctor" << ios::GetCurrentThreadDescription(); + thread_ = rtc::Thread::Current(); + audio_session_observer_ = + [[RTCAudioSessionDelegateAdapter alloc] initWithObserver:this]; } AudioDeviceIOS::~AudioDeviceIOS() { LOGI() << "~dtor" << ios::GetCurrentThreadDescription(); + audio_session_observer_ = nil; RTC_DCHECK(thread_checker_.CalledOnValidThread()); Terminate(); } @@ -332,6 +340,80 @@ int AudioDeviceIOS::GetRecordAudioParameters(AudioParameters* params) const { return 0; } +void AudioDeviceIOS::OnInterruptionBegin() { + RTC_DCHECK(async_invoker_); + RTC_DCHECK(thread_); + if (thread_->IsCurrent()) { + HandleInterruptionBegin(); + return; + } + async_invoker_->AsyncInvoke( + thread_, + rtc::Bind(&webrtc::AudioDeviceIOS::HandleInterruptionBegin, this)); +} + +void AudioDeviceIOS::OnInterruptionEnd() { + RTC_DCHECK(async_invoker_); + RTC_DCHECK(thread_); + if (thread_->IsCurrent()) { + HandleInterruptionEnd(); + return; + } + async_invoker_->AsyncInvoke( + thread_, + rtc::Bind(&webrtc::AudioDeviceIOS::HandleInterruptionEnd, this)); +} + +void AudioDeviceIOS::OnValidRouteChange() { + RTC_DCHECK(async_invoker_); + RTC_DCHECK(thread_); + if (thread_->IsCurrent()) { + HandleValidRouteChange(); + return; + } + async_invoker_->AsyncInvoke( + thread_, + rtc::Bind(&webrtc::AudioDeviceIOS::HandleValidRouteChange, this)); +} + +void AudioDeviceIOS::HandleInterruptionBegin() { + RTC_DCHECK(thread_checker_.CalledOnValidThread()); + RTCLog(@"Stopping the audio unit due to interruption begin."); + LOG_IF_ERROR(AudioOutputUnitStop(vpio_unit_), + "Failed to stop the the Voice-Processing I/O unit"); + is_interrupted_ = true; +} + +void AudioDeviceIOS::HandleInterruptionEnd() { + RTC_DCHECK(thread_checker_.CalledOnValidThread()); + RTCLog(@"Starting the audio unit due to interruption end."); + LOG_IF_ERROR(AudioOutputUnitStart(vpio_unit_), + "Failed to start the the Voice-Processing I/O unit"); + is_interrupted_ = false; +} + +void AudioDeviceIOS::HandleValidRouteChange() { + RTC_DCHECK(thread_checker_.CalledOnValidThread()); + + // Don't do anything if we're interrupted. + if (is_interrupted_) { + return; + } + + // Only restart audio for a valid route change if the session sample rate + // has changed. + RTCAudioSession* session = [RTCAudioSession sharedInstance]; + const double current_sample_rate = playout_parameters_.sample_rate(); + const double session_sample_rate = session.sampleRate; + if (current_sample_rate != session_sample_rate) { + RTCLog(@"Route changed caused sample rate to change from %f to %f. " + "Restarting audio unit.", current_sample_rate, session_sample_rate); + if (!RestartAudioUnitWithNewFormat(session_sample_rate)) { + RTCLogError(@"Audio restart failed."); + } + } +} + void AudioDeviceIOS::UpdateAudioDeviceBuffer() { LOGI() << "UpdateAudioDevicebuffer"; // AttachAudioBuffer() is called at construction by the main class but check @@ -345,155 +427,14 @@ void AudioDeviceIOS::UpdateAudioDeviceBuffer() { audio_device_buffer_->SetRecordingChannels(record_parameters_.channels()); } -void AudioDeviceIOS::RegisterNotificationObservers() { - LOGI() << "RegisterNotificationObservers"; - // This code block will be called when AVAudioSessionInterruptionNotification - // is observed. - void (^interrupt_block)(NSNotification*) = ^(NSNotification* notification) { - NSNumber* type_number = - notification.userInfo[AVAudioSessionInterruptionTypeKey]; - AVAudioSessionInterruptionType type = - (AVAudioSessionInterruptionType)type_number.unsignedIntegerValue; - LOG(LS_INFO) << "Audio session interruption:"; - switch (type) { - case AVAudioSessionInterruptionTypeBegan: - // The system has deactivated our audio session. - // Stop the active audio unit. - LOG(LS_INFO) << " Began => stopping the audio unit"; - LOG_IF_ERROR(AudioOutputUnitStop(vpio_unit_), - "Failed to stop the the Voice-Processing I/O unit"); - break; - case AVAudioSessionInterruptionTypeEnded: - // The interruption has ended. Restart the audio session and start the - // initialized audio unit again. - LOG(LS_INFO) << " Ended => restarting audio session and audio unit"; - NSError* error = nil; - BOOL success = NO; - AVAudioSession* session = [AVAudioSession sharedInstance]; - success = [session setActive:YES error:&error]; - if (CheckAndLogError(success, error)) { - LOG_IF_ERROR(AudioOutputUnitStart(vpio_unit_), - "Failed to start the the Voice-Processing I/O unit"); - } - break; - } - }; - - // This code block will be called when AVAudioSessionRouteChangeNotification - // is observed. - void (^route_change_block)(NSNotification*) = - ^(NSNotification* notification) { - // Get reason for current route change. - NSNumber* reason_number = - notification.userInfo[AVAudioSessionRouteChangeReasonKey]; - AVAudioSessionRouteChangeReason reason = - (AVAudioSessionRouteChangeReason)reason_number.unsignedIntegerValue; - bool valid_route_change = true; - LOG(LS_INFO) << "Route change:"; - switch (reason) { - case AVAudioSessionRouteChangeReasonUnknown: - LOG(LS_INFO) << " ReasonUnknown"; - break; - case AVAudioSessionRouteChangeReasonNewDeviceAvailable: - LOG(LS_INFO) << " NewDeviceAvailable"; - break; - case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: - LOG(LS_INFO) << " OldDeviceUnavailable"; - break; - case AVAudioSessionRouteChangeReasonCategoryChange: - // It turns out that we see this notification (at least in iOS 9.2) - // when making a switch from a BT device to e.g. Speaker using the - // iOS Control Center and that we therefore must check if the sample - // rate has changed. And if so is the case, restart the audio unit. - LOG(LS_INFO) << " CategoryChange"; - LOG(LS_INFO) << " New category: " << ios::GetAudioSessionCategory(); - break; - case AVAudioSessionRouteChangeReasonOverride: - LOG(LS_INFO) << " Override"; - break; - case AVAudioSessionRouteChangeReasonWakeFromSleep: - LOG(LS_INFO) << " WakeFromSleep"; - break; - case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory: - LOG(LS_INFO) << " NoSuitableRouteForCategory"; - break; - case AVAudioSessionRouteChangeReasonRouteConfigurationChange: - // The set of input and output ports has not changed, but their - // configuration has, e.g., a port’s selected data source has - // changed. Ignore this type of route change since we are focusing - // on detecting headset changes. - LOG(LS_INFO) << " RouteConfigurationChange (ignored)"; - valid_route_change = false; - break; - } - - if (valid_route_change) { - // Log previous route configuration. - AVAudioSessionRouteDescription* prev_route = - notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey]; - LOG(LS_INFO) << "Previous route:"; - LOG(LS_INFO) << ios::StdStringFromNSString( - [NSString stringWithFormat:@"%@", prev_route]); - - // Only restart audio for a valid route change and if the - // session sample rate has changed. - RTCAudioSession* session = [RTCAudioSession sharedInstance]; - const double session_sample_rate = session.sampleRate; - LOG(LS_INFO) << "session sample rate: " << session_sample_rate; - if (playout_parameters_.sample_rate() != session_sample_rate) { - if (!RestartAudioUnitWithNewFormat(session_sample_rate)) { - LOG(LS_ERROR) << "Audio restart failed"; - } - } - } - }; - - // Get the default notification center of the current process. - NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; - - // Add AVAudioSessionInterruptionNotification observer. - id interruption_observer = - [center addObserverForName:AVAudioSessionInterruptionNotification - object:nil - queue:[NSOperationQueue mainQueue] - usingBlock:interrupt_block]; - // Add AVAudioSessionRouteChangeNotification observer. - id route_change_observer = - [center addObserverForName:AVAudioSessionRouteChangeNotification - object:nil - queue:[NSOperationQueue mainQueue] - usingBlock:route_change_block]; - - // Increment refcount on observers using ARC bridge. Instance variable is a - // void* instead of an id because header is included in other pure C++ - // files. - audio_interruption_observer_ = (__bridge_retained void*)interruption_observer; - route_change_observer_ = (__bridge_retained void*)route_change_observer; -} - -void AudioDeviceIOS::UnregisterNotificationObservers() { - LOGI() << "UnregisterNotificationObservers"; - // Transfer ownership of observer back to ARC, which will deallocate the - // observer once it exits this scope. - NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; - if (audio_interruption_observer_ != nullptr) { - id observer = (__bridge_transfer id)audio_interruption_observer_; - [center removeObserver:observer]; - audio_interruption_observer_ = nullptr; - } - if (route_change_observer_ != nullptr) { - id observer = (__bridge_transfer id)route_change_observer_; - [center removeObserver:observer]; - route_change_observer_ = nullptr; - } -} - void AudioDeviceIOS::SetupAudioBuffersForActiveAudioSession() { LOGI() << "SetupAudioBuffersForActiveAudioSession"; // Verify the current values once the audio session has been activated. RTCAudioSession* session = [RTCAudioSession sharedInstance]; - LOG(LS_INFO) << " sample rate: " << session.sampleRate; - LOG(LS_INFO) << " IO buffer duration: " << session.IOBufferDuration; + double sample_rate = session.sampleRate; + NSTimeInterval io_buffer_duration = session.IOBufferDuration; + LOG(LS_INFO) << " sample rate: " << sample_rate; + LOG(LS_INFO) << " IO buffer duration: " << io_buffer_duration; LOG(LS_INFO) << " output channels: " << session.outputNumberOfChannels; LOG(LS_INFO) << " input channels: " << session.inputNumberOfChannels; LOG(LS_INFO) << " output latency: " << session.outputLatency; @@ -505,7 +446,7 @@ void AudioDeviceIOS::SetupAudioBuffersForActiveAudioSession() { // 16kHz. RTCAudioSessionConfiguration* webRTCConfig = [RTCAudioSessionConfiguration webRTCConfiguration]; - if (session.sampleRate != webRTCConfig.sampleRate) { + if (sample_rate != webRTCConfig.sampleRate) { LOG(LS_WARNING) << "Unable to set the preferred sample rate"; } @@ -514,11 +455,11 @@ void AudioDeviceIOS::SetupAudioBuffersForActiveAudioSession() { // number of audio frames. // Example: IO buffer size = 0.008 seconds <=> 128 audio frames at 16kHz. // Hence, 128 is the size we expect to see in upcoming render callbacks. - playout_parameters_.reset(session.sampleRate, playout_parameters_.channels(), - session.IOBufferDuration); + playout_parameters_.reset(sample_rate, playout_parameters_.channels(), + io_buffer_duration); RTC_DCHECK(playout_parameters_.is_complete()); - record_parameters_.reset(session.sampleRate, record_parameters_.channels(), - session.IOBufferDuration); + record_parameters_.reset(sample_rate, record_parameters_.channels(), + io_buffer_duration); RTC_DCHECK(record_parameters_.is_complete()); LOG(LS_INFO) << " frames per I/O buffer: " << playout_parameters_.frames_per_buffer(); @@ -784,7 +725,7 @@ bool AudioDeviceIOS::InitPlayOrRecord() { } // Start observing audio session interruptions and route changes. - RegisterNotificationObservers(); + [session pushDelegate:audio_session_observer_]; // Ensure that we got what what we asked for in our active audio session. SetupAudioBuffersForActiveAudioSession(); @@ -816,11 +757,11 @@ void AudioDeviceIOS::ShutdownPlayOrRecord() { } // Remove audio session notification observers. - UnregisterNotificationObservers(); + RTCAudioSession* session = [RTCAudioSession sharedInstance]; + [session removeDelegate:audio_session_observer_]; // All I/O should be stopped or paused prior to deactivating the audio // session, hence we deactivate as last action. - RTCAudioSession* session = [RTCAudioSession sharedInstance]; [session lockForConfiguration]; [session setActive:NO error:nil]; [session unlockForConfiguration]; diff --git a/webrtc/modules/audio_device/ios/audio_session_observer.h b/webrtc/modules/audio_device/ios/audio_session_observer.h new file mode 100644 index 0000000000..d05e8c9a3e --- /dev/null +++ b/webrtc/modules/audio_device/ios/audio_session_observer.h @@ -0,0 +1,37 @@ +/* + * Copyright 2016 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef WEBRTC_MODULES_AUDIO_DEVICE_IOS_AUDIO_SESSION_OBSERVER_H_ +#define WEBRTC_MODULES_AUDIO_DEVICE_IOS_AUDIO_SESSION_OBSERVER_H_ + +#include "webrtc/base/asyncinvoker.h" +#include "webrtc/base/thread.h" + +namespace webrtc { + +// Observer interface for listening to AVAudioSession events. +class AudioSessionObserver { + public: + // Called when audio session interruption begins. + virtual void OnInterruptionBegin() = 0; + + // Called when audio session interruption ends. + virtual void OnInterruptionEnd() = 0; + + // Called when audio route changes. + virtual void OnValidRouteChange() = 0; + + protected: + virtual ~AudioSessionObserver() {} +}; + +} // namespace webrtc + +#endif // WEBRTC_MODULES_AUDIO_DEVICE_IOS_AUDIO_SESSION_OBSERVER_H_ diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Configuration.mm b/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Configuration.mm index 7b0bb58639..83320b62f0 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Configuration.mm +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Configuration.mm @@ -49,7 +49,8 @@ } } - if (self.sampleRate != configuration.sampleRate) { + // self.sampleRate is accurate only if the audio session is active. + if (!self.isActive || self.sampleRate != configuration.sampleRate) { NSError *sampleRateError = nil; if (![self setPreferredSampleRate:configuration.sampleRate error:&sampleRateError]) { @@ -59,7 +60,9 @@ } } - if (self.IOBufferDuration != configuration.ioBufferDuration) { + // self.IOBufferDuration is accurate only if the audio session is active. + if (!self.isActive || + self.IOBufferDuration != configuration.ioBufferDuration) { NSError *bufferDurationError = nil; if (![self setPreferredIOBufferDuration:configuration.ioBufferDuration error:&bufferDurationError]) { diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h b/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h index cc92ba7742..43af7c86e2 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h @@ -10,13 +10,12 @@ #import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h" +#include + NS_ASSUME_NONNULL_BEGIN @interface RTCAudioSession () -/** The delegates. */ -@property(nonatomic, readonly) NSSet *delegates; - /** Number of times setActive:YES has succeeded without a balanced call to * setActive:NO. */ @@ -24,6 +23,23 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)checkLock:(NSError **)outError; +/** Adds the delegate to the list of delegates, and places it at the front of + * the list. This delegate will be notified before other delegates of + * audio events. + */ +- (void)pushDelegate:(id)delegate; + +// Properties and methods for tests. +@property(nonatomic, readonly) + std::vector<__weak id > delegates; +- (void)notifyDidBeginInterruption; +- (void)notifyDidEndInterruptionWithShouldResumeSession: + (BOOL)shouldResumeSession; +- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason + previousRoute:(AVAudioSessionRouteDescription *)previousRoute; +- (void)notifyMediaServicesWereLost; +- (void)notifyMediaServicesWereReset; + @end NS_ASSUME_NONNULL_END diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession.h b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.h index 0a04ce9d74..73ba0c01fa 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSession.h +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.h @@ -122,10 +122,7 @@ extern NSInteger const kRTCAudioSessionErrorConfiguration; /** Default constructor. Do not call init. */ + (instancetype)sharedInstance; -/** Adds a delegate, which is held weakly. Even though it's held weakly, callers - * should still call |removeDelegate| when it's no longer required to ensure - * proper dealloc. This is due to internal use of an NSHashTable. - */ +/** Adds a delegate, which is held weakly. */ - (void)addDelegate:(id)delegate; /** Removes an added delegate. */ - (void)removeDelegate:(id)delegate; diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm index b80a7fd255..c2985764d4 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm @@ -12,6 +12,7 @@ #include "webrtc/base/checks.h" #include "webrtc/base/criticalsection.h" +#include "webrtc/modules/audio_device/ios/audio_device_ios.h" #import "webrtc/base/objc/RTCLogging.h" #import "webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h" @@ -26,7 +27,6 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; @implementation RTCAudioSession { rtc::CriticalSection _crit; AVAudioSession *_session; - NSHashTable *_delegates; NSInteger _activationCount; NSInteger _lockRecursionCount; BOOL _isActive; @@ -34,6 +34,7 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; } @synthesize session = _session; +@synthesize delegates = _delegates; + (instancetype)sharedInstance { static dispatch_once_t onceToken; @@ -47,7 +48,6 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; - (instancetype)init { if (self = [super init]) { _session = [AVAudioSession sharedInstance]; - _delegates = [NSHashTable weakObjectsHashTable]; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self @@ -109,14 +109,24 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; } - (void)addDelegate:(id)delegate { + if (!delegate) { + return; + } @synchronized(self) { - [_delegates addObject:delegate]; + _delegates.push_back(delegate); + [self removeZeroedDelegates]; } } - (void)removeDelegate:(id)delegate { + if (!delegate) { + return; + } @synchronized(self) { - [_delegates removeObject:delegate]; + _delegates.erase(std::remove(_delegates.begin(), + _delegates.end(), + delegate)); + [self removeZeroedDelegates]; } } @@ -227,6 +237,8 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; return self.session.IOBufferDuration; } +// TODO(tkchin): Simplify the amount of locking happening here. Likely that we +// can just do atomic increments / decrements. - (BOOL)setActive:(BOOL)active error:(NSError **)outError { if (![self checkLock:outError]) { @@ -459,9 +471,26 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; return error; } -- (NSSet *)delegates { +- (std::vector<__weak id >)delegates { @synchronized(self) { - return _delegates.setRepresentation; + // Note: this returns a copy. + return _delegates; + } +} + +- (void)pushDelegate:(id)delegate { + @synchronized(self) { + _delegates.insert(_delegates.begin(), delegate); + } +} + +- (void)removeZeroedDelegates { + @synchronized(self) { + for (auto it = _delegates.begin(); it != _delegates.end(); ++it) { + if (!*it) { + _delegates.erase(it); + } + } } } @@ -513,14 +542,14 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; } - (void)notifyDidBeginInterruption { - for (id delegate in self.delegates) { + for (auto delegate : self.delegates) { [delegate audioSessionDidBeginInterruption:self]; } } - (void)notifyDidEndInterruptionWithShouldResumeSession: (BOOL)shouldResumeSession { - for (id delegate in self.delegates) { + for (auto delegate : self.delegates) { [delegate audioSessionDidEndInterruption:self shouldResumeSession:shouldResumeSession]; } @@ -529,7 +558,7 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; - (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason previousRoute:(AVAudioSessionRouteDescription *)previousRoute { - for (id delegate in self.delegates) { + for (auto delegate : self.delegates) { [delegate audioSessionDidChangeRoute:self reason:reason previousRoute:previousRoute]; @@ -537,13 +566,13 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; } - (void)notifyMediaServicesWereLost { - for (id delegate in self.delegates) { + for (auto delegate : self.delegates) { [delegate audioSessionMediaServicesWereLost:self]; } } - (void)notifyMediaServicesWereReset { - for (id delegate in self.delegates) { + for (auto delegate : self.delegates) { [delegate audioSessionMediaServicesWereReset:self]; } } diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.h b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.h new file mode 100644 index 0000000000..0140aa043a --- /dev/null +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.h @@ -0,0 +1,30 @@ +/* + * Copyright 2016 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h" + +namespace webrtc { +class AudioSessionObserver; +} + +/** Adapter that forwards RTCAudioSessionDelegate calls to the appropriate + * methods on the AudioSessionObserver. + */ +@interface RTCAudioSessionDelegateAdapter : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** |observer| is a raw pointer and should be kept alive + * for this object's lifetime. + */ +- (instancetype)initWithObserver:(webrtc::AudioSessionObserver *)observer + NS_DESIGNATED_INITIALIZER; + +@end diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.mm b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.mm new file mode 100644 index 0000000000..dccead02fb --- /dev/null +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.mm @@ -0,0 +1,79 @@ +/* + * Copyright 2016 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#import "webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.h" + +#include "webrtc/modules/audio_device/ios/audio_session_observer.h" + +#import "webrtc/base/objc/RTCLogging.h" + +@implementation RTCAudioSessionDelegateAdapter { + webrtc::AudioSessionObserver *_observer; +} + +- (instancetype)initWithObserver:(webrtc::AudioSessionObserver *)observer { + NSParameterAssert(observer); + if (self = [super init]) { + _observer = observer; + } + return self; +} + +#pragma mark - RTCAudioSessionDelegate + +- (void)audioSessionDidBeginInterruption:(RTCAudioSession *)session { + _observer->OnInterruptionBegin(); +} + +- (void)audioSessionDidEndInterruption:(RTCAudioSession *)session + shouldResumeSession:(BOOL)shouldResumeSession { + _observer->OnInterruptionEnd(); +} + +- (void)audioSessionDidChangeRoute:(RTCAudioSession *)session + reason:(AVAudioSessionRouteChangeReason)reason + previousRoute:(AVAudioSessionRouteDescription *)previousRoute { + switch (reason) { + case AVAudioSessionRouteChangeReasonUnknown: + case AVAudioSessionRouteChangeReasonNewDeviceAvailable: + case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: + case AVAudioSessionRouteChangeReasonCategoryChange: + // It turns out that we see a category change (at least in iOS 9.2) + // when making a switch from a BT device to e.g. Speaker using the + // iOS Control Center and that we therefore must check if the sample + // rate has changed. And if so is the case, restart the audio unit. + case AVAudioSessionRouteChangeReasonOverride: + case AVAudioSessionRouteChangeReasonWakeFromSleep: + case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory: + _observer->OnValidRouteChange(); + break; + case AVAudioSessionRouteChangeReasonRouteConfigurationChange: + // The set of input and output ports has not changed, but their + // configuration has, e.g., a port’s selected data source has + // changed. Ignore this type of route change since we are focusing + // on detecting headset changes. + RTCLog(@"Ignoring RouteConfigurationChange"); + break; + } +} + +- (void)audioSessionMediaServicesWereLost:(RTCAudioSession *)session { +} + +- (void)audioSessionMediaServicesWereReset:(RTCAudioSession *)session { +} + +- (void)audioSessionShouldConfigure:(RTCAudioSession *)session { +} + +- (void)audioSessionShouldUnconfigure:(RTCAudioSession *)session { +} + +@end diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSessionTest.mm b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionTest.mm index ac065c7e37..603e450c75 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSessionTest.mm +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionTest.mm @@ -13,6 +13,39 @@ #include "testing/gtest/include/gtest/gtest.h" #import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h" +#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h" + +@interface RTCAudioSessionTestDelegate : NSObject +@end + +@implementation RTCAudioSessionTestDelegate + +- (void)audioSessionDidBeginInterruption:(RTCAudioSession *)session { +} + +- (void)audioSessionDidEndInterruption:(RTCAudioSession *)session + shouldResumeSession:(BOOL)shouldResumeSession { +} + +- (void)audioSessionDidChangeRoute:(RTCAudioSession *)session + reason:(AVAudioSessionRouteChangeReason)reason + previousRoute:(AVAudioSessionRouteDescription *)previousRoute { +} + +- (void)audioSessionMediaServicesWereLost:(RTCAudioSession *)session { +} + +- (void)audioSessionMediaServicesWereReset:(RTCAudioSession *)session { +} + +- (void)audioSessionShouldConfigure:(RTCAudioSession *)session { +} + +- (void)audioSessionShouldUnconfigure:(RTCAudioSession *)session { +} + +@end + @interface RTCAudioSessionTest : NSObject @@ -36,9 +69,111 @@ EXPECT_FALSE(session.isLocked); } +- (void)testAddAndRemoveDelegates { + RTCAudioSession *session = [RTCAudioSession sharedInstance]; + NSMutableArray *delegates = [NSMutableArray array]; + const size_t count = 5; + for (size_t i = 0; i < count; ++i) { + RTCAudioSessionTestDelegate *delegate = + [[RTCAudioSessionTestDelegate alloc] init]; + [session addDelegate:delegate]; + [delegates addObject:delegate]; + EXPECT_EQ(i + 1, session.delegates.size()); + } + [delegates enumerateObjectsUsingBlock:^(RTCAudioSessionTestDelegate *obj, + NSUInteger idx, + BOOL *stop) { + [session removeDelegate:obj]; + }]; + EXPECT_EQ(0u, session.delegates.size()); +} + +- (void)testPushDelegate { + RTCAudioSession *session = [RTCAudioSession sharedInstance]; + NSMutableArray *delegates = [NSMutableArray array]; + const size_t count = 2; + for (size_t i = 0; i < count; ++i) { + RTCAudioSessionTestDelegate *delegate = + [[RTCAudioSessionTestDelegate alloc] init]; + [session addDelegate:delegate]; + [delegates addObject:delegate]; + } + // Test that it gets added to the front of the list. + RTCAudioSessionTestDelegate *pushedDelegate = + [[RTCAudioSessionTestDelegate alloc] init]; + [session pushDelegate:pushedDelegate]; + EXPECT_TRUE(pushedDelegate == session.delegates[0]); + + // Test that it stays at the front of the list. + for (size_t i = 0; i < count; ++i) { + RTCAudioSessionTestDelegate *delegate = + [[RTCAudioSessionTestDelegate alloc] init]; + [session addDelegate:delegate]; + [delegates addObject:delegate]; + } + EXPECT_TRUE(pushedDelegate == session.delegates[0]); + + // Test that the next one goes to the front too. + pushedDelegate = [[RTCAudioSessionTestDelegate alloc] init]; + [session pushDelegate:pushedDelegate]; + EXPECT_TRUE(pushedDelegate == session.delegates[0]); +} + +// Tests that delegates added to the audio session properly zero out. This is +// checking an implementation detail (that vectors of __weak work as expected). +- (void)testZeroingWeakDelegate { + RTCAudioSession *session = [RTCAudioSession sharedInstance]; + @autoreleasepool { + // Add a delegate to the session. There should be one delegate at this + // point. + RTCAudioSessionTestDelegate *delegate = + [[RTCAudioSessionTestDelegate alloc] init]; + [session addDelegate:delegate]; + EXPECT_EQ(1u, session.delegates.size()); + EXPECT_TRUE(session.delegates[0]); + } + // The previously created delegate should've de-alloced, leaving a nil ptr. + EXPECT_FALSE(session.delegates[0]); + RTCAudioSessionTestDelegate *delegate = + [[RTCAudioSessionTestDelegate alloc] init]; + [session addDelegate:delegate]; + // On adding a new delegate, nil ptrs should've been cleared. + EXPECT_EQ(1u, session.delegates.size()); + EXPECT_TRUE(session.delegates[0]); +} + @end -TEST(RTCAudioSessionTest, LockForConfiguration) { +namespace webrtc { + +class AudioSessionTest : public ::testing::Test { + protected: + void TearDown() { + RTCAudioSession *session = [RTCAudioSession sharedInstance]; + for (id delegate : session.delegates) { + [session removeDelegate:delegate]; + } + } +}; + +TEST_F(AudioSessionTest, LockForConfiguration) { RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; [test testLockForConfiguration]; } + +TEST_F(AudioSessionTest, AddAndRemoveDelegates) { + RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; + [test testAddAndRemoveDelegates]; +} + +TEST_F(AudioSessionTest, PushDelegate) { + RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; + [test testPushDelegate]; +} + +TEST_F(AudioSessionTest, ZeroingWeakDelegate) { + RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; + [test testZeroingWeakDelegate]; +} + +} // namespace webrtc diff --git a/webrtc/modules/modules.gyp b/webrtc/modules/modules.gyp index d65507d316..3e0df4cd3d 100644 --- a/webrtc/modules/modules.gyp +++ b/webrtc/modules/modules.gyp @@ -481,6 +481,9 @@ 'audio_device/ios/audio_device_unittest_ios.cc', 'audio_device/ios/objc/RTCAudioSessionTest.mm', ], + 'xcode_settings': { + 'OTHER_LDFLAGS': ['-ObjC'], + }, # This needs to be kept in sync with modules_unittests.isolate. 'mac_bundle_resources': [ '<(DEPTH)/data/audio_processing/output_data_float.pb',