| /* |
| * 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. ``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 |
| * 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. |
| */ |
| |
| #include "config.h" |
| #include "CoreAudioCaptureDeviceManager.h" |
| |
| #if ENABLE(MEDIA_STREAM) && PLATFORM(MAC) |
| |
| #include "CoreAudioCaptureDevice.h" |
| #include "CoreAudioCaptureSource.h" |
| #include "Logging.h" |
| #include "RealtimeMediaSourceCenter.h" |
| #include <AudioUnit/AudioUnit.h> |
| #include <CoreMedia/CMSync.h> |
| #include <pal/spi/cf/CoreAudioSPI.h> |
| #include <wtf/Assertions.h> |
| #include <wtf/MainThread.h> |
| #include <wtf/NeverDestroyed.h> |
| |
| #import <pal/cf/CoreMediaSoftLink.h> |
| |
| namespace WebCore { |
| |
| CoreAudioCaptureDeviceManager& CoreAudioCaptureDeviceManager::singleton() |
| { |
| static NeverDestroyed<CoreAudioCaptureDeviceManager> manager; |
| return manager; |
| } |
| |
| const Vector<CaptureDevice>& CoreAudioCaptureDeviceManager::captureDevices() |
| { |
| coreAudioCaptureDevices(); |
| return m_captureDevices; |
| } |
| |
| std::optional<CaptureDevice> CoreAudioCaptureDeviceManager::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; |
| } |
| |
| static bool deviceHasStreams(AudioObjectID deviceID, const AudioObjectPropertyAddress& address) |
| { |
| UInt32 dataSize = 0; |
| auto err = AudioObjectGetPropertyDataSize(deviceID, &address, 0, nullptr, &dataSize); |
| if (err || !dataSize) |
| return false; |
| |
| auto bufferList = std::unique_ptr<AudioBufferList>((AudioBufferList*) ::operator new (dataSize)); |
| memset(bufferList.get(), 0, dataSize); |
| err = AudioObjectGetPropertyData(deviceID, &address, 0, nullptr, &dataSize, bufferList.get()); |
| |
| return !err && bufferList->mNumberBuffers; |
| } |
| |
| static bool deviceHasInputStreams(AudioObjectID deviceID) |
| { |
| AudioObjectPropertyAddress address = { |
| kAudioDevicePropertyStreamConfiguration, |
| kAudioDevicePropertyScopeInput, |
| #if HAVE(AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN) |
| kAudioObjectPropertyElementMain |
| #else |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| kAudioObjectPropertyElementMaster |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| #endif |
| }; |
| return deviceHasStreams(deviceID, address); |
| } |
| |
| static bool deviceHasOutputStreams(AudioObjectID deviceID) |
| { |
| AudioObjectPropertyAddress address = { |
| kAudioDevicePropertyStreamConfiguration, |
| kAudioDevicePropertyScopeOutput, |
| #if HAVE(AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN) |
| kAudioObjectPropertyElementMain |
| #else |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| kAudioObjectPropertyElementMaster |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| #endif |
| }; |
| return deviceHasStreams(deviceID, address); |
| } |
| |
| static bool isValidCaptureDevice(const CoreAudioCaptureDevice& device, bool filterTapEnabledDevices) |
| { |
| if (filterTapEnabledDevices) { |
| // Ignore output devices that have input only for echo cancellation. |
| AudioObjectPropertyAddress address = { |
| kAudioDevicePropertyTapEnabled, |
| kAudioDevicePropertyScopeOutput, |
| #if HAVE(AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN) |
| kAudioObjectPropertyElementMain |
| #else |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| kAudioObjectPropertyElementMaster |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| #endif |
| }; |
| if (AudioObjectHasProperty(device.deviceID(), &address)) { |
| RELEASE_LOG(WebRTC, "Ignoring output device that have input only for echo cancellation"); |
| return false; |
| } |
| } |
| |
| // Ignore non-aggregable devices. |
| UInt32 dataSize = 0; |
| AudioObjectPropertyAddress address = { |
| kAudioObjectPropertyCreator, |
| kAudioObjectPropertyScopeGlobal, |
| #if HAVE(AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN) |
| kAudioObjectPropertyElementMain |
| #else |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| kAudioObjectPropertyElementMaster |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| #endif |
| }; |
| CFStringRef name = nullptr; |
| dataSize = sizeof(name); |
| AudioObjectGetPropertyData(device.deviceID(), &address, 0, nullptr, &dataSize, &name); |
| bool isNonAggregable = !name || !String(name).startsWith("com.apple.audio.CoreAudio"); |
| if (name) |
| CFRelease(name); |
| if (isNonAggregable) { |
| RELEASE_LOG(WebRTC, "Ignoring output device that is non aggregable"); |
| return false; |
| } |
| |
| // Ignore unnamed devices and aggregate devices created by VPIO. |
| if (device.label().isEmpty()) { |
| RELEASE_LOG(WebRTC, "Ignoring output device that is unnamed"); |
| return false; |
| } |
| |
| if (device.label().startsWith("VPAUAggregateAudioDevice")) { |
| RELEASE_LOG(WebRTC, "Ignoring output VPAUAggregateAudioDevice device"); |
| return false; |
| } |
| |
| if (device.label().contains("WebexMediaAudioDevice")) { |
| RELEASE_LOG(WebRTC, "Ignoring webex audio device"); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void CoreAudioCaptureDeviceManager::scheduleUpdateCaptureDevices() |
| { |
| if (m_wasRefreshAudioCaptureDevicesScheduled) |
| return; |
| |
| m_wasRefreshAudioCaptureDevicesScheduled = true; |
| callOnMainThread([this] { |
| refreshAudioCaptureDevices(NotifyIfDevicesHaveChanged::Notify); |
| m_wasRefreshAudioCaptureDevicesScheduled = false; |
| }); |
| } |
| |
| Vector<CoreAudioCaptureDevice>& CoreAudioCaptureDeviceManager::coreAudioCaptureDevices() |
| { |
| static bool initialized; |
| if (!initialized) { |
| initialized = true; |
| refreshAudioCaptureDevices(NotifyIfDevicesHaveChanged::DoNotNotify); |
| |
| auto listener = ^(UInt32 count, const AudioObjectPropertyAddress properties[]) { |
| bool notify = false; |
| for (UInt32 i = 0; i < count; ++i) |
| notify |= (properties[i].mSelector == kAudioHardwarePropertyDevices || properties[i].mSelector == kAudioHardwarePropertyDefaultInputDevice || properties[i].mSelector == kAudioHardwarePropertyDefaultOutputDevice); |
| |
| if (notify) |
| CoreAudioCaptureDeviceManager::singleton().scheduleUpdateCaptureDevices(); |
| }; |
| |
| AudioObjectPropertyAddress address = { |
| kAudioHardwarePropertyDevices, |
| kAudioObjectPropertyScopeGlobal, |
| #if HAVE(AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN) |
| kAudioObjectPropertyElementMain |
| #else |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| kAudioObjectPropertyElementMaster |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| #endif |
| }; |
| auto err = AudioObjectAddPropertyListenerBlock(kAudioObjectSystemObject, &address, dispatch_get_main_queue(), listener); |
| if (err) |
| LOG_ERROR("CoreAudioCaptureDeviceManager::devices(%p) AudioObjectAddPropertyListener for kAudioHardwarePropertyDevices returned error %d (%.4s)", this, (int)err, (char*)&err); |
| |
| address = { |
| kAudioHardwarePropertyDefaultInputDevice, |
| kAudioObjectPropertyScopeGlobal, |
| #if HAVE(AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN) |
| kAudioObjectPropertyElementMain |
| #else |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| kAudioObjectPropertyElementMaster |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| #endif |
| }; |
| err = AudioObjectAddPropertyListenerBlock(kAudioObjectSystemObject, &address, dispatch_get_main_queue(), listener); |
| if (err) |
| LOG_ERROR("CoreAudioCaptureDeviceManager::devices(%p) AudioObjectAddPropertyListener for kAudioHardwarePropertyDefaultInputDevice returned error %d (%.4s)", this, (int)err, (char*)&err); |
| |
| address = { |
| kAudioHardwarePropertyDefaultOutputDevice, |
| kAudioObjectPropertyScopeGlobal, |
| #if HAVE(AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN) |
| kAudioObjectPropertyElementMain |
| #else |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| kAudioObjectPropertyElementMaster |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| #endif |
| }; |
| err = AudioObjectAddPropertyListenerBlock(kAudioObjectSystemObject, &address, dispatch_get_main_queue(), listener); |
| if (err) |
| LOG_ERROR("CoreAudioCaptureDeviceManager::devices(%p) AudioObjectAddPropertyListener for kAudioHardwarePropertyDefaultOutputDevice returned error %d (%.4s)", this, (int)err, (char*)&err); |
| } |
| |
| return m_coreAudioCaptureDevices; |
| } |
| |
| std::optional<CoreAudioCaptureDevice> CoreAudioCaptureDeviceManager::coreAudioDeviceWithUID(const String& deviceID) |
| { |
| for (auto& device : coreAudioCaptureDevices()) { |
| if (device.persistentId() == deviceID && device.enabled()) |
| return device; |
| } |
| return std::nullopt; |
| } |
| |
| static inline bool hasDevice(const Vector<CoreAudioCaptureDevice>& devices, uint32_t deviceID, CaptureDevice::DeviceType deviceType) |
| { |
| return std::any_of(devices.begin(), devices.end(), [&deviceID, deviceType](auto& device) { |
| return device.deviceID() == deviceID && device.type() == deviceType; |
| }); |
| } |
| |
| static inline Vector<CoreAudioCaptureDevice> computeAudioDeviceList(bool filterTapEnabledDevices) |
| { |
| AudioObjectPropertyAddress address = { |
| kAudioHardwarePropertyDevices, |
| kAudioObjectPropertyScopeGlobal, |
| #if HAVE(AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN) |
| kAudioObjectPropertyElementMain |
| #else |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| kAudioObjectPropertyElementMaster |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| #endif |
| }; |
| UInt32 dataSize = 0; |
| auto err = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &address, 0, nullptr, &dataSize); |
| if (err) { |
| RELEASE_LOG(WebRTC, "computeAudioDeviceList failed to get size of device list %d (%.4s)", (int)err, (char*)&err); |
| return { }; |
| } |
| |
| size_t deviceCount = dataSize / sizeof(AudioObjectID); |
| Vector<AudioObjectID> deviceIDs(deviceCount); |
| err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &address, 0, nullptr, &dataSize, deviceIDs.data()); |
| if (err) { |
| RELEASE_LOG(WebRTC, "computeAudioDeviceList failed to get device list %d (%.4s)", (int)err, (char*)&err); |
| return { }; |
| } |
| |
| Vector<CoreAudioCaptureDevice> audioDevices; |
| |
| // Microphones |
| for (size_t i = 0; i < deviceCount; i++) { |
| AudioObjectID deviceID = deviceIDs[i]; |
| |
| if (!deviceHasInputStreams(deviceID) || hasDevice(audioDevices, deviceID, CaptureDevice::DeviceType::Microphone)) |
| continue; |
| |
| auto microphoneDevice = CoreAudioCaptureDevice::create(deviceID, CaptureDevice::DeviceType::Microphone, { }); |
| if (microphoneDevice && isValidCaptureDevice(microphoneDevice.value(), filterTapEnabledDevices)) |
| audioDevices.append(WTFMove(microphoneDevice.value())); |
| } |
| |
| // Speakers |
| for (size_t i = 0; i < deviceCount; i++) { |
| AudioObjectID deviceID = deviceIDs[i]; |
| |
| if (!deviceHasOutputStreams(deviceID) || hasDevice(audioDevices, deviceID, CaptureDevice::DeviceType::Speaker)) |
| continue; |
| |
| String groupID; |
| for (auto relatedDeviceID : CoreAudioCaptureDevice::relatedAudioDeviceIDs(deviceID)) { |
| for (auto& device : audioDevices) { |
| if (device.deviceID() == relatedDeviceID && device.type() == CaptureDevice::DeviceType::Microphone) { |
| groupID = device.persistentId(); |
| break; |
| } |
| } |
| } |
| |
| auto device = CoreAudioCaptureDevice::create(deviceID, CaptureDevice::DeviceType::Speaker, groupID); |
| if (device) { |
| // If there is no groupID, relate devices if the label is matching. |
| if (groupID.isNull()) { |
| for (auto& existingDevice : audioDevices) { |
| if (existingDevice.label() == device->label() && existingDevice.type() == CaptureDevice::DeviceType::Microphone) { |
| device->setGroupId(existingDevice.persistentId()); |
| break; |
| } |
| } |
| } |
| audioDevices.append(WTFMove(device.value())); |
| } |
| } |
| return audioDevices; |
| } |
| |
| void CoreAudioCaptureDeviceManager::refreshAudioCaptureDevices(NotifyIfDevicesHaveChanged notify) |
| { |
| ASSERT(isMainThread()); |
| |
| auto audioDevices = computeAudioDeviceList(m_filterTapEnabledDevices); |
| bool haveDeviceChanges = audioDevices.size() != m_coreAudioCaptureDevices.size(); |
| if (!haveDeviceChanges) { |
| for (size_t cptr = 0; cptr < audioDevices.size(); ++cptr) { |
| auto& oldDevice = m_coreAudioCaptureDevices[cptr]; |
| auto& newDevice = audioDevices[cptr]; |
| if (newDevice.type() != oldDevice.type() || newDevice.deviceID() != oldDevice.deviceID() || newDevice.isDefault() != oldDevice.isDefault() || newDevice.enabled() != oldDevice.enabled() || newDevice.isDefault() != oldDevice.isDefault()) |
| haveDeviceChanges = true; |
| } |
| } |
| if (!haveDeviceChanges) |
| return; |
| |
| std::sort(audioDevices.begin(), audioDevices.end(), [] (auto& first, auto& second) -> bool { |
| return first.isDefault() && !second.isDefault(); |
| }); |
| m_coreAudioCaptureDevices = WTFMove(audioDevices); |
| |
| m_captureDevices.clear(); |
| m_speakerDevices.clear(); |
| for (auto& device : m_coreAudioCaptureDevices) { |
| if (device.type() == CaptureDevice::DeviceType::Microphone) |
| m_captureDevices.append(device); |
| else |
| m_speakerDevices.append(device); |
| } |
| |
| if (notify == NotifyIfDevicesHaveChanged::Notify) { |
| deviceChanged(); |
| CoreAudioCaptureSourceFactory::singleton().devicesChanged(m_captureDevices); |
| } |
| } |
| |
| } // namespace WebCore |
| |
| #endif // ENABLE(MEDIA_STREAM) && PLATFORM(MAC) |