/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AudioDeviceInfo.h" #include "CubebDeviceEnumerator.h" #include "MockCubeb.h" #include "gmock/gmock.h" #include "gtest/gtest-printers.h" #include "gtest/gtest.h" #include "mozilla/media/MediaUtils.h" #include "nsTArray.h" using namespace mozilla; using AudioDeviceSet = CubebDeviceEnumerator::AudioDeviceSet; using ::testing::ElementsAre; using ::testing::Eq; using ::testing::Pointwise; // Custom matcher for comparing AudioDeviceInfo by device ID MATCHER(DeviceIdEq, "") { const auto& [actual, expected_devid] = arg; return actual->DeviceID() == expected_devid; } // Custom matcher for checking device preferences MATCHER(PreferredEq, "") { const auto& [actual, expected_pref] = arg; uint16_t pref; nsresult rv = actual->GetPreferred(&pref); return NS_SUCCEEDED(rv) && pref == expected_pref; } const bool DEBUG_PRINTS = false; enum DeviceOperation { ADD, REMOVE }; void TestEnumeration(MockCubeb* aMock, uint32_t aExpectedDeviceCount, DeviceOperation aOperation, cubeb_device_type aType) { RefPtr enumerator = CubebDeviceEnumerator::GetInstance(); RefPtr devices; if (aType == CUBEB_DEVICE_TYPE_INPUT) { devices = enumerator->EnumerateAudioInputDevices(); } if (aType == CUBEB_DEVICE_TYPE_OUTPUT) { devices = enumerator->EnumerateAudioOutputDevices(); } EXPECT_EQ(devices->Length(), aExpectedDeviceCount) << "Device count is correct when enumerating"; if (DEBUG_PRINTS) { for (const auto& deviceInfo : *devices) { printf("=== Before removal\n"); PrintDevice(deviceInfo); } } if (aOperation == DeviceOperation::REMOVE) { aMock->RemoveDevice(reinterpret_cast(1)); } else { aMock->AddDevice(DeviceTemplate(reinterpret_cast(123), aType)); } if (aType == CUBEB_DEVICE_TYPE_INPUT) { devices = enumerator->EnumerateAudioInputDevices(); } if (aType == CUBEB_DEVICE_TYPE_OUTPUT) { devices = enumerator->EnumerateAudioOutputDevices(); } uint32_t newExpectedDeviceCount = aOperation == DeviceOperation::REMOVE ? aExpectedDeviceCount - 1 : aExpectedDeviceCount + 1; EXPECT_EQ(devices->Length(), newExpectedDeviceCount) << "Device count is correct when enumerating after operation"; if (DEBUG_PRINTS) { for (const auto& deviceInfo : *devices) { printf("=== After removal\n"); PrintDevice(deviceInfo); } } } #ifndef ANDROID TEST(CubebDeviceEnumerator, EnumerateSimple) { // It looks like we're leaking this object, but in fact it will be owned by // CubebUtils: `cubeb_destroy()` is called when `ForceSetCubebContext()` is // called again or when layout statics are shutdown, and we cast back to a // MockCubeb* and call the dtor. MockCubeb* mock = new MockCubeb(); mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); // We want to test whether CubebDeviceEnumerator works with and without a // backend that can notify of a device collection change via callback. // Additionally, we're testing that both adding and removing a device // invalidates the list correctly. bool supportsDeviceChangeCallback[2] = {true, false}; DeviceOperation operations[2] = {DeviceOperation::ADD, DeviceOperation::REMOVE}; for (bool supports : supportsDeviceChangeCallback) { // Shutdown for `supports` to take effect CubebDeviceEnumerator::Shutdown(); mock->SetSupportDeviceChangeCallback(supports); for (DeviceOperation op : operations) { uint32_t device_count = 4; cubeb_device_type deviceType = CUBEB_DEVICE_TYPE_INPUT; AddDevices(mock, device_count, deviceType); TestEnumeration(mock, device_count, op, deviceType); deviceType = CUBEB_DEVICE_TYPE_OUTPUT; AddDevices(mock, device_count, deviceType); TestEnumeration(mock, device_count, op, deviceType); } } // Shutdown to clean up the last `supports` effect CubebDeviceEnumerator::Shutdown(); } TEST(CubebDeviceEnumerator, ZeroChannelDevices) { MockCubeb* mock = new MockCubeb(); mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); // Create devices with different channel count, including 0-channel cubeb_device_info dev1 = DeviceTemplate(reinterpret_cast(1), CUBEB_DEVICE_TYPE_INPUT, "dev 1"); dev1.max_channels = 1; mock->AddDevice(dev1); cubeb_device_info dev2 = DeviceTemplate(reinterpret_cast(2), CUBEB_DEVICE_TYPE_INPUT, "dev 2"); dev2.max_channels = 0; mock->AddDevice(dev2); cubeb_device_info dev3 = DeviceTemplate(reinterpret_cast(3), CUBEB_DEVICE_TYPE_OUTPUT, "dev 3"); dev3.max_channels = 2; mock->AddDevice(dev3); cubeb_device_info dev4 = DeviceTemplate(reinterpret_cast(4), CUBEB_DEVICE_TYPE_OUTPUT, "dev 4"); dev4.max_channels = 0; mock->AddDevice(dev4); // Make sure the devices are added to cubeb. cubeb_device_collection inputCollection = {nullptr, 0}; mock->EnumerateDevices(CUBEB_DEVICE_TYPE_INPUT, &inputCollection); EXPECT_EQ(inputCollection.count, 2U); EXPECT_EQ(inputCollection.device[0].devid, dev1.devid); EXPECT_EQ(inputCollection.device[1].devid, dev2.devid); mock->DestroyDeviceCollection(&inputCollection); EXPECT_EQ(inputCollection.count, 0U); cubeb_device_collection outputCollection = {nullptr, 0}; mock->EnumerateDevices(CUBEB_DEVICE_TYPE_OUTPUT, &outputCollection); EXPECT_EQ(outputCollection.count, 2U); EXPECT_EQ(outputCollection.device[0].devid, dev3.devid); EXPECT_EQ(outputCollection.device[1].devid, dev4.devid); mock->DestroyDeviceCollection(&outputCollection); EXPECT_EQ(outputCollection.count, 0U); // Enumerate the devices. The result should exclude the 0-channel devices. RefPtr enumerator = CubebDeviceEnumerator::GetInstance(); RefPtr inputDevices = enumerator->EnumerateAudioInputDevices(); EXPECT_EQ(inputDevices->Length(), 1U); EXPECT_EQ(inputDevices->ElementAt(0)->DeviceID(), dev1.devid); EXPECT_EQ(inputDevices->ElementAt(0)->MaxChannels(), dev1.max_channels); RefPtr outputDevices = enumerator->EnumerateAudioOutputDevices(); EXPECT_EQ(outputDevices->Length(), 1U); EXPECT_EQ(outputDevices->ElementAt(0)->DeviceID(), dev3.devid); EXPECT_EQ(outputDevices->ElementAt(0)->MaxChannels(), dev3.max_channels); } #else // building for Android, which has no device enumeration support TEST(CubebDeviceEnumerator, EnumerateAndroid) { MockCubeb* mock = new MockCubeb(); mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); RefPtr enumerator = CubebDeviceEnumerator::GetInstance(); RefPtr inputDevices = enumerator->EnumerateAudioInputDevices(); EXPECT_EQ(inputDevices->Length(), 1u) << "Android always exposes a single input device."; EXPECT_EQ((*inputDevices)[0]->MaxChannels(), 1u) << "With a single channel."; EXPECT_EQ((*inputDevices)[0]->DeviceID(), nullptr) << "It's always the default input device."; EXPECT_TRUE((*inputDevices)[0]->Preferred()) << "it's always the prefered input device."; RefPtr outputDevices = enumerator->EnumerateAudioOutputDevices(); EXPECT_EQ(outputDevices->Length(), 1u) << "Android always exposes a single output device."; EXPECT_EQ((*outputDevices)[0]->MaxChannels(), 2u) << "With stereo channels."; EXPECT_EQ((*outputDevices)[0]->DeviceID(), nullptr) << "It's always the default output device."; EXPECT_TRUE((*outputDevices)[0]->Preferred()) << "it's always the prefered output device."; } #endif TEST(CubebDeviceEnumerator, ForceNullCubebContext) { mozilla::CubebUtils::ForceSetCubebContext(nullptr); RefPtr enumerator = CubebDeviceEnumerator::GetInstance(); RefPtr inputDevices = enumerator->EnumerateAudioInputDevices(); EXPECT_EQ(inputDevices->Length(), 0u) << "Enumeration must fail, input device list must be empty."; RefPtr outputDevices = enumerator->EnumerateAudioOutputDevices(); EXPECT_EQ(outputDevices->Length(), 0u) << "Enumeration must fail, output device list must be empty."; // Shutdown to clean up the null context effect CubebDeviceEnumerator::Shutdown(); } TEST(CubebDeviceEnumerator, DeviceInfoFromName) { MockCubeb* mock = new MockCubeb(); mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); cubeb_device_type deviceTypes[2] = {CUBEB_DEVICE_TYPE_INPUT, CUBEB_DEVICE_TYPE_OUTPUT}; bool supportsDeviceChangeCallback[2] = {true, false}; for (bool supports : supportsDeviceChangeCallback) { // Shutdown for `supports` to take effect CubebDeviceEnumerator::Shutdown(); mock->SetSupportDeviceChangeCallback(supports); for (cubeb_device_type& deviceType : deviceTypes) { cubeb_devid id_1 = reinterpret_cast(1); mock->AddDevice(DeviceTemplate(id_1, deviceType, "device name 1")); cubeb_devid id_2 = reinterpret_cast(2); nsCString device_name = "device name 2"_ns; mock->AddDevice(DeviceTemplate(id_2, deviceType, device_name.get())); cubeb_devid id_3 = reinterpret_cast(3); mock->AddDevice(DeviceTemplate(id_3, deviceType, "device name 3")); RefPtr enumerator = CubebDeviceEnumerator::GetInstance(); EnumeratorSide side = (deviceType == CUBEB_DEVICE_TYPE_INPUT) ? EnumeratorSide::INPUT : EnumeratorSide::OUTPUT; RefPtr devInfo = enumerator->DeviceInfoFromName( NS_ConvertUTF8toUTF16(device_name), side); EXPECT_TRUE(devInfo) << "the device exist"; EXPECT_EQ(devInfo->Name(), NS_ConvertUTF8toUTF16(device_name)) << "verify the device"; mock->RemoveDevice(id_2); devInfo = enumerator->DeviceInfoFromName( NS_ConvertUTF8toUTF16(device_name), side); EXPECT_FALSE(devInfo) << "the device does not exist any more"; } } // Shutdown for `supports` to take effect CubebDeviceEnumerator::Shutdown(); } TEST(CubebDeviceEnumerator, PreferredDeviceFirst) { MockCubeb* mock = new MockCubeb(); mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); // Add multiple devices where the preferred one is not first cubeb_device_info dev1 = DeviceTemplate(reinterpret_cast(1), CUBEB_DEVICE_TYPE_INPUT, "dev 1"); dev1.preferred = CUBEB_DEVICE_PREF_NONE; mock->AddDevice(dev1); cubeb_device_info dev2 = DeviceTemplate(reinterpret_cast(2), CUBEB_DEVICE_TYPE_INPUT, "dev 2"); dev2.preferred = CUBEB_DEVICE_PREF_NONE; mock->AddDevice(dev2); cubeb_device_info dev3 = DeviceTemplate(reinterpret_cast(3), CUBEB_DEVICE_TYPE_INPUT, "dev 3"); dev3.preferred = CUBEB_DEVICE_PREF_ALL; mock->AddDevice(dev3); RefPtr enumerator = CubebDeviceEnumerator::GetInstance(); RefPtr inputDevices = enumerator->EnumerateAudioInputDevices(); EXPECT_EQ(inputDevices->Length(), 3U) << "Should have 3 devices"; EXPECT_TRUE((*inputDevices)[0]->Preferred()) << "First device should be the default device"; EXPECT_EQ((*inputDevices)[0]->DeviceID(), dev3.devid) << "First device should be dev3"; EXPECT_FALSE((*inputDevices)[1]->Preferred()) << "Second device should not be marked as default"; EXPECT_FALSE((*inputDevices)[2]->Preferred()) << "Third device should not be marked as default"; CubebDeviceEnumerator::Shutdown(); } TEST(CubebDeviceEnumerator, MultiplePreferredDeviceOrdering) { MockCubeb* mock = new MockCubeb(); mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); // Add devices in mixed order to test proper sorting cubeb_device_info dev1 = DeviceTemplate(reinterpret_cast(1), CUBEB_DEVICE_TYPE_OUTPUT, "non-preferred"); dev1.preferred = CUBEB_DEVICE_PREF_NONE; mock->AddDevice(dev1); cubeb_device_info dev2 = DeviceTemplate(reinterpret_cast(2), CUBEB_DEVICE_TYPE_OUTPUT, "notification"); dev2.preferred = CUBEB_DEVICE_PREF_NOTIFICATION; mock->AddDevice(dev2); cubeb_device_info dev3 = DeviceTemplate( reinterpret_cast(3), CUBEB_DEVICE_TYPE_OUTPUT, "multimedia"); dev3.preferred = CUBEB_DEVICE_PREF_MULTIMEDIA; mock->AddDevice(dev3); cubeb_device_info dev4 = DeviceTemplate(reinterpret_cast(4), CUBEB_DEVICE_TYPE_OUTPUT, "voice"); dev4.preferred = CUBEB_DEVICE_PREF_VOICE; mock->AddDevice(dev4); cubeb_device_info dev5 = DeviceTemplate(reinterpret_cast(5), CUBEB_DEVICE_TYPE_OUTPUT, "non-preferred2"); dev5.preferred = CUBEB_DEVICE_PREF_NONE; mock->AddDevice(dev5); RefPtr enumerator = CubebDeviceEnumerator::GetInstance(); RefPtr outputDevices = enumerator->EnumerateAudioOutputDevices(); // Casting to the non-refcountable type for the pointwise assertion below, // that matches on nsTArray const nsTArray>& outputDevicesCasted = *outputDevices; // We expect multimedia -> voice -> notification -> non-preferred devices EXPECT_THAT(outputDevicesCasted, Pointwise(DeviceIdEq(), {dev3.devid, dev4.devid, dev2.devid, dev1.devid, dev5.devid})); EXPECT_THAT(outputDevicesCasted, Pointwise(PreferredEq(), {nsIAudioDeviceInfo::PREF_MULTIMEDIA, nsIAudioDeviceInfo::PREF_VOICE, nsIAudioDeviceInfo::PREF_NOTIFICATION, nsIAudioDeviceInfo::PREF_NONE, nsIAudioDeviceInfo::PREF_NONE})); CubebDeviceEnumerator::Shutdown(); } TEST(CubebDeviceEnumerator, MultiplePreferencesPerDevice) { MockCubeb* mock = new MockCubeb(); mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); // Add a device with multiple preferences cubeb_device_info dev1 = DeviceTemplate(reinterpret_cast(1), CUBEB_DEVICE_TYPE_OUTPUT, "multimedia-notification"); dev1.preferred = static_cast( CUBEB_DEVICE_PREF_MULTIMEDIA | CUBEB_DEVICE_PREF_NOTIFICATION); mock->AddDevice(dev1); // Add a device with single preference cubeb_device_info dev2 = DeviceTemplate(reinterpret_cast(2), CUBEB_DEVICE_TYPE_OUTPUT, "voice"); dev2.preferred = CUBEB_DEVICE_PREF_VOICE; mock->AddDevice(dev2); // Add a non-preferred device cubeb_device_info dev3 = DeviceTemplate(reinterpret_cast(3), CUBEB_DEVICE_TYPE_OUTPUT, "non-preferred"); dev3.preferred = CUBEB_DEVICE_PREF_NONE; mock->AddDevice(dev3); RefPtr enumerator = CubebDeviceEnumerator::GetInstance(); RefPtr outputDevices = enumerator->EnumerateAudioOutputDevices(); // Casting to the non-refcountable type for the pointwise assertion below, // that matches on nsTArray const nsTArray>& outputDevicesCasted = *outputDevices; // Device with multimedia preference should come first EXPECT_THAT(outputDevicesCasted, Pointwise(DeviceIdEq(), {dev1.devid, dev2.devid, dev3.devid})); // Check device preferences - dev1 has both multimedia and notification EXPECT_THAT(outputDevicesCasted, Pointwise(PreferredEq(), (uint16_t[]){nsIAudioDeviceInfo::PREF_MULTIMEDIA | nsIAudioDeviceInfo::PREF_NOTIFICATION, nsIAudioDeviceInfo::PREF_VOICE, 0})); CubebDeviceEnumerator::Shutdown(); } #undef ENABLE_SET_CUBEB_BACKEND