TestAudioDeviceEnumerator.cpp (16817B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "AudioDeviceInfo.h" 8 #include "CubebDeviceEnumerator.h" 9 #include "MockCubeb.h" 10 #include "gmock/gmock.h" 11 #include "gtest/gtest-printers.h" 12 #include "gtest/gtest.h" 13 #include "mozilla/media/MediaUtils.h" 14 #include "nsTArray.h" 15 16 using namespace mozilla; 17 using AudioDeviceSet = CubebDeviceEnumerator::AudioDeviceSet; 18 using ::testing::ElementsAre; 19 using ::testing::Eq; 20 using ::testing::Pointwise; 21 22 // Custom matcher for comparing AudioDeviceInfo by device ID 23 MATCHER(DeviceIdEq, "") { 24 const auto& [actual, expected_devid] = arg; 25 return actual->DeviceID() == expected_devid; 26 } 27 28 // Custom matcher for checking device preferences 29 MATCHER(PreferredEq, "") { 30 const auto& [actual, expected_pref] = arg; 31 uint16_t pref; 32 nsresult rv = actual->GetPreferred(&pref); 33 return NS_SUCCEEDED(rv) && pref == expected_pref; 34 } 35 36 const bool DEBUG_PRINTS = false; 37 38 enum DeviceOperation { ADD, REMOVE }; 39 40 void TestEnumeration(MockCubeb* aMock, uint32_t aExpectedDeviceCount, 41 DeviceOperation aOperation, cubeb_device_type aType) { 42 RefPtr<CubebDeviceEnumerator> enumerator = 43 CubebDeviceEnumerator::GetInstance(); 44 45 RefPtr<const AudioDeviceSet> devices; 46 47 if (aType == CUBEB_DEVICE_TYPE_INPUT) { 48 devices = enumerator->EnumerateAudioInputDevices(); 49 } 50 51 if (aType == CUBEB_DEVICE_TYPE_OUTPUT) { 52 devices = enumerator->EnumerateAudioOutputDevices(); 53 } 54 55 EXPECT_EQ(devices->Length(), aExpectedDeviceCount) 56 << "Device count is correct when enumerating"; 57 58 if (DEBUG_PRINTS) { 59 for (const auto& deviceInfo : *devices) { 60 printf("=== Before removal\n"); 61 PrintDevice(deviceInfo); 62 } 63 } 64 65 if (aOperation == DeviceOperation::REMOVE) { 66 aMock->RemoveDevice(reinterpret_cast<cubeb_devid>(1)); 67 } else { 68 aMock->AddDevice(DeviceTemplate(reinterpret_cast<cubeb_devid>(123), aType)); 69 } 70 71 if (aType == CUBEB_DEVICE_TYPE_INPUT) { 72 devices = enumerator->EnumerateAudioInputDevices(); 73 } 74 75 if (aType == CUBEB_DEVICE_TYPE_OUTPUT) { 76 devices = enumerator->EnumerateAudioOutputDevices(); 77 } 78 79 uint32_t newExpectedDeviceCount = aOperation == DeviceOperation::REMOVE 80 ? aExpectedDeviceCount - 1 81 : aExpectedDeviceCount + 1; 82 83 EXPECT_EQ(devices->Length(), newExpectedDeviceCount) 84 << "Device count is correct when enumerating after operation"; 85 86 if (DEBUG_PRINTS) { 87 for (const auto& deviceInfo : *devices) { 88 printf("=== After removal\n"); 89 PrintDevice(deviceInfo); 90 } 91 } 92 } 93 94 #ifndef ANDROID 95 TEST(CubebDeviceEnumerator, EnumerateSimple) 96 { 97 // It looks like we're leaking this object, but in fact it will be owned by 98 // CubebUtils: `cubeb_destroy()` is called when `ForceSetCubebContext()` is 99 // called again or when layout statics are shutdown, and we cast back to a 100 // MockCubeb* and call the dtor. 101 MockCubeb* mock = new MockCubeb(); 102 mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); 103 104 // We want to test whether CubebDeviceEnumerator works with and without a 105 // backend that can notify of a device collection change via callback. 106 // Additionally, we're testing that both adding and removing a device 107 // invalidates the list correctly. 108 bool supportsDeviceChangeCallback[2] = {true, false}; 109 DeviceOperation operations[2] = {DeviceOperation::ADD, 110 DeviceOperation::REMOVE}; 111 112 for (bool supports : supportsDeviceChangeCallback) { 113 // Shutdown for `supports` to take effect 114 CubebDeviceEnumerator::Shutdown(); 115 mock->SetSupportDeviceChangeCallback(supports); 116 for (DeviceOperation op : operations) { 117 uint32_t device_count = 4; 118 119 cubeb_device_type deviceType = CUBEB_DEVICE_TYPE_INPUT; 120 AddDevices(mock, device_count, deviceType); 121 TestEnumeration(mock, device_count, op, deviceType); 122 123 deviceType = CUBEB_DEVICE_TYPE_OUTPUT; 124 AddDevices(mock, device_count, deviceType); 125 TestEnumeration(mock, device_count, op, deviceType); 126 } 127 } 128 // Shutdown to clean up the last `supports` effect 129 CubebDeviceEnumerator::Shutdown(); 130 } 131 132 TEST(CubebDeviceEnumerator, ZeroChannelDevices) 133 { 134 MockCubeb* mock = new MockCubeb(); 135 mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); 136 137 // Create devices with different channel count, including 0-channel 138 139 cubeb_device_info dev1 = DeviceTemplate(reinterpret_cast<cubeb_devid>(1), 140 CUBEB_DEVICE_TYPE_INPUT, "dev 1"); 141 dev1.max_channels = 1; 142 mock->AddDevice(dev1); 143 144 cubeb_device_info dev2 = DeviceTemplate(reinterpret_cast<cubeb_devid>(2), 145 CUBEB_DEVICE_TYPE_INPUT, "dev 2"); 146 dev2.max_channels = 0; 147 mock->AddDevice(dev2); 148 149 cubeb_device_info dev3 = DeviceTemplate(reinterpret_cast<cubeb_devid>(3), 150 CUBEB_DEVICE_TYPE_OUTPUT, "dev 3"); 151 dev3.max_channels = 2; 152 mock->AddDevice(dev3); 153 154 cubeb_device_info dev4 = DeviceTemplate(reinterpret_cast<cubeb_devid>(4), 155 CUBEB_DEVICE_TYPE_OUTPUT, "dev 4"); 156 dev4.max_channels = 0; 157 mock->AddDevice(dev4); 158 159 // Make sure the devices are added to cubeb. 160 161 cubeb_device_collection inputCollection = {nullptr, 0}; 162 mock->EnumerateDevices(CUBEB_DEVICE_TYPE_INPUT, &inputCollection); 163 EXPECT_EQ(inputCollection.count, 2U); 164 EXPECT_EQ(inputCollection.device[0].devid, dev1.devid); 165 EXPECT_EQ(inputCollection.device[1].devid, dev2.devid); 166 mock->DestroyDeviceCollection(&inputCollection); 167 EXPECT_EQ(inputCollection.count, 0U); 168 169 cubeb_device_collection outputCollection = {nullptr, 0}; 170 mock->EnumerateDevices(CUBEB_DEVICE_TYPE_OUTPUT, &outputCollection); 171 EXPECT_EQ(outputCollection.count, 2U); 172 EXPECT_EQ(outputCollection.device[0].devid, dev3.devid); 173 EXPECT_EQ(outputCollection.device[1].devid, dev4.devid); 174 mock->DestroyDeviceCollection(&outputCollection); 175 EXPECT_EQ(outputCollection.count, 0U); 176 177 // Enumerate the devices. The result should exclude the 0-channel devices. 178 179 RefPtr<CubebDeviceEnumerator> enumerator = 180 CubebDeviceEnumerator::GetInstance(); 181 182 RefPtr<const AudioDeviceSet> inputDevices = 183 enumerator->EnumerateAudioInputDevices(); 184 EXPECT_EQ(inputDevices->Length(), 1U); 185 EXPECT_EQ(inputDevices->ElementAt(0)->DeviceID(), dev1.devid); 186 EXPECT_EQ(inputDevices->ElementAt(0)->MaxChannels(), dev1.max_channels); 187 188 RefPtr<const AudioDeviceSet> outputDevices = 189 enumerator->EnumerateAudioOutputDevices(); 190 EXPECT_EQ(outputDevices->Length(), 1U); 191 EXPECT_EQ(outputDevices->ElementAt(0)->DeviceID(), dev3.devid); 192 EXPECT_EQ(outputDevices->ElementAt(0)->MaxChannels(), dev3.max_channels); 193 } 194 195 #else // building for Android, which has no device enumeration support 196 TEST(CubebDeviceEnumerator, EnumerateAndroid) 197 { 198 MockCubeb* mock = new MockCubeb(); 199 mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); 200 201 RefPtr<CubebDeviceEnumerator> enumerator = 202 CubebDeviceEnumerator::GetInstance(); 203 204 RefPtr<const AudioDeviceSet> inputDevices = 205 enumerator->EnumerateAudioInputDevices(); 206 EXPECT_EQ(inputDevices->Length(), 1u) 207 << "Android always exposes a single input device."; 208 EXPECT_EQ((*inputDevices)[0]->MaxChannels(), 1u) << "With a single channel."; 209 EXPECT_EQ((*inputDevices)[0]->DeviceID(), nullptr) 210 << "It's always the default input device."; 211 EXPECT_TRUE((*inputDevices)[0]->Preferred()) 212 << "it's always the prefered input device."; 213 214 RefPtr<const AudioDeviceSet> outputDevices = 215 enumerator->EnumerateAudioOutputDevices(); 216 EXPECT_EQ(outputDevices->Length(), 1u) 217 << "Android always exposes a single output device."; 218 EXPECT_EQ((*outputDevices)[0]->MaxChannels(), 2u) << "With stereo channels."; 219 EXPECT_EQ((*outputDevices)[0]->DeviceID(), nullptr) 220 << "It's always the default output device."; 221 EXPECT_TRUE((*outputDevices)[0]->Preferred()) 222 << "it's always the prefered output device."; 223 } 224 #endif 225 226 TEST(CubebDeviceEnumerator, ForceNullCubebContext) 227 { 228 mozilla::CubebUtils::ForceSetCubebContext(nullptr); 229 RefPtr<CubebDeviceEnumerator> enumerator = 230 CubebDeviceEnumerator::GetInstance(); 231 232 RefPtr inputDevices = enumerator->EnumerateAudioInputDevices(); 233 EXPECT_EQ(inputDevices->Length(), 0u) 234 << "Enumeration must fail, input device list must be empty."; 235 236 RefPtr outputDevices = enumerator->EnumerateAudioOutputDevices(); 237 EXPECT_EQ(outputDevices->Length(), 0u) 238 << "Enumeration must fail, output device list must be empty."; 239 240 // Shutdown to clean up the null context effect 241 CubebDeviceEnumerator::Shutdown(); 242 } 243 244 TEST(CubebDeviceEnumerator, DeviceInfoFromName) 245 { 246 MockCubeb* mock = new MockCubeb(); 247 mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); 248 249 cubeb_device_type deviceTypes[2] = {CUBEB_DEVICE_TYPE_INPUT, 250 CUBEB_DEVICE_TYPE_OUTPUT}; 251 252 bool supportsDeviceChangeCallback[2] = {true, false}; 253 for (bool supports : supportsDeviceChangeCallback) { 254 // Shutdown for `supports` to take effect 255 CubebDeviceEnumerator::Shutdown(); 256 mock->SetSupportDeviceChangeCallback(supports); 257 for (cubeb_device_type& deviceType : deviceTypes) { 258 cubeb_devid id_1 = reinterpret_cast<cubeb_devid>(1); 259 mock->AddDevice(DeviceTemplate(id_1, deviceType, "device name 1")); 260 cubeb_devid id_2 = reinterpret_cast<cubeb_devid>(2); 261 nsCString device_name = "device name 2"_ns; 262 mock->AddDevice(DeviceTemplate(id_2, deviceType, device_name.get())); 263 cubeb_devid id_3 = reinterpret_cast<cubeb_devid>(3); 264 mock->AddDevice(DeviceTemplate(id_3, deviceType, "device name 3")); 265 266 RefPtr<CubebDeviceEnumerator> enumerator = 267 CubebDeviceEnumerator::GetInstance(); 268 269 EnumeratorSide side = (deviceType == CUBEB_DEVICE_TYPE_INPUT) 270 ? EnumeratorSide::INPUT 271 : EnumeratorSide::OUTPUT; 272 RefPtr<AudioDeviceInfo> devInfo = enumerator->DeviceInfoFromName( 273 NS_ConvertUTF8toUTF16(device_name), side); 274 EXPECT_TRUE(devInfo) << "the device exist"; 275 EXPECT_EQ(devInfo->Name(), NS_ConvertUTF8toUTF16(device_name)) 276 << "verify the device"; 277 278 mock->RemoveDevice(id_2); 279 280 devInfo = enumerator->DeviceInfoFromName( 281 NS_ConvertUTF8toUTF16(device_name), side); 282 EXPECT_FALSE(devInfo) << "the device does not exist any more"; 283 } 284 } 285 // Shutdown for `supports` to take effect 286 CubebDeviceEnumerator::Shutdown(); 287 } 288 289 TEST(CubebDeviceEnumerator, PreferredDeviceFirst) 290 { 291 MockCubeb* mock = new MockCubeb(); 292 mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); 293 294 // Add multiple devices where the preferred one is not first 295 cubeb_device_info dev1 = DeviceTemplate(reinterpret_cast<cubeb_devid>(1), 296 CUBEB_DEVICE_TYPE_INPUT, "dev 1"); 297 dev1.preferred = CUBEB_DEVICE_PREF_NONE; 298 mock->AddDevice(dev1); 299 300 cubeb_device_info dev2 = DeviceTemplate(reinterpret_cast<cubeb_devid>(2), 301 CUBEB_DEVICE_TYPE_INPUT, "dev 2"); 302 dev2.preferred = CUBEB_DEVICE_PREF_NONE; 303 mock->AddDevice(dev2); 304 305 cubeb_device_info dev3 = DeviceTemplate(reinterpret_cast<cubeb_devid>(3), 306 CUBEB_DEVICE_TYPE_INPUT, "dev 3"); 307 dev3.preferred = CUBEB_DEVICE_PREF_ALL; 308 mock->AddDevice(dev3); 309 310 RefPtr<CubebDeviceEnumerator> enumerator = 311 CubebDeviceEnumerator::GetInstance(); 312 313 RefPtr<const AudioDeviceSet> inputDevices = 314 enumerator->EnumerateAudioInputDevices(); 315 EXPECT_EQ(inputDevices->Length(), 3U) << "Should have 3 devices"; 316 317 EXPECT_TRUE((*inputDevices)[0]->Preferred()) 318 << "First device should be the default device"; 319 EXPECT_EQ((*inputDevices)[0]->DeviceID(), dev3.devid) 320 << "First device should be dev3"; 321 322 EXPECT_FALSE((*inputDevices)[1]->Preferred()) 323 << "Second device should not be marked as default"; 324 EXPECT_FALSE((*inputDevices)[2]->Preferred()) 325 << "Third device should not be marked as default"; 326 327 CubebDeviceEnumerator::Shutdown(); 328 } 329 330 TEST(CubebDeviceEnumerator, MultiplePreferredDeviceOrdering) 331 { 332 MockCubeb* mock = new MockCubeb(); 333 mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); 334 335 // Add devices in mixed order to test proper sorting 336 cubeb_device_info dev1 = 337 DeviceTemplate(reinterpret_cast<cubeb_devid>(1), CUBEB_DEVICE_TYPE_OUTPUT, 338 "non-preferred"); 339 dev1.preferred = CUBEB_DEVICE_PREF_NONE; 340 mock->AddDevice(dev1); 341 342 cubeb_device_info dev2 = 343 DeviceTemplate(reinterpret_cast<cubeb_devid>(2), CUBEB_DEVICE_TYPE_OUTPUT, 344 "notification"); 345 dev2.preferred = CUBEB_DEVICE_PREF_NOTIFICATION; 346 mock->AddDevice(dev2); 347 348 cubeb_device_info dev3 = DeviceTemplate( 349 reinterpret_cast<cubeb_devid>(3), CUBEB_DEVICE_TYPE_OUTPUT, "multimedia"); 350 dev3.preferred = CUBEB_DEVICE_PREF_MULTIMEDIA; 351 mock->AddDevice(dev3); 352 353 cubeb_device_info dev4 = DeviceTemplate(reinterpret_cast<cubeb_devid>(4), 354 CUBEB_DEVICE_TYPE_OUTPUT, "voice"); 355 dev4.preferred = CUBEB_DEVICE_PREF_VOICE; 356 mock->AddDevice(dev4); 357 358 cubeb_device_info dev5 = 359 DeviceTemplate(reinterpret_cast<cubeb_devid>(5), CUBEB_DEVICE_TYPE_OUTPUT, 360 "non-preferred2"); 361 dev5.preferred = CUBEB_DEVICE_PREF_NONE; 362 mock->AddDevice(dev5); 363 364 RefPtr<CubebDeviceEnumerator> enumerator = 365 CubebDeviceEnumerator::GetInstance(); 366 367 RefPtr<const AudioDeviceSet> outputDevices = 368 enumerator->EnumerateAudioOutputDevices(); 369 370 // Casting to the non-refcountable type for the pointwise assertion below, 371 // that matches on nsTArray 372 const nsTArray<RefPtr<AudioDeviceInfo>>& outputDevicesCasted = *outputDevices; 373 374 // We expect multimedia -> voice -> notification -> non-preferred devices 375 EXPECT_THAT(outputDevicesCasted, 376 Pointwise(DeviceIdEq(), {dev3.devid, dev4.devid, dev2.devid, 377 dev1.devid, dev5.devid})); 378 379 EXPECT_THAT(outputDevicesCasted, 380 Pointwise(PreferredEq(), {nsIAudioDeviceInfo::PREF_MULTIMEDIA, 381 nsIAudioDeviceInfo::PREF_VOICE, 382 nsIAudioDeviceInfo::PREF_NOTIFICATION, 383 nsIAudioDeviceInfo::PREF_NONE, 384 nsIAudioDeviceInfo::PREF_NONE})); 385 386 CubebDeviceEnumerator::Shutdown(); 387 } 388 389 TEST(CubebDeviceEnumerator, MultiplePreferencesPerDevice) 390 { 391 MockCubeb* mock = new MockCubeb(); 392 mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); 393 394 // Add a device with multiple preferences 395 cubeb_device_info dev1 = 396 DeviceTemplate(reinterpret_cast<cubeb_devid>(1), CUBEB_DEVICE_TYPE_OUTPUT, 397 "multimedia-notification"); 398 dev1.preferred = static_cast<cubeb_device_pref>( 399 CUBEB_DEVICE_PREF_MULTIMEDIA | CUBEB_DEVICE_PREF_NOTIFICATION); 400 mock->AddDevice(dev1); 401 402 // Add a device with single preference 403 cubeb_device_info dev2 = DeviceTemplate(reinterpret_cast<cubeb_devid>(2), 404 CUBEB_DEVICE_TYPE_OUTPUT, "voice"); 405 dev2.preferred = CUBEB_DEVICE_PREF_VOICE; 406 mock->AddDevice(dev2); 407 408 // Add a non-preferred device 409 cubeb_device_info dev3 = 410 DeviceTemplate(reinterpret_cast<cubeb_devid>(3), CUBEB_DEVICE_TYPE_OUTPUT, 411 "non-preferred"); 412 dev3.preferred = CUBEB_DEVICE_PREF_NONE; 413 mock->AddDevice(dev3); 414 415 RefPtr<CubebDeviceEnumerator> enumerator = 416 CubebDeviceEnumerator::GetInstance(); 417 418 RefPtr<const AudioDeviceSet> outputDevices = 419 enumerator->EnumerateAudioOutputDevices(); 420 421 // Casting to the non-refcountable type for the pointwise assertion below, 422 // that matches on nsTArray 423 const nsTArray<RefPtr<AudioDeviceInfo>>& outputDevicesCasted = *outputDevices; 424 425 // Device with multimedia preference should come first 426 EXPECT_THAT(outputDevicesCasted, 427 Pointwise(DeviceIdEq(), {dev1.devid, dev2.devid, dev3.devid})); 428 429 // Check device preferences - dev1 has both multimedia and notification 430 EXPECT_THAT(outputDevicesCasted, 431 Pointwise(PreferredEq(), 432 (uint16_t[]){nsIAudioDeviceInfo::PREF_MULTIMEDIA | 433 nsIAudioDeviceInfo::PREF_NOTIFICATION, 434 nsIAudioDeviceInfo::PREF_VOICE, 0})); 435 436 CubebDeviceEnumerator::Shutdown(); 437 } 438 #undef ENABLE_SET_CUBEB_BACKEND