diff --git a/webrtc/examples/objc/AppRTCDemo/ios/ARDMainView.h b/webrtc/examples/objc/AppRTCDemo/ios/ARDMainView.h index d3a583dfa9..a783ca1a1c 100644 --- a/webrtc/examples/objc/AppRTCDemo/ios/ARDMainView.h +++ b/webrtc/examples/objc/AppRTCDemo/ios/ARDMainView.h @@ -15,10 +15,10 @@ @protocol ARDMainViewDelegate - (void)mainView:(ARDMainView *)mainView - didInputRoom:(NSString *)room - isLoopback:(BOOL)isLoopback - isAudioOnly:(BOOL)isAudioOnly - shouldDelayAudioConfig:(BOOL)shouldDelayAudioConfig; + didInputRoom:(NSString *)room + isLoopback:(BOOL)isLoopback + isAudioOnly:(BOOL)isAudioOnly + useManualAudio:(BOOL)useManualAudio; - (void)mainViewDidToggleAudioLoop:(ARDMainView *)mainView; diff --git a/webrtc/examples/objc/AppRTCDemo/ios/ARDMainView.m b/webrtc/examples/objc/AppRTCDemo/ios/ARDMainView.m index b2647630e1..e7e0f940d1 100644 --- a/webrtc/examples/objc/AppRTCDemo/ios/ARDMainView.m +++ b/webrtc/examples/objc/AppRTCDemo/ios/ARDMainView.m @@ -122,8 +122,8 @@ static CGFloat const kAppLabelHeight = 20; UILabel *_audioOnlyLabel; UISwitch *_loopbackSwitch; UILabel *_loopbackLabel; - UISwitch *_audioConfigDelaySwitch; - UILabel *_audioConfigDelayLabel; + UISwitch *_useManualAudioSwitch; + UILabel *_useManualAudioLabel; UIButton *_startCallButton; UIButton *_audioLoopButton; } @@ -175,17 +175,17 @@ static CGFloat const kAppLabelHeight = 20; [_loopbackLabel sizeToFit]; [self addSubview:_loopbackLabel]; - _audioConfigDelaySwitch = [[UISwitch alloc] initWithFrame:CGRectZero]; - [_audioConfigDelaySwitch sizeToFit]; - _audioConfigDelaySwitch.on = YES; - [self addSubview:_audioConfigDelaySwitch]; + _useManualAudioSwitch = [[UISwitch alloc] initWithFrame:CGRectZero]; + [_useManualAudioSwitch sizeToFit]; + _useManualAudioSwitch.on = YES; + [self addSubview:_useManualAudioSwitch]; - _audioConfigDelayLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - _audioConfigDelayLabel.text = @"Delay audio config"; - _audioConfigDelayLabel.font = controlFont; - _audioConfigDelayLabel.textColor = controlFontColor; - [_audioConfigDelayLabel sizeToFit]; - [self addSubview:_audioConfigDelayLabel]; + _useManualAudioLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + _useManualAudioLabel.text = @"Use manual audio config"; + _useManualAudioLabel.font = controlFont; + _useManualAudioLabel.textColor = controlFontColor; + [_useManualAudioLabel sizeToFit]; + [self addSubview:_useManualAudioLabel]; _startCallButton = [UIButton buttonWithType:UIButtonTypeSystem]; _startCallButton.backgroundColor = [UIColor blueColor]; @@ -275,22 +275,22 @@ static CGFloat const kAppLabelHeight = 20; _loopbackLabel.center = CGPointMake(loopbackModeLabelCenterX, CGRectGetMidY(loopbackModeRect)); - CGFloat audioConfigDelayTop = + CGFloat useManualAudioTop = CGRectGetMaxY(_loopbackSwitch.frame) + kCallControlMargin; - CGRect audioConfigDelayRect = + CGRect useManualAudioRect = CGRectMake(kCallControlMargin * 3, - audioConfigDelayTop, - _audioConfigDelaySwitch.frame.size.width, - _audioConfigDelaySwitch.frame.size.height); - _audioConfigDelaySwitch.frame = audioConfigDelayRect; - CGFloat audioConfigDelayLabelCenterX = CGRectGetMaxX(audioConfigDelayRect) + - kCallControlMargin + _audioConfigDelayLabel.frame.size.width / 2; - _audioConfigDelayLabel.center = - CGPointMake(audioConfigDelayLabelCenterX, - CGRectGetMidY(audioConfigDelayRect)); + useManualAudioTop, + _useManualAudioSwitch.frame.size.width, + _useManualAudioSwitch.frame.size.height); + _useManualAudioSwitch.frame = useManualAudioRect; + CGFloat useManualAudioLabelCenterX = CGRectGetMaxX(useManualAudioRect) + + kCallControlMargin + _useManualAudioLabel.frame.size.width / 2; + _useManualAudioLabel.center = + CGPointMake(useManualAudioLabelCenterX, + CGRectGetMidY(useManualAudioRect)); CGFloat audioLoopTop = - CGRectGetMaxY(audioConfigDelayRect) + kCallControlMargin * 3; + CGRectGetMaxY(useManualAudioRect) + kCallControlMargin * 3; _audioLoopButton.frame = CGRectMake(kCallControlMargin, audioLoopTop, _audioLoopButton.frame.size.width, @@ -335,7 +335,7 @@ static CGFloat const kAppLabelHeight = 20; didInputRoom:room isLoopback:_loopbackSwitch.isOn isAudioOnly:_audioOnlySwitch.isOn - shouldDelayAudioConfig:_audioConfigDelaySwitch.isOn]; + useManualAudio:_useManualAudioSwitch.isOn]; } @end diff --git a/webrtc/examples/objc/AppRTCDemo/ios/ARDMainViewController.m b/webrtc/examples/objc/AppRTCDemo/ios/ARDMainViewController.m index d11f09222f..a684ba32b0 100644 --- a/webrtc/examples/objc/AppRTCDemo/ios/ARDMainViewController.m +++ b/webrtc/examples/objc/AppRTCDemo/ios/ARDMainViewController.m @@ -23,13 +23,14 @@ @interface ARDMainViewController () < ARDMainViewDelegate, + ARDVideoCallViewControllerDelegate, RTCAudioSessionDelegate> @end @implementation ARDMainViewController { ARDMainView *_mainView; AVAudioPlayer *_audioPlayer; - BOOL _shouldDelayAudioConfig; + BOOL _useManualAudio; } - (void)loadView { @@ -37,17 +38,26 @@ _mainView.delegate = self; self.view = _mainView; - [self setupAudioSession]; + RTCAudioSessionConfiguration *webRTCConfig = + [RTCAudioSessionConfiguration webRTCConfiguration]; + webRTCConfig.categoryOptions = webRTCConfig.categoryOptions | + AVAudioSessionCategoryOptionDefaultToSpeaker; + [RTCAudioSessionConfiguration setWebRTCConfiguration:webRTCConfig]; + + RTCAudioSession *session = [RTCAudioSession sharedInstance]; + [session addDelegate:self]; + + [self configureAudioSession]; [self setupAudioPlayer]; } #pragma mark - ARDMainViewDelegate - (void)mainView:(ARDMainView *)mainView - didInputRoom:(NSString *)room - isLoopback:(BOOL)isLoopback - isAudioOnly:(BOOL)isAudioOnly - shouldDelayAudioConfig:(BOOL)shouldDelayAudioConfig { + didInputRoom:(NSString *)room + isLoopback:(BOOL)isLoopback + isAudioOnly:(BOOL)isAudioOnly + useManualAudio:(BOOL)useManualAudio { if (!room.length) { [self showAlertWithMessage:@"Missing room name."]; return; @@ -77,15 +87,16 @@ return; } - _shouldDelayAudioConfig = shouldDelayAudioConfig; RTCAudioSession *session = [RTCAudioSession sharedInstance]; - session.shouldDelayAudioConfiguration = _shouldDelayAudioConfig; + session.useManualAudio = useManualAudio; + session.isAudioEnabled = NO; // Kick off the video call. ARDVideoCallViewController *videoCallViewController = [[ARDVideoCallViewController alloc] initForRoom:trimmedRoom isLoopback:isLoopback - isAudioOnly:isAudioOnly]; + isAudioOnly:isAudioOnly + delegate:self]; videoCallViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; [self presentViewController:videoCallViewController @@ -102,10 +113,22 @@ mainView.isAudioLoopPlaying = _audioPlayer.playing; } +#pragma mark - ARDVideoCallViewControllerDelegate + +- (void)viewControllerDidFinish:(ARDVideoCallViewController *)viewController { + if (![viewController isBeingDismissed]) { + RTCLog(@"Dismissing VC"); + [self dismissViewControllerAnimated:YES completion:^{ + [self restartAudioPlayerIfNeeded]; + }]; + } + RTCAudioSession *session = [RTCAudioSession sharedInstance]; + session.isAudioEnabled = NO; +} + #pragma mark - RTCAudioSessionDelegate -- (void)audioSessionShouldConfigure:(RTCAudioSession *)session { - // Won't get called unless audio config is delayed. +- (void)audioSessionDidStartPlayOrRecord:(RTCAudioSession *)session { // Stop playback on main queue and then configure WebRTC. [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeMain block:^{ @@ -113,35 +136,23 @@ RTCLog(@"Stopping audio loop due to WebRTC start."); [_audioPlayer stop]; } - // TODO(tkchin): Shouldn't lock on main queue. Figure out better way to - // check audio loop state. - [session lockForConfiguration]; - [session configureWebRTCSession:nil]; - [session unlockForConfiguration]; + RTCLog(@"Setting isAudioEnabled to YES."); + session.isAudioEnabled = YES; }]; } -- (void)audioSessionShouldUnconfigure:(RTCAudioSession *)session { - // Won't get called unless audio config is delayed. - [session lockForConfiguration]; - [session unconfigureWebRTCSession:nil]; - [session unlockForConfiguration]; -} - -- (void)audioSessionDidUnconfigure:(RTCAudioSession *)session { +- (void)audioSessionDidStopPlayOrRecord:(RTCAudioSession *)session { // WebRTC is done with the audio session. Restart playback. [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeMain block:^{ - if (_mainView.isAudioLoopPlaying) { - RTCLog(@"Starting audio loop due to WebRTC end."); - [_audioPlayer play]; - } + RTCLog(@"audioSessionDidStopPlayOrRecord"); + [self restartAudioPlayerIfNeeded]; }]; } #pragma mark - Private -- (void)setupAudioSession { +- (void)configureAudioSession { RTCAudioSessionConfiguration *configuration = [[RTCAudioSessionConfiguration alloc] init]; configuration.category = AVAudioSessionCategoryAmbient; @@ -149,10 +160,17 @@ configuration.mode = AVAudioSessionModeDefault; RTCAudioSession *session = [RTCAudioSession sharedInstance]; - [session addDelegate:self]; [session lockForConfiguration]; + BOOL hasSucceeded = NO; NSError *error = nil; - if (![session setConfiguration:configuration active:YES error:&error]) { + if (session.isActive) { + hasSucceeded = [session setConfiguration:configuration error:&error]; + } else { + hasSucceeded = [session setConfiguration:configuration + active:YES + error:&error]; + } + if (!hasSucceeded) { RTCLogError(@"Error setting configuration: %@", error.localizedDescription); } [session unlockForConfiguration]; @@ -169,6 +187,14 @@ [_audioPlayer prepareToPlay]; } +- (void)restartAudioPlayerIfNeeded { + if (_mainView.isAudioLoopPlaying && !self.presentedViewController) { + RTCLog(@"Starting audio loop due to WebRTC end."); + [self configureAudioSession]; + [_audioPlayer play]; + } +} + - (void)showAlertWithMessage:(NSString*)message { UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:nil message:message diff --git a/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.h b/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.h index 7dee2d8537..f3a9554820 100644 --- a/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.h +++ b/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.h @@ -10,10 +10,20 @@ #import +@class ARDVideoCallViewController; +@protocol ARDVideoCallViewControllerDelegate + +- (void)viewControllerDidFinish:(ARDVideoCallViewController *)viewController; + +@end + @interface ARDVideoCallViewController : UIViewController +@property(nonatomic, weak) id delegate; + - (instancetype)initForRoom:(NSString *)room isLoopback:(BOOL)isLoopback - isAudioOnly:(BOOL)isAudioOnly; + isAudioOnly:(BOOL)isAudioOnly + delegate:(id)delegate; @end diff --git a/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m b/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m index 80bbb10bb3..d35e1e6b2d 100644 --- a/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m +++ b/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m @@ -34,11 +34,14 @@ } @synthesize videoCallView = _videoCallView; +@synthesize delegate = _delegate; - (instancetype)initForRoom:(NSString *)room isLoopback:(BOOL)isLoopback - isAudioOnly:(BOOL)isAudioOnly { + isAudioOnly:(BOOL)isAudioOnly + delegate:(id)delegate { if (self = [super init]) { + _delegate = delegate; _client = [[ARDAppClient alloc] initWithDelegate:self]; [_client connectToRoomWithId:room isLoopback:isLoopback @@ -177,10 +180,7 @@ self.remoteVideoTrack = nil; self.localVideoTrack = nil; [_client disconnect]; - if (![self isBeingDismissed]) { - [self.presentingViewController dismissViewControllerAnimated:YES - completion:nil]; - } + [_delegate viewControllerDidFinish:self]; } - (void)switchCamera { diff --git a/webrtc/modules/audio_device/ios/audio_device_ios.h b/webrtc/modules/audio_device/ios/audio_device_ios.h index 8e630dd1a8..9616a285c0 100644 --- a/webrtc/modules/audio_device/ios/audio_device_ios.h +++ b/webrtc/modules/audio_device/ios/audio_device_ios.h @@ -14,7 +14,6 @@ #include #include "WebRTC/RTCMacros.h" -#include "webrtc/base/asyncinvoker.h" #include "webrtc/base/thread.h" #include "webrtc/base/thread_checker.h" #include "webrtc/modules/audio_device/audio_device_generic.h" @@ -42,7 +41,8 @@ class FineAudioBuffer; // same thread. class AudioDeviceIOS : public AudioDeviceGeneric, public AudioSessionObserver, - public VoiceProcessingAudioUnitObserver { + public VoiceProcessingAudioUnitObserver, + public rtc::MessageHandler { public: AudioDeviceIOS(); ~AudioDeviceIOS(); @@ -162,7 +162,7 @@ class AudioDeviceIOS : public AudioDeviceGeneric, void OnInterruptionBegin() override; void OnInterruptionEnd() override; void OnValidRouteChange() override; - void OnConfiguredForWebRTC() override; + void OnCanPlayOrRecordChange(bool can_play_or_record) override; // VoiceProcessingAudioUnitObserver methods. OSStatus OnDeliverRecordedData(AudioUnitRenderActionFlags* flags, @@ -176,12 +176,16 @@ class AudioDeviceIOS : public AudioDeviceGeneric, UInt32 num_frames, AudioBufferList* io_data) override; + // Handles messages from posts. + void OnMessage(rtc::Message *msg) override; + private: // Called by the relevant AudioSessionObserver methods on |thread_|. void HandleInterruptionBegin(); void HandleInterruptionEnd(); void HandleValidRouteChange(); - void HandleConfiguredForWebRTC(); + void HandleCanPlayOrRecordChange(bool can_play_or_record); + void HandleSampleRateChange(float sample_rate); // Uses current |playout_parameters_| and |record_parameters_| to inform the // audio device buffer (ADB) about our internal audio parameters. @@ -197,9 +201,13 @@ class AudioDeviceIOS : public AudioDeviceGeneric, // Creates the audio unit. bool CreateAudioUnit(); - // Restarts active audio streams using a new sample rate. Required when e.g. - // a BT headset is enabled or disabled. - bool RestartAudioUnit(float sample_rate); + // Updates the audio unit state based on current state. + void UpdateAudioUnit(bool can_play_or_record); + + // Configures the audio session for WebRTC. + void ConfigureAudioSession(); + // Unconfigures the audio session. + void UnconfigureAudioSession(); // Activates our audio session, creates and initializes the voice-processing // audio unit and verifies that we got the preferred native audio parameters. @@ -213,8 +221,6 @@ class AudioDeviceIOS : public AudioDeviceGeneric, 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 AudioDeviceModule::Create(). @@ -284,6 +290,9 @@ class AudioDeviceIOS : public AudioDeviceGeneric, // Audio interruption observer instance. RTCAudioSessionDelegateAdapter* audio_session_observer_; + + // Set to true if we've activated the audio session. + bool has_configured_session_; }; } // namespace webrtc diff --git a/webrtc/modules/audio_device/ios/audio_device_ios.mm b/webrtc/modules/audio_device/ios/audio_device_ios.mm index 0e127c9dc2..8f6fb4d9b6 100644 --- a/webrtc/modules/audio_device/ios/audio_device_ios.mm +++ b/webrtc/modules/audio_device/ios/audio_device_ios.mm @@ -61,6 +61,13 @@ namespace webrtc { const UInt16 kFixedPlayoutDelayEstimate = 30; const UInt16 kFixedRecordDelayEstimate = 30; +enum AudioDeviceMessageType : uint32_t { + kMessageTypeInterruptionBegin, + kMessageTypeInterruptionEnd, + kMessageTypeValidRouteChange, + kMessageTypeCanPlayOrRecordChange, +}; + using ios::CheckAndLogError; #if !defined(NDEBUG) @@ -85,15 +92,15 @@ static void LogDeviceInfo() { #endif // !defined(NDEBUG) AudioDeviceIOS::AudioDeviceIOS() - : async_invoker_(new rtc::AsyncInvoker()), - audio_device_buffer_(nullptr), + : audio_device_buffer_(nullptr), audio_unit_(nullptr), recording_(0), playing_(0), initialized_(false), rec_is_initialized_(false), play_is_initialized_(false), - is_interrupted_(false) { + is_interrupted_(false), + has_configured_session_(false) { LOGI() << "ctor" << ios::GetCurrentThreadDescription(); thread_ = rtc::Thread::Current(); audio_session_observer_ = @@ -191,6 +198,7 @@ int32_t AudioDeviceIOS::StartPlayout() { RTC_DCHECK(thread_checker_.CalledOnValidThread()); RTC_DCHECK(play_is_initialized_); RTC_DCHECK(!playing_); + RTC_DCHECK(audio_unit_); if (fine_audio_buffer_) { fine_audio_buffer_->ResetPlayout(); } @@ -209,7 +217,11 @@ int32_t AudioDeviceIOS::StartPlayout() { int32_t AudioDeviceIOS::StopPlayout() { LOGI() << "StopPlayout"; RTC_DCHECK(thread_checker_.CalledOnValidThread()); - if (!play_is_initialized_ || !playing_) { + if (!play_is_initialized_) { + return 0; + } + if (!playing_) { + play_is_initialized_ = false; return 0; } if (!recording_) { @@ -225,6 +237,7 @@ int32_t AudioDeviceIOS::StartRecording() { RTC_DCHECK(thread_checker_.CalledOnValidThread()); RTC_DCHECK(rec_is_initialized_); RTC_DCHECK(!recording_); + RTC_DCHECK(audio_unit_); if (fine_audio_buffer_) { fine_audio_buffer_->ResetRecord(); } @@ -243,7 +256,11 @@ int32_t AudioDeviceIOS::StartRecording() { int32_t AudioDeviceIOS::StopRecording() { LOGI() << "StopRecording"; RTC_DCHECK(thread_checker_.CalledOnValidThread()); - if (!rec_is_initialized_ || !recording_) { + if (!rec_is_initialized_) { + return 0; + } + if (!recording_) { + rec_is_initialized_ = false; return 0; } if (!playing_) { @@ -318,51 +335,24 @@ int AudioDeviceIOS::GetRecordAudioParameters(AudioParameters* params) const { } 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)); + thread_->Post(this, kMessageTypeInterruptionBegin); } 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)); + thread_->Post(this, kMessageTypeInterruptionEnd); } 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)); + thread_->Post(this, kMessageTypeValidRouteChange); } -void AudioDeviceIOS::OnConfiguredForWebRTC() { - RTC_DCHECK(async_invoker_); +void AudioDeviceIOS::OnCanPlayOrRecordChange(bool can_play_or_record) { RTC_DCHECK(thread_); - if (thread_->IsCurrent()) { - HandleValidRouteChange(); - return; - } - async_invoker_->AsyncInvoke( - thread_, - rtc::Bind(&webrtc::AudioDeviceIOS::HandleConfiguredForWebRTC, this)); + thread_->Post(this, kMessageTypeCanPlayOrRecordChange, + new rtc::TypedMessageData(can_play_or_record)); } OSStatus AudioDeviceIOS::OnDeliverRecordedData( @@ -385,6 +375,9 @@ OSStatus AudioDeviceIOS::OnDeliverRecordedData( RTCLogWarning(@"Expected %u frames but got %u", static_cast(frames_per_buffer), static_cast(num_frames)); + + RTCAudioSession *session = [RTCAudioSession sharedInstance]; + RTCLogWarning(@"Session:\n %@", session); return result; } @@ -447,12 +440,36 @@ OSStatus AudioDeviceIOS::OnGetPlayoutData(AudioUnitRenderActionFlags* flags, return noErr; } +void AudioDeviceIOS::OnMessage(rtc::Message *msg) { + switch (msg->message_id) { + case kMessageTypeInterruptionBegin: + HandleInterruptionBegin(); + break; + case kMessageTypeInterruptionEnd: + HandleInterruptionEnd(); + break; + case kMessageTypeValidRouteChange: + HandleValidRouteChange(); + break; + case kMessageTypeCanPlayOrRecordChange: { + rtc::TypedMessageData* data = + static_cast*>(msg->pdata); + HandleCanPlayOrRecordChange(data->data()); + delete data; + break; + } + } +} + void AudioDeviceIOS::HandleInterruptionBegin() { RTC_DCHECK(thread_checker_.CalledOnValidThread()); - RTCLog(@"Stopping the audio unit due to interruption begin."); - if (!audio_unit_->Stop()) { - RTCLogError(@"Failed to stop the audio unit."); + if (audio_unit_ && + audio_unit_->GetState() == VoiceProcessingAudioUnit::kStarted) { + RTCLog(@"Stopping the audio unit due to interruption begin."); + if (!audio_unit_->Stop()) { + RTCLogError(@"Failed to stop the audio unit for interruption begin."); + } } is_interrupted_ = true; } @@ -460,66 +477,95 @@ void AudioDeviceIOS::HandleInterruptionBegin() { void AudioDeviceIOS::HandleInterruptionEnd() { RTC_DCHECK(thread_checker_.CalledOnValidThread()); - RTCLog(@"Starting the audio unit due to interruption end."); - if (!audio_unit_->Start()) { - RTCLogError(@"Failed to start the audio unit."); - } is_interrupted_ = false; + RTCLog(@"Interruption ended. Updating audio unit state."); + UpdateAudioUnit([RTCAudioSession sharedInstance].canPlayOrRecord); } 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 (!RestartAudioUnit(session_sample_rate)) { - RTCLogError(@"Audio restart failed."); - } - } + HandleSampleRateChange(session.sampleRate); } -void AudioDeviceIOS::HandleConfiguredForWebRTC() { +void AudioDeviceIOS::HandleCanPlayOrRecordChange(bool can_play_or_record) { + RTCLog(@"Handling CanPlayOrRecord change to: %d", can_play_or_record); + UpdateAudioUnit(can_play_or_record); +} + +void AudioDeviceIOS::HandleSampleRateChange(float sample_rate) { RTC_DCHECK(thread_checker_.CalledOnValidThread()); + RTCLog(@"Handling sample rate change to %f.", sample_rate); - // If we're not initialized we don't need to do anything. Audio unit will - // be initialized on initialization. - if (!rec_is_initialized_ && !play_is_initialized_) + // Don't do anything if we're interrupted. + if (is_interrupted_) { + RTCLog(@"Ignoring sample rate change to %f due to interruption.", + sample_rate); return; + } - // If we're initialized, we must have an audio unit. - RTC_DCHECK(audio_unit_); + // If we don't have an audio unit yet, or the audio unit is uninitialized, + // there is no work to do. + if (!audio_unit_ || + audio_unit_->GetState() < VoiceProcessingAudioUnit::kInitialized) { + return; + } - // Use configured audio session's settings to set up audio device buffer. - // TODO(tkchin): Use RTCAudioSessionConfiguration to pick up settings and - // pass it along. + // The audio unit is already initialized or started. + // Check to see if the sample rate or buffer size has changed. + RTCAudioSession* session = [RTCAudioSession sharedInstance]; + const double session_sample_rate = session.sampleRate; + const NSTimeInterval session_buffer_duration = session.IOBufferDuration; + const size_t session_frames_per_buffer = + static_cast(session_sample_rate * session_buffer_duration + .5); + const double current_sample_rate = playout_parameters_.sample_rate(); + const size_t current_frames_per_buffer = + playout_parameters_.frames_per_buffer(); + RTCLog(@"Handling playout sample rate change to: %f\n" + " Session sample rate: %f frames_per_buffer: %lu\n" + " ADM sample rate: %f frames_per_buffer: %lu", + sample_rate, + session_sample_rate, (unsigned long)session_frames_per_buffer, + current_sample_rate, (unsigned long)current_frames_per_buffer);; + + // Sample rate and buffer size are the same, no work to do. + if (abs(current_sample_rate - session_sample_rate) <= DBL_EPSILON && + current_frames_per_buffer == session_frames_per_buffer) { + return; + } + + // We need to adjust our format and buffer sizes. + // The stream format is about to be changed and it requires that we first + // stop and uninitialize the audio unit to deallocate its resources. + RTCLog(@"Stopping and uninitializing audio unit to adjust buffers."); + bool restart_audio_unit = false; + if (audio_unit_->GetState() == VoiceProcessingAudioUnit::kStarted) { + audio_unit_->Stop(); + restart_audio_unit = true; + } + if (audio_unit_->GetState() == VoiceProcessingAudioUnit::kInitialized) { + audio_unit_->Uninitialize(); + } + + // Allocate new buffers given the new stream format. SetupAudioBuffersForActiveAudioSession(); - // Initialize the audio unit. This will affect any existing audio playback. - if (!audio_unit_->Initialize(playout_parameters_.sample_rate())) { - RTCLogError(@"Failed to initialize audio unit after configuration."); + // Initialize the audio unit again with the new sample rate. + RTC_DCHECK_EQ(playout_parameters_.sample_rate(), session_sample_rate); + if (!audio_unit_->Initialize(session_sample_rate)) { + RTCLogError(@"Failed to initialize the audio unit with sample rate: %f", + session_sample_rate); return; } - // If we haven't started playing or recording there's nothing more to do. - if (!playing_ && !recording_) - return; - - // We are in a play or record state, start the audio unit. - if (!audio_unit_->Start()) { - RTCLogError(@"Failed to start audio unit after configuration."); + // Restart the audio unit if it was already running. + if (restart_audio_unit && !audio_unit_->Start()) { + RTCLogError(@"Failed to start audio unit with sample rate: %f", + session_sample_rate); return; } + RTCLog(@"Successfully handled sample rate change."); } void AudioDeviceIOS::UpdateAudioDeviceBuffer() { @@ -597,6 +643,7 @@ void AudioDeviceIOS::SetupAudioBuffersForActiveAudioSession() { // at each input callback when calling AudioUnitRender(). const int data_byte_size = record_parameters_.GetBytesPerBuffer(); record_audio_buffer_.reset(new SInt8[data_byte_size]); + memset(record_audio_buffer_.get(), 0, data_byte_size); audio_record_buffer_list_.mNumberBuffers = 1; AudioBuffer* audio_buffer = &audio_record_buffer_list_.mBuffers[0]; audio_buffer->mNumberChannels = record_parameters_.channels(); @@ -616,46 +663,117 @@ bool AudioDeviceIOS::CreateAudioUnit() { return true; } -bool AudioDeviceIOS::RestartAudioUnit(float sample_rate) { - RTCLog(@"Restarting audio unit with new sample rate: %f", sample_rate); +void AudioDeviceIOS::UpdateAudioUnit(bool can_play_or_record) { + RTC_DCHECK(thread_checker_.CalledOnValidThread()); + RTCLog(@"Updating audio unit state. CanPlayOrRecord=%d IsInterrupted=%d", + can_play_or_record, is_interrupted_); - // Stop the active audio unit. - if (!audio_unit_->Stop()) { - RTCLogError(@"Failed to stop the audio unit."); - return false; + if (is_interrupted_) { + RTCLog(@"Ignoring audio unit update due to interruption."); + return; } - // The stream format is about to be changed and it requires that we first - // uninitialize it to deallocate its resources. - if (!audio_unit_->Uninitialize()) { - RTCLogError(@"Failed to uninitialize the audio unit."); - return false; + // If we're not initialized we don't need to do anything. Audio unit will + // be initialized on initialization. + if (!rec_is_initialized_ && !play_is_initialized_) + return; + + // If we're initialized, we must have an audio unit. + RTC_DCHECK(audio_unit_); + + bool should_initialize_audio_unit = false; + bool should_uninitialize_audio_unit = false; + bool should_start_audio_unit = false; + bool should_stop_audio_unit = false; + + switch (audio_unit_->GetState()) { + case VoiceProcessingAudioUnit::kInitRequired: + RTC_NOTREACHED(); + break; + case VoiceProcessingAudioUnit::kUninitialized: + should_initialize_audio_unit = can_play_or_record; + should_start_audio_unit = should_initialize_audio_unit && + (playing_ || recording_); + break; + case VoiceProcessingAudioUnit::kInitialized: + should_start_audio_unit = + can_play_or_record && (playing_ || recording_); + should_uninitialize_audio_unit = !can_play_or_record; + break; + case VoiceProcessingAudioUnit::kStarted: + RTC_DCHECK(playing_ || recording_); + should_stop_audio_unit = !can_play_or_record; + should_uninitialize_audio_unit = should_stop_audio_unit; + break; } - // Allocate new buffers given the new stream format. - SetupAudioBuffersForActiveAudioSession(); - - // Initialize the audio unit again with the new sample rate. - RTC_DCHECK_EQ(playout_parameters_.sample_rate(), sample_rate); - if (!audio_unit_->Initialize(sample_rate)) { - RTCLogError(@"Failed to initialize the audio unit with sample rate: %f", - sample_rate); - return false; + if (should_initialize_audio_unit) { + RTCLog(@"Initializing audio unit for UpdateAudioUnit"); + ConfigureAudioSession(); + SetupAudioBuffersForActiveAudioSession(); + if (!audio_unit_->Initialize(playout_parameters_.sample_rate())) { + RTCLogError(@"Failed to initialize audio unit."); + return; + } } - // Restart the audio unit. - if (!audio_unit_->Start()) { - RTCLogError(@"Failed to start audio unit."); - return false; + if (should_start_audio_unit) { + RTCLog(@"Starting audio unit for UpdateAudioUnit"); + if (!audio_unit_->Start()) { + RTCLogError(@"Failed to start audio unit."); + return; + } } - RTCLog(@"Successfully restarted audio unit."); - return true; + if (should_stop_audio_unit) { + RTCLog(@"Stopping audio unit for UpdateAudioUnit"); + if (!audio_unit_->Stop()) { + RTCLogError(@"Failed to stop audio unit."); + return; + } + } + + if (should_uninitialize_audio_unit) { + RTCLog(@"Uninitializing audio unit for UpdateAudioUnit"); + audio_unit_->Uninitialize(); + UnconfigureAudioSession(); + } +} + +void AudioDeviceIOS::ConfigureAudioSession() { + RTC_DCHECK(thread_checker_.CalledOnValidThread()); + RTCLog(@"Configuring audio session."); + if (has_configured_session_) { + RTCLogWarning(@"Audio session already configured."); + return; + } + RTCAudioSession* session = [RTCAudioSession sharedInstance]; + [session lockForConfiguration]; + [session configureWebRTCSession:nil]; + [session unlockForConfiguration]; + has_configured_session_ = true; + RTCLog(@"Configured audio session."); +} + +void AudioDeviceIOS::UnconfigureAudioSession() { + RTC_DCHECK(thread_checker_.CalledOnValidThread()); + RTCLog(@"Unconfiguring audio session."); + if (!has_configured_session_) { + RTCLogWarning(@"Audio session already unconfigured."); + return; + } + RTCAudioSession* session = [RTCAudioSession sharedInstance]; + [session lockForConfiguration]; + [session unconfigureWebRTCSession:nil]; + [session unlockForConfiguration]; + has_configured_session_ = false; + RTCLog(@"Unconfigured audio session."); } bool AudioDeviceIOS::InitPlayOrRecord() { LOGI() << "InitPlayOrRecord"; + // There should be no audio unit at this point. if (!CreateAudioUnit()) { return false; } @@ -674,14 +792,11 @@ bool AudioDeviceIOS::InitPlayOrRecord() { return false; } - // If we are already configured properly, we can initialize the audio unit. - if (session.isConfiguredForWebRTC) { - [session unlockForConfiguration]; + // If we are ready to play or record, initialize the audio unit. + if (session.canPlayOrRecord) { + ConfigureAudioSession(); SetupAudioBuffersForActiveAudioSession(); - // Audio session has been marked ready for WebRTC so we can initialize the - // audio unit now. audio_unit_->Initialize(playout_parameters_.sample_rate()); - return true; } // Release the lock. @@ -694,9 +809,7 @@ void AudioDeviceIOS::ShutdownPlayOrRecord() { LOGI() << "ShutdownPlayOrRecord"; // Close and delete the voice-processing I/O unit. - if (audio_unit_) { - audio_unit_.reset(); - } + audio_unit_.reset(); // Remove audio session notification observers. RTCAudioSession* session = [RTCAudioSession sharedInstance]; @@ -705,6 +818,7 @@ void AudioDeviceIOS::ShutdownPlayOrRecord() { // All I/O should be stopped or paused prior to deactivating the audio // session, hence we deactivate as last action. [session lockForConfiguration]; + UnconfigureAudioSession(); [session endWebRTCSession: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 index 6c4a9cd9e2..def8c2322b 100644 --- a/webrtc/modules/audio_device/ios/audio_session_observer.h +++ b/webrtc/modules/audio_device/ios/audio_session_observer.h @@ -28,8 +28,8 @@ class AudioSessionObserver { // Called when audio route changes. virtual void OnValidRouteChange() = 0; - // Called when audio session has been configured for WebRTC. - virtual void OnConfiguredForWebRTC() = 0; + // Called when the ability to play or record changes. + virtual void OnCanPlayOrRecordChange(bool can_play_or_record) = 0; protected: virtual ~AudioSessionObserver() {} diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Configuration.mm b/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Configuration.mm index 85d442aab2..5a7600a5d3 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Configuration.mm +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Configuration.mm @@ -16,13 +16,29 @@ @implementation RTCAudioSession (Configuration) -- (BOOL)isConfiguredForWebRTC { - return self.savedConfiguration != nil; +- (BOOL)setConfiguration:(RTCAudioSessionConfiguration *)configuration + error:(NSError **)outError { + return [self setConfiguration:configuration + active:NO + shouldSetActive:NO + error:outError]; } - (BOOL)setConfiguration:(RTCAudioSessionConfiguration *)configuration active:(BOOL)active error:(NSError **)outError { + return [self setConfiguration:configuration + active:active + shouldSetActive:YES + error:outError]; +} + +#pragma mark - Private + +- (BOOL)setConfiguration:(RTCAudioSessionConfiguration *)configuration + active:(BOOL)active + shouldSetActive:(BOOL)shouldSetActive + error:(NSError **)outError { NSParameterAssert(configuration); if (outError) { *outError = nil; @@ -61,8 +77,22 @@ } } - // self.sampleRate is accurate only if the audio session is active. - if (!self.isActive || self.sampleRate != configuration.sampleRate) { + // Sometimes category options don't stick after setting mode. + if (self.categoryOptions != configuration.categoryOptions) { + NSError *categoryError = nil; + if (![self setCategory:configuration.category + withOptions:configuration.categoryOptions + error:&categoryError]) { + RTCLogError(@"Failed to set category options: %@", + categoryError.localizedDescription); + error = categoryError; + } else { + RTCLog(@"Set category options to: %ld", + (long)configuration.categoryOptions); + } + } + + if (self.preferredSampleRate != configuration.sampleRate) { NSError *sampleRateError = nil; if (![self setPreferredSampleRate:configuration.sampleRate error:&sampleRateError]) { @@ -75,9 +105,7 @@ } } - // self.IOBufferDuration is accurate only if the audio session is active. - if (!self.isActive || - self.IOBufferDuration != configuration.ioBufferDuration) { + if (self.preferredIOBufferDuration != configuration.ioBufferDuration) { NSError *bufferDurationError = nil; if (![self setPreferredIOBufferDuration:configuration.ioBufferDuration error:&bufferDurationError]) { @@ -90,11 +118,13 @@ } } - NSError *activeError = nil; - if (![self setActive:active error:&activeError]) { - RTCLogError(@"Failed to setActive to %d: %@", - active, activeError.localizedDescription); - error = activeError; + if (shouldSetActive) { + NSError *activeError = nil; + if (![self setActive:active error:&activeError]) { + RTCLogError(@"Failed to setActive to %d: %@", + active, activeError.localizedDescription); + error = activeError; + } } if (self.isActive && @@ -138,84 +168,4 @@ return error == nil; } -- (BOOL)configureWebRTCSession:(NSError **)outError { - if (outError) { - *outError = nil; - } - if (![self checkLock:outError]) { - return NO; - } - RTCLog(@"Configuring audio session for WebRTC."); - - if (self.isConfiguredForWebRTC) { - RTCLogError(@"Already configured."); - if (outError) { - *outError = - [self configurationErrorWithDescription:@"Already configured."]; - } - return NO; - } - - // Configure the AVAudioSession and activate it. - // Provide an error even if there isn't one so we can log it. - NSError *error = nil; - RTCAudioSessionConfiguration *currentConfig = - [RTCAudioSessionConfiguration currentConfiguration]; - RTCAudioSessionConfiguration *webRTCConfig = - [RTCAudioSessionConfiguration webRTCConfiguration]; - self.savedConfiguration = currentConfig; - if (![self setConfiguration:webRTCConfig active:YES error:&error]) { - RTCLogError(@"Failed to set WebRTC audio configuration: %@", - error.localizedDescription); - [self unconfigureWebRTCSession:nil]; - if (outError) { - *outError = error; - } - return NO; - } - - // Ensure that the device currently supports audio input. - // TODO(tkchin): Figure out if this is really necessary. - if (!self.inputAvailable) { - RTCLogError(@"No audio input path is available!"); - [self unconfigureWebRTCSession:nil]; - if (outError) { - *outError = [self configurationErrorWithDescription:@"No input path."]; - } - return NO; - } - - // Give delegates a chance to process the event. In particular, the audio - // devices listening to this event will initialize their audio units. - [self notifyDidConfigure]; - - return YES; -} - -- (BOOL)unconfigureWebRTCSession:(NSError **)outError { - if (outError) { - *outError = nil; - } - if (![self checkLock:outError]) { - return NO; - } - RTCLog(@"Unconfiguring audio session for WebRTC."); - - if (!self.isConfiguredForWebRTC) { - RTCLogError(@"Already unconfigured."); - if (outError) { - *outError = - [self configurationErrorWithDescription:@"Already unconfigured."]; - } - return NO; - } - - [self setConfiguration:self.savedConfiguration active:NO error:outError]; - self.savedConfiguration = nil; - - [self notifyDidUnconfigure]; - - return YES; -} - @end diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h b/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h index c6738e7a03..cb506c345a 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h @@ -28,11 +28,8 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, readonly) int webRTCSessionCount; -/** The configuration of the audio session before configureWebRTCSession - * was first called. - */ -@property(nonatomic, strong, nullable) - RTCAudioSessionConfiguration *savedConfiguration; +/** Convenience BOOL that checks useManualAudio and isAudioEnebled. */ +@property(readonly) BOOL canPlayOrRecord; - (BOOL)checkLock:(NSError **)outError; @@ -55,6 +52,22 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)endWebRTCSession:(NSError **)outError; +/** Configure the audio session for WebRTC. This call will fail if the session + * is already configured. On other failures, we will attempt to restore the + * previously used audio session configuration. + * |lockForConfiguration| must be called first. + * Successful calls to configureWebRTCSession must be matched by calls to + * |unconfigureWebRTCSession|. + */ +- (BOOL)configureWebRTCSession:(NSError **)outError; + +/** Unconfigures the session for WebRTC. This will attempt to restore the + * audio session to the settings used before |configureWebRTCSession| was + * called. + * |lockForConfiguration| must be called first. + */ +- (BOOL)unconfigureWebRTCSession:(NSError **)outError; + /** Returns a configuration error with the given description. */ - (NSError *)configurationErrorWithDescription:(NSString *)description; @@ -69,10 +82,9 @@ NS_ASSUME_NONNULL_BEGIN previousRoute:(AVAudioSessionRouteDescription *)previousRoute; - (void)notifyMediaServicesWereLost; - (void)notifyMediaServicesWereReset; -- (void)notifyShouldConfigure; -- (void)notifyShouldUnconfigure; -- (void)notifyDidConfigure; -- (void)notifyDidUnconfigure; +- (void)notifyDidChangeCanPlayOrRecord:(BOOL)canPlayOrRecord; +- (void)notifyDidStartPlayOrRecord; +- (void)notifyDidStopPlayOrRecord; @end diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession.h b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.h index ab8dbc8859..274cc2bc97 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSession.h +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.h @@ -58,29 +58,18 @@ extern NSInteger const kRTCAudioSessionErrorConfiguration; // TODO(tkchin): Maybe handle SilenceSecondaryAudioHintNotification. -/** Called on a WebRTC thread when WebRTC needs to take over audio. Applications - * should call -[RTCAudioSession configureWebRTCSession] to allow WebRTC to - * play and record audio. Will only occur if shouldDelayAudioConfiguration is - * set to YES. - */ -- (void)audioSessionShouldConfigure:(RTCAudioSession *)session; +- (void)audioSession:(RTCAudioSession *)session + didChangeCanPlayOrRecord:(BOOL)canPlayOrRecord; -/** Called on a WebRTC thread when WebRTC no longer requires audio. Applications - * should call -[RTCAudioSession unconfigureWebRTCSession] to restore their - * audio session settings. Will only occur if shouldDelayAudioConfiguration is - * set to YES. +/** Called on a WebRTC thread when the audio device is notified to begin + * playback or recording. */ -- (void)audioSessionShouldUnconfigure:(RTCAudioSession *)session; +- (void)audioSessionDidStartPlayOrRecord:(RTCAudioSession *)session; -/** Called on a WebRTC thread when WebRTC has configured the audio session for - * WebRTC audio. +/** Called on a WebRTC thread when the audio device is notified to stop + * playback or recording. */ -- (void)audioSessionDidConfigure:(RTCAudioSession *)session; - -/** Called on a WebRTC thread when WebRTC has unconfigured the audio session for - * WebRTC audio. - */ -- (void)audioSessionDidUnconfigure:(RTCAudioSession *)session; +- (void)audioSessionDidStopPlayOrRecord:(RTCAudioSession *)session; @end @@ -108,11 +97,24 @@ extern NSInteger const kRTCAudioSessionErrorConfiguration; /** If YES, WebRTC will not initialize the audio unit automatically when an * audio track is ready for playout or recording. Instead, applications should - * listen to the delegate method |audioSessionShouldConfigure| and configure - * the session manually. This should be set before making WebRTC media calls - * and should not be changed while a call is active. + * call setIsAudioEnabled. If NO, WebRTC will initialize the audio unit + * as soon as an audio track is ready for playout or recording. */ -@property(nonatomic, assign) BOOL shouldDelayAudioConfiguration; +@property(nonatomic, assign) BOOL useManualAudio; + +/** This property is only effective if useManualAudio is YES. + * Represents permission for WebRTC to initialize the VoIP audio unit. + * When set to NO, if the VoIP audio unit used by WebRTC is active, it will be + * stopped and uninitialized. This will stop incoming and outgoing audio. + * When set to YES, WebRTC will initialize and start the audio unit when it is + * needed (e.g. due to establishing an audio connection). + * This property was introduced to work around an issue where if an AVPlayer is + * playing audio while the VoIP audio unit is initialized, its audio would be + * either cut off completely or played at a reduced volume. By preventing + * the audio unit from being initialized until after the audio has completed, + * we are able to prevent the abrupt cutoff. + */ +@property(nonatomic, assign) BOOL isAudioEnabled; // Proxy properties. @property(readonly) NSString *category; @@ -134,12 +136,14 @@ extern NSInteger const kRTCAudioSessionErrorConfiguration; @property(readonly, nullable) AVAudioSessionDataSourceDescription *outputDataSource; @property(readonly) double sampleRate; +@property(readonly) double preferredSampleRate; @property(readonly) NSInteger inputNumberOfChannels; @property(readonly) NSInteger outputNumberOfChannels; @property(readonly) float outputVolume; @property(readonly) NSTimeInterval inputLatency; @property(readonly) NSTimeInterval outputLatency; @property(readonly) NSTimeInterval IOBufferDuration; +@property(readonly) NSTimeInterval preferredIOBufferDuration; /** Default constructor. */ + (instancetype)sharedInstance; @@ -196,37 +200,21 @@ extern NSInteger const kRTCAudioSessionErrorConfiguration; @interface RTCAudioSession (Configuration) -/** Whether or not |configureWebRTCSession| has been called without a balanced - * call to |unconfigureWebRTCSession|. This is not an indication of whether the - * audio session has the right settings. - */ -@property(readonly) BOOL isConfiguredForWebRTC; - /** Applies the configuration to the current session. Attempts to set all * properties even if previous ones fail. Only the last error will be - * returned. Also calls setActive with |active|. + * returned. + * |lockForConfiguration| must be called first. + */ +- (BOOL)setConfiguration:(RTCAudioSessionConfiguration *)configuration + error:(NSError **)outError; + +/** Convenience method that calls both setConfiguration and setActive. * |lockForConfiguration| must be called first. */ - (BOOL)setConfiguration:(RTCAudioSessionConfiguration *)configuration active:(BOOL)active error:(NSError **)outError; -/** Configure the audio session for WebRTC. This call will fail if the session - * is already configured. On other failures, we will attempt to restore the - * previously used audio session configuration. - * |lockForConfiguration| must be called first. - * Successful calls to configureWebRTCSession must be matched by calls to - * |unconfigureWebRTCSession|. - */ -- (BOOL)configureWebRTCSession:(NSError **)outError; - -/** Unconfigures the session for WebRTC. This will attempt to restore the - * audio session to the settings used before |configureWebRTCSession| was - * called. - * |lockForConfiguration| must be called first. - */ -- (BOOL)unconfigureWebRTCSession:(NSError **)outError; - @end NS_ASSUME_NONNULL_END diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm index e6b3657a1d..7ef5110f47 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm @@ -17,6 +17,7 @@ #import "WebRTC/RTCLogging.h" #import "webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h" +#import "webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.h" NSString * const kRTCAudioSessionErrorDomain = @"org.webrtc.RTCAudioSession"; NSInteger const kRTCAudioSessionErrorLockRequired = -1; @@ -32,12 +33,13 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; volatile int _lockRecursionCount; volatile int _webRTCSessionCount; BOOL _isActive; - BOOL _shouldDelayAudioConfiguration; + BOOL _useManualAudio; + BOOL _isAudioEnabled; + BOOL _canPlayOrRecord; } @synthesize session = _session; @synthesize delegates = _delegates; -@synthesize savedConfiguration = _savedConfiguration; + (instancetype)sharedInstance { static dispatch_once_t onceToken; @@ -81,6 +83,9 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; - (NSString *)description { NSString *format = @"RTCAudioSession: {\n" + " category: %@\n" + " categoryOptions: %ld\n" + " mode: %@\n" " isActive: %d\n" " sampleRate: %.2f\n" " IOBufferDuration: %f\n" @@ -90,6 +95,7 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; " inputLatency: %f\n" "}"; NSString *description = [NSString stringWithFormat:format, + self.category, (long)self.categoryOptions, self.mode, self.isActive, self.sampleRate, self.IOBufferDuration, self.outputNumberOfChannels, self.inputNumberOfChannels, self.outputLatency, self.inputLatency]; @@ -112,20 +118,35 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; return _lockRecursionCount > 0; } -- (void)setShouldDelayAudioConfiguration:(BOOL)shouldDelayAudioConfiguration { +- (void)setUseManualAudio:(BOOL)useManualAudio { @synchronized(self) { - // No one should be changing this while an audio device is active. - RTC_DCHECK(!self.isConfiguredForWebRTC); - if (_shouldDelayAudioConfiguration == shouldDelayAudioConfiguration) { + if (_useManualAudio == useManualAudio) { return; } - _shouldDelayAudioConfiguration = shouldDelayAudioConfiguration; + _useManualAudio = useManualAudio; + } + [self updateCanPlayOrRecord]; +} + +- (BOOL)useManualAudio { + @synchronized(self) { + return _useManualAudio; } } -- (BOOL)shouldDelayAudioConfiguration { +- (void)setIsAudioEnabled:(BOOL)isAudioEnabled { @synchronized(self) { - return _shouldDelayAudioConfiguration; + if (_isAudioEnabled == isAudioEnabled) { + return; + } + _isAudioEnabled = isAudioEnabled; + } + [self updateCanPlayOrRecord]; +} + +- (BOOL)isAudioEnabled { + @synchronized(self) { + return _isAudioEnabled; } } @@ -232,6 +253,10 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; return self.session.sampleRate; } +- (double)preferredSampleRate { + return self.session.preferredSampleRate; +} + - (NSInteger)inputNumberOfChannels { return self.session.inputNumberOfChannels; } @@ -256,6 +281,10 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; return self.session.IOBufferDuration; } +- (NSTimeInterval)preferredIOBufferDuration { + return self.session.preferredIOBufferDuration; +} + // TODO(tkchin): Simplify the amount of locking happening here. Likely that we // can just do atomic increments / decrements. - (BOOL)setActive:(BOOL)active @@ -497,21 +526,6 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; } } -- (void)setSavedConfiguration:(RTCAudioSessionConfiguration *)configuration { - @synchronized(self) { - if (_savedConfiguration == configuration) { - return; - } - _savedConfiguration = configuration; - } -} - -- (RTCAudioSessionConfiguration *)savedConfiguration { - @synchronized(self) { - return _savedConfiguration; - } -} - // TODO(tkchin): check for duplicates. - (void)pushDelegate:(id)delegate { @synchronized(self) { @@ -547,6 +561,10 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; return _webRTCSessionCount; } +- (BOOL)canPlayOrRecord { + return !self.useManualAudio || self.isAudioEnabled; +} + - (BOOL)checkLock:(NSError **)outError { // Check ivar instead of trying to acquire lock so that we won't accidentally // acquire lock if it hasn't already been called. @@ -566,38 +584,8 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; if (![self checkLock:outError]) { return NO; } - NSInteger sessionCount = rtc::AtomicOps::Increment(&_webRTCSessionCount); - if (sessionCount > 1) { - // Should already be configured. - RTC_DCHECK(self.isConfiguredForWebRTC); - return YES; - } - - // Only perform configuration steps once. Application might have already - // configured the session. - if (self.isConfiguredForWebRTC) { - // Nothing more to do, already configured. - return YES; - } - - // If application has prevented automatic configuration, return here and wait - // for application to call configureWebRTCSession. - if (self.shouldDelayAudioConfiguration) { - [self notifyShouldConfigure]; - return YES; - } - - // Configure audio session. - NSError *error = nil; - if (![self configureWebRTCSession:&error]) { - RTCLogError(@"Error configuring audio session: %@", - error.localizedDescription); - if (outError) { - *outError = error; - } - return NO; - } - + rtc::AtomicOps::Increment(&_webRTCSessionCount); + [self notifyDidStartPlayOrRecord]; return YES; } @@ -608,38 +596,59 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; if (![self checkLock:outError]) { return NO; } - int sessionCount = rtc::AtomicOps::Decrement(&_webRTCSessionCount); - RTC_DCHECK_GE(sessionCount, 0); - if (sessionCount != 0) { - // Should still be configured. - RTC_DCHECK(self.isConfiguredForWebRTC); - return YES; - } + rtc::AtomicOps::Decrement(&_webRTCSessionCount); + [self notifyDidStopPlayOrRecord]; + return YES; +} - // Only unconfigure if application has not done it. - if (!self.isConfiguredForWebRTC) { - // Nothing more to do, already unconfigured. - return YES; +- (BOOL)configureWebRTCSession:(NSError **)outError { + if (outError) { + *outError = nil; } - - // If application has prevented automatic configuration, return here and wait - // for application to call unconfigureWebRTCSession. - if (self.shouldDelayAudioConfiguration) { - [self notifyShouldUnconfigure]; - return YES; + if (![self checkLock:outError]) { + return NO; } + RTCLog(@"Configuring audio session for WebRTC."); - // Unconfigure audio session. + // Configure the AVAudioSession and activate it. + // Provide an error even if there isn't one so we can log it. NSError *error = nil; - if (![self unconfigureWebRTCSession:&error]) { - RTCLogError(@"Error unconfiguring audio session: %@", + RTCAudioSessionConfiguration *webRTCConfig = + [RTCAudioSessionConfiguration webRTCConfiguration]; + if (![self setConfiguration:webRTCConfig active:YES error:&error]) { + RTCLogError(@"Failed to set WebRTC audio configuration: %@", error.localizedDescription); + [self unconfigureWebRTCSession:nil]; if (outError) { *outError = error; } return NO; } + // Ensure that the device currently supports audio input. + // TODO(tkchin): Figure out if this is really necessary. + if (!self.inputAvailable) { + RTCLogError(@"No audio input path is available!"); + [self unconfigureWebRTCSession:nil]; + if (outError) { + *outError = [self configurationErrorWithDescription:@"No input path."]; + } + return NO; + } + + return YES; +} + +- (BOOL)unconfigureWebRTCSession:(NSError **)outError { + if (outError) { + *outError = nil; + } + if (![self checkLock:outError]) { + return NO; + } + RTCLog(@"Unconfiguring audio session for WebRTC."); + [self setActive:NO error:outError]; + return YES; } @@ -667,6 +676,22 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; } } +- (void)updateCanPlayOrRecord { + BOOL canPlayOrRecord = NO; + BOOL shouldNotify = NO; + @synchronized(self) { + canPlayOrRecord = !self.useManualAudio || self.isAudioEnabled; + if (_canPlayOrRecord == canPlayOrRecord) { + return; + } + _canPlayOrRecord = canPlayOrRecord; + shouldNotify = YES; + } + if (shouldNotify) { + [self notifyDidChangeCanPlayOrRecord:canPlayOrRecord]; + } +} + - (void)notifyDidBeginInterruption { for (auto delegate : self.delegates) { SEL sel = @selector(audioSessionDidBeginInterruption:); @@ -717,38 +742,29 @@ NSInteger const kRTCAudioSessionErrorConfiguration = -2; } } -- (void)notifyShouldConfigure { +- (void)notifyDidChangeCanPlayOrRecord:(BOOL)canPlayOrRecord { for (auto delegate : self.delegates) { - SEL sel = @selector(audioSessionShouldConfigure:); + SEL sel = @selector(audioSession:didChangeCanPlayOrRecord:); if ([delegate respondsToSelector:sel]) { - [delegate audioSessionShouldConfigure:self]; + [delegate audioSession:self didChangeCanPlayOrRecord:canPlayOrRecord]; } } } -- (void)notifyShouldUnconfigure { +- (void)notifyDidStartPlayOrRecord { for (auto delegate : self.delegates) { - SEL sel = @selector(audioSessionShouldUnconfigure:); + SEL sel = @selector(audioSessionDidStartPlayOrRecord:); if ([delegate respondsToSelector:sel]) { - [delegate audioSessionShouldUnconfigure:self]; + [delegate audioSessionDidStartPlayOrRecord:self]; } } } -- (void)notifyDidConfigure { +- (void)notifyDidStopPlayOrRecord { for (auto delegate : self.delegates) { - SEL sel = @selector(audioSessionDidConfigure:); + SEL sel = @selector(audioSessionDidStopPlayOrRecord:); if ([delegate respondsToSelector:sel]) { - [delegate audioSessionDidConfigure:self]; - } - } -} - -- (void)notifyDidUnconfigure { - for (auto delegate : self.delegates) { - SEL sel = @selector(audioSessionDidUnconfigure:); - if ([delegate respondsToSelector:sel]) { - [delegate audioSessionDidUnconfigure:self]; + [delegate audioSessionDidStopPlayOrRecord:self]; } } } diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.h b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.h index 4273d4d863..7832a82d4f 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.h +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.h @@ -37,6 +37,8 @@ extern const double kRTCAudioSessionLowComplexityIOBufferDuration; + (instancetype)currentConfiguration; /** Returns the configuration that WebRTC needs. */ + (instancetype)webRTCConfiguration; +/** Provide a way to override the default configuration. */ ++ (void)setWebRTCConfiguration:(RTCAudioSessionConfiguration *)configuration; @end diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.m b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.m index 086725172b..d2d0483549 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.m +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.m @@ -10,6 +10,8 @@ #import "webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.h" +#import "WebRTC/RTCDispatcher.h" + #import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h" // Try to use mono to save resources. Also avoids channel format conversion @@ -49,6 +51,8 @@ const double kRTCAudioSessionHighPerformanceIOBufferDuration = 0.01; // TODO(henrika): monitor this size and determine if it should be modified. const double kRTCAudioSessionLowComplexityIOBufferDuration = 0.06; +static RTCAudioSessionConfiguration *gWebRTCConfiguration = nil; + @implementation RTCAudioSessionConfiguration @synthesize category = _category; @@ -96,6 +100,10 @@ const double kRTCAudioSessionLowComplexityIOBufferDuration = 0.06; return self; } ++ (void)initialize { + gWebRTCConfiguration = [[self alloc] init]; +} + + (instancetype)currentConfiguration { RTCAudioSession *session = [RTCAudioSession sharedInstance]; RTCAudioSessionConfiguration *config = @@ -111,7 +119,15 @@ const double kRTCAudioSessionLowComplexityIOBufferDuration = 0.06; } + (instancetype)webRTCConfiguration { - return [[self alloc] init]; + @synchronized(self) { + return (RTCAudioSessionConfiguration *)gWebRTCConfiguration; + } +} + ++ (void)setWebRTCConfiguration:(RTCAudioSessionConfiguration *)configuration { + @synchronized(self) { + gWebRTCConfiguration = configuration; + } } @end diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.mm b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.mm index 2df502e087..b554e51ece 100644 --- a/webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.mm +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSessionDelegateAdapter.mm @@ -70,14 +70,15 @@ - (void)audioSessionMediaServicesWereReset:(RTCAudioSession *)session { } -- (void)audioSessionShouldConfigure:(RTCAudioSession *)session { +- (void)audioSession:(RTCAudioSession *)session + didChangeCanPlayOrRecord:(BOOL)canPlayOrRecord { + _observer->OnCanPlayOrRecordChange(canPlayOrRecord); } -- (void)audioSessionShouldUnconfigure:(RTCAudioSession *)session { +- (void)audioSessionDidStartPlayOrRecord:(RTCAudioSession *)session { } -- (void)audioSessionDidConfigure:(RTCAudioSession *)session { - _observer->OnConfiguredForWebRTC(); +- (void)audioSessionDidStopPlayOrRecord:(RTCAudioSession *)session { } @end diff --git a/webrtc/modules/audio_device/ios/voice_processing_audio_unit.mm b/webrtc/modules/audio_device/ios/voice_processing_audio_unit.mm index 08816a4152..db7f42edcb 100644 --- a/webrtc/modules/audio_device/ios/voice_processing_audio_unit.mm +++ b/webrtc/modules/audio_device/ios/voice_processing_audio_unit.mm @@ -175,7 +175,7 @@ VoiceProcessingAudioUnit::State VoiceProcessingAudioUnit::GetState() const { bool VoiceProcessingAudioUnit::Initialize(Float64 sample_rate) { RTC_DCHECK_GE(state_, kUninitialized); - RTCLog(@"Initializing audio unit."); + RTCLog(@"Initializing audio unit with sample rate: %f", sample_rate); OSStatus result = noErr; AudioStreamBasicDescription format = GetFormat(sample_rate); @@ -228,7 +228,9 @@ bool VoiceProcessingAudioUnit::Initialize(Float64 sample_rate) { [NSThread sleepForTimeInterval:0.1f]; result = AudioUnitInitialize(vpio_unit_); } - RTCLog(@"Voice Processing I/O unit is now initialized."); + if (result == noErr) { + RTCLog(@"Voice Processing I/O unit is now initialized."); + } state_ = kInitialized; return true; } @@ -241,6 +243,8 @@ bool VoiceProcessingAudioUnit::Start() { if (result != noErr) { RTCLogError(@"Failed to start audio unit. Error=%ld", (long)result); return false; + } else { + RTCLog(@"Started audio unit"); } state_ = kStarted; return true; @@ -254,7 +258,10 @@ bool VoiceProcessingAudioUnit::Stop() { if (result != noErr) { RTCLogError(@"Failed to stop audio unit. Error=%ld", (long)result); return false; + } else { + RTCLog(@"Stopped audio unit"); } + state_ = kInitialized; return true; } @@ -267,7 +274,11 @@ bool VoiceProcessingAudioUnit::Uninitialize() { if (result != noErr) { RTCLogError(@"Failed to uninitialize audio unit. Error=%ld", (long)result); return false; + } else { + RTCLog(@"Uninitialized audio unit."); } + + state_ = kUninitialized; return true; }