/* * Copyright (C) 2013-2021 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "AudioSessionIOS.h" #if USE(AUDIO_SESSION) && PLATFORM(IOS_FAMILY) #import "AVAudioSessionCaptureDeviceManager.h" #import "Logging.h" #import #import #import #import #import #import #import #import @interface WebInterruptionObserverHelper : NSObject { WebCore::AudioSession* _callback; } - (id)initWithCallback:(WebCore::AudioSession*)callback; - (void)clearCallback; - (void)interruption:(NSNotification *)notification; @end @implementation WebInterruptionObserverHelper - (id)initWithCallback:(WebCore::AudioSession*)callback { if (!(self = [super init])) return nil; _callback = callback; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(interruption:) name:AVAudioSessionInterruptionNotification object:[PAL::getAVAudioSessionClass() sharedInstance]]; return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } - (void)clearCallback { _callback = nil; } - (void)interruption:(NSNotification *)notification { if (!_callback) return; NSUInteger type = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; auto flags = (type == AVAudioSessionInterruptionTypeEnded && [[[notification userInfo] objectForKey:AVAudioSessionInterruptionOptionKey] unsignedIntegerValue] == AVAudioSessionInterruptionOptionShouldResume) ? WebCore::AudioSession::MayResume::Yes : WebCore::AudioSession::MayResume::No; callOnWebThreadOrDispatchAsyncOnMainThread([protectedSelf = retainPtr(self), type, flags]() mutable { auto* callback = protectedSelf->_callback; if (!callback) return; if (type == AVAudioSessionInterruptionTypeBegan) callback->beginInterruption(); else callback->endInterruption(flags); }); } @end namespace WebCore { static WeakHashSet& audioSessionCategoryChangedObservers() { static NeverDestroyed> observers; return observers; } void AudioSessionIOS::addAudioSessionCategoryChangedObserver(const CategoryChangedObserver& observer) { audioSessionCategoryChangedObservers().add(observer); observer(AudioSession::sharedSession(), AudioSession::sharedSession().category()); } static void setEligibleForSmartRouting(bool eligible) { #if PLATFORM(IOS) if (!AudioSession::shouldManageAudioSessionCategory()) return; AVAudioSession *session = [PAL::getAVAudioSessionClass() sharedInstance]; if (![session respondsToSelector:@selector(setEligibleForBTSmartRoutingConsideration:error:)] || ![session respondsToSelector:@selector(eligibleForBTSmartRoutingConsideration)]) return; if (session.eligibleForBTSmartRoutingConsideration == eligible) return; NSError *error = nil; if (![session setEligibleForBTSmartRoutingConsideration:eligible error:&error]) RELEASE_LOG_ERROR(Media, "failed to set eligible to %d with error: %@", eligible, error.localizedDescription); #else UNUSED_PARAM(eligible); #endif } AudioSessionIOS::AudioSessionIOS() : m_workQueue(WorkQueue::create("AudioSession Activation Queue")) { BEGIN_BLOCK_OBJC_EXCEPTIONS m_interruptionObserverHelper = adoptNS([[WebInterruptionObserverHelper alloc] initWithCallback:this]); END_BLOCK_OBJC_EXCEPTIONS m_workQueue->dispatch([] { setEligibleForSmartRouting(false); }); } AudioSessionIOS::~AudioSessionIOS() { [m_interruptionObserverHelper clearCallback]; } void AudioSessionIOS::setHostProcessAttribution(audit_token_t auditToken) { #if ENABLE(APP_PRIVACY_REPORT) && !PLATFORM(MACCATALYST) NSError *error = nil; auto bundleProxy = [LSBundleProxy bundleProxyWithAuditToken:auditToken error:&error]; if (error) { RELEASE_LOG_ERROR(WebRTC, "Failed to get attribution bundleID from audit token with error: %@.", error.localizedDescription); return; } [[PAL::getAVAudioSessionClass() sharedInstance] setHostProcessAttribution:@[ bundleProxy.bundleIdentifier ] error:&error]; if (error) RELEASE_LOG_ERROR(WebRTC, "Failed to set attribution bundleID with error: %@.", error.localizedDescription); #else UNUSED_PARAM(auditToken); #endif }; void AudioSessionIOS::setPresentingProcesses(Vector&& auditTokens) { #if HAVE(AUDIOSESSION_PROCESSASSERTION) AVAudioSession *session = [PAL::getAVAudioSessionClass() sharedInstance]; if (![session respondsToSelector:@selector(setAuditTokensForProcessAssertion:error:)]) return; auto nsAuditTokens = adoptNS([[NSMutableArray alloc] init]); for (auto& token : auditTokens) { auto nsToken = adoptNS([[NSData alloc] initWithBytes:token.val length:sizeof(token.val)]); [nsAuditTokens addObject:nsToken.get()]; } NSError *error = nil; [session setAuditTokensForProcessAssertion:nsAuditTokens.get() error:&error]; if (error) RELEASE_LOG_ERROR(Media, "Failed to set audit tokens for process assertion with error: %@", error.localizedDescription); #else UNUSED_PARAM(auditTokens); #endif } void AudioSessionIOS::setCategory(CategoryType newCategory, RouteSharingPolicy policy) { #if !HAVE(ROUTE_SHARING_POLICY_LONG_FORM_VIDEO) if (policy == RouteSharingPolicy::LongFormVideo) policy = RouteSharingPolicy::LongFormAudio; #endif LOG(Media, "AudioSession::setCategory() - category = %s", convertEnumerationToString(newCategory).ascii().data()); if (categoryOverride() != CategoryType::None && categoryOverride() != newCategory) { LOG(Media, "AudioSession::setCategory() - override set, NOT changing"); return; } NSString *categoryString; NSString *categoryMode = AVAudioSessionModeDefault; AVAudioSessionCategoryOptions options = 0; switch (newCategory) { case CategoryType::AmbientSound: categoryString = AVAudioSessionCategoryAmbient; break; case CategoryType::SoloAmbientSound: categoryString = AVAudioSessionCategorySoloAmbient; break; case CategoryType::MediaPlayback: categoryString = AVAudioSessionCategoryPlayback; break; case CategoryType::RecordAudio: categoryString = AVAudioSessionCategoryRecord; break; case CategoryType::PlayAndRecord: categoryString = AVAudioSessionCategoryPlayAndRecord; categoryMode = AVAudioSessionModeVideoChat; options |= AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionAllowBluetoothA2DP | AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowAirPlay; break; case CategoryType::AudioProcessing: categoryString = AVAudioSessionCategoryAudioProcessing; break; case CategoryType::None: categoryString = AVAudioSessionCategoryAmbient; break; } bool needDeviceUpdate = false; #if ENABLE(MEDIA_STREAM) auto preferredDeviceUID = AVAudioSessionCaptureDeviceManager::singleton().preferredAudioSessionDeviceUID(); if ((newCategory == CategoryType::PlayAndRecord || newCategory == CategoryType::RecordAudio) && !preferredDeviceUID.isEmpty()) { if (m_lastSetPreferredAudioDeviceUID != preferredDeviceUID) needDeviceUpdate = true; } else m_lastSetPreferredAudioDeviceUID = emptyString(); #endif AVAudioSession *session = [PAL::getAVAudioSessionClass() sharedInstance]; auto *currentCategory = [session category]; auto currentOptions = [session categoryOptions]; auto currentPolicy = [session routeSharingPolicy]; auto needSessionUpdate = ![currentCategory isEqualToString:categoryString] || currentOptions != options || currentPolicy != static_cast(policy); if (!needSessionUpdate && !needDeviceUpdate) return; if (needSessionUpdate) { NSError *error = nil; [session setCategory:categoryString mode:categoryMode routeSharingPolicy:static_cast(policy) options:options error:&error]; #if !PLATFORM(IOS_FAMILY_SIMULATOR) && !PLATFORM(MACCATALYST) ASSERT(!error); #endif } #if ENABLE(MEDIA_STREAM) if (needDeviceUpdate) { AVAudioSessionCaptureDeviceManager::singleton().configurePreferredAudioCaptureDevice(); m_lastSetPreferredAudioDeviceUID = AVAudioSessionCaptureDeviceManager::singleton().preferredAudioSessionDeviceUID(); } #endif for (auto& observer : audioSessionCategoryChangedObservers()) observer(*this, category()); } AudioSession::CategoryType AudioSessionIOS::category() const { NSString *categoryString = [[PAL::getAVAudioSessionClass() sharedInstance] category]; if ([categoryString isEqual:AVAudioSessionCategoryAmbient]) return CategoryType::AmbientSound; if ([categoryString isEqual:AVAudioSessionCategorySoloAmbient]) return CategoryType::SoloAmbientSound; if ([categoryString isEqual:AVAudioSessionCategoryPlayback]) return CategoryType::MediaPlayback; if ([categoryString isEqual:AVAudioSessionCategoryRecord]) return CategoryType::RecordAudio; if ([categoryString isEqual:AVAudioSessionCategoryPlayAndRecord]) return CategoryType::PlayAndRecord; if ([categoryString isEqual:AVAudioSessionCategoryAudioProcessing]) return CategoryType::AudioProcessing; return CategoryType::None; } RouteSharingPolicy AudioSessionIOS::routeSharingPolicy() const { static_assert(static_cast(RouteSharingPolicy::Default) == static_cast(AVAudioSessionRouteSharingPolicyDefault), "RouteSharingPolicy::Default is not AVAudioSessionRouteSharingPolicyDefault as expected"); #if HAVE(ROUTE_SHARING_POLICY_LONG_FORM_VIDEO) static_assert(static_cast(RouteSharingPolicy::LongFormAudio) == static_cast(AVAudioSessionRouteSharingPolicyLongFormAudio), "RouteSharingPolicy::LongFormAudio is not AVAudioSessionRouteSharingPolicyLongFormAudio as expected"); static_assert(static_cast(RouteSharingPolicy::LongFormVideo) == static_cast(AVAudioSessionRouteSharingPolicyLongFormVideo), "RouteSharingPolicy::LongFormVideo is not AVAudioSessionRouteSharingPolicyLongFormVideo as expected"); #else ALLOW_DEPRECATED_DECLARATIONS_BEGIN static_assert(static_cast(RouteSharingPolicy::LongFormAudio) == static_cast(AVAudioSessionRouteSharingPolicyLongForm), "RouteSharingPolicy::LongFormAudio is not AVAudioSessionRouteSharingPolicyLongForm as expected"); ALLOW_DEPRECATED_DECLARATIONS_END #endif static_assert(static_cast(RouteSharingPolicy::Independent) == static_cast(AVAudioSessionRouteSharingPolicyIndependent), "RouteSharingPolicy::Independent is not AVAudioSessionRouteSharingPolicyIndependent as expected"); AVAudioSessionRouteSharingPolicy policy = [[PAL::getAVAudioSessionClass() sharedInstance] routeSharingPolicy]; ASSERT(static_cast(policy) <= RouteSharingPolicy::LongFormVideo); return static_cast(policy); } String AudioSessionIOS::routingContextUID() const { #if !PLATFORM(IOS_FAMILY_SIMULATOR) && !PLATFORM(MACCATALYST) && !PLATFORM(WATCHOS) return [[PAL::getAVAudioSessionClass() sharedInstance] routingContextUID]; #else return emptyString(); #endif } float AudioSessionIOS::sampleRate() const { return [[PAL::getAVAudioSessionClass() sharedInstance] sampleRate]; } size_t AudioSessionIOS::bufferSize() const { return [[PAL::getAVAudioSessionClass() sharedInstance] IOBufferDuration] * sampleRate(); } size_t AudioSessionIOS::numberOfOutputChannels() const { return [[PAL::getAVAudioSessionClass() sharedInstance] outputNumberOfChannels]; } size_t AudioSessionIOS::maximumNumberOfOutputChannels() const { return [[PAL::getAVAudioSessionClass() sharedInstance] maximumOutputNumberOfChannels]; } bool AudioSessionIOS::tryToSetActiveInternal(bool active) { // We need to deactivate the session on another queue because the AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation option // means that AVAudioSession may synchronously unduck previously ducked clients. Activation needs to complete before this method // returns, so do it synchronously on the same serial queue. if (active) { bool success = false; m_workQueue->dispatchSync([&success] { NSError *error = nil; setEligibleForSmartRouting(true); [[PAL::getAVAudioSessionClass() sharedInstance] setActive:YES withOptions:0 error:&error]; success = !error; }); return success; } m_workQueue->dispatch([] { NSError *error = nil; [[PAL::getAVAudioSessionClass() sharedInstance] setActive:NO withOptions:0 error:&error]; setEligibleForSmartRouting(false); }); return true; } size_t AudioSessionIOS::preferredBufferSize() const { return [[PAL::getAVAudioSessionClass() sharedInstance] preferredIOBufferDuration] * sampleRate(); } void AudioSessionIOS::setPreferredBufferSize(size_t bufferSize) { NSError *error = nil; float duration = bufferSize / sampleRate(); [[PAL::getAVAudioSessionClass() sharedInstance] setPreferredIOBufferDuration:duration error:&error]; RELEASE_LOG_ERROR_IF(error, Media, "failed to set preferred buffer duration to %f with error: %@", duration, error.localizedDescription); ASSERT(!error); } bool AudioSessionIOS::isMuted() const { return false; } void AudioSessionIOS::handleMutedStateChange() { } } #endif // USE(AUDIO_SESSION) && PLATFORM(IOS_FAMILY)