1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "base/json/json_writer.h" 6 #include "base/strings/string_util.h" 7 #include "base/strings/stringprintf.h" 8 #include "base/strings/utf_string_conversions.h" 9 #include "base/synchronization/waitable_event.h" 10 #include "base/threading/platform_thread.h" 11 #include "base/time/time.h" 12 #include "chrome/browser/browser_process.h" 13 #include "chrome/browser/extensions/api/webrtc_audio_private/webrtc_audio_private_api.h" 14 #include "chrome/browser/extensions/component_loader.h" 15 #include "chrome/browser/extensions/extension_apitest.h" 16 #include "chrome/browser/extensions/extension_function_test_utils.h" 17 #include "chrome/browser/extensions/extension_tab_util.h" 18 #include "chrome/browser/media/webrtc_log_uploader.h" 19 #include "chrome/browser/ui/browser.h" 20 #include "chrome/browser/ui/tabs/tab_strip_model.h" 21 #include "chrome/test/base/in_process_browser_test.h" 22 #include "chrome/test/base/ui_test_utils.h" 23 #include "content/public/browser/browser_thread.h" 24 #include "content/public/browser/media_device_id.h" 25 #include "content/public/browser/web_contents.h" 26 #include "content/public/test/browser_test_utils.h" 27 #include "extensions/common/permissions/permission_set.h" 28 #include "extensions/common/permissions/permissions_data.h" 29 #include "media/audio/audio_manager.h" 30 #include "media/audio/audio_manager_base.h" 31 #include "net/test/embedded_test_server/embedded_test_server.h" 32 #include "testing/gtest/include/gtest/gtest.h" 33 34 using base::JSONWriter; 35 using content::RenderViewHost; 36 using content::WebContents; 37 using media::AudioDeviceNames; 38 using media::AudioManager; 39 40 namespace extensions { 41 42 using extension_function_test_utils::RunFunctionAndReturnError; 43 using extension_function_test_utils::RunFunctionAndReturnSingleResult; 44 45 class AudioWaitingExtensionTest : public ExtensionApiTest { 46 protected: 47 void WaitUntilAudioIsPlaying(WebContents* tab) { 48 // Wait for audio to start playing. We gate this on there being one 49 // or more AudioOutputController objects for our tab. 50 bool audio_playing = false; 51 for (size_t remaining_tries = 50; remaining_tries > 0; --remaining_tries) { 52 tab->GetRenderViewHost()->GetAudioOutputControllers( 53 base::Bind(OnAudioControllers, &audio_playing)); 54 base::MessageLoop::current()->RunUntilIdle(); 55 if (audio_playing) 56 break; 57 58 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100)); 59 } 60 61 if (!audio_playing) 62 FAIL() << "Audio did not start playing within ~5 seconds."; 63 } 64 65 // Used by the test above to wait until audio is playing. 66 static void OnAudioControllers( 67 bool* audio_playing, 68 const RenderViewHost::AudioOutputControllerList& list) { 69 if (!list.empty()) 70 *audio_playing = true; 71 } 72 }; 73 74 class WebrtcAudioPrivateTest : public AudioWaitingExtensionTest { 75 public: 76 WebrtcAudioPrivateTest() 77 : enumeration_event_(false, false) { 78 } 79 80 virtual void SetUpOnMainThread() OVERRIDE { 81 AudioWaitingExtensionTest::SetUpOnMainThread(); 82 // Needs to happen after chrome's schemes are added. 83 source_url_ = GURL("chrome-extension://fakeid012345678/fakepage.html"); 84 } 85 86 protected: 87 std::string InvokeGetActiveSink(int tab_id) { 88 base::ListValue parameters; 89 parameters.AppendInteger(tab_id); 90 std::string parameter_string; 91 JSONWriter::Write(¶meters, ¶meter_string); 92 93 scoped_refptr<WebrtcAudioPrivateGetActiveSinkFunction> function = 94 new WebrtcAudioPrivateGetActiveSinkFunction(); 95 function->set_source_url(source_url_); 96 scoped_ptr<base::Value> result( 97 RunFunctionAndReturnSingleResult(function.get(), 98 parameter_string, 99 browser())); 100 std::string device_id; 101 result->GetAsString(&device_id); 102 return device_id; 103 } 104 105 scoped_ptr<base::Value> InvokeGetSinks(base::ListValue** sink_list) { 106 scoped_refptr<WebrtcAudioPrivateGetSinksFunction> function = 107 new WebrtcAudioPrivateGetSinksFunction(); 108 function->set_source_url(source_url_); 109 110 scoped_ptr<base::Value> result( 111 RunFunctionAndReturnSingleResult(function.get(), "[]", browser())); 112 result->GetAsList(sink_list); 113 return result.Pass(); 114 } 115 116 // Synchronously (from the calling thread's point of view) runs the 117 // given enumeration function on the device thread. On return, 118 // |device_names| has been filled with the device names resulting 119 // from that call. 120 void GetAudioDeviceNames( 121 void (AudioManager::*EnumerationFunc)(AudioDeviceNames*), 122 AudioDeviceNames* device_names) { 123 AudioManager* audio_manager = AudioManager::Get(); 124 125 if (!audio_manager->GetWorkerTaskRunner()->BelongsToCurrentThread()) { 126 audio_manager->GetWorkerTaskRunner()->PostTask( 127 FROM_HERE, 128 base::Bind(&WebrtcAudioPrivateTest::GetAudioDeviceNames, this, 129 EnumerationFunc, device_names)); 130 enumeration_event_.Wait(); 131 } else { 132 (audio_manager->*EnumerationFunc)(device_names); 133 enumeration_event_.Signal(); 134 } 135 } 136 137 // Synchronously (from the calling thread's point of view) retrieve the 138 // device id in the |origin| on the IO thread. On return, 139 // |id_in_origin| contains the id |raw_device_id| is known by in 140 // the origin. 141 void GetIDInOrigin(content::ResourceContext* resource_context, 142 GURL origin, 143 const std::string& raw_device_id, 144 std::string* id_in_origin) { 145 if (!content::BrowserThread::CurrentlyOn(content::BrowserThread::IO)) { 146 content::BrowserThread::PostTask( 147 content::BrowserThread::IO, FROM_HERE, 148 base::Bind(&WebrtcAudioPrivateTest::GetIDInOrigin, 149 this, resource_context, origin, raw_device_id, 150 id_in_origin)); 151 enumeration_event_.Wait(); 152 } else { 153 *id_in_origin = content::GetHMACForMediaDeviceID( 154 resource_context->GetMediaDeviceIDSalt(), 155 origin, 156 raw_device_id); 157 enumeration_event_.Signal(); 158 } 159 } 160 161 // Event used to signal completion of enumeration. 162 base::WaitableEvent enumeration_event_; 163 164 GURL source_url_; 165 }; 166 167 #if !defined(OS_MACOSX) 168 // http://crbug.com/334579 169 IN_PROC_BROWSER_TEST_F(WebrtcAudioPrivateTest, GetSinks) { 170 AudioDeviceNames devices; 171 GetAudioDeviceNames(&AudioManager::GetAudioOutputDeviceNames, &devices); 172 173 base::ListValue* sink_list = NULL; 174 scoped_ptr<base::Value> result = InvokeGetSinks(&sink_list); 175 176 std::string result_string; 177 JSONWriter::Write(result.get(), &result_string); 178 VLOG(2) << result_string; 179 180 EXPECT_EQ(devices.size(), sink_list->GetSize()); 181 182 // Iterate through both lists in lockstep and compare. The order 183 // should be identical. 184 size_t ix = 0; 185 AudioDeviceNames::const_iterator it = devices.begin(); 186 for (; ix < sink_list->GetSize() && it != devices.end(); 187 ++ix, ++it) { 188 base::DictionaryValue* dict = NULL; 189 sink_list->GetDictionary(ix, &dict); 190 std::string sink_id; 191 dict->GetString("sinkId", &sink_id); 192 193 std::string expected_id; 194 if (it->unique_id.empty() || 195 it->unique_id == media::AudioManagerBase::kDefaultDeviceId) { 196 expected_id = media::AudioManagerBase::kDefaultDeviceId; 197 } else { 198 GetIDInOrigin(profile()->GetResourceContext(), 199 source_url_.GetOrigin(), 200 it->unique_id, 201 &expected_id); 202 } 203 204 EXPECT_EQ(expected_id, sink_id); 205 std::string sink_label; 206 dict->GetString("sinkLabel", &sink_label); 207 EXPECT_EQ(it->device_name, sink_label); 208 209 // TODO(joi): Verify the contents of these once we start actually 210 // filling them in. 211 EXPECT_TRUE(dict->HasKey("isDefault")); 212 EXPECT_TRUE(dict->HasKey("isReady")); 213 EXPECT_TRUE(dict->HasKey("sampleRate")); 214 } 215 } 216 #endif // OS_MACOSX 217 218 // This exercises the case where you have a tab with no active media 219 // stream and try to retrieve the currently active audio sink. 220 IN_PROC_BROWSER_TEST_F(WebrtcAudioPrivateTest, GetActiveSinkNoMediaStream) { 221 WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents(); 222 int tab_id = ExtensionTabUtil::GetTabId(tab); 223 base::ListValue parameters; 224 parameters.AppendInteger(tab_id); 225 std::string parameter_string; 226 JSONWriter::Write(¶meters, ¶meter_string); 227 228 scoped_refptr<WebrtcAudioPrivateGetActiveSinkFunction> function = 229 new WebrtcAudioPrivateGetActiveSinkFunction(); 230 function->set_source_url(source_url_); 231 scoped_ptr<base::Value> result( 232 RunFunctionAndReturnSingleResult(function.get(), 233 parameter_string, 234 browser())); 235 236 std::string result_string; 237 JSONWriter::Write(result.get(), &result_string); 238 EXPECT_EQ("\"\"", result_string); 239 } 240 241 // This exercises the case where you have a tab with no active media 242 // stream and try to set the audio sink. 243 IN_PROC_BROWSER_TEST_F(WebrtcAudioPrivateTest, SetActiveSinkNoMediaStream) { 244 WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents(); 245 int tab_id = ExtensionTabUtil::GetTabId(tab); 246 base::ListValue parameters; 247 parameters.AppendInteger(tab_id); 248 parameters.AppendString("no such id"); 249 std::string parameter_string; 250 JSONWriter::Write(¶meters, ¶meter_string); 251 252 scoped_refptr<WebrtcAudioPrivateSetActiveSinkFunction> function = 253 new WebrtcAudioPrivateSetActiveSinkFunction(); 254 function->set_source_url(source_url_); 255 std::string error(RunFunctionAndReturnError(function.get(), 256 parameter_string, 257 browser())); 258 EXPECT_EQ(base::StringPrintf("No active stream for tab with id: %d.", tab_id), 259 error); 260 } 261 262 IN_PROC_BROWSER_TEST_F(WebrtcAudioPrivateTest, GetAndSetWithMediaStream) { 263 // First retrieve the list of all sinks, so that we can run a test 264 // where we set the active sink to each of the different available 265 // sinks in turn. 266 base::ListValue* sink_list = NULL; 267 scoped_ptr<base::Value> result = InvokeGetSinks(&sink_list); 268 269 ASSERT_TRUE(StartEmbeddedTestServer()); 270 271 // Open a normal page that uses an audio sink. 272 ui_test_utils::NavigateToURL( 273 browser(), 274 GURL(embedded_test_server()->GetURL("/extensions/loop_audio.html"))); 275 276 WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents(); 277 int tab_id = ExtensionTabUtil::GetTabId(tab); 278 279 WaitUntilAudioIsPlaying(tab); 280 281 std::string current_device = InvokeGetActiveSink(tab_id); 282 VLOG(2) << "Before setting, current device: " << current_device; 283 EXPECT_NE("", current_device); 284 285 // Set to each of the other devices in turn. 286 for (size_t ix = 0; ix < sink_list->GetSize(); ++ix) { 287 base::DictionaryValue* dict = NULL; 288 sink_list->GetDictionary(ix, &dict); 289 std::string target_device; 290 dict->GetString("sinkId", &target_device); 291 292 base::ListValue parameters; 293 parameters.AppendInteger(tab_id); 294 parameters.AppendString(target_device); 295 std::string parameter_string; 296 JSONWriter::Write(¶meters, ¶meter_string); 297 298 scoped_refptr<WebrtcAudioPrivateSetActiveSinkFunction> function = 299 new WebrtcAudioPrivateSetActiveSinkFunction(); 300 function->set_source_url(source_url_); 301 scoped_ptr<base::Value> result(RunFunctionAndReturnSingleResult( 302 function.get(), parameter_string, browser())); 303 // The function was successful if the above invocation doesn't 304 // fail. Just for kicks, also check that it returns no result. 305 EXPECT_EQ(NULL, result.get()); 306 307 current_device = InvokeGetActiveSink(tab_id); 308 VLOG(2) << "After setting to " << target_device 309 << ", current device is " << current_device; 310 EXPECT_EQ(target_device, current_device); 311 } 312 } 313 314 IN_PROC_BROWSER_TEST_F(WebrtcAudioPrivateTest, GetAssociatedSink) { 315 // Get the list of input devices. We can cheat in the unit test and 316 // run this on the main thread since nobody else will be running at 317 // the same time. 318 AudioDeviceNames devices; 319 GetAudioDeviceNames(&AudioManager::GetAudioInputDeviceNames, &devices); 320 321 // Try to get an associated sink for each source. 322 for (AudioDeviceNames::const_iterator device = devices.begin(); 323 device != devices.end(); 324 ++device) { 325 scoped_refptr<WebrtcAudioPrivateGetAssociatedSinkFunction> function = 326 new WebrtcAudioPrivateGetAssociatedSinkFunction(); 327 function->set_source_url(source_url_); 328 329 std::string raw_device_id = device->unique_id; 330 VLOG(2) << "Trying to find associated sink for device " << raw_device_id; 331 std::string source_id_in_origin; 332 GURL origin(GURL("http://www.google.com/").GetOrigin()); 333 GetIDInOrigin(profile()->GetResourceContext(), 334 origin, 335 raw_device_id, 336 &source_id_in_origin); 337 338 base::ListValue parameters; 339 parameters.AppendString(origin.spec()); 340 parameters.AppendString(source_id_in_origin); 341 std::string parameter_string; 342 JSONWriter::Write(¶meters, ¶meter_string); 343 344 scoped_ptr<base::Value> result( 345 RunFunctionAndReturnSingleResult(function.get(), 346 parameter_string, 347 browser())); 348 std::string result_string; 349 JSONWriter::Write(result.get(), &result_string); 350 VLOG(2) << "Results: " << result_string; 351 } 352 } 353 354 IN_PROC_BROWSER_TEST_F(WebrtcAudioPrivateTest, TriggerEvent) { 355 WebrtcAudioPrivateEventService* service = 356 WebrtcAudioPrivateEventService::GetFactoryInstance()->Get(profile()); 357 358 // Just trigger, without any extension listening. 359 service->OnDevicesChanged(base::SystemMonitor::DEVTYPE_AUDIO_CAPTURE); 360 361 // Now load our test extension and do it again. 362 const extensions::Extension* extension = LoadExtension( 363 test_data_dir_.AppendASCII("webrtc_audio_private_event_listener")); 364 service->OnDevicesChanged(base::SystemMonitor::DEVTYPE_AUDIO_CAPTURE); 365 366 // Check that the extension got the notification. 367 std::string result = ExecuteScriptInBackgroundPage(extension->id(), 368 "reportIfGot()"); 369 EXPECT_EQ("true", result); 370 } 371 372 class HangoutServicesBrowserTest : public AudioWaitingExtensionTest { 373 public: 374 virtual void SetUp() OVERRIDE { 375 // Make sure the Hangout Services component extension gets loaded. 376 ComponentLoader::EnableBackgroundExtensionsForTesting(); 377 AudioWaitingExtensionTest::SetUp(); 378 } 379 }; 380 381 #if defined(GOOGLE_CHROME_BUILD) || defined(ENABLE_HANGOUT_SERVICES_EXTENSION) 382 IN_PROC_BROWSER_TEST_F(HangoutServicesBrowserTest, 383 RunComponentExtensionTest) { 384 // This runs the end-to-end JavaScript test for the Hangout Services 385 // component extension, which uses the webrtcAudioPrivate API among 386 // others. 387 ASSERT_TRUE(StartEmbeddedTestServer()); 388 GURL url(embedded_test_server()->GetURL( 389 "/extensions/hangout_services_test.html")); 390 // The "externally connectable" extension permission doesn't seem to 391 // like when we use 127.0.0.1 as the host, but using localhost works. 392 std::string url_spec = url.spec(); 393 ReplaceFirstSubstringAfterOffset(&url_spec, 0, "127.0.0.1", "localhost"); 394 GURL localhost_url(url_spec); 395 ui_test_utils::NavigateToURL(browser(), localhost_url); 396 397 WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents(); 398 WaitUntilAudioIsPlaying(tab); 399 400 // Override, i.e. disable, uploading. We don't want to try sending data to 401 // servers when running the test. We don't bother about the contents of the 402 // buffer |dummy|, that's tested in other tests. 403 std::string dummy; 404 g_browser_process->webrtc_log_uploader()-> 405 OverrideUploadWithBufferForTesting(&dummy); 406 407 ASSERT_TRUE(content::ExecuteScript(tab, "browsertestRunAllTests();")); 408 409 content::TitleWatcher title_watcher(tab, base::ASCIIToUTF16("success")); 410 title_watcher.AlsoWaitForTitle(base::ASCIIToUTF16("failure")); 411 base::string16 result = title_watcher.WaitAndGetTitle(); 412 EXPECT_EQ(base::ASCIIToUTF16("success"), result); 413 414 g_browser_process->webrtc_log_uploader()->OverrideUploadWithBufferForTesting( 415 NULL); 416 } 417 #endif // defined(GOOGLE_CHROME_BUILD) || defined(ENABLE_HANGOUT_SERVICES_EXTENSION) 418 419 } // namespace extensions 420