/* * Copyright (C) 2017-2018 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 "AVAudioSessionCaptureDeviceManager.h" #if ENABLE(MEDIA_STREAM) && PLATFORM(IOS_FAMILY) #import "AVAudioSessionCaptureDevice.h" #import "Logging.h" #import "RealtimeMediaSourceCenter.h" #import #import #import #import #import #import #import @interface WebAVAudioSessionAvailableInputsListener : NSObject { WebCore::AVAudioSessionCaptureDeviceManager* _callback; } @end @implementation WebAVAudioSessionAvailableInputsListener - (id)initWithCallback:(WebCore::AVAudioSessionCaptureDeviceManager *)callback audioSession:(AVAudioSession *)session { self = [super init]; if (!self) return nil; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeDidChange:) name:PAL::get_AVFoundation_AVAudioSessionRouteChangeNotification() object:session]; _callback = callback; return self; } - (void)invalidate { _callback = nullptr; [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)routeDidChange:(NSNotification *)notification { if (!_callback) return; callOnWebThreadOrDispatchAsyncOnMainThread([protectedSelf = retainPtr(self)]() mutable { if (auto* callback = protectedSelf->_callback) callback->scheduleUpdateCaptureDevices(); }); } @end namespace WebCore { AVAudioSessionCaptureDeviceManager& AVAudioSessionCaptureDeviceManager::singleton() { static NeverDestroyed manager; return manager; } AVAudioSessionCaptureDeviceManager::AVAudioSessionCaptureDeviceManager() : m_dispatchQueue(WorkQueue::create("com.apple.WebKit.AVAudioSessionCaptureDeviceManager")) { m_dispatchQueue->dispatch([this] { createAudioSession(); }); } void AVAudioSessionCaptureDeviceManager::createAudioSession() { #if !PLATFORM(MACCATALYST) m_audioSession = adoptNS([[PAL::getAVAudioSessionClass() alloc] initAuxiliarySession]); #else // FIXME: Figure out if this is correct for Catalyst, where auxiliary session isn't available. m_audioSession = [PAL::getAVAudioSessionClass() sharedInstance]; #endif NSError *error = nil; auto options = AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionMixWithOthers; [m_audioSession setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeDefault options:options error:&error]; RELEASE_LOG_ERROR_IF(error, WebRTC, "Failed to set audio session category with error: %@.", error.localizedDescription); } AVAudioSessionCaptureDeviceManager::~AVAudioSessionCaptureDeviceManager() { [m_listener invalidate]; m_listener = nullptr; } const Vector& AVAudioSessionCaptureDeviceManager::captureDevices() { if (!m_devices) refreshAudioCaptureDevices(); return m_devices.value(); } std::optional AVAudioSessionCaptureDeviceManager::captureDeviceWithPersistentID(CaptureDevice::DeviceType type, const String& deviceID) { ASSERT_UNUSED(type, type == CaptureDevice::DeviceType::Microphone); for (auto& device : captureDevices()) { if (device.persistentId() == deviceID) return device; } return std::nullopt; } std::optional AVAudioSessionCaptureDeviceManager::audioSessionDeviceWithUID(const String& deviceID) { if (!m_audioSessionCaptureDevices) refreshAudioCaptureDevices(); for (auto& device : *m_audioSessionCaptureDevices) { if (device.persistentId() == deviceID) return device; } return std::nullopt; } void AVAudioSessionCaptureDeviceManager::setPreferredAudioSessionDeviceUID(const String& deviceUID) { if (setPreferredAudioSessionDeviceUIDInternal(deviceUID)) m_preferredAudioDeviceUID = deviceUID; } void AVAudioSessionCaptureDeviceManager::configurePreferredAudioCaptureDevice() { ASSERT(!m_preferredAudioDeviceUID.isEmpty()); if (!m_preferredAudioDeviceUID.isEmpty()) setPreferredAudioSessionDeviceUIDInternal(m_preferredAudioDeviceUID); } bool AVAudioSessionCaptureDeviceManager::setPreferredAudioSessionDeviceUIDInternal(const String& deviceUID) { AVAudioSessionPortDescription *preferredPort = nil; NSString *nsDeviceUID = deviceUID; for (AVAudioSessionPortDescription *portDescription in [m_audioSession availableInputs]) { if ([portDescription.UID isEqualToString:nsDeviceUID]) { preferredPort = portDescription; break; } } if (!preferredPort) { RELEASE_LOG_ERROR(WebRTC, "failed to find preferred input '%{public}s'", deviceUID.ascii().data()); return false; } NSError *error = nil; if (![[PAL::getAVAudioSessionClass() sharedInstance] setPreferredInput:preferredPort error:&error]) { RELEASE_LOG_ERROR(WebRTC, "failed to set preferred input to '%{public}s' with error: %@", deviceUID.ascii().data(), error.localizedDescription); return false; } return true; } void AVAudioSessionCaptureDeviceManager::scheduleUpdateCaptureDevices() { computeCaptureDevices([] { }); } void AVAudioSessionCaptureDeviceManager::refreshAudioCaptureDevices() { if (m_audioSessionState == AudioSessionState::Inactive) { m_audioSessionState = AudioSessionState::Active; m_dispatchQueue->dispatchSync([this] { activateAudioSession(); }); } Vector newAudioDevices; m_dispatchQueue->dispatchSync([&] { newAudioDevices = retrieveAudioSessionCaptureDevices(); }); setAudioCaptureDevices(crossThreadCopy(WTFMove(newAudioDevices))); } void AVAudioSessionCaptureDeviceManager::computeCaptureDevices(CompletionHandler&& completion) { if (m_audioSessionState == AudioSessionState::Inactive) { m_audioSessionState = AudioSessionState::Active; m_dispatchQueue->dispatch([this] { activateAudioSession(); }); } m_dispatchQueue->dispatch([this, completion = WTFMove(completion)] () mutable { auto newAudioDevices = retrieveAudioSessionCaptureDevices(); callOnWebThreadOrDispatchAsyncOnMainThread(makeBlockPtr([this, completion = WTFMove(completion), newAudioDevices = crossThreadCopy(WTFMove(newAudioDevices))] () mutable { setAudioCaptureDevices(WTFMove(newAudioDevices)); completion(); }).get()); }); } void AVAudioSessionCaptureDeviceManager::activateAudioSession() { if (!m_listener) m_listener = adoptNS([[WebAVAudioSessionAvailableInputsListener alloc] initWithCallback:this audioSession:m_audioSession.get()]); NSError *error = nil; [m_audioSession setActive:YES withOptions:0 error:&error]; if (error) RELEASE_LOG_ERROR(WebRTC, "Failed to activate audio session with error: %@.", error.localizedDescription); } Vector AVAudioSessionCaptureDeviceManager::retrieveAudioSessionCaptureDevices() const { auto *defaultInput = [m_audioSession currentRoute].inputs.firstObject; if (!defaultInput) { RELEASE_LOG_INFO(WebRTC, "AVAudioSessionCaptureDeviceManager using previous default input"); defaultInput = m_lastDefaultMicrophone.get(); } else m_lastDefaultMicrophone = defaultInput; auto availableInputs = [m_audioSession availableInputs]; Vector newAudioDevices; newAudioDevices.reserveInitialCapacity(availableInputs.count); for (AVAudioSessionPortDescription *portDescription in availableInputs) newAudioDevices.uncheckedAppend(AVAudioSessionCaptureDevice::create(portDescription, defaultInput)); return newAudioDevices; } void AVAudioSessionCaptureDeviceManager::setAudioCaptureDevices(Vector&& newAudioDevices) { bool firstTime = !m_devices; bool deviceListChanged = newAudioDevices.size() != m_devices->size(); bool defaultDeviceChanged = false; if (!deviceListChanged && !firstTime) { for (auto& newState : newAudioDevices) { std::optional oldState; for (const auto& device : m_devices.value()) { if (device.type() == newState.type() && device.persistentId() == newState.persistentId()) { oldState = device; break; } } if (!oldState.has_value()) { deviceListChanged = true; break; } if (newState.isDefault() != oldState.value().isDefault()) defaultDeviceChanged = true; if (newState.enabled() != oldState.value().enabled()) { deviceListChanged = true; break; } } } if (!deviceListChanged && !firstTime && !defaultDeviceChanged) return; auto newDevices = copyToVectorOf(newAudioDevices); m_audioSessionCaptureDevices = WTFMove(newAudioDevices); std::sort(newDevices.begin(), newDevices.end(), [] (auto& first, auto& second) -> bool { return first.isDefault() && !second.isDefault(); }); m_devices = WTFMove(newDevices); if (deviceListChanged && !firstTime) deviceChanged(); } void AVAudioSessionCaptureDeviceManager::enableAllDevicesQuery() { if (m_audioSessionState != AudioSessionState::NotNeeded) return; m_audioSessionState = AudioSessionState::Inactive; refreshAudioCaptureDevices(); } void AVAudioSessionCaptureDeviceManager::disableAllDevicesQuery() { if (m_audioSessionState == AudioSessionState::NotNeeded) return; if (m_audioSessionState == AudioSessionState::Active) { m_dispatchQueue->dispatch([this] { if (m_audioSessionState != AudioSessionState::NotNeeded) return; NSError *error = nil; [m_audioSession setActive:NO withOptions:0 error:&error]; if (error) RELEASE_LOG_ERROR(WebRTC, "Failed to disactivate audio session with error: %@.", error.localizedDescription); }); } m_audioSessionState = AudioSessionState::NotNeeded; } } // namespace WebCore #endif // ENABLE(MEDIA_STREAM) && PLATFORM(IOS_FAMILY)