diff options
Diffstat (limited to 'chromium/content/browser/media')
29 files changed, 4563 insertions, 0 deletions
diff --git a/chromium/content/browser/media/OWNERS b/chromium/content/browser/media/OWNERS new file mode 100644 index 00000000000..d132d0e6061 --- /dev/null +++ b/chromium/content/browser/media/OWNERS @@ -0,0 +1,11 @@ +acolwell@chromium.org +dalecurtis@chromium.org +ddorwin@chromium.org +fischman@chromium.org +scherkus@chromium.org +shadi@chromium.org +tommi@chromium.org +vrk@chromium.org +wjia@chromium.org +xhwang@chromium.org +xians@chromium.org diff --git a/chromium/content/browser/media/encrypted_media_browsertest.cc b/chromium/content/browser/media/encrypted_media_browsertest.cc new file mode 100644 index 00000000000..ce5dc4e6ba0 --- /dev/null +++ b/chromium/content/browser/media/encrypted_media_browsertest.cc @@ -0,0 +1,310 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/command_line.h" +#include "base/path_service.h" +#include "base/strings/utf_string_conversions.h" +#include "base/win/windows_version.h" +#include "content/browser/media/media_browsertest.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "content/shell/shell.h" + +#include "widevine_cdm_version.h" // In SHARED_INTERMEDIATE_DIR. + +#if defined(WIDEVINE_CDM_AVAILABLE) && defined(OS_LINUX) +#include <gnu/libc-version.h> +#endif // defined(WIDEVINE_CDM_AVAILABLE) && defined(OS_LINUX) + +#if defined(ENABLE_PEPPER_CDMS) +// Platform-specific filename relative to the chrome executable. +static const char kClearKeyCdmAdapterFileName[] = +#if defined(OS_MACOSX) + "clearkeycdmadapter.plugin"; +#elif defined(OS_WIN) + "clearkeycdmadapter.dll"; +#elif defined(OS_POSIX) + "libclearkeycdmadapter.so"; +#endif +#endif // defined(ENABLE_PEPPER_CDMS) + +// Available key systems. +static const char kClearKeyKeySystem[] = "webkit-org.w3.clearkey"; +static const char kExternalClearKeyKeySystem[] = + "org.chromium.externalclearkey"; + +// Supported media types. +static const char kWebMAudioOnly[] = "audio/webm; codecs=\"vorbis\""; +static const char kWebMVideoOnly[] = "video/webm; codecs=\"vp8\""; +static const char kWebMAudioVideo[] = "video/webm; codecs=\"vorbis, vp8\""; +static const char kMP4AudioOnly[] = "audio/mp4; codecs=\"mp4a.40.2\""; +static const char kMP4VideoOnly[] = "video/mp4; codecs=\"avc1.4D4041\""; + +// Common test expectations. +static const char kKeyError[] = "KEYERROR"; + +// The type of video src used to load media. +enum SrcType { + SRC, + MSE +}; + +namespace content { + +// Tests encrypted media playback with a combination of parameters: +// - char*: Key system name. +// - bool: True to load media using MSE, otherwise use src. +class EncryptedMediaTest : public content::MediaBrowserTest, + public testing::WithParamInterface<std::tr1::tuple<const char*, SrcType> > { + public: + void TestSimplePlayback(const char* encrypted_media, const char* media_type, + const std::tr1::tuple<const char*, SrcType> test_params, + const char* expectation) { + const char* key_system = std::tr1::get<0>(test_params); + SrcType src_type = std::tr1::get<1>(test_params); + RunEncryptedMediaTest("encrypted_media_player.html", encrypted_media, + media_type, key_system, src_type, expectation); + } + + void TestMSESimplePlayback(const char* encrypted_media, + const char* media_type, const char* key_system, + const char* expectation) { + RunEncryptedMediaTest("encrypted_media_player.html", encrypted_media, + media_type, key_system, MSE, expectation); + } + + void TestFrameSizeChange( + const std::tr1::tuple<const char*, SrcType> test_params, + const char* expectation) { + const char* key_system = std::tr1::get<0>(test_params); + SrcType src_type = std::tr1::get<1>(test_params); + RunEncryptedMediaTest("encrypted_frame_size_change.html", + "frame_size_change-av-enc-v.webm", kWebMAudioVideo, + key_system, src_type, expectation); + } + + void TestConfigChange(const char* key_system, const char* expectation) { + std::vector<StringPair> query_params; + query_params.push_back(std::make_pair("keysystem", key_system)); + query_params.push_back(std::make_pair("runencrypted", "1")); + RunMediaTestPage("mse_config_change.html", &query_params, expectation, + true); + } + + void RunEncryptedMediaTest(const char* html_page, const char* media_file, + const char* media_type, const char* key_system, + SrcType src_type, const char* expectation) { + std::vector<StringPair> query_params; + query_params.push_back(std::make_pair("mediafile", media_file)); + query_params.push_back(std::make_pair("mediatype", media_type)); + query_params.push_back(std::make_pair("keysystem", key_system)); + if (src_type == MSE) + query_params.push_back(std::make_pair("usemse", "1")); + RunMediaTestPage(html_page, &query_params, expectation, true); + } + + protected: +#if defined(ENABLE_PEPPER_CDMS) + virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { + RegisterPepperCdm(command_line, kClearKeyCdmAdapterFileName, + kExternalClearKeyKeySystem); + } + + virtual void RegisterPepperCdm(CommandLine* command_line, + const std::string& adapter_name, + const std::string& key_system) { + // Append the switch to register the Clear Key CDM Adapter. + base::FilePath plugin_dir; + EXPECT_TRUE(PathService::Get(base::DIR_MODULE, &plugin_dir)); + base::FilePath plugin_lib = plugin_dir.AppendASCII(adapter_name); + EXPECT_TRUE(base::PathExists(plugin_lib)); + base::FilePath::StringType pepper_plugin = plugin_lib.value(); + pepper_plugin.append(FILE_PATH_LITERAL("#CDM#0.1.0.0;")); +#if defined(OS_WIN) + pepper_plugin.append(ASCIIToWide(GetPepperType(key_system))); +#else + pepper_plugin.append(GetPepperType(key_system)); +#endif + command_line->AppendSwitchNative(switches::kRegisterPepperPlugins, + pepper_plugin); + } + + // Adapted from key_systems.cc. + std::string GetPepperType(const std::string& key_system) { + if (key_system == kExternalClearKeyKeySystem) + return "application/x-ppapi-clearkey-cdm"; +#if defined(WIDEVINE_CDM_AVAILABLE) + if (key_system == kWidevineKeySystem) + return "application/x-ppapi-widevine-cdm"; +#endif // WIDEVINE_CDM_AVAILABLE + + NOTREACHED(); + return ""; + } +#endif // defined(ENABLE_PEPPER_CDMS) +}; + +#if defined(WIDEVINE_CDM_AVAILABLE) +class WVEncryptedMediaTest : public EncryptedMediaTest { + public: + // Tests that the following happen after trying to play encrypted media: + // - webkitneedkey event is fired. + // - webkitGenerateKeyRequest() does not fail. + // - webkitkeymessage is fired + // - webkitAddKey() generates a WebKitKeyError since no real WV key is added. + void TestMSESimplePlayback(const char* encrypted_media, + const char* media_type, const char* key_system) { + // TODO(shadi): Remove after bots upgrade to precise. + // Don't run on lucid bots since the CDM is not compatible (glibc < 2.14) +#if defined(OS_LINUX) + if (strcmp(gnu_get_libc_version(), "2.11.1") == 0) { + LOG(INFO) << "Skipping test; not supported on glibc version: " + << gnu_get_libc_version(); + return; + } +#endif // defined(OS_LINUX) + EncryptedMediaTest::TestMSESimplePlayback(encrypted_media, media_type, + key_system, kKeyError); + bool receivedKeyMessage = false; + EXPECT_TRUE(ExecuteScriptAndExtractBool( + shell()->web_contents(), + "window.domAutomationController.send(video.receivedKeyMessage);", + &receivedKeyMessage)); + ASSERT_TRUE(receivedKeyMessage); + } + + protected: +#if defined(ENABLE_PEPPER_CDMS) + virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { + RegisterPepperCdm(command_line, kWidevineCdmAdapterFileName, + kWidevineKeySystem); + } +#endif // defined(ENABLE_PEPPER_CDMS) +}; +#endif // defined(WIDEVINE_CDM_AVAILABLE) + +INSTANTIATE_TEST_CASE_P(ClearKey, EncryptedMediaTest, + ::testing::Combine( + ::testing::Values(kClearKeyKeySystem), ::testing::Values(SRC, MSE))); + +// External Clear Key is currently only used on platforms that use Pepper CDMs. +#if defined(ENABLE_PEPPER_CDMS) +INSTANTIATE_TEST_CASE_P(ExternalClearKey, EncryptedMediaTest, + ::testing::Combine( + ::testing::Values(kExternalClearKeyKeySystem), + ::testing::Values(SRC, MSE))); + +IN_PROC_BROWSER_TEST_F(EncryptedMediaTest, ConfigChangeVideo_ExternalClearKey) { + TestConfigChange(kExternalClearKeyKeySystem, kEnded); +} +#endif // defined(ENABLE_PEPPER_CDMS) + +IN_PROC_BROWSER_TEST_F(EncryptedMediaTest, ConfigChangeVideo_ClearKey) { + TestConfigChange(kClearKeyKeySystem, kEnded); +} + +IN_PROC_BROWSER_TEST_F(EncryptedMediaTest, InvalidKeySystem) { + TestMSESimplePlayback("bear-320x240-av-enc_av.webm", kWebMAudioVideo, + "com.example.invalid", + "GENERATE_KEY_REQUEST_EXCEPTION"); +} + +IN_PROC_BROWSER_TEST_P(EncryptedMediaTest, Playback_AudioOnly_WebM) { + TestSimplePlayback("bear-a-enc_a.webm", kWebMAudioOnly, GetParam(), kEnded); +} + +IN_PROC_BROWSER_TEST_P(EncryptedMediaTest, Playback_AudioClearVideo_WebM) { + TestSimplePlayback("bear-320x240-av-enc_a.webm", kWebMAudioVideo, GetParam(), + kEnded); +} + +IN_PROC_BROWSER_TEST_P(EncryptedMediaTest, Playback_VideoAudio_WebM) { + TestSimplePlayback("bear-320x240-av-enc_av.webm", kWebMAudioVideo, GetParam(), + kEnded); +} + +IN_PROC_BROWSER_TEST_P(EncryptedMediaTest, Playback_VideoOnly_WebM) { + TestSimplePlayback("bear-320x240-v-enc_v.webm", kWebMVideoOnly, GetParam(), + kEnded); +} + +IN_PROC_BROWSER_TEST_P(EncryptedMediaTest, Playback_VideoClearAudio_WebM) { + TestSimplePlayback("bear-320x240-av-enc_v.webm", kWebMAudioVideo, GetParam(), + kEnded); +} + +IN_PROC_BROWSER_TEST_P(EncryptedMediaTest, FrameChangeVideo) { + // Times out on Windows XP. http://crbug.com/171937 +#if defined(OS_WIN) + if (base::win::GetVersion() < base::win::VERSION_VISTA) + return; +#endif + TestFrameSizeChange(GetParam(), kEnded); +} + +#if defined(GOOGLE_CHROME_BUILD) || defined(USE_PROPRIETARY_CODECS) +IN_PROC_BROWSER_TEST_P(EncryptedMediaTest, Playback_VideoOnly_MP4) { + std::tr1::tuple<const char*, SrcType> test_params = GetParam(); + // MP4 without MSE is not support yet, http://crbug.com/170793. + if (std::tr1::get<1>(test_params) != MSE) { + LOG(INFO) << "Skipping test; Can only play MP4 encrypted streams by MSE."; + return; + } + TestMSESimplePlayback("bear-640x360-v_frag-cenc.mp4", kMP4VideoOnly, + std::tr1::get<0>(test_params), kEnded); +} + +IN_PROC_BROWSER_TEST_P(EncryptedMediaTest, Playback_AudioOnly_MP4) { + std::tr1::tuple<const char*, SrcType> test_params = GetParam(); + // MP4 without MSE is not support yet, http://crbug.com/170793. + if (std::tr1::get<1>(test_params) != MSE) { + LOG(INFO) << "Skipping test; Can only play MP4 encrypted streams by MSE."; + return; + } + TestMSESimplePlayback("bear-640x360-a_frag-cenc.mp4", kMP4AudioOnly, + std::tr1::get<0>(test_params), kEnded); +} +#endif + +// Run only when WV CDM is available. +#if defined(WIDEVINE_CDM_AVAILABLE) +IN_PROC_BROWSER_TEST_F(WVEncryptedMediaTest, Playback_AudioOnly_WebM) { + TestMSESimplePlayback("bear-a-enc_a.webm", kWebMAudioOnly, + kWidevineKeySystem); +} + +IN_PROC_BROWSER_TEST_F(WVEncryptedMediaTest, Playback_AudioClearVideo_WebM) { + TestMSESimplePlayback("bear-320x240-av-enc_a.webm", kWebMAudioVideo, + kWidevineKeySystem); +} + +IN_PROC_BROWSER_TEST_F(WVEncryptedMediaTest, Playback_VideoAudio_WebM) { + TestMSESimplePlayback("bear-320x240-av-enc_av.webm", kWebMAudioVideo, + kWidevineKeySystem); +} + +IN_PROC_BROWSER_TEST_F(WVEncryptedMediaTest, Playback_VideoOnly_WebM) { + TestMSESimplePlayback("bear-320x240-v-enc_v.webm", kWebMVideoOnly, + kWidevineKeySystem); +} + +IN_PROC_BROWSER_TEST_F(WVEncryptedMediaTest, Playback_VideoClearAudio_WebM) { + TestMSESimplePlayback("bear-320x240-av-enc_v.webm", kWebMAudioVideo, + kWidevineKeySystem); +} + +#if defined(GOOGLE_CHROME_BUILD) || defined(USE_PROPRIETARY_CODECS) +IN_PROC_BROWSER_TEST_F(WVEncryptedMediaTest, Playback_VideoOnly_MP4) { + TestMSESimplePlayback("bear-640x360-v_frag-cenc.mp4", kMP4VideoOnly, + kWidevineKeySystem); +} + +IN_PROC_BROWSER_TEST_F(WVEncryptedMediaTest, Playback_AudioOnly_MP4) { + TestMSESimplePlayback("bear-640x360-a_frag-cenc.mp4", kMP4AudioOnly, + kWidevineKeySystem); +} +#endif // defined(GOOGLE_CHROME_BUILD) || defined(USE_PROPRIETARY_CODECS) +#endif // defined(WIDEVINE_CDM_AVAILABLE) + +} // namespace content diff --git a/chromium/content/browser/media/media_browsertest.cc b/chromium/content/browser/media/media_browsertest.cc new file mode 100644 index 00000000000..1d830354c9e --- /dev/null +++ b/chromium/content/browser/media/media_browsertest.cc @@ -0,0 +1,245 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/media/media_browsertest.h" + +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/url_constants.h" +#include "content/public/test/browser_test_utils.h" +#include "content/shell/shell.h" +#include "content/test/content_browser_test_utils.h" + +// TODO(wolenetz): Fix Media.YUV* tests on MSVS 2012 x64. crbug.com/180074 +#if defined(OS_WIN) && defined(ARCH_CPU_X86_64) && _MSC_VER == 1700 +#define MAYBE(x) DISABLED_##x +#else +#define MAYBE(x) x +#endif + +namespace content { + +// Common test results. +const char MediaBrowserTest::kEnded[] = "ENDED"; +const char MediaBrowserTest::kError[] = "ERROR"; +const char MediaBrowserTest::kFailed[] = "FAILED"; + +void MediaBrowserTest::SetUp() { + // TODO(danakj): The GPU Video Decoder needs real GL bindings. + // crbug.com/269087 + UseRealGLBindings(); + + ContentBrowserTest::SetUp(); +} + +void MediaBrowserTest::RunMediaTestPage( + const char* html_page, std::vector<StringPair>* query_params, + const char* expected, bool http) { + GURL gurl; + std::string query = ""; + if (query_params != NULL && !query_params->empty()) { + std::vector<StringPair>::const_iterator itr = query_params->begin(); + query = base::StringPrintf("%s=%s", itr->first, itr->second); + ++itr; + for (; itr != query_params->end(); ++itr) { + query.append(base::StringPrintf("&%s=%s", itr->first, itr->second)); + } + } + if (http) { + ASSERT_TRUE(test_server()->Start()); + gurl = test_server()->GetURL( + base::StringPrintf("files/media/%s?%s", html_page, query.c_str())); + } else { + base::FilePath test_file_path = GetTestFilePath("media", html_page); + gurl = GetFileUrlWithQuery(test_file_path, query); + } + RunTest(gurl, expected); +} + +void MediaBrowserTest::RunTest(const GURL& gurl, const char* expected) { + const string16 kExpected = ASCIIToUTF16(expected); + DVLOG(1) << "Running test URL: " << gurl; + TitleWatcher title_watcher(shell()->web_contents(), kExpected); + title_watcher.AlsoWaitForTitle(ASCIIToUTF16(kEnded)); + title_watcher.AlsoWaitForTitle(ASCIIToUTF16(kError)); + title_watcher.AlsoWaitForTitle(ASCIIToUTF16(kFailed)); + NavigateToURL(shell(), gurl); + + string16 final_title = title_watcher.WaitAndGetTitle(); + EXPECT_EQ(kExpected, final_title); +} + +// Tests playback and seeking of an audio or video file over file or http based +// on a test parameter. Test starts with playback, then, after X seconds or the +// ended event fires, seeks near end of file; see player.html for details. The +// test completes when either the last 'ended' or an 'error' event fires. +class MediaTest : public testing::WithParamInterface<bool>, + public MediaBrowserTest { + public: + // Play specified audio over http:// or file:// depending on |http| setting. + void PlayAudio(const char* media_file, bool http) { + PlayMedia("audio", media_file, http); + } + + // Play specified video over http:// or file:// depending on |http| setting. + void PlayVideo(const char* media_file, bool http) { + PlayMedia("video", media_file, http); + } + + // Run specified color format test with the expected result. + void RunColorFormatTest(const char* media_file, const char* expected) { + base::FilePath test_file_path = GetTestFilePath("media", "blackwhite.html"); + RunTest(GetFileUrlWithQuery(test_file_path, media_file), expected); + } + + void PlayMedia(const char* tag, const char* media_file, bool http) { + std::vector<StringPair> query_params; + query_params.push_back(std::make_pair(tag, media_file)); + RunMediaTestPage("player.html", &query_params, kEnded, http); + } +}; + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearTheora) { + PlayVideo("bear.ogv", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearSilentTheora) { + PlayVideo("bear_silent.ogv", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearWebm) { + PlayVideo("bear.webm", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearSilentWebm) { + PlayVideo("bear_silent.webm", GetParam()); +} + +#if defined(GOOGLE_CHROME_BUILD) || defined(USE_PROPRIETARY_CODECS) +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearMp4) { + PlayVideo("bear.mp4", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearSilentMp4) { + PlayVideo("bear_silent.mp4", GetParam()); +} + +// While we support the big endian (be) PCM codecs on Chromium, Quicktime seems +// to be the only creator of this format and only for .mov files. +// TODO(dalecurtis/ihf): Find or create some .wav test cases for "be" format. +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearMovPcmS16be) { + PlayVideo("bear_pcm_s16be.mov", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearMovPcmS24be) { + PlayVideo("bear_pcm_s24be.mov", GetParam()); +} +#endif + +#if defined(OS_CHROMEOS) +#if defined(GOOGLE_CHROME_BUILD) || defined(USE_PROPRIETARY_CODECS) +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearAviMp3Mpeg4) { + PlayVideo("bear_mpeg4_mp3.avi", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearAviMp3Mpeg4Asp) { + PlayVideo("bear_mpeg4asp_mp3.avi", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearAviMp3Divx) { + PlayVideo("bear_divx_mp3.avi", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBear3gpAacH264) { + PlayVideo("bear_h264_aac.3gp", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBear3gpAmrnbMpeg4) { + PlayVideo("bear_mpeg4_amrnb.3gp", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearWavGsmms) { + PlayAudio("bear_gsm_ms.wav", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearWavMulaw) { + PlayAudio("bear_mulaw.wav", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearFlac) { + PlayAudio("bear.flac", GetParam()); +} +#endif +#endif + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearWavPcm) { + PlayAudio("bear_pcm.wav", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearWavPcm3kHz) { + PlayAudio("bear_3kHz.wav", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoBearWavPcm192kHz) { + PlayAudio("bear_192kHz.wav", GetParam()); +} + +IN_PROC_BROWSER_TEST_P(MediaTest, VideoTulipWebm) { + PlayVideo("tulip2.webm", GetParam()); +} + +// Covers tear-down when navigating away as opposed to browser exiting. +IN_PROC_BROWSER_TEST_F(MediaTest, Navigate) { + PlayVideo("bear.ogv", false); + NavigateToURL(shell(), GURL(kAboutBlankURL)); + EXPECT_FALSE(shell()->web_contents()->IsCrashed()); +} + +INSTANTIATE_TEST_CASE_P(File, MediaTest, ::testing::Values(false)); +INSTANTIATE_TEST_CASE_P(Http, MediaTest, ::testing::Values(true)); + +IN_PROC_BROWSER_TEST_F(MediaTest, MAYBE(Yuv420pTheora)) { + RunColorFormatTest("yuv420p.ogv", "ENDED"); +} + +IN_PROC_BROWSER_TEST_F(MediaTest, MAYBE(Yuv422pTheora)) { + RunColorFormatTest("yuv422p.ogv", "ENDED"); +} + +IN_PROC_BROWSER_TEST_F(MediaTest, MAYBE(Yuv444pTheora)) { + // TODO(scherkus): Support YUV444 http://crbug.com/104711 + RunColorFormatTest("yuv424p.ogv", "ERROR"); +} + +IN_PROC_BROWSER_TEST_F(MediaTest, MAYBE(Yuv420pVp8)) { + RunColorFormatTest("yuv420p.webm", "ENDED"); +} + +#if defined(GOOGLE_CHROME_BUILD) || defined(USE_PROPRIETARY_CODECS) +IN_PROC_BROWSER_TEST_F(MediaTest, MAYBE(Yuv420pH264)) { + RunColorFormatTest("yuv420p.mp4", "ENDED"); +} + +IN_PROC_BROWSER_TEST_F(MediaTest, MAYBE(Yuvj420pH264)) { + RunColorFormatTest("yuvj420p.mp4", "ENDED"); +} + +IN_PROC_BROWSER_TEST_F(MediaTest, MAYBE(Yuv422pH264)) { + RunColorFormatTest("yuv422p.mp4", "ENDED"); +} + +IN_PROC_BROWSER_TEST_F(MediaTest, MAYBE(Yuv444pH264)) { + // TODO(scherkus): Support YUV444 http://crbug.com/104711 + RunColorFormatTest("yuv444p.mp4", "ERROR"); +} + +#if defined(OS_CHROMEOS) +IN_PROC_BROWSER_TEST_F(MediaTest, Yuv420pMpeg4) { + RunColorFormatTest("yuv420p.avi", "ENDED"); +} +#endif +#endif + +} // namespace content diff --git a/chromium/content/browser/media/media_browsertest.h b/chromium/content/browser/media/media_browsertest.h new file mode 100644 index 00000000000..013283b1ed6 --- /dev/null +++ b/chromium/content/browser/media/media_browsertest.h @@ -0,0 +1,35 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/test/content_browser_test.h" + +namespace content { + +// Class used to automate running media related browser tests. The functions +// assume that media files are located under files/media/ folder known to +// the test http server. +class MediaBrowserTest : public ContentBrowserTest { + public: + static const char kEnded[]; + static const char kError[]; + static const char kFailed[]; + + typedef std::pair<const char*, const char*> StringPair; + + virtual void SetUp() OVERRIDE; + + // Runs a html page with a list of URL query parameters. + // If http is true, the test starts a local http test server to load the test + // page, otherwise a local file URL is loaded inside the content shell. + // It uses RunTest() to check for expected test output. + void RunMediaTestPage(const char* html_page, + std::vector<StringPair>* query_params, + const char* expected, bool http); + + // Opens a URL and waits for the document title to match either one of the + // default strings or the expected string. + void RunTest(const GURL& gurl, const char* expected); +}; + +} // namespace content diff --git a/chromium/content/browser/media/media_internals.cc b/chromium/content/browser/media/media_internals.cc new file mode 100644 index 00000000000..273eeaeaf9f --- /dev/null +++ b/chromium/content/browser/media/media_internals.cc @@ -0,0 +1,136 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/media/media_internals.h" + +#include "base/memory/scoped_ptr.h" +#include "base/strings/string16.h" +#include "base/strings/stringprintf.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/web_ui.h" +#include "media/base/media_log.h" +#include "media/base/media_log_event.h" + +namespace content { + +MediaInternals* MediaInternals::GetInstance() { + return Singleton<MediaInternals>::get(); +} + +MediaInternals::~MediaInternals() {} + +void MediaInternals::OnDeleteAudioStream(void* host, int stream_id) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + std::string stream = base::StringPrintf("audio_streams.%p:%d", + host, stream_id); + DeleteItem(stream); +} + +void MediaInternals::OnSetAudioStreamPlaying( + void* host, int stream_id, bool playing) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + UpdateAudioStream(host, stream_id, + "playing", new base::FundamentalValue(playing)); +} + +void MediaInternals::OnSetAudioStreamStatus( + void* host, int stream_id, const std::string& status) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + UpdateAudioStream(host, stream_id, + "status", new base::StringValue(status)); +} + +void MediaInternals::OnSetAudioStreamVolume( + void* host, int stream_id, double volume) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + UpdateAudioStream(host, stream_id, + "volume", new base::FundamentalValue(volume)); +} + +void MediaInternals::OnMediaEvents( + int render_process_id, const std::vector<media::MediaLogEvent>& events) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + + // Notify observers that |event| has occured. + for (std::vector<media::MediaLogEvent>::const_iterator event = events.begin(); + event != events.end(); ++event) { + base::DictionaryValue dict; + dict.SetInteger("renderer", render_process_id); + dict.SetInteger("player", event->id); + dict.SetString("type", media::MediaLog::EventTypeToString(event->type)); + + int64 ticks = event->time.ToInternalValue(); + double ticks_millis = + ticks / static_cast<double>(base::Time::kMicrosecondsPerMillisecond); + + dict.SetDouble("ticksMillis", ticks_millis); + dict.Set("params", event->params.DeepCopy()); + SendUpdate("media.onMediaEvent", &dict); + } +} + +void MediaInternals::AddUpdateCallback(const UpdateCallback& callback) { + update_callbacks_.push_back(callback); +} + +void MediaInternals::RemoveUpdateCallback(const UpdateCallback& callback) { + for (size_t i = 0; i < update_callbacks_.size(); ++i) { + if (update_callbacks_[i].Equals(callback)) { + update_callbacks_.erase(update_callbacks_.begin() + i); + return; + } + } + NOTREACHED(); +} + +void MediaInternals::SendEverything() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + SendUpdate("media.onReceiveEverything", &data_); +} + +MediaInternals::MediaInternals() { +} + +void MediaInternals::UpdateAudioStream(void* host, + int stream_id, + const std::string& property, + base::Value* value) { + std::string stream = base::StringPrintf("audio_streams.%p:%d", + host, stream_id); + UpdateItem("media.addAudioStream", stream, property, value); +} + +void MediaInternals::DeleteItem(const std::string& item) { + data_.Remove(item, NULL); + scoped_ptr<base::Value> value(new base::StringValue(item)); + SendUpdate("media.onItemDeleted", value.get()); +} + +void MediaInternals::UpdateItem( + const std::string& update_fn, const std::string& id, + const std::string& property, base::Value* value) { + base::DictionaryValue* item_properties; + if (!data_.GetDictionary(id, &item_properties)) { + item_properties = new base::DictionaryValue(); + data_.Set(id, item_properties); + item_properties->SetString("id", id); + } + item_properties->Set(property, value); + SendUpdate(update_fn, item_properties); +} + +void MediaInternals::SendUpdate(const std::string& function, + base::Value* value) { + // Only bother serializing the update to JSON if someone is watching. + if (update_callbacks_.empty()) + return; + + std::vector<const base::Value*> args; + args.push_back(value); + string16 update = WebUI::GetJavascriptCall(function, args); + for (size_t i = 0; i < update_callbacks_.size(); i++) + update_callbacks_[i].Run(update); +} + +} // namespace content diff --git a/chromium/content/browser/media/media_internals.h b/chromium/content/browser/media/media_internals.h new file mode 100644 index 00000000000..4a4d2efc7d1 --- /dev/null +++ b/chromium/content/browser/media/media_internals.h @@ -0,0 +1,96 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_H_ +#define CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_H_ + +#include <string> +#include <vector> + +#include "base/memory/ref_counted.h" +#include "base/memory/singleton.h" +#include "base/strings/string16.h" +#include "base/values.h" +#include "content/common/content_export.h" +#include "content/public/common/media_stream_request.h" + +namespace media { +struct MediaLogEvent; +} + +namespace content { + +// This class stores information about currently active media. +// It's constructed on the UI thread but all of its methods are called on the IO +// thread. +class CONTENT_EXPORT MediaInternals { + public: + virtual ~MediaInternals(); + + static MediaInternals* GetInstance(); + + // The following methods are virtual for gmock. + + // Called when an audio stream is deleted. + virtual void OnDeleteAudioStream(void* host, int stream_id); + + // Called when an audio stream is set to playing or paused. + virtual void OnSetAudioStreamPlaying(void* host, int stream_id, + bool playing); + + // Called when the status of an audio stream is set to "created", "closed", or + // "error". + virtual void OnSetAudioStreamStatus(void* host, int stream_id, + const std::string& status); + + // Called when the volume of an audio stream is set. + virtual void OnSetAudioStreamVolume(void* host, int stream_id, + double volume); + + // Called when a MediaEvent occurs. + virtual void OnMediaEvents(int render_process_id, + const std::vector<media::MediaLogEvent>& events); + + // Called with the update string. + typedef base::Callback<void(const string16&)> UpdateCallback; + + // Add/remove update callbacks (see above). + void AddUpdateCallback(const UpdateCallback& callback); + void RemoveUpdateCallback(const UpdateCallback& callback); + void SendEverything(); + + private: + friend class MockMediaInternals; + friend class MediaInternalsTest; + friend struct DefaultSingletonTraits<MediaInternals>; + + MediaInternals(); + + // Sets |property| of an audio stream to |value| and notifies observers. + // (host, stream_id) is a unique id for the audio stream. + // |host| will never be dereferenced. + void UpdateAudioStream(void* host, int stream_id, + const std::string& property, base::Value* value); + + // Removes |item| from |data_|. + void DeleteItem(const std::string& item); + + // Sets data_.id.property = value and notifies attached UIs using update_fn. + // id may be any depth, e.g. "video.decoders.1.2.3" + void UpdateItem(const std::string& update_fn, const std::string& id, + const std::string& property, base::Value* value); + + // Calls javascript |function|(|value|) on each attached UI. + void SendUpdate(const std::string& function, base::Value* value); + + base::DictionaryValue data_; + + std::vector<UpdateCallback> update_callbacks_; + + DISALLOW_COPY_AND_ASSIGN(MediaInternals); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_H_ diff --git a/chromium/content/browser/media/media_internals_handler.cc b/chromium/content/browser/media/media_internals_handler.cc new file mode 100644 index 00000000000..1ea6b04c663 --- /dev/null +++ b/chromium/content/browser/media/media_internals_handler.cc @@ -0,0 +1,46 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/media/media_internals_handler.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/values.h" +#include "content/browser//media/media_internals_proxy.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_view_host.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" + +namespace content { + +MediaInternalsMessageHandler::MediaInternalsMessageHandler() + : proxy_(new MediaInternalsProxy()) {} + +MediaInternalsMessageHandler::~MediaInternalsMessageHandler() { + proxy_->Detach(); +} + +void MediaInternalsMessageHandler::RegisterMessages() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + proxy_->Attach(this); + + web_ui()->RegisterMessageCallback("getEverything", + base::Bind(&MediaInternalsMessageHandler::OnGetEverything, + base::Unretained(this))); +} + +void MediaInternalsMessageHandler::OnGetEverything( + const base::ListValue* list) { + proxy_->GetEverything(); +} + +void MediaInternalsMessageHandler::OnUpdate(const string16& update) { + // Don't try to execute JavaScript in a RenderView that no longer exists. + RenderViewHost* host = web_ui()->GetWebContents()->GetRenderViewHost(); + if (host) + host->ExecuteJavascriptInWebFrame(string16(), update); +} + +} // namespace content diff --git a/chromium/content/browser/media/media_internals_handler.h b/chromium/content/browser/media/media_internals_handler.h new file mode 100644 index 00000000000..4833aee285f --- /dev/null +++ b/chromium/content/browser/media/media_internals_handler.h @@ -0,0 +1,43 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_HANDLER_H_ +#define CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_HANDLER_H_ + +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "content/public/browser/web_ui_message_handler.h" + +namespace base { +class ListValue; +} + +namespace content { +class MediaInternalsProxy; + +// This class handles messages to and from MediaInternalsUI. +// It does all its work on the IO thread through the proxy below. +class MediaInternalsMessageHandler : public WebUIMessageHandler { + public: + MediaInternalsMessageHandler(); + virtual ~MediaInternalsMessageHandler(); + + // WebUIMessageHandler implementation. + virtual void RegisterMessages() OVERRIDE; + + // Javascript message handlers. + void OnGetEverything(const base::ListValue* list); + + // MediaInternals message handlers. + void OnUpdate(const string16& update); + + private: + scoped_refptr<MediaInternalsProxy> proxy_; + + DISALLOW_COPY_AND_ASSIGN(MediaInternalsMessageHandler); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_HANDLER_H_ diff --git a/chromium/content/browser/media/media_internals_proxy.cc b/chromium/content/browser/media/media_internals_proxy.cc new file mode 100644 index 00000000000..d0e0622b41b --- /dev/null +++ b/chromium/content/browser/media/media_internals_proxy.cc @@ -0,0 +1,182 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/media/media_internals_proxy.h" + +#include "base/bind.h" +#include "base/message_loop/message_loop.h" +#include "content/browser/media/media_internals_handler.h" +#include "content/public/browser/content_browser_client.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/web_ui.h" + +namespace content { + +static const int kMediaInternalsProxyEventDelayMilliseconds = 100; + +static const net::NetLog::EventType kNetEventTypeFilter[] = { + net::NetLog::TYPE_DISK_CACHE_ENTRY_IMPL, + net::NetLog::TYPE_SPARSE_READ, + net::NetLog::TYPE_SPARSE_WRITE, + net::NetLog::TYPE_URL_REQUEST_START_JOB, + net::NetLog::TYPE_HTTP_TRANSACTION_READ_RESPONSE_HEADERS, +}; + +MediaInternalsProxy::MediaInternalsProxy() { + registrar_.Add(this, NOTIFICATION_RENDERER_PROCESS_TERMINATED, + NotificationService::AllBrowserContextsAndSources()); +} + +void MediaInternalsProxy::Observe(int type, + const NotificationSource& source, + const NotificationDetails& details) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + DCHECK_EQ(type, NOTIFICATION_RENDERER_PROCESS_TERMINATED); + RenderProcessHost* process = Source<RenderProcessHost>(source).ptr(); + CallJavaScriptFunctionOnUIThread("media.onRendererTerminated", + new base::FundamentalValue(process->GetID())); +} + +void MediaInternalsProxy::Attach(MediaInternalsMessageHandler* handler) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + handler_ = handler; + BrowserThread::PostTask( + BrowserThread::IO, FROM_HERE, + base::Bind(&MediaInternalsProxy::ObserveMediaInternalsOnIOThread, this)); +} + +void MediaInternalsProxy::Detach() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + handler_ = NULL; + BrowserThread::PostTask( + BrowserThread::IO, FROM_HERE, + base::Bind( + &MediaInternalsProxy::StopObservingMediaInternalsOnIOThread, this)); +} + +void MediaInternalsProxy::GetEverything() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + // Ask MediaInternals for all its data. + BrowserThread::PostTask( + BrowserThread::IO, FROM_HERE, + base::Bind(&MediaInternalsProxy::GetEverythingOnIOThread, this)); + + // Send the page names for constants. + CallJavaScriptFunctionOnUIThread("media.onReceiveConstants", GetConstants()); +} + +void MediaInternalsProxy::OnUpdate(const string16& update) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + BrowserThread::PostTask( + BrowserThread::UI, FROM_HERE, + base::Bind(&MediaInternalsProxy::UpdateUIOnUIThread, this, update)); +} + +void MediaInternalsProxy::OnAddEntry(const net::NetLog::Entry& entry) { + bool is_event_interesting = false; + for (size_t i = 0; i < arraysize(kNetEventTypeFilter); i++) { + if (entry.type() == kNetEventTypeFilter[i]) { + is_event_interesting = true; + break; + } + } + + if (!is_event_interesting) + return; + + BrowserThread::PostTask( + BrowserThread::UI, FROM_HERE, + base::Bind(&MediaInternalsProxy::AddNetEventOnUIThread, this, + entry.ToValue())); +} + +MediaInternalsProxy::~MediaInternalsProxy() {} + +base::Value* MediaInternalsProxy::GetConstants() { + base::DictionaryValue* event_phases = new base::DictionaryValue(); + event_phases->SetInteger( + net::NetLog::EventPhaseToString(net::NetLog::PHASE_NONE), + net::NetLog::PHASE_NONE); + event_phases->SetInteger( + net::NetLog::EventPhaseToString(net::NetLog::PHASE_BEGIN), + net::NetLog::PHASE_BEGIN); + event_phases->SetInteger( + net::NetLog::EventPhaseToString(net::NetLog::PHASE_END), + net::NetLog::PHASE_END); + + base::DictionaryValue* constants = new base::DictionaryValue(); + constants->Set("eventTypes", net::NetLog::GetEventTypesAsValue()); + constants->Set("eventPhases", event_phases); + + return constants; +} + +void MediaInternalsProxy::ObserveMediaInternalsOnIOThread() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + update_callback_ = base::Bind(&MediaInternalsProxy::OnUpdate, + base::Unretained(this)); + MediaInternals::GetInstance()->AddUpdateCallback(update_callback_); + if (GetContentClient()->browser()->GetNetLog()) { + net::NetLog* net_log = GetContentClient()->browser()->GetNetLog(); + net_log->AddThreadSafeObserver(this, net::NetLog::LOG_ALL_BUT_BYTES); + } +} + +void MediaInternalsProxy::StopObservingMediaInternalsOnIOThread() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + MediaInternals::GetInstance()->RemoveUpdateCallback(update_callback_); + if (GetContentClient()->browser()->GetNetLog()) { + net::NetLog* net_log = GetContentClient()->browser()->GetNetLog(); + net_log->RemoveThreadSafeObserver(this); + } +} + +void MediaInternalsProxy::GetEverythingOnIOThread() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + MediaInternals::GetInstance()->SendEverything(); +} + +void MediaInternalsProxy::UpdateUIOnUIThread(const string16& update) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + // Don't forward updates to a destructed UI. + if (handler_) + handler_->OnUpdate(update); +} + +void MediaInternalsProxy::AddNetEventOnUIThread(base::Value* entry) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + // Send the updates to the page in kMediaInternalsProxyEventDelayMilliseconds + // if an update is not already pending. + if (!pending_net_updates_) { + pending_net_updates_.reset(new base::ListValue()); + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&MediaInternalsProxy::SendNetEventsOnUIThread, this), + base::TimeDelta::FromMilliseconds( + kMediaInternalsProxyEventDelayMilliseconds)); + } + pending_net_updates_->Append(entry); +} + +void MediaInternalsProxy::SendNetEventsOnUIThread() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + CallJavaScriptFunctionOnUIThread("media.onNetUpdate", + pending_net_updates_.release()); +} + +void MediaInternalsProxy::CallJavaScriptFunctionOnUIThread( + const std::string& function, base::Value* args) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + scoped_ptr<base::Value> args_value(args); + std::vector<const base::Value*> args_vector; + args_vector.push_back(args_value.get()); + string16 update = WebUI::GetJavascriptCall(function, args_vector); + UpdateUIOnUIThread(update); +} + +} // namespace content diff --git a/chromium/content/browser/media/media_internals_proxy.h b/chromium/content/browser/media/media_internals_proxy.h new file mode 100644 index 00000000000..5d173b3e738 --- /dev/null +++ b/chromium/content/browser/media/media_internals_proxy.h @@ -0,0 +1,90 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_PROXY_H_ +#define CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_PROXY_H_ + +#include "base/memory/ref_counted.h" +#include "base/sequenced_task_runner_helpers.h" +#include "content/browser/media/media_internals.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" +#include "net/base/net_log.h" + +namespace base { +class ListValue; +class Value; +} + +namespace content { +class MediaInternalsMessageHandler; + +// This class is a proxy between MediaInternals (on the IO thread) and +// MediaInternalsMessageHandler (on the UI thread). +// It is ref_counted to ensure that it completes all pending Tasks on both +// threads before destruction. +class MediaInternalsProxy + : public base::RefCountedThreadSafe< + MediaInternalsProxy, + BrowserThread::DeleteOnUIThread>, + public net::NetLog::ThreadSafeObserver, + public NotificationObserver { + public: + MediaInternalsProxy(); + + // NotificationObserver implementation. + virtual void Observe(int type, + const NotificationSource& source, + const NotificationDetails& details) OVERRIDE; + + // Register a Handler and start receiving callbacks from MediaInternals. + void Attach(MediaInternalsMessageHandler* handler); + + // Unregister the same and stop receiving callbacks. + void Detach(); + + // Have MediaInternals send all the data it has. + void GetEverything(); + + // MediaInternals callback. Called on the IO thread. + void OnUpdate(const string16& update); + + // net::NetLog::ThreadSafeObserver implementation. Callable from any thread: + virtual void OnAddEntry(const net::NetLog::Entry& entry) OVERRIDE; + + private: + friend struct BrowserThread::DeleteOnThread<BrowserThread::UI>; + friend class base::DeleteHelper<MediaInternalsProxy>; + virtual ~MediaInternalsProxy(); + + // Build a dictionary mapping constant names to values. + base::Value* GetConstants(); + + void ObserveMediaInternalsOnIOThread(); + void StopObservingMediaInternalsOnIOThread(); + void GetEverythingOnIOThread(); + void UpdateUIOnUIThread(const string16& update); + + // Put |entry| on a list of events to be sent to the page. + void AddNetEventOnUIThread(base::Value* entry); + + // Send all pending events to the page. + void SendNetEventsOnUIThread(); + + // Call a JavaScript function on the page. Takes ownership of |args|. + void CallJavaScriptFunctionOnUIThread(const std::string& function, + base::Value* args); + + MediaInternalsMessageHandler* handler_; + scoped_ptr<base::ListValue> pending_net_updates_; + NotificationRegistrar registrar_; + MediaInternals::UpdateCallback update_callback_; + + DISALLOW_COPY_AND_ASSIGN(MediaInternalsProxy); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_PROXY_H_ diff --git a/chromium/content/browser/media/media_internals_ui.cc b/chromium/content/browser/media/media_internals_ui.cc new file mode 100644 index 00000000000..83638148bf9 --- /dev/null +++ b/chromium/content/browser/media/media_internals_ui.cc @@ -0,0 +1,53 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/media/media_internals_ui.h" + +#include "base/command_line.h" +#include "content/browser/media/media_internals_handler.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" +#include "content/public/browser/web_ui_data_source.h" +#include "content/public/common/content_switches.h" +#include "content/public/common/url_constants.h" +#include "grit/content_resources.h" + +namespace content { +namespace { + +WebUIDataSource* CreateMediaInternalsHTMLSource() { + WebUIDataSource* source = + WebUIDataSource::Create(kChromeUIMediaInternalsHost); + + source->SetJsonPath("strings.js"); + if (CommandLine::ForCurrentProcess()->HasSwitch( + switches::kEnableNewMediaInternals)) { + source->AddResourcePath("media_internals.js", IDR_MEDIA_INTERNALS_NEW_JS); + source->SetDefaultResource(IDR_MEDIA_INTERNALS_NEW_HTML); + return source; + } + + source->AddResourcePath("media_internals.js", IDR_MEDIA_INTERNALS_JS); + source->SetDefaultResource(IDR_MEDIA_INTERNALS_HTML); + return source; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +// +// MediaInternalsUI +// +//////////////////////////////////////////////////////////////////////////////// + +MediaInternalsUI::MediaInternalsUI(WebUI* web_ui) + : WebUIController(web_ui) { + web_ui->AddMessageHandler(new MediaInternalsMessageHandler()); + + BrowserContext* browser_context = + web_ui->GetWebContents()->GetBrowserContext(); + WebUIDataSource::Add(browser_context, CreateMediaInternalsHTMLSource()); +} + +} // namespace content diff --git a/chromium/content/browser/media/media_internals_ui.h b/chromium/content/browser/media/media_internals_ui.h new file mode 100644 index 00000000000..f7a17a6765a --- /dev/null +++ b/chromium/content/browser/media/media_internals_ui.h @@ -0,0 +1,23 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_UI_H_ +#define CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_UI_H_ + +#include "content/public/browser/web_ui_controller.h" + +namespace content { + +// The implementation for the chrome://media-internals page. +class MediaInternalsUI : public WebUIController { + public: + explicit MediaInternalsUI(WebUI* web_ui); + + private: + DISALLOW_COPY_AND_ASSIGN(MediaInternalsUI); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_MEDIA_MEDIA_INTERNALS_UI_H_ diff --git a/chromium/content/browser/media/media_internals_unittest.cc b/chromium/content/browser/media/media_internals_unittest.cc new file mode 100644 index 00000000000..58c8f91dc68 --- /dev/null +++ b/chromium/content/browser/media/media_internals_unittest.cc @@ -0,0 +1,134 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/media/media_internals.h" + +#include "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "content/public/test/test_browser_thread.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { +namespace { + +class MockObserverBaseClass { + public: + ~MockObserverBaseClass() {} + virtual void OnUpdate(const string16& javascript) = 0; +}; + +class MockMediaInternalsObserver : public MockObserverBaseClass { + public: + virtual ~MockMediaInternalsObserver() {} + MOCK_METHOD1(OnUpdate, void(const string16& javascript)); +}; + +} // namespace + +class MediaInternalsTest : public testing::Test { + public: + MediaInternalsTest() : io_thread_(BrowserThread::IO, &loop_) {} + base::DictionaryValue* data() { + return &internals_->data_; + } + + void DeleteItem(const std::string& item) { + internals_->DeleteItem(item); + } + + void UpdateItem(const std::string& item, const std::string& property, + base::Value* value) { + internals_->UpdateItem(std::string(), item, property, value); + } + + void SendUpdate(const std::string& function, base::Value* value) { + internals_->SendUpdate(function, value); + } + + protected: + virtual void SetUp() { + internals_.reset(new MediaInternals()); + } + + base::MessageLoop loop_; + TestBrowserThread io_thread_; + scoped_ptr<MediaInternals> internals_; +}; + +TEST_F(MediaInternalsTest, UpdateAddsNewItem) { + UpdateItem("some.item", "testing", new base::FundamentalValue(true)); + bool testing = false; + std::string id; + + EXPECT_TRUE(data()->GetBoolean("some.item.testing", &testing)); + EXPECT_TRUE(testing); + + EXPECT_TRUE(data()->GetString("some.item.id", &id)); + EXPECT_EQ(id, "some.item"); +} + +TEST_F(MediaInternalsTest, UpdateModifiesExistingItem) { + UpdateItem("some.item", "testing", new base::FundamentalValue(true)); + UpdateItem("some.item", "value", new base::FundamentalValue(5)); + UpdateItem("some.item", "testing", new base::FundamentalValue(false)); + bool testing = true; + int value = 0; + std::string id; + + EXPECT_TRUE(data()->GetBoolean("some.item.testing", &testing)); + EXPECT_FALSE(testing); + + EXPECT_TRUE(data()->GetInteger("some.item.value", &value)); + EXPECT_EQ(value, 5); + + EXPECT_TRUE(data()->GetString("some.item.id", &id)); + EXPECT_EQ(id, "some.item"); +} + +TEST_F(MediaInternalsTest, ObserversReceiveNotifications) { + scoped_ptr<MockMediaInternalsObserver> observer( + new MockMediaInternalsObserver()); + + EXPECT_CALL(*observer.get(), OnUpdate(testing::_)).Times(1); + + MediaInternals::UpdateCallback callback = base::Bind( + &MockMediaInternalsObserver::OnUpdate, base::Unretained(observer.get())); + + internals_->AddUpdateCallback(callback); + SendUpdate("fn", data()); +} + +TEST_F(MediaInternalsTest, RemovedObserversReceiveNoNotifications) { + scoped_ptr<MockMediaInternalsObserver> observer( + new MockMediaInternalsObserver()); + + EXPECT_CALL(*observer.get(), OnUpdate(testing::_)).Times(0); + + MediaInternals::UpdateCallback callback = base::Bind( + &MockMediaInternalsObserver::OnUpdate, base::Unretained(observer.get())); + + internals_->AddUpdateCallback(callback); + internals_->RemoveUpdateCallback(callback); + SendUpdate("fn", data()); +} + +TEST_F(MediaInternalsTest, DeleteRemovesItem) { + base::Value* out; + + UpdateItem("some.item", "testing", base::Value::CreateNullValue()); + EXPECT_TRUE(data()->Get("some.item", &out)); + EXPECT_TRUE(data()->Get("some", &out)); + + DeleteItem("some.item"); + EXPECT_FALSE(data()->Get("some.item", &out)); + EXPECT_TRUE(data()->Get("some", &out)); + + DeleteItem("some"); + EXPECT_FALSE(data()->Get("some.item", &out)); + EXPECT_FALSE(data()->Get("some", &out)); +} + +} // namespace content diff --git a/chromium/content/browser/media/media_source_browsertest.cc b/chromium/content/browser/media/media_source_browsertest.cc new file mode 100644 index 00000000000..bd119ddff07 --- /dev/null +++ b/chromium/content/browser/media/media_source_browsertest.cc @@ -0,0 +1,57 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/command_line.h" +#include "content/browser/media/media_browsertest.h" +#include "content/public/common/content_switches.h" + +// Common media types. +static const char kWebMAudioOnly[] = "audio/webm; codecs=\"vorbis\""; +static const char kWebMVideoOnly[] = "video/webm; codecs=\"vp8\""; +static const char kWebMAudioVideo[] = "video/webm; codecs=\"vorbis, vp8\""; + +namespace content { + +class MediaSourceTest : public content::MediaBrowserTest { + public: + void TestSimplePlayback(const char* media_file, const char* media_type, + const char* expectation) { + std::vector<StringPair> query_params; + query_params.push_back(std::make_pair("mediafile", media_file)); + query_params.push_back(std::make_pair("mediatype", media_type)); + RunMediaTestPage("media_source_player.html", &query_params, expectation, + true); + } + +#if defined(OS_ANDROID) + virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { + command_line->AppendSwitch( + switches::kDisableGestureRequirementForMediaPlayback); + } +#endif +}; + +IN_PROC_BROWSER_TEST_F(MediaSourceTest, Playback_VideoAudio_WebM) { + TestSimplePlayback("bear-320x240.webm", kWebMAudioVideo, kEnded); +} + +IN_PROC_BROWSER_TEST_F(MediaSourceTest, Playback_VideoOnly_WebM) { + TestSimplePlayback("bear-320x240-video-only.webm", kWebMVideoOnly, kEnded); +} + +IN_PROC_BROWSER_TEST_F(MediaSourceTest, Playback_AudioOnly_WebM) { + TestSimplePlayback("bear-320x240-audio-only.webm", kWebMAudioOnly, kEnded); +} + +IN_PROC_BROWSER_TEST_F(MediaSourceTest, Playback_Type_Error) { + TestSimplePlayback("bear-320x240-video-only.webm", kWebMAudioOnly, kError); +} + +// Flaky test crbug.com/246308 +// Test changed to skip checks resulting in flakiness. Proper fix still needed. +IN_PROC_BROWSER_TEST_F(MediaSourceTest, ConfigChangeVideo) { + RunMediaTestPage("mse_config_change.html", NULL, kEnded, true); +} + +} // namespace content diff --git a/chromium/content/browser/media/webrtc_browsertest.cc b/chromium/content/browser/media/webrtc_browsertest.cc new file mode 100644 index 00000000000..7cf48b0ad77 --- /dev/null +++ b/chromium/content/browser/media/webrtc_browsertest.cc @@ -0,0 +1,301 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/command_line.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "content/browser/web_contents/web_contents_impl.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "content/shell/shell.h" +#include "content/test/content_browser_test.h" +#include "content/test/content_browser_test_utils.h" +#include "net/test/embedded_test_server/embedded_test_server.h" + +#if defined(OS_WIN) +#include "base/win/windows_version.h" +#endif + +namespace { + +std::string GenerateGetUserMediaCall(int min_width, + int max_width, + int min_height, + int max_height, + int min_frame_rate, + int max_frame_rate) { + return base::StringPrintf( + "getUserMedia({video: {mandatory: {minWidth: %d, maxWidth: %d, " + "minHeight: %d, maxHeight: %d, minFrameRate: %d, maxFrameRate: %d}, " + "optional: []}});", + min_width, + max_width, + min_height, + max_height, + min_frame_rate, + max_frame_rate); +} +} + +namespace content { + +class WebrtcBrowserTest: public ContentBrowserTest { + public: + WebrtcBrowserTest() {} + virtual ~WebrtcBrowserTest() {} + + virtual void SetUpOnMainThread() OVERRIDE { + ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); + } + + virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { + // We need fake devices in this test since we want to run on naked VMs. We + // assume these switches are set by default in content_browsertests. + ASSERT_TRUE(CommandLine::ForCurrentProcess()->HasSwitch( + switches::kUseFakeDeviceForMediaStream)); + ASSERT_TRUE(CommandLine::ForCurrentProcess()->HasSwitch( + switches::kUseFakeUIForMediaStream)); + + // The video playback will not work without a GPU, so force its use here. + // This may not be available on all VMs though. + command_line->AppendSwitch(switches::kUseGpuInTests); + } + + protected: + bool ExecuteJavascript(const std::string& javascript) { + return ExecuteScript(shell()->web_contents(), javascript); + } + + void ExpectTitle(const std::string& expected_title) const { + string16 expected_title16(ASCIIToUTF16(expected_title)); + TitleWatcher title_watcher(shell()->web_contents(), expected_title16); + EXPECT_EQ(expected_title16, title_watcher.WaitAndGetTitle()); + } +}; + +// These tests will all make a getUserMedia call with different constraints and +// see that the success callback is called. If the error callback is called or +// none of the callbacks are called the tests will simply time out and fail. +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, GetVideoStreamAndStop) { + GURL url(embedded_test_server()->GetURL("/media/getusermedia.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE(ExecuteJavascript("getUserMedia({video: true});")); + + ExpectTitle("OK"); +} + +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, GetAudioAndVideoStreamAndStop) { + GURL url(embedded_test_server()->GetURL("/media/getusermedia.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE(ExecuteJavascript("getUserMedia({video: true, audio: true});")); + + ExpectTitle("OK"); +} + +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, GetAudioAndVideoStreamAndClone) { + GURL url(embedded_test_server()->GetURL("/media/getusermedia.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE(ExecuteJavascript("getUserMediaAndClone();")); + + ExpectTitle("OK"); +} + + +#if defined(OS_LINUX) && !defined(OS_CHROMEOS) && defined(ARCH_CPU_ARM_FAMILY) +// Timing out on ARM linux bot: http://crbug.com/238490 +#define MAYBE_CanSetupVideoCall DISABLED_CanSetupVideoCall +#else +#define MAYBE_CanSetupVideoCall CanSetupVideoCall +#endif + +// These tests will make a complete PeerConnection-based call and verify that +// video is playing for the call. +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, MAYBE_CanSetupVideoCall) { + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE(ExecuteJavascript("call({video: true});")); + ExpectTitle("OK"); +} + +#if defined(OS_LINUX) && !defined(OS_CHROMEOS) && defined(ARCH_CPU_ARM_FAMILY) +// Timing out on ARM linux, see http://crbug.com/240376 +#define MAYBE_CanSetupAudioAndVideoCall DISABLED_CanSetupAudioAndVideoCall +#else +#define MAYBE_CanSetupAudioAndVideoCall CanSetupAudioAndVideoCall +#endif + +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, MAYBE_CanSetupAudioAndVideoCall) { + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE(ExecuteJavascript("call({video: true, audio: true});")); + ExpectTitle("OK"); +} + +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, MANUAL_CanSetupCallAndSendDtmf) { + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE( + ExecuteJavascript("callAndSendDtmf('123,abc');")); +} + +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, + CanMakeEmptyCallThenAddStreamsAndRenegotiate) { + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + + const char* kJavascript = + "callEmptyThenAddOneStreamAndRenegotiate({video: true, audio: true});"; + EXPECT_TRUE(ExecuteJavascript(kJavascript)); + ExpectTitle("OK"); +} + +// This test will make a complete PeerConnection-based call but remove the +// MSID and bundle attribute from the initial offer to verify that +// video is playing for the call even if the initiating client don't support +// MSID. http://tools.ietf.org/html/draft-alvestrand-rtcweb-msid-02 +#if defined(OS_WIN) && defined(USE_AURA) +// Disabled for win7_aura, see http://crbug.com/235089. +#define MAYBE_CanSetupAudioAndVideoCallWithoutMsidAndBundle\ + DISABLED_CanSetupAudioAndVideoCallWithoutMsidAndBundle +#elif defined(OS_LINUX) && !defined(OS_CHROMEOS) && defined(ARCH_CPU_ARM_FAMILY) +// Timing out on ARM linux, see http://crbug.com/240373 +#define MAYBE_CanSetupAudioAndVideoCallWithoutMsidAndBundle\ + DISABLED_CanSetupAudioAndVideoCallWithoutMsidAndBundle +#else +#define MAYBE_CanSetupAudioAndVideoCallWithoutMsidAndBundle\ + CanSetupAudioAndVideoCallWithoutMsidAndBundle +#endif +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, + MAYBE_CanSetupAudioAndVideoCallWithoutMsidAndBundle) { + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE(ExecuteJavascript("callWithoutMsidAndBundle();")); + ExpectTitle("OK"); +} + +// This test will make a PeerConnection-based call and test an unreliable text +// dataChannel. +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, CallWithDataOnly) { + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE(ExecuteJavascript("callWithDataOnly();")); + ExpectTitle("OK"); +} + +#if defined(OS_LINUX) && !defined(OS_CHROMEOS) && defined(ARCH_CPU_ARM_FAMILY) +// Timing out on ARM linux bot: http://crbug.com/238490 +#define MAYBE_CallWithDataAndMedia DISABLED_CallWithDataAndMedia +#else +#define MAYBE_CallWithDataAndMedia CallWithDataAndMedia +#endif + +// This test will make a PeerConnection-based call and test an unreliable text +// dataChannel and audio and video tracks. +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, MAYBE_CallWithDataAndMedia) { + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE(ExecuteJavascript("callWithDataAndMedia();")); + ExpectTitle("OK"); +} + +#if defined(OS_LINUX) && !defined(OS_CHROMEOS) && defined(ARCH_CPU_ARM_FAMILY) +// Timing out on ARM linux bot: http://crbug.com/238490 +#define MAYBE_CallWithDataAndLaterAddMedia DISABLED_CallWithDataAndLaterAddMedia +#else +#define MAYBE_CallWithDataAndLaterAddMedia CallWithDataAndLaterAddMedia +#endif + +// This test will make a PeerConnection-based call and test an unreliable text +// dataChannel and later add an audio and video track. +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, MAYBE_CallWithDataAndLaterAddMedia) { + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE(ExecuteJavascript("callWithDataAndLaterAddMedia();")); + ExpectTitle("OK"); +} + +#if defined(OS_LINUX) && !defined(OS_CHROMEOS) && defined(ARCH_CPU_ARM_FAMILY) +// Timing out on ARM linux bot: http://crbug.com/238490 +#define MAYBE_CallWithNewVideoMediaStream DISABLED_CallWithNewVideoMediaStream +#else +#define MAYBE_CallWithNewVideoMediaStream CallWithNewVideoMediaStream +#endif + +// This test will make a PeerConnection-based call and send a new Video +// MediaStream that has been created based on a MediaStream created with +// getUserMedia. +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, MAYBE_CallWithNewVideoMediaStream) { + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE(ExecuteJavascript("callWithNewVideoMediaStream();")); + ExpectTitle("OK"); +} + +// This test will make a PeerConnection-based call and send a new Video +// MediaStream that has been created based on a MediaStream created with +// getUserMedia. When video is flowing, the VideoTrack is removed and an +// AudioTrack is added instead. +// TODO(phoglund): This test is manual since not all buildbots has an audio +// input. +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, MANUAL_CallAndModifyStream) { + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE( + ExecuteJavascript("callWithNewVideoMediaStreamLaterSwitchToAudio();")); + ExpectTitle("OK"); +} + +// This test calls getUserMedia in sequence with different constraints. +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, TestGetUserMediaConstraints) { + GURL url(embedded_test_server()->GetURL("/media/getusermedia.html")); + + std::vector<std::string> list_of_get_user_media_calls; + list_of_get_user_media_calls.push_back( + GenerateGetUserMediaCall(320, 320, 180, 180, 30, 30)); + list_of_get_user_media_calls.push_back( + GenerateGetUserMediaCall(320, 320, 240, 240, 30, 30)); + list_of_get_user_media_calls.push_back( + GenerateGetUserMediaCall(640, 640, 360, 360, 30, 30)); + list_of_get_user_media_calls.push_back( + GenerateGetUserMediaCall(640, 640, 480, 480, 30, 30)); + list_of_get_user_media_calls.push_back( + GenerateGetUserMediaCall(960, 960, 720, 720, 30, 30)); + list_of_get_user_media_calls.push_back( + GenerateGetUserMediaCall(1280, 1280, 720, 720, 30, 30)); + list_of_get_user_media_calls.push_back( + GenerateGetUserMediaCall(1920, 1920, 1080, 1080, 30, 30)); + + for (std::vector<std::string>::iterator const_iterator = + list_of_get_user_media_calls.begin(); + const_iterator != list_of_get_user_media_calls.end(); + ++const_iterator) { + DVLOG(1) << "Calling getUserMedia: " << *const_iterator; + NavigateToURL(shell(), url); + EXPECT_TRUE(ExecuteJavascript(*const_iterator)); + ExpectTitle("OK"); + } +} + +IN_PROC_BROWSER_TEST_F(WebrtcBrowserTest, AddTwoMediaStreamsToOnePC) { + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + + EXPECT_TRUE( + ExecuteJavascript("addTwoMediaStreamsToOneConnection();")); + ExpectTitle("OK"); +} + +} // namespace content diff --git a/chromium/content/browser/media/webrtc_identity_store.cc b/chromium/content/browser/media/webrtc_identity_store.cc new file mode 100644 index 00000000000..67a25851fde --- /dev/null +++ b/chromium/content/browser/media/webrtc_identity_store.cc @@ -0,0 +1,307 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/media/webrtc_identity_store.h" + +#include <map> + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/logging.h" +#include "base/rand_util.h" +#include "base/threading/worker_pool.h" +#include "content/browser/media/webrtc_identity_store_backend.h" +#include "content/public/browser/browser_thread.h" +#include "crypto/rsa_private_key.h" +#include "net/base/net_errors.h" +#include "net/cert/x509_util.h" +#include "url/gurl.h" + +namespace content { + +struct WebRTCIdentityRequestResult { + WebRTCIdentityRequestResult(int error, + const std::string& certificate, + const std::string& private_key) + : error(error), certificate(certificate), private_key(private_key) {} + + int error; + std::string certificate; + std::string private_key; +}; + +// Generates a new identity using |common_name| and returns the result in +// |result|. +static void GenerateIdentityWorker(const std::string& common_name, + WebRTCIdentityRequestResult* result) { + result->error = net::OK; + int serial_number = base::RandInt(0, std::numeric_limits<int>::max()); + + scoped_ptr<crypto::RSAPrivateKey> key(crypto::RSAPrivateKey::Create(1024)); + if (!key.get()) { + DLOG(ERROR) << "Unable to create key pair for client"; + result->error = net::ERR_KEY_GENERATION_FAILED; + return; + } + + base::Time now = base::Time::Now(); + bool success = + net::x509_util::CreateSelfSignedCert(key.get(), + "CN=" + common_name, + serial_number, + now, + now + base::TimeDelta::FromDays(30), + &result->certificate); + if (!success) { + DLOG(ERROR) << "Unable to create x509 cert for client"; + result->error = net::ERR_SELF_SIGNED_CERT_GENERATION_FAILED; + return; + } + + std::vector<uint8> private_key_info; + if (!key->ExportPrivateKey(&private_key_info)) { + DLOG(ERROR) << "Unable to export private key"; + result->error = net::ERR_PRIVATE_KEY_EXPORT_FAILED; + return; + } + + result->private_key = + std::string(private_key_info.begin(), private_key_info.end()); +} + +class WebRTCIdentityRequestHandle; + +// The class represents an identity request internal to WebRTCIdentityStore. +// It has a one-to-many mapping to the external version of the request, +// WebRTCIdentityRequestHandle, i.e. multiple identical external requests are +// combined into one internal request. +// It's deleted automatically when the request is completed. +class WebRTCIdentityRequest { + public: + WebRTCIdentityRequest(const GURL& origin, + const std::string& identity_name, + const std::string& common_name) + : origin_(origin), + identity_name_(identity_name), + common_name_(common_name) {} + + void Cancel(WebRTCIdentityRequestHandle* handle) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + if (callbacks_.find(handle) == callbacks_.end()) + return; + callbacks_.erase(handle); + } + + private: + friend class WebRTCIdentityStore; + + void AddCallback(WebRTCIdentityRequestHandle* handle, + const WebRTCIdentityStore::CompletionCallback& callback) { + DCHECK(callbacks_.find(handle) == callbacks_.end()); + callbacks_[handle] = callback; + } + + // This method deletes "this" and no one should access it after the request + // completes. + // We do not use base::Owned to tie its lifetime to the callback for + // WebRTCIdentityStoreBackend::FindIdentity, because it needs to live longer + // than that if the identity does not exist in DB. + void Post(const WebRTCIdentityRequestResult& result) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + for (CallbackMap::iterator it = callbacks_.begin(); it != callbacks_.end(); + ++it) + it->second.Run(result.error, result.certificate, result.private_key); + delete this; + } + + GURL origin_; + std::string identity_name_; + std::string common_name_; + typedef std::map<WebRTCIdentityRequestHandle*, + WebRTCIdentityStore::CompletionCallback> CallbackMap; + CallbackMap callbacks_; +}; + +// The class represents an identity request which calls back to the external +// client when the request completes. +// Its lifetime is tied with the Callback held by the corresponding +// WebRTCIdentityRequest. +class WebRTCIdentityRequestHandle { + public: + WebRTCIdentityRequestHandle( + WebRTCIdentityStore* store, + const WebRTCIdentityStore::CompletionCallback& callback) + : store_(store), request_(NULL), callback_(callback) {} + + private: + friend class WebRTCIdentityStore; + + // Cancel the request. Does nothing if the request finished or was already + // cancelled. + void Cancel() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + if (!request_) + return; + + callback_.Reset(); + WebRTCIdentityRequest* request = request_; + request_ = NULL; + // "this" will be deleted after the following call, because "this" is + // owned by the Callback held by |request|. + request->Cancel(this); + } + + void OnRequestStarted(WebRTCIdentityRequest* request) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + DCHECK(request); + request_ = request; + } + + void OnRequestComplete(int error, + const std::string& certificate, + const std::string& private_key) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + DCHECK(request_); + request_ = NULL; + base::ResetAndReturn(&callback_).Run(error, certificate, private_key); + } + + WebRTCIdentityStore* store_; + WebRTCIdentityRequest* request_; + WebRTCIdentityStore::CompletionCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(WebRTCIdentityRequestHandle); +}; + +WebRTCIdentityStore::WebRTCIdentityStore(const base::FilePath& path, + quota::SpecialStoragePolicy* policy) + : task_runner_(base::WorkerPool::GetTaskRunner(true)), + backend_(new WebRTCIdentityStoreBackend(path, policy)) {} + +WebRTCIdentityStore::~WebRTCIdentityStore() { backend_->Close(); } + +base::Closure WebRTCIdentityStore::RequestIdentity( + const GURL& origin, + const std::string& identity_name, + const std::string& common_name, + const CompletionCallback& callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + WebRTCIdentityRequest* request = + FindRequest(origin, identity_name, common_name); + // If there is no identical request in flight, create a new one, queue it, + // and make the backend request. + if (!request) { + request = new WebRTCIdentityRequest(origin, identity_name, common_name); + // |request| will delete itself after the result is posted. + if (!backend_->FindIdentity( + origin, + identity_name, + common_name, + base::Bind( + &WebRTCIdentityStore::BackendFindCallback, this, request))) { + // Bail out if the backend failed to start the task. + delete request; + return base::Closure(); + } + in_flight_requests_.push_back(request); + } + + WebRTCIdentityRequestHandle* handle = + new WebRTCIdentityRequestHandle(this, callback); + + request->AddCallback( + handle, + base::Bind(&WebRTCIdentityRequestHandle::OnRequestComplete, + base::Owned(handle))); + handle->OnRequestStarted(request); + return base::Bind(&WebRTCIdentityRequestHandle::Cancel, + base::Unretained(handle)); +} + +void WebRTCIdentityStore::DeleteBetween(base::Time delete_begin, + base::Time delete_end, + const base::Closure& callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + backend_->DeleteBetween(delete_begin, delete_end, callback); +} + +void WebRTCIdentityStore::SetTaskRunnerForTesting( + const scoped_refptr<base::TaskRunner>& task_runner) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + task_runner_ = task_runner; +} + +void WebRTCIdentityStore::BackendFindCallback(WebRTCIdentityRequest* request, + int error, + const std::string& certificate, + const std::string& private_key) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + if (error == net::OK) { + DVLOG(2) << "Identity found in DB."; + WebRTCIdentityRequestResult result(error, certificate, private_key); + PostRequestResult(request, result); + return; + } + // Generate a new identity if not found in the DB. + WebRTCIdentityRequestResult* result = + new WebRTCIdentityRequestResult(0, "", ""); + if (!task_runner_->PostTaskAndReply( + FROM_HERE, + base::Bind(&GenerateIdentityWorker, request->common_name_, result), + base::Bind(&WebRTCIdentityStore::GenerateIdentityCallback, + this, + request, + base::Owned(result)))) { + // Completes the request with error if failed to post the task. + WebRTCIdentityRequestResult result(net::ERR_UNEXPECTED, "", ""); + PostRequestResult(request, result); + } +} + +void WebRTCIdentityStore::GenerateIdentityCallback( + WebRTCIdentityRequest* request, + WebRTCIdentityRequestResult* result) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + if (result->error == net::OK) { + DVLOG(2) << "New identity generated and added to the backend."; + backend_->AddIdentity(request->origin_, + request->identity_name_, + request->common_name_, + result->certificate, + result->private_key); + } + PostRequestResult(request, *result); +} + +void WebRTCIdentityStore::PostRequestResult( + WebRTCIdentityRequest* request, + const WebRTCIdentityRequestResult& result) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + // Removes the in flight request from the queue. + for (size_t i = 0; i < in_flight_requests_.size(); ++i) { + if (in_flight_requests_[i] == request) { + in_flight_requests_.erase(in_flight_requests_.begin() + i); + break; + } + } + // |request| will be deleted after this call. + request->Post(result); +} + +// Find an identical request from the in flight requests. +WebRTCIdentityRequest* WebRTCIdentityStore::FindRequest( + const GURL& origin, + const std::string& identity_name, + const std::string& common_name) { + for (size_t i = 0; i < in_flight_requests_.size(); ++i) { + if (in_flight_requests_[i]->origin_ == origin && + in_flight_requests_[i]->identity_name_ == identity_name && + in_flight_requests_[i]->common_name_ == common_name) { + return in_flight_requests_[i]; + } + } + return NULL; +} + +} // namespace content diff --git a/chromium/content/browser/media/webrtc_identity_store.h b/chromium/content/browser/media/webrtc_identity_store.h new file mode 100644 index 00000000000..c2b523e2dfe --- /dev/null +++ b/chromium/content/browser/media/webrtc_identity_store.h @@ -0,0 +1,114 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_MEDIA_WEBRTC_IDENTITY_STORE_H_ +#define CONTENT_BROWSER_MEDIA_WEBRTC_IDENTITY_STORE_H_ + +#include <string> +#include <vector> + +#include "base/callback.h" +#include "base/time/time.h" +#include "content/common/content_export.h" + +class GURL; + +namespace base { +class FilePath; +class TaskRunner; +} // namespace base + +namespace quota { +class SpecialStoragePolicy; +} // namespace quota + +namespace content { +class WebRTCIdentityRequest; +struct WebRTCIdentityRequestResult; +class WebRTCIdentityStoreBackend; +class WebRTCIdentityStoreTest; + +// A class for creating and fetching DTLS identities, i.e. the private key and +// the self-signed certificate. +// It can be created/destroyed on any thread, but the public methods must be +// called on the IO thread. +class CONTENT_EXPORT WebRTCIdentityStore + : public base::RefCountedThreadSafe<WebRTCIdentityStore> { + public: + typedef base::Callback<void(int error, + const std::string& certificate, + const std::string& private_key)> + CompletionCallback; + + // If |path| is empty, nothing will be saved to disk. + WebRTCIdentityStore(const base::FilePath& path, + quota::SpecialStoragePolicy* policy); + + // Retrieve the cached DTLS private key and certificate, i.e. identity, for + // the |origin| and |identity_name| pair, or generate a new identity using + // |common_name| if such an identity does not exist. + // If the given |common_name| is different from the common name in the cached + // identity that has the same origin and identity_name, a new private key and + // a new certificate will be generated, overwriting the old one. + // + // |origin| is the origin of the DTLS connection; + // |identity_name| is used to identify an identity within an origin; it is + // opaque to WebRTCIdentityStore and remains private to the caller, i.e. not + // present in the certificate; + // |common_name| is the common name used to generate the certificate and will + // be shared with the peer of the DTLS connection. Identities created for + // different origins or different identity names may have the same common + // name. + // |callback| is the callback to return the result as DER strings. + // + // Returns the Closure used to cancel the request if the request is accepted. + // The Closure can only be called before the request completes. + virtual base::Closure RequestIdentity(const GURL& origin, + const std::string& identity_name, + const std::string& common_name, + const CompletionCallback& callback); + + // Delete the identities created between |delete_begin| and |delete_end|. + // |callback| will be called when the operation is done. + void DeleteBetween(base::Time delete_begin, + base::Time delete_end, + const base::Closure& callback); + + protected: + // Only virtual to allow subclassing for test mock. + virtual ~WebRTCIdentityStore(); + + private: + friend class base::RefCountedThreadSafe<WebRTCIdentityStore>; + friend class WebRTCIdentityStoreTest; + + void SetTaskRunnerForTesting( + const scoped_refptr<base::TaskRunner>& task_runner); + + void BackendFindCallback(WebRTCIdentityRequest* request, + int error, + const std::string& certificate, + const std::string& private_key); + void GenerateIdentityCallback(WebRTCIdentityRequest* request, + WebRTCIdentityRequestResult* result); + WebRTCIdentityRequest* FindRequest(const GURL& origin, + const std::string& identity_name, + const std::string& common_name); + void PostRequestResult(WebRTCIdentityRequest* request, + const WebRTCIdentityRequestResult& result); + + // The TaskRunner for doing work on a worker thread. + scoped_refptr<base::TaskRunner> task_runner_; + // Weak references of the in flight requests. Used to join identical external + // requests. + std::vector<WebRTCIdentityRequest*> in_flight_requests_; + + scoped_refptr<WebRTCIdentityStoreBackend> backend_; + + DISALLOW_COPY_AND_ASSIGN(WebRTCIdentityStore); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_MEDIA_WEBRTC_IDENTITY_STORE_H_ diff --git a/chromium/content/browser/media/webrtc_identity_store_backend.cc b/chromium/content/browser/media/webrtc_identity_store_backend.cc new file mode 100644 index 00000000000..9ec73e92f69 --- /dev/null +++ b/chromium/content/browser/media/webrtc_identity_store_backend.cc @@ -0,0 +1,546 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/media/webrtc_identity_store_backend.h" + +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "content/public/browser/browser_thread.h" +#include "net/base/net_errors.h" +#include "sql/error_delegate_util.h" +#include "sql/statement.h" +#include "sql/transaction.h" +#include "url/gurl.h" +#include "webkit/browser/quota/special_storage_policy.h" + +namespace content { + +static const char* kWebRTCIdentityStoreDBName = "webrtc_identity_store"; + +static const base::FilePath::CharType kWebRTCIdentityStoreDirectory[] = + FILE_PATH_LITERAL("WebRTCIdentityStore"); + +// Initializes the identity table, returning true on success. +static bool InitDB(sql::Connection* db) { + if (db->DoesTableExist(kWebRTCIdentityStoreDBName)) { + if (db->DoesColumnExist(kWebRTCIdentityStoreDBName, "origin") && + db->DoesColumnExist(kWebRTCIdentityStoreDBName, "identity_name") && + db->DoesColumnExist(kWebRTCIdentityStoreDBName, "common_name") && + db->DoesColumnExist(kWebRTCIdentityStoreDBName, "certificate") && + db->DoesColumnExist(kWebRTCIdentityStoreDBName, "private_key") && + db->DoesColumnExist(kWebRTCIdentityStoreDBName, "creation_time")) + return true; + if (!db->Execute("DROP TABLE webrtc_identity_store")) + return false; + } + + return db->Execute( + "CREATE TABLE webrtc_identity_store" + " (" + "origin TEXT NOT NULL," + "identity_name TEXT NOT NULL," + "common_name TEXT NOT NULL," + "certificate BLOB NOT NULL," + "private_key BLOB NOT NULL," + "creation_time INTEGER)"); +} + +struct WebRTCIdentityStoreBackend::IdentityKey { + IdentityKey(const GURL& origin, const std::string& identity_name) + : origin(origin), identity_name(identity_name) {} + + bool operator<(const IdentityKey& other) const { + return origin < other.origin || + (origin == other.origin && identity_name < other.identity_name); + } + + GURL origin; + std::string identity_name; +}; + +struct WebRTCIdentityStoreBackend::Identity { + Identity(const std::string& common_name, + const std::string& certificate, + const std::string& private_key) + : common_name(common_name), + certificate(certificate), + private_key(private_key), + creation_time(base::Time::Now().ToInternalValue()) {} + + Identity(const std::string& common_name, + const std::string& certificate, + const std::string& private_key, + int64 creation_time) + : common_name(common_name), + certificate(certificate), + private_key(private_key), + creation_time(creation_time) {} + + std::string common_name; + std::string certificate; + std::string private_key; + int64 creation_time; +}; + +struct WebRTCIdentityStoreBackend::PendingFindRequest { + PendingFindRequest(const GURL& origin, + const std::string& identity_name, + const std::string& common_name, + const FindIdentityCallback& callback) + : origin(origin), + identity_name(identity_name), + common_name(common_name), + callback(callback) {} + + ~PendingFindRequest() {} + + GURL origin; + std::string identity_name; + std::string common_name; + FindIdentityCallback callback; +}; + +// The class encapsulates the database operations. All members except ctor and +// dtor should be accessed on the DB thread. +// It can be created/destroyed on any thread. +class WebRTCIdentityStoreBackend::SqlLiteStorage + : public base::RefCountedThreadSafe<SqlLiteStorage> { + public: + SqlLiteStorage(const base::FilePath& path, + quota::SpecialStoragePolicy* policy) + : special_storage_policy_(policy) { + if (!path.empty()) + path_ = path.Append(kWebRTCIdentityStoreDirectory); + } + + void Load(IdentityMap* out_map); + void Close(); + void AddIdentity(const GURL& origin, + const std::string& identity_name, + const Identity& identity); + void DeleteIdentity(const GURL& origin, + const std::string& identity_name, + const Identity& identity); + void DeleteBetween(base::Time delete_begin, + base::Time delete_end, + const base::Closure& callback); + + private: + friend class base::RefCountedThreadSafe<SqlLiteStorage>; + + enum OperationType { + ADD_IDENTITY, + DELETE_IDENTITY + }; + struct PendingOperation { + PendingOperation(OperationType type, + const GURL& origin, + const std::string& identity_name, + const Identity& identity) + : type(type), + origin(origin), + identity_name(identity_name), + identity(identity) {} + + OperationType type; + GURL origin; + std::string identity_name; + Identity identity; + }; + typedef std::vector<PendingOperation*> PendingOperationList; + + virtual ~SqlLiteStorage() {} + void OnDatabaseError(int error, sql::Statement* stmt); + void BatchOperation(OperationType type, + const GURL& origin, + const std::string& identity_name, + const Identity& identity); + void Commit(); + + // The file path of the DB. Empty if temporary. + base::FilePath path_; + scoped_refptr<quota::SpecialStoragePolicy> special_storage_policy_; + scoped_ptr<sql::Connection> db_; + // Batched DB operations pending to commit. + PendingOperationList pending_operations_; + + DISALLOW_COPY_AND_ASSIGN(SqlLiteStorage); +}; + +WebRTCIdentityStoreBackend::WebRTCIdentityStoreBackend( + const base::FilePath& path, + quota::SpecialStoragePolicy* policy) + : state_(NOT_STARTED), + sql_lite_storage_(new SqlLiteStorage(path, policy)) {} + +bool WebRTCIdentityStoreBackend::FindIdentity( + const GURL& origin, + const std::string& identity_name, + const std::string& common_name, + const FindIdentityCallback& callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + if (state_ == CLOSED) + return false; + + if (state_ != LOADED) { + // Queues the request to wait for the DB to load. + pending_find_requests_.push_back( + new PendingFindRequest(origin, identity_name, common_name, callback)); + if (state_ == LOADING) + return true; + + DCHECK_EQ(state_, NOT_STARTED); + + // Kick off loading the DB. + scoped_ptr<IdentityMap> out_map(new IdentityMap()); + base::Closure task( + base::Bind(&SqlLiteStorage::Load, sql_lite_storage_, out_map.get())); + // |out_map| will be NULL after this call. + if (BrowserThread::PostTaskAndReply( + BrowserThread::DB, + FROM_HERE, + task, + base::Bind(&WebRTCIdentityStoreBackend::OnLoaded, + this, + base::Passed(&out_map)))) { + state_ = LOADING; + return true; + } + // If it fails to post task, falls back to ERR_FILE_NOT_FOUND. + } + + IdentityKey key(origin, identity_name); + IdentityMap::iterator iter = identities_.find(key); + if (iter != identities_.end() && iter->second.common_name == common_name) { + // Identity found. + return BrowserThread::PostTask(BrowserThread::IO, + FROM_HERE, + base::Bind(callback, + net::OK, + iter->second.certificate, + iter->second.private_key)); + } + + return BrowserThread::PostTask( + BrowserThread::IO, + FROM_HERE, + base::Bind(callback, net::ERR_FILE_NOT_FOUND, "", "")); +} + +void WebRTCIdentityStoreBackend::AddIdentity(const GURL& origin, + const std::string& identity_name, + const std::string& common_name, + const std::string& certificate, + const std::string& private_key) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + if (state_ == CLOSED) + return; + + // If there is an existing identity for the same origin and identity_name, + // delete it. + IdentityKey key(origin, identity_name); + Identity identity(common_name, certificate, private_key); + + if (identities_.find(key) != identities_.end()) { + if (!BrowserThread::PostTask(BrowserThread::DB, + FROM_HERE, + base::Bind(&SqlLiteStorage::DeleteIdentity, + sql_lite_storage_, + origin, + identity_name, + identities_.find(key)->second))) + return; + } + identities_.insert(std::pair<IdentityKey, Identity>(key, identity)); + + BrowserThread::PostTask(BrowserThread::DB, + FROM_HERE, + base::Bind(&SqlLiteStorage::AddIdentity, + sql_lite_storage_, + origin, + identity_name, + identity)); +} + +void WebRTCIdentityStoreBackend::Close() { + if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) { + BrowserThread::PostTask( + BrowserThread::IO, + FROM_HERE, + base::Bind(&WebRTCIdentityStoreBackend::Close, this)); + return; + } + + if (state_ == CLOSED) + return; + + state_ = CLOSED; + BrowserThread::PostTask( + BrowserThread::DB, + FROM_HERE, + base::Bind(&SqlLiteStorage::Close, sql_lite_storage_)); +} + +void WebRTCIdentityStoreBackend::DeleteBetween(base::Time delete_begin, + base::Time delete_end, + const base::Closure& callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + // Delete the in-memory cache. + IdentityMap::iterator it = identities_.begin(); + while (it != identities_.end()) { + if (it->second.creation_time >= delete_begin.ToInternalValue() && + it->second.creation_time <= delete_end.ToInternalValue()) + identities_.erase(it++); + else + it++; + } + + BrowserThread::PostTaskAndReply(BrowserThread::DB, + FROM_HERE, + base::Bind(&SqlLiteStorage::DeleteBetween, + sql_lite_storage_, + delete_begin, + delete_end, + callback), + callback); +} + +WebRTCIdentityStoreBackend::~WebRTCIdentityStoreBackend() {} + +void WebRTCIdentityStoreBackend::OnLoaded(scoped_ptr<IdentityMap> out_map) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + state_ = LOADED; + identities_.swap(*out_map); + + for (size_t i = 0; i < pending_find_requests_.size(); ++i) { + FindIdentity(pending_find_requests_[i]->origin, + pending_find_requests_[i]->identity_name, + pending_find_requests_[i]->common_name, + pending_find_requests_[i]->callback); + delete pending_find_requests_[i]; + } + pending_find_requests_.clear(); +} + +// +// Implementation of SqlLiteStorage. +// + +void WebRTCIdentityStoreBackend::SqlLiteStorage::Load(IdentityMap* out_map) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); + DCHECK(!db_.get()); + + // Ensure the parent directory for storing certs is created before reading + // from it. + const base::FilePath dir = path_.DirName(); + if (!base::PathExists(dir) && !file_util::CreateDirectory(dir)) { + DLOG(ERROR) << "Unable to open DB file path."; + return; + } + + db_.reset(new sql::Connection()); + + db_->set_error_callback(base::Bind(&SqlLiteStorage::OnDatabaseError, this)); + + if (!db_->Open(path_)) { + DLOG(ERROR) << "Unable to open DB."; + db_.reset(); + return; + } + + if (!InitDB(db_.get())) { + DLOG(ERROR) << "Unable to init DB."; + db_.reset(); + return; + } + + db_->Preload(); + + // Slurp all the identities into the out_map. + sql::Statement stmt(db_->GetUniqueStatement( + "SELECT origin, identity_name, common_name, " + "certificate, private_key, creation_time " + "FROM webrtc_identity_store")); + CHECK(stmt.is_valid()); + + while (stmt.Step()) { + IdentityKey key(GURL(stmt.ColumnString(0)), stmt.ColumnString(1)); + std::string common_name(stmt.ColumnString(2)); + std::string cert, private_key; + stmt.ColumnBlobAsString(3, &cert); + stmt.ColumnBlobAsString(4, &private_key); + int64 creation_time = stmt.ColumnInt64(5); + std::pair<IdentityMap::iterator, bool> result = + out_map->insert(std::pair<IdentityKey, Identity>( + key, Identity(common_name, cert, private_key, creation_time))); + DCHECK(result.second); + } +} + +void WebRTCIdentityStoreBackend::SqlLiteStorage::Close() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); + Commit(); + db_.reset(); +} + +void WebRTCIdentityStoreBackend::SqlLiteStorage::AddIdentity( + const GURL& origin, + const std::string& identity_name, + const Identity& identity) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); + if (!db_.get()) + return; + + // Do not add for session only origins. + if (special_storage_policy_.get() && + !special_storage_policy_->IsStorageProtected(origin) && + special_storage_policy_->IsStorageSessionOnly(origin)) { + return; + } + BatchOperation(ADD_IDENTITY, origin, identity_name, identity); +} + +void WebRTCIdentityStoreBackend::SqlLiteStorage::DeleteIdentity( + const GURL& origin, + const std::string& identity_name, + const Identity& identity) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); + if (!db_.get()) + return; + BatchOperation(DELETE_IDENTITY, origin, identity_name, identity); +} + +void WebRTCIdentityStoreBackend::SqlLiteStorage::OnDatabaseError( + int error, + sql::Statement* stmt) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); + if (!sql::IsErrorCatastrophic(error)) + return; + db_->RazeAndClose(); +} + +void WebRTCIdentityStoreBackend::SqlLiteStorage::BatchOperation( + OperationType type, + const GURL& origin, + const std::string& identity_name, + const Identity& identity) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); + // Commit every 30 seconds. + static const base::TimeDelta kCommitInterval( + base::TimeDelta::FromSeconds(30)); + // Commit right away if we have more than 512 outstanding operations. + static const size_t kCommitAfterBatchSize = 512; + + // We do a full copy of the cert here, and hopefully just here. + scoped_ptr<PendingOperation> operation( + new PendingOperation(type, origin, identity_name, identity)); + + pending_operations_.push_back(operation.release()); + + if (pending_operations_.size() == 1) { + // We've gotten our first entry for this batch, fire off the timer. + BrowserThread::PostDelayedTask(BrowserThread::DB, + FROM_HERE, + base::Bind(&SqlLiteStorage::Commit, this), + kCommitInterval); + } else if (pending_operations_.size() >= kCommitAfterBatchSize) { + // We've reached a big enough batch, fire off a commit now. + BrowserThread::PostTask(BrowserThread::DB, + FROM_HERE, + base::Bind(&SqlLiteStorage::Commit, this)); + } +} + +void WebRTCIdentityStoreBackend::SqlLiteStorage::Commit() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); + // Maybe an old timer fired or we are already Close()'ed. + if (!db_.get() || pending_operations_.empty()) + return; + + sql::Statement add_stmt(db_->GetCachedStatement( + SQL_FROM_HERE, + "INSERT INTO webrtc_identity_store " + "(origin, identity_name, common_name, certificate," + " private_key, creation_time) VALUES" + " (?,?,?,?,?,?)")); + + CHECK(add_stmt.is_valid()); + + sql::Statement del_stmt(db_->GetCachedStatement( + SQL_FROM_HERE, + "DELETE FROM webrtc_identity_store WHERE origin=? AND identity_name=?")); + + CHECK(del_stmt.is_valid()); + + sql::Transaction transaction(db_.get()); + if (!transaction.Begin()) { + DLOG(ERROR) << "Failed to begin the transaction."; + return; + } + + for (PendingOperationList::iterator it = pending_operations_.begin(); + it != pending_operations_.end(); + ++it) { + scoped_ptr<PendingOperation> po(*it); + switch (po->type) { + case ADD_IDENTITY: { + add_stmt.Reset(true); + add_stmt.BindString(0, po->origin.spec()); + add_stmt.BindString(1, po->identity_name); + add_stmt.BindString(2, po->identity.common_name); + const std::string& cert = po->identity.certificate; + add_stmt.BindBlob(3, cert.data(), cert.size()); + const std::string& private_key = po->identity.private_key; + add_stmt.BindBlob(4, private_key.data(), private_key.size()); + add_stmt.BindInt64(5, po->identity.creation_time); + CHECK(add_stmt.Run()); + break; + } + case DELETE_IDENTITY: + del_stmt.Reset(true); + del_stmt.BindString(0, po->origin.spec()); + add_stmt.BindString(1, po->identity_name); + CHECK(del_stmt.Run()); + break; + + default: + NOTREACHED(); + break; + } + } + transaction.Commit(); + pending_operations_.clear(); +} + +void WebRTCIdentityStoreBackend::SqlLiteStorage::DeleteBetween( + base::Time delete_begin, + base::Time delete_end, + const base::Closure& callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); + if (!db_.get()) + return; + + // Commit pending operations first. + Commit(); + + sql::Statement del_stmt(db_->GetCachedStatement( + SQL_FROM_HERE, + "DELETE FROM webrtc_identity_store" + " WHERE creation_time >= ? AND creation_time <= ?")); + CHECK(del_stmt.is_valid()); + + del_stmt.BindInt64(0, delete_begin.ToInternalValue()); + del_stmt.BindInt64(1, delete_end.ToInternalValue()); + + sql::Transaction transaction(db_.get()); + if (!transaction.Begin()) { + DLOG(ERROR) << "Failed to begin the transaction."; + return; + } + + CHECK(del_stmt.Run()); + transaction.Commit(); +} + +} // namespace content diff --git a/chromium/content/browser/media/webrtc_identity_store_backend.h b/chromium/content/browser/media/webrtc_identity_store_backend.h new file mode 100644 index 00000000000..ab4e1ed7e1e --- /dev/null +++ b/chromium/content/browser/media/webrtc_identity_store_backend.h @@ -0,0 +1,111 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_MEDIA_WEBRTC_IDENTITY_STORE_BACKEND_H_ +#define CONTENT_BROWSER_MEDIA_WEBRTC_IDENTITY_STORE_BACKEND_H_ + +#include <map> +#include <string> + +#include "base/time/time.h" +#include "sql/connection.h" +#include "sql/meta_table.h" + +class GURL; + +namespace base { +class FilePath; +} // namespace base + +namespace quota { +class SpecialStoragePolicy; +} // namespace quota + +namespace content { + +// This class represents a persistent cache of WebRTC identities. +// It can be created/destroyed/Close() on any thread. All other members should +// be accessed on the IO thread. +class WebRTCIdentityStoreBackend + : public base::RefCountedThreadSafe<WebRTCIdentityStoreBackend> { + public: + typedef base::Callback<void(int error, + const std::string& certificate, + const std::string& private_key)> + FindIdentityCallback; + + // No data is saved on disk if |path| is empty. + WebRTCIdentityStoreBackend(const base::FilePath& path, + quota::SpecialStoragePolicy* policy); + + // Finds the identity with |origin|, |identity_name|, and |common_name| from + // the DB. + // |origin| is the origin of the identity; + // |identity_name| is used to identify an identity within an origin; + // |common_name| is the common name used to generate the certificate; + // |callback| is the callback to return the find result. + // Returns true if |callback| will be called. + // Should be called on the IO thread. + bool FindIdentity(const GURL& origin, + const std::string& identity_name, + const std::string& common_name, + const FindIdentityCallback& callback); + + // Adds the identity to the DB and overwrites any existing identity having the + // same origin and identity_name. + // |origin| is the origin of the identity; + // |identity_name| is used to identify an identity within an origin; + // |common_name| is the common name used to generate the certificate; + // |certificate| is the DER string of the certificate; + // |private_key| is the DER string of the private key. + // Should be called on the IO thread. + void AddIdentity(const GURL& origin, + const std::string& identity_name, + const std::string& common_name, + const std::string& certificate, + const std::string& private_key); + + // Commits all pending DB operations and closes the DB connection. Any API + // call after this will fail. + // Can be called on any thread. + void Close(); + + // Delete the data created between |delete_begin| and |delete_end|. + // Should be called on the IO thread. + void DeleteBetween(base::Time delete_begin, + base::Time delete_end, + const base::Closure& callback); + + private: + friend class base::RefCountedThreadSafe<WebRTCIdentityStoreBackend>; + class SqlLiteStorage; + enum LoadingState { + NOT_STARTED, + LOADING, + LOADED, + CLOSED, + }; + struct PendingFindRequest; + struct IdentityKey; + struct Identity; + typedef std::map<IdentityKey, Identity> IdentityMap; + + ~WebRTCIdentityStoreBackend(); + + void OnLoaded(scoped_ptr<IdentityMap> out_map); + + // In-memory copy of the identities. + IdentityMap identities_; + // "Find identity" requests waiting for the DB to load. + std::vector<PendingFindRequest*> pending_find_requests_; + // The persistent storage loading state. + LoadingState state_; + // The persistent storage of identities. + scoped_refptr<SqlLiteStorage> sql_lite_storage_; + + DISALLOW_COPY_AND_ASSIGN(WebRTCIdentityStoreBackend); +}; +} + +#endif // CONTENT_BROWSER_MEDIA_WEBRTC_IDENTITY_STORE_BACKEND_H_ diff --git a/chromium/content/browser/media/webrtc_identity_store_unittest.cc b/chromium/content/browser/media/webrtc_identity_store_unittest.cc new file mode 100644 index 00000000000..81c25d7f63f --- /dev/null +++ b/chromium/content/browser/media/webrtc_identity_store_unittest.cc @@ -0,0 +1,278 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/test/sequenced_worker_pool_owner.h" +#include "content/browser/media/webrtc_identity_store.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "content/public/test/test_utils.h" +#include "net/base/net_errors.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace content { + +// TODO(jiayl): the tests fail on Android since the openssl version of +// CreateSelfSignedCert is not implemented. We should mock out this dependency +// and remove the if-defined. +#if !defined(OS_ANDROID) + +static const char* kFakeOrigin = "http://foo.com"; +static const char* kFakeIdentityName1 = "name1"; +static const char* kFakeIdentityName2 = "name2"; +static const char* kFakeCommonName1 = "cname1"; +static const char* kFakeCommonName2 = "cname2"; + +static void OnRequestCompleted(bool* completed, + std::string* out_cert, + std::string* out_key, + int error, + const std::string& certificate, + const std::string& private_key) { + ASSERT_EQ(net::OK, error); + ASSERT_NE("", certificate); + ASSERT_NE("", private_key); + *completed = true; + *out_cert = certificate; + *out_key = private_key; +} + +class WebRTCIdentityStoreTest : public testing::Test { + public: + WebRTCIdentityStoreTest() + : browser_thread_bundle_(TestBrowserThreadBundle::IO_MAINLOOP | + TestBrowserThreadBundle::REAL_DB_THREAD), + pool_owner_( + new base::SequencedWorkerPoolOwner(3, "WebRTCIdentityStoreTest")), + webrtc_identity_store_( + new WebRTCIdentityStore(base::FilePath(), NULL)) { + webrtc_identity_store_->SetTaskRunnerForTesting(pool_owner_->pool()); + } + + virtual ~WebRTCIdentityStoreTest() { + pool_owner_->pool()->Shutdown(); + } + + void RunUntilIdle() { + RunAllPendingInMessageLoop(BrowserThread::DB); + RunAllPendingInMessageLoop(BrowserThread::IO); + pool_owner_->pool()->FlushForTesting(); + base::RunLoop().RunUntilIdle(); + } + + base::Closure RequestIdentityAndRunUtilIdle(const std::string& origin, + const std::string& identity_name, + const std::string& common_name, + bool* completed, + std::string* certificate, + std::string* private_key) { + base::Closure cancel_callback = webrtc_identity_store_->RequestIdentity( + GURL(origin), + identity_name, + common_name, + base::Bind(&OnRequestCompleted, completed, certificate, private_key)); + EXPECT_FALSE(cancel_callback.is_null()); + RunUntilIdle(); + return cancel_callback; + } + + protected: + TestBrowserThreadBundle browser_thread_bundle_; + scoped_ptr<base::SequencedWorkerPoolOwner> pool_owner_; + scoped_refptr<WebRTCIdentityStore> webrtc_identity_store_; +}; + +TEST_F(WebRTCIdentityStoreTest, RequestIdentity) { + bool completed = false; + std::string dummy; + base::Closure cancel_callback = + RequestIdentityAndRunUtilIdle(kFakeOrigin, + kFakeIdentityName1, + kFakeCommonName1, + &completed, + &dummy, + &dummy); + EXPECT_TRUE(completed); +} + +TEST_F(WebRTCIdentityStoreTest, CancelRequest) { + bool completed = false; + std::string dummy; + base::Closure cancel_callback = webrtc_identity_store_->RequestIdentity( + GURL(kFakeOrigin), + kFakeIdentityName1, + kFakeCommonName1, + base::Bind(&OnRequestCompleted, &completed, &dummy, &dummy)); + ASSERT_FALSE(cancel_callback.is_null()); + cancel_callback.Run(); + + RunUntilIdle(); + EXPECT_FALSE(completed); +} + +TEST_F(WebRTCIdentityStoreTest, ConcurrentUniqueRequests) { + bool completed_1 = false; + bool completed_2 = false; + std::string dummy; + base::Closure cancel_callback_1 = webrtc_identity_store_->RequestIdentity( + GURL(kFakeOrigin), + kFakeIdentityName1, + kFakeCommonName1, + base::Bind(&OnRequestCompleted, &completed_1, &dummy, &dummy)); + ASSERT_FALSE(cancel_callback_1.is_null()); + + base::Closure cancel_callback_2 = webrtc_identity_store_->RequestIdentity( + GURL(kFakeOrigin), + kFakeIdentityName2, + kFakeCommonName1, + base::Bind(&OnRequestCompleted, &completed_2, &dummy, &dummy)); + ASSERT_FALSE(cancel_callback_2.is_null()); + + RunUntilIdle(); + EXPECT_TRUE(completed_1); + EXPECT_TRUE(completed_2); +} + +TEST_F(WebRTCIdentityStoreTest, DifferentCommonNameReturnNewIdentity) { + bool completed_1 = false; + bool completed_2 = false; + std::string cert_1, cert_2, key_1, key_2; + + base::Closure cancel_callback_1 = + RequestIdentityAndRunUtilIdle(kFakeOrigin, + kFakeIdentityName1, + kFakeCommonName1, + &completed_1, + &cert_1, + &key_1); + + base::Closure cancel_callback_2 = + RequestIdentityAndRunUtilIdle(kFakeOrigin, + kFakeIdentityName1, + kFakeCommonName2, + &completed_2, + &cert_2, + &key_2); + + EXPECT_TRUE(completed_1); + EXPECT_TRUE(completed_2); + EXPECT_NE(cert_1, cert_2); + EXPECT_NE(key_1, key_2); +} + +TEST_F(WebRTCIdentityStoreTest, SerialIdenticalRequests) { + bool completed_1 = false; + bool completed_2 = false; + std::string cert_1, cert_2, key_1, key_2; + + base::Closure cancel_callback_1 = + RequestIdentityAndRunUtilIdle(kFakeOrigin, + kFakeIdentityName1, + kFakeCommonName1, + &completed_1, + &cert_1, + &key_1); + + base::Closure cancel_callback_2 = + RequestIdentityAndRunUtilIdle(kFakeOrigin, + kFakeIdentityName1, + kFakeCommonName1, + &completed_2, + &cert_2, + &key_2); + + EXPECT_TRUE(completed_1); + EXPECT_TRUE(completed_2); + EXPECT_EQ(cert_1, cert_2); + EXPECT_EQ(key_1, key_2); +} + +TEST_F(WebRTCIdentityStoreTest, ConcurrentIdenticalRequestsJoined) { + bool completed_1 = false; + bool completed_2 = false; + std::string cert_1, cert_2, key_1, key_2; + + base::Closure cancel_callback_1 = webrtc_identity_store_->RequestIdentity( + GURL(kFakeOrigin), + kFakeIdentityName1, + kFakeCommonName1, + base::Bind(&OnRequestCompleted, &completed_1, &cert_1, &key_1)); + ASSERT_FALSE(cancel_callback_1.is_null()); + + base::Closure cancel_callback_2 = webrtc_identity_store_->RequestIdentity( + GURL(kFakeOrigin), + kFakeIdentityName1, + kFakeCommonName1, + base::Bind(&OnRequestCompleted, &completed_2, &cert_2, &key_2)); + ASSERT_FALSE(cancel_callback_2.is_null()); + + RunUntilIdle(); + EXPECT_TRUE(completed_1); + EXPECT_TRUE(completed_2); + EXPECT_EQ(cert_1, cert_2); + EXPECT_EQ(key_1, key_2); +} + +TEST_F(WebRTCIdentityStoreTest, CancelOneOfIdenticalRequests) { + bool completed_1 = false; + bool completed_2 = false; + std::string cert_1, cert_2, key_1, key_2; + + base::Closure cancel_callback_1 = webrtc_identity_store_->RequestIdentity( + GURL(kFakeOrigin), + kFakeIdentityName1, + kFakeCommonName1, + base::Bind(&OnRequestCompleted, &completed_1, &cert_1, &key_1)); + ASSERT_FALSE(cancel_callback_1.is_null()); + + base::Closure cancel_callback_2 = webrtc_identity_store_->RequestIdentity( + GURL(kFakeOrigin), + kFakeIdentityName1, + kFakeCommonName1, + base::Bind(&OnRequestCompleted, &completed_2, &cert_2, &key_2)); + ASSERT_FALSE(cancel_callback_2.is_null()); + + cancel_callback_1.Run(); + + RunUntilIdle(); + EXPECT_FALSE(completed_1); + EXPECT_TRUE(completed_2); +} + +TEST_F(WebRTCIdentityStoreTest, DeleteDataAndGenerateNewIdentity) { + bool completed_1 = false; + bool completed_2 = false; + std::string cert_1, cert_2, key_1, key_2; + + // Generate the first identity. + base::Closure cancel_callback_1 = + RequestIdentityAndRunUtilIdle(kFakeOrigin, + kFakeIdentityName1, + kFakeCommonName1, + &completed_1, + &cert_1, + &key_1); + + // Clear the data and the second request should return a new identity. + webrtc_identity_store_->DeleteBetween( + base::Time(), base::Time::Now(), base::Bind(&base::DoNothing)); + RunUntilIdle(); + + base::Closure cancel_callback_2 = + RequestIdentityAndRunUtilIdle(kFakeOrigin, + kFakeIdentityName1, + kFakeCommonName1, + &completed_2, + &cert_2, + &key_2); + + EXPECT_TRUE(completed_1); + EXPECT_TRUE(completed_2); + EXPECT_NE(cert_1, cert_2); + EXPECT_NE(key_1, key_2); +} + +#endif +} // namespace content diff --git a/chromium/content/browser/media/webrtc_internals.cc b/chromium/content/browser/media/webrtc_internals.cc new file mode 100644 index 00000000000..f8bcdc3d321 --- /dev/null +++ b/chromium/content/browser/media/webrtc_internals.cc @@ -0,0 +1,243 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/media/webrtc_internals.h" + +#include "content/browser/media/webrtc_internals_ui_observer.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/child_process_data.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/render_process_host.h" + +using base::ProcessId; +using std::string; + +namespace content { + +namespace { +// Makes sure that |dict| has a ListValue under path "log". +static base::ListValue* EnsureLogList(base::DictionaryValue* dict) { + base::ListValue* log = NULL; + if (!dict->GetList("log", &log)) { + log = new base::ListValue(); + if (log) + dict->Set("log", log); + } + return log; +} + +} // namespace + +WebRTCInternals::WebRTCInternals() : is_recording_rtp_(false) { + registrar_.Add(this, NOTIFICATION_RENDERER_PROCESS_TERMINATED, + NotificationService::AllBrowserContextsAndSources()); + + BrowserChildProcessObserver::Add(this); +} + +WebRTCInternals::~WebRTCInternals() { + BrowserChildProcessObserver::Remove(this); +} + +WebRTCInternals* WebRTCInternals::GetInstance() { + return Singleton<WebRTCInternals>::get(); +} + +void WebRTCInternals::OnAddPeerConnection(int render_process_id, + ProcessId pid, + int lid, + const string& url, + const string& servers, + const string& constraints) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + base::DictionaryValue* dict = new base::DictionaryValue(); + if (!dict) + return; + + dict->SetInteger("rid", render_process_id); + dict->SetInteger("pid", static_cast<int>(pid)); + dict->SetInteger("lid", lid); + dict->SetString("servers", servers); + dict->SetString("constraints", constraints); + dict->SetString("url", url); + peer_connection_data_.Append(dict); + + if (observers_.size() > 0) + SendUpdate("addPeerConnection", dict); +} + +void WebRTCInternals::OnRemovePeerConnection(ProcessId pid, int lid) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + for (size_t i = 0; i < peer_connection_data_.GetSize(); ++i) { + base::DictionaryValue* dict = NULL; + peer_connection_data_.GetDictionary(i, &dict); + + int this_pid = 0; + int this_lid = 0; + dict->GetInteger("pid", &this_pid); + dict->GetInteger("lid", &this_lid); + + if (this_pid != static_cast<int>(pid) || this_lid != lid) + continue; + + peer_connection_data_.Remove(i, NULL); + + if (observers_.size() > 0) { + base::DictionaryValue id; + id.SetInteger("pid", static_cast<int>(pid)); + id.SetInteger("lid", lid); + SendUpdate("removePeerConnection", &id); + } + break; + } +} + +void WebRTCInternals::OnUpdatePeerConnection( + ProcessId pid, int lid, const string& type, const string& value) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + for (size_t i = 0; i < peer_connection_data_.GetSize(); ++i) { + base::DictionaryValue* record = NULL; + peer_connection_data_.GetDictionary(i, &record); + + int this_pid = 0, this_lid = 0; + record->GetInteger("pid", &this_pid); + record->GetInteger("lid", &this_lid); + + if (this_pid != static_cast<int>(pid) || this_lid != lid) + continue; + + // Append the update to the end of the log. + base::ListValue* log = EnsureLogList(record); + if (!log) + return; + + base::DictionaryValue* log_entry = new base::DictionaryValue(); + if (!log_entry) + return; + + log_entry->SetString("type", type); + log_entry->SetString("value", value); + log->Append(log_entry); + + if (observers_.size() > 0) { + base::DictionaryValue update; + update.SetInteger("pid", static_cast<int>(pid)); + update.SetInteger("lid", lid); + update.SetString("type", type); + update.SetString("value", value); + + SendUpdate("updatePeerConnection", &update); + } + return; + } +} + +void WebRTCInternals::OnAddStats(base::ProcessId pid, int lid, + const base::ListValue& value) { + if (observers_.size() == 0) + return; + + base::DictionaryValue dict; + dict.SetInteger("pid", static_cast<int>(pid)); + dict.SetInteger("lid", lid); + + base::ListValue* list = value.DeepCopy(); + if (!list) + return; + + dict.Set("reports", list); + + SendUpdate("addStats", &dict); +} + +void WebRTCInternals::AddObserver(WebRTCInternalsUIObserver *observer) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + observers_.AddObserver(observer); +} + +void WebRTCInternals::RemoveObserver(WebRTCInternalsUIObserver *observer) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + observers_.RemoveObserver(observer); +} + +void WebRTCInternals::SendAllUpdates() { + if (observers_.size() > 0) + SendUpdate("updateAllPeerConnections", &peer_connection_data_); +} + +void WebRTCInternals::StartRtpRecording() { + if (!is_recording_rtp_) { + is_recording_rtp_ = true; + // TODO(justinlin): start RTP recording. + } +} + +void WebRTCInternals::StopRtpRecording() { + if (is_recording_rtp_) { + is_recording_rtp_ = false; + // TODO(justinlin): stop RTP recording. + } +} + +void WebRTCInternals::SendUpdate(const string& command, base::Value* value) { + DCHECK_GT(observers_.size(), (size_t)0); + + FOR_EACH_OBSERVER(WebRTCInternalsUIObserver, + observers_, + OnUpdate(command, value)); +} + +void WebRTCInternals::BrowserChildProcessCrashed( + const ChildProcessData& data) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + OnRendererExit(data.id); +} + +void WebRTCInternals::Observe(int type, + const NotificationSource& source, + const NotificationDetails& details) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + DCHECK_EQ(type, NOTIFICATION_RENDERER_PROCESS_TERMINATED); + OnRendererExit(Source<RenderProcessHost>(source)->GetID()); +} + +void WebRTCInternals::OnRendererExit(int render_process_id) { + // Iterates from the end of the list to remove the PeerConnections created + // by the exitting renderer. + for (int i = peer_connection_data_.GetSize() - 1; i >= 0; --i) { + base::DictionaryValue* record = NULL; + peer_connection_data_.GetDictionary(i, &record); + + int this_rid = 0; + record->GetInteger("rid", &this_rid); + + if (this_rid == render_process_id) { + if (observers_.size() > 0) { + int lid = 0, pid = 0; + record->GetInteger("lid", &lid); + record->GetInteger("pid", &pid); + + base::DictionaryValue update; + update.SetInteger("lid", lid); + update.SetInteger("pid", pid); + SendUpdate("removePeerConnection", &update); + } + peer_connection_data_.Remove(i, NULL); + } + } +} + +// TODO(justlin): Calls this method as necessary to update the recording status +// UI. +void WebRTCInternals::SendRtpRecordingUpdate() { + DCHECK(is_recording_rtp_); + base::DictionaryValue update; + // TODO(justinlin): Fill in |update| with values as appropriate. + SendUpdate("updateDumpStatus", &update); +} + +} // namespace content diff --git a/chromium/content/browser/media/webrtc_internals.h b/chromium/content/browser/media/webrtc_internals.h new file mode 100644 index 00000000000..58e9a9fbb9b --- /dev/null +++ b/chromium/content/browser/media/webrtc_internals.h @@ -0,0 +1,117 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_H_ +#define CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_H_ + +#include "base/memory/singleton.h" +#include "base/observer_list.h" +#include "base/process/process.h" +#include "base/values.h" +#include "content/common/content_export.h" +#include "content/public/browser/browser_child_process_observer.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" + +namespace content { +class WebRTCInternalsUIObserver; + +// This is a singleton class running in the browser UI thread. +// It collects peer connection infomation from the renderers, +// forwards the data to WebRTCInternalsUIObserver and +// sends data collecting commands to the renderers. +class CONTENT_EXPORT WebRTCInternals : public BrowserChildProcessObserver, + public NotificationObserver { + public: + static WebRTCInternals* GetInstance(); + + // This method is called when a PeerConnection is created. + // |render_process_id| is the id of the render process (not OS pid), which is + // needed because we might not be able to get the OS process id when the + // render process terminates and we want to clean up. + // |pid| is the renderer process id, |lid| is the renderer local id used to + // identify a PeerConnection, |url| is the url of the tab owning the + // PeerConnection, |servers| is the servers configuration, |constraints| is + // the media constraints used to initialize the PeerConnection. + void OnAddPeerConnection(int render_process_id, + base::ProcessId pid, + int lid, + const std::string& url, + const std::string& servers, + const std::string& constraints); + + // This method is called when PeerConnection is destroyed. + // |pid| is the renderer process id, |lid| is the renderer local id. + void OnRemovePeerConnection(base::ProcessId pid, int lid); + + // This method is called when a PeerConnection is updated. + // |pid| is the renderer process id, |lid| is the renderer local id, + // |type| is the update type, |value| is the detail of the update. + void OnUpdatePeerConnection(base::ProcessId pid, + int lid, + const std::string& type, + const std::string& value); + + // This method is called when results from PeerConnectionInterface::GetStats + // are available. |pid| is the renderer process id, |lid| is the renderer + // local id, |value| is the list of stats reports. + void OnAddStats(base::ProcessId pid, int lid, const base::ListValue& value); + + // Methods for adding or removing WebRTCInternalsUIObserver. + void AddObserver(WebRTCInternalsUIObserver *observer); + void RemoveObserver(WebRTCInternalsUIObserver *observer); + + // Sends all update data to the observers. + void SendAllUpdates(); + + // Tells the renderer processes to start or stop recording RTP packets. + void StartRtpRecording(); + void StopRtpRecording(); + + private: + friend struct DefaultSingletonTraits<WebRTCInternals>; + + WebRTCInternals(); + virtual ~WebRTCInternals(); + + void SendUpdate(const std::string& command, base::Value* value); + + // BrowserChildProcessObserver implementation. + virtual void BrowserChildProcessCrashed( + const ChildProcessData& data) OVERRIDE; + + // NotificationObserver implementation. + virtual void Observe(int type, + const NotificationSource& source, + const NotificationDetails& details) OVERRIDE; + + // Called when a renderer exits (including crashes). + void OnRendererExit(int render_process_id); + + void SendRtpRecordingUpdate(); + + ObserverList<WebRTCInternalsUIObserver> observers_; + + // |peer_connection_data_| is a list containing all the PeerConnection + // updates. + // Each item of the list represents the data for one PeerConnection, which + // contains these fields: + // "pid" -- processId of the renderer that creates the PeerConnection. + // "lid" -- local Id assigned to the PeerConnection. + // "url" -- url of the web page that created the PeerConnection. + // "servers" and "constraints" -- server configuration and media constraints + // used to initialize the PeerConnection respectively. + // "log" -- a ListValue contains all the updates for the PeerConnection. Each + // list item is a DictionaryValue containing "type" and "value", both of which + // are strings. + base::ListValue peer_connection_data_; + + NotificationRegistrar registrar_; + + bool is_recording_rtp_; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_H_ diff --git a/chromium/content/browser/media/webrtc_internals_browsertest.cc b/chromium/content/browser/media/webrtc_internals_browsertest.cc new file mode 100644 index 00000000000..71bbe53ed9f --- /dev/null +++ b/chromium/content/browser/media/webrtc_internals_browsertest.cc @@ -0,0 +1,709 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/command_line.h" +#include "base/json/json_reader.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "base/values.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "content/shell/shell.h" +#include "content/test/content_browser_test.h" +#include "content/test/content_browser_test_utils.h" +#include "net/test/embedded_test_server/embedded_test_server.h" + +using std::string; +namespace content { + +struct SsrcEntry { + string GetSsrcAttributeString() const { + std::stringstream ss; + ss << "a=ssrc:" << id; + std::map<string, string>::const_iterator iter; + for (iter = properties.begin(); iter != properties.end(); ++iter) { + ss << " " << iter->first << ":" << iter->second; + } + return ss.str(); + } + + string GetAsJSON() const { + std::stringstream ss; + ss << "{"; + std::map<string, string>::const_iterator iter; + for (iter = properties.begin(); iter != properties.end(); ++iter) { + if (iter != properties.begin()) + ss << ","; + ss << "\"" << iter->first << "\":\"" << iter->second << "\""; + } + ss << "}"; + return ss.str(); + } + + string id; + std::map<string, string> properties; +}; + +struct EventEntry { + string type; + string value; +}; + +struct StatsUnit { + string GetString() const { + std::stringstream ss; + ss << "{timestamp:" << timestamp << ", values:["; + std::map<string, string>::const_iterator iter; + for (iter = values.begin(); iter != values.end(); ++iter) { + ss << "'" << iter->first << "','" << iter->second << "',"; + } + ss << "]}"; + return ss.str(); + } + + int64 timestamp; + std::map<string, string> values; +}; + +struct StatsEntry { + string type; + string id; + StatsUnit stats; +}; + +typedef std::map<string, std::vector<string> > StatsMap; + +class PeerConnectionEntry { + public: + PeerConnectionEntry(int pid, int lid) : pid_(pid), lid_(lid) {} + + void AddEvent(const string& type, const string& value) { + EventEntry entry = {type, value}; + events_.push_back(entry); + } + + string getIdString() const { + std::stringstream ss; + ss << pid_ << "-" << lid_; + return ss.str(); + } + + string getLogIdString() const { + std::stringstream ss; + ss << pid_ << "-" << lid_ << "-update-log"; + return ss.str(); + } + + string getAllUpdateString() const { + std::stringstream ss; + ss << "{pid:" << pid_ << ", lid:" << lid_ << ", log:["; + for (size_t i = 0; i < events_.size(); ++i) { + ss << "{type:'" << events_[i].type << + "', value:'" << events_[i].value << "'},"; + } + ss << "]}"; + return ss.str(); + } + + int pid_; + int lid_; + std::vector<EventEntry> events_; + // This is a record of the history of stats value reported for each stats + // report id (e.g. ssrc-1234) for each stats name (e.g. framerate). + // It a 2-D map with each map entry is a vector of reported values. + // It is used to verify the graph data series. + std::map<string, StatsMap> stats_; +}; + +static const int64 FAKE_TIME_STAMP = 3600000; + +class WebRTCInternalsBrowserTest: public ContentBrowserTest { + public: + WebRTCInternalsBrowserTest() {} + virtual ~WebRTCInternalsBrowserTest() {} + + virtual void SetUpOnMainThread() OVERRIDE { + // We need fake devices in this test since we want to run on naked VMs. We + // assume these switches are set by default in content_browsertests. + ASSERT_TRUE(CommandLine::ForCurrentProcess()->HasSwitch( + switches::kUseFakeDeviceForMediaStream)); + ASSERT_TRUE(CommandLine::ForCurrentProcess()->HasSwitch( + switches::kUseFakeUIForMediaStream)); + } + + protected: + bool ExecuteJavascript(const string& javascript) { + return ExecuteScript(shell()->web_contents(), javascript); + } + + void ExpectTitle(const std::string& expected_title) const { + string16 expected_title16(ASCIIToUTF16(expected_title)); + TitleWatcher title_watcher(shell()->web_contents(), expected_title16); + EXPECT_EQ(expected_title16, title_watcher.WaitAndGetTitle()); + } + + // Execute the javascript of addPeerConnection. + void ExecuteAddPeerConnectionJs(const PeerConnectionEntry& pc) { + std::stringstream ss; + ss << "{pid:" << pc.pid_ <<", lid:" << pc.lid_ << ", " << + "url:'u', servers:'s', constraints:'c'}"; + ASSERT_TRUE(ExecuteJavascript("addPeerConnection(" + ss.str() + ");")); + } + + // Execute the javascript of removePeerConnection. + void ExecuteRemovePeerConnectionJs(const PeerConnectionEntry& pc) { + std::stringstream ss; + ss << "{pid:" << pc.pid_ <<", lid:" << pc.lid_ << "}"; + + ASSERT_TRUE(ExecuteJavascript("removePeerConnection(" + ss.str() + ");")); + } + + // Verifies that the DOM element with id |id| exists. + void VerifyElementWithId(const string& id) { + bool result = false; + ASSERT_TRUE(ExecuteScriptAndExtractBool( + shell()->web_contents(), + "window.domAutomationController.send($('" + id + "') != null);", + &result)); + EXPECT_TRUE(result); + } + + // Verifies that the DOM element with id |id| does not exist. + void VerifyNoElementWithId(const string& id) { + bool result = false; + ASSERT_TRUE(ExecuteScriptAndExtractBool( + shell()->web_contents(), + "window.domAutomationController.send($('" + id + "') == null);", + &result)); + EXPECT_TRUE(result); + } + + // Verifies that DOM for |pc| is correctly created with the right content. + void VerifyPeerConnectionEntry(const PeerConnectionEntry& pc) { + VerifyElementWithId(pc.getIdString()); + if (pc.events_.size() == 0) + return; + + string log_id = pc.getLogIdString(); + VerifyElementWithId(log_id); + string result; + for (size_t i = 0; i < pc.events_.size(); ++i) { + std::stringstream ss; + ss << "var row = $('" << log_id << "').rows[" << (i + 1) << "];" + "var cell = row.lastChild;" + "window.domAutomationController.send(cell.firstChild.textContent);"; + ASSERT_TRUE(ExecuteScriptAndExtractString( + shell()->web_contents(), ss.str(), &result)); + EXPECT_EQ(pc.events_[i].type + pc.events_[i].value, result); + } + } + + // Executes the javascript of updatePeerConnection and verifies the result. + void ExecuteAndVerifyUpdatePeerConnection( + PeerConnectionEntry& pc, const string& type, const string& value) { + pc.AddEvent(type, value); + + std::stringstream ss; + ss << "{pid:" << pc.pid_ <<", lid:" << pc.lid_ << + ", type:'" << type << "', value:'" << value << "'}"; + ASSERT_TRUE(ExecuteJavascript("updatePeerConnection(" + ss.str() + ")")); + + VerifyPeerConnectionEntry(pc); + } + + // Execute addStats and verifies that the stats table has the right content. + void ExecuteAndVerifyAddStats( + PeerConnectionEntry& pc, const string& type, const string& id, + StatsUnit& stats) { + StatsEntry entry = {type, id, stats}; + + // Adds each new value to the map of stats history. + std::map<string, string>::iterator iter; + for (iter = stats.values.begin(); iter != stats.values.end(); iter++) { + pc.stats_[id][iter->first].push_back(iter->second); + } + std::stringstream ss; + ss << "{pid:" << pc.pid_ << ", lid:" << pc.lid_ << "," + "reports:[" << "{id:'" << id << "', type:'" << type << "', " + "stats:" << stats.GetString() << "}]}"; + + ASSERT_TRUE(ExecuteJavascript("addStats(" + ss.str() + ")")); + VerifyStatsTable(pc, entry); + } + + + // Verifies that the stats table has the right content. + void VerifyStatsTable(const PeerConnectionEntry& pc, + const StatsEntry& report) { + string table_id = + pc.getIdString() + "-table-" + report.id; + VerifyElementWithId(table_id); + + std::map<string, string>::const_iterator iter; + for (iter = report.stats.values.begin(); + iter != report.stats.values.end(); iter++) { + VerifyStatsTableRow(table_id, iter->first, iter->second); + } + } + + // Verifies that the row named as |name| of the stats table |table_id| has + // the correct content as |name| : |value|. + void VerifyStatsTableRow(const string& table_id, + const string& name, + const string& value) { + VerifyElementWithId(table_id + "-" + name); + + string result; + ASSERT_TRUE(ExecuteScriptAndExtractString( + shell()->web_contents(), + "var row = $('" + table_id + "-" + name + "');" + "var name = row.cells[0].textContent;" + "var value = row.cells[1].textContent;" + "window.domAutomationController.send(name + ':' + value)", + &result)); + EXPECT_EQ(name + ":" + value, result); + } + + // Verifies that the graph data series consistent with pc.stats_. + void VerifyStatsGraph(const PeerConnectionEntry& pc) { + std::map<string, StatsMap>::const_iterator stream_iter; + for (stream_iter = pc.stats_.begin(); + stream_iter != pc.stats_.end(); stream_iter++) { + StatsMap::const_iterator stats_iter; + for (stats_iter = stream_iter->second.begin(); + stats_iter != stream_iter->second.end(); + stats_iter++) { + string graph_id = stream_iter->first + "-" + stats_iter->first; + for (size_t i = 0; i < stats_iter->second.size(); ++i) { + float number; + std::stringstream stream(stats_iter->second[i]); + stream >> number; + if (stream.fail()) + continue; + VerifyGraphDataPoint( + pc.getIdString(), graph_id, i, stats_iter->second[i]); + } + } + } + } + + // Verifies that the graph data point at index |index| has value |value|. + void VerifyGraphDataPoint(const string& pc_id, const string& graph_id, + int index, const string& value) { + bool result = false; + ASSERT_TRUE(ExecuteScriptAndExtractBool( + shell()->web_contents(), + "window.domAutomationController.send(" + "graphViews['" + pc_id + "-" + graph_id + "'] != null)", + &result)); + EXPECT_TRUE(result); + + std::stringstream ss; + ss << "var dp = peerConnectionDataStore['" << pc_id << "']" + ".getDataSeries('" << graph_id << "').dataPoints_[" << index << "];" + "window.domAutomationController.send(dp.value.toString())"; + string actual_value; + ASSERT_TRUE(ExecuteScriptAndExtractString( + shell()->web_contents(), ss.str(), &actual_value)); + EXPECT_EQ(value, actual_value); + } + + // Get the JSON string of the ssrc info from the page. + string GetSsrcInfo(const string& ssrc_id) { + string result; + EXPECT_TRUE(ExecuteScriptAndExtractString( + shell()->web_contents(), + "window.domAutomationController.send(JSON.stringify(" + "ssrcInfoManager.streamInfoContainer_['" + ssrc_id + "']))", + &result)); + return result; + } + + int GetSsrcInfoBlockCount(Shell* shell) { + int count = 0; + EXPECT_TRUE(ExecuteScriptAndExtractInt( + shell->web_contents(), + "window.domAutomationController.send(" + "document.getElementsByClassName(" + "ssrcInfoManager.SSRC_INFO_BLOCK_CLASS).length);", + &count)); + return count; + } + + // Verifies |dump| contains |peer_connection_number| peer connection dumps, + // each containing |update_number| updates and |stats_number| stats tables. + void VerifyPageDumpStructure(base::Value* dump, + int peer_connection_number, + int update_number, + int stats_number) { + EXPECT_NE((base::Value*)NULL, dump); + EXPECT_EQ(base::Value::TYPE_DICTIONARY, dump->GetType()); + + base::DictionaryValue* dict_dump = + static_cast<base::DictionaryValue*>(dump); + EXPECT_EQ((size_t) peer_connection_number, dict_dump->size()); + + base::DictionaryValue::Iterator it(*dict_dump); + for (; !it.IsAtEnd(); it.Advance()) { + base::Value* value = NULL; + dict_dump->Get(it.key(), &value); + EXPECT_EQ(base::Value::TYPE_DICTIONARY, value->GetType()); + base::DictionaryValue* pc_dump = + static_cast<base::DictionaryValue*>(value); + EXPECT_TRUE(pc_dump->HasKey("updateLog")); + EXPECT_TRUE(pc_dump->HasKey("stats")); + + // Verifies the number of updates. + pc_dump->Get("updateLog", &value); + EXPECT_EQ(base::Value::TYPE_LIST, value->GetType()); + base::ListValue* list = static_cast<base::ListValue*>(value); + EXPECT_EQ((size_t) update_number, list->GetSize()); + + // Verifies the number of stats tables. + pc_dump->Get("stats", &value); + EXPECT_EQ(base::Value::TYPE_DICTIONARY, value->GetType()); + base::DictionaryValue* dict = static_cast<base::DictionaryValue*>(value); + EXPECT_EQ((size_t) stats_number, dict->size()); + } + } + + // Verifies |dump| contains the correct statsTable and statsDataSeries for + // |pc|. + void VerifyStatsDump(base::Value* dump, + const PeerConnectionEntry& pc, + const string& report_type, + const string& report_id, + const StatsUnit& stats) { + EXPECT_NE((base::Value*)NULL, dump); + EXPECT_EQ(base::Value::TYPE_DICTIONARY, dump->GetType()); + + base::DictionaryValue* dict_dump = + static_cast<base::DictionaryValue*>(dump); + base::Value* value = NULL; + dict_dump->Get(pc.getIdString(), &value); + base::DictionaryValue* pc_dump = static_cast<base::DictionaryValue*>(value); + + // Verifies there is one data series per stats name. + value = NULL; + pc_dump->Get("stats", &value); + EXPECT_EQ(base::Value::TYPE_DICTIONARY, value->GetType()); + + base::DictionaryValue* dataSeries = + static_cast<base::DictionaryValue*>(value); + EXPECT_EQ(stats.values.size(), dataSeries->size()); + } +}; + +IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, AddAndRemovePeerConnection) { + GURL url("chrome://webrtc-internals"); + NavigateToURL(shell(), url); + + // Add two PeerConnections and then remove them. + PeerConnectionEntry pc_1(1, 0); + ExecuteAddPeerConnectionJs(pc_1); + VerifyPeerConnectionEntry(pc_1); + + PeerConnectionEntry pc_2(2, 1); + ExecuteAddPeerConnectionJs(pc_2); + VerifyPeerConnectionEntry(pc_2); + + ExecuteRemovePeerConnectionJs(pc_1); + VerifyNoElementWithId(pc_1.getIdString()); + VerifyPeerConnectionEntry(pc_2); + + ExecuteRemovePeerConnectionJs(pc_2); + VerifyNoElementWithId(pc_2.getIdString()); +} + +IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, UpdateAllPeerConnections) { + GURL url("chrome://webrtc-internals"); + NavigateToURL(shell(), url); + + PeerConnectionEntry pc_0(1, 0); + pc_0.AddEvent("e1", "v1"); + pc_0.AddEvent("e2", "v2"); + PeerConnectionEntry pc_1(1, 1); + pc_1.AddEvent("e3", "v3"); + pc_1.AddEvent("e4", "v4"); + string pc_array = "[" + pc_0.getAllUpdateString() + ", " + + pc_1.getAllUpdateString() + "]"; + EXPECT_TRUE(ExecuteJavascript("updateAllPeerConnections(" + pc_array + ");")); + VerifyPeerConnectionEntry(pc_0); + VerifyPeerConnectionEntry(pc_1); +} + +IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, UpdatePeerConnection) { + GURL url("chrome://webrtc-internals"); + NavigateToURL(shell(), url); + + // Add one PeerConnection and send one update. + PeerConnectionEntry pc_1(1, 0); + ExecuteAddPeerConnectionJs(pc_1); + + ExecuteAndVerifyUpdatePeerConnection(pc_1, "e1", "v1"); + + // Add another PeerConnection and send two updates. + PeerConnectionEntry pc_2(1, 1); + ExecuteAddPeerConnectionJs(pc_2); + + SsrcEntry ssrc1, ssrc2; + ssrc1.id = "ssrcid1"; + ssrc1.properties["msid"] = "mymsid"; + ssrc2.id = "ssrcid2"; + ssrc2.properties["label"] = "mylabel"; + ssrc2.properties["cname"] = "mycname"; + + ExecuteAndVerifyUpdatePeerConnection(pc_2, "setRemoteDescription", + ssrc1.GetSsrcAttributeString()); + + ExecuteAndVerifyUpdatePeerConnection(pc_2, "setLocalDescription", + ssrc2.GetSsrcAttributeString()); + + EXPECT_EQ(ssrc1.GetAsJSON(), GetSsrcInfo(ssrc1.id)); + EXPECT_EQ(ssrc2.GetAsJSON(), GetSsrcInfo(ssrc2.id)); + + StatsUnit stats = {FAKE_TIME_STAMP}; + stats.values["ssrc"] = ssrc1.id; + ExecuteAndVerifyAddStats(pc_2, "ssrc", "dummyId", stats); + EXPECT_GT(GetSsrcInfoBlockCount(shell()), 0); +} + +// Tests that adding random named stats updates the dataSeries and graphs. +IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, AddStats) { + GURL url("chrome://webrtc-internals"); + NavigateToURL(shell(), url); + + PeerConnectionEntry pc(1, 0); + ExecuteAddPeerConnectionJs(pc); + + const string type = "ssrc"; + const string id = "ssrc-1234"; + StatsUnit stats = {FAKE_TIME_STAMP}; + stats.values["trackId"] = "abcd"; + stats.values["bitrate"] = "2000"; + stats.values["framerate"] = "30"; + + // Add new stats and verify the stats table and graphs. + ExecuteAndVerifyAddStats(pc, type, id, stats); + VerifyStatsGraph(pc); + + // Update existing stats and verify the stats table and graphs. + stats.values["bitrate"] = "2001"; + stats.values["framerate"] = "31"; + ExecuteAndVerifyAddStats(pc, type, id, stats); + VerifyStatsGraph(pc); +} + +// Tests that the bandwidth estimation values are drawn on a single graph. +IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, BweCompoundGraph) { + GURL url("chrome://webrtc-internals"); + NavigateToURL(shell(), url); + + PeerConnectionEntry pc(1, 0); + ExecuteAddPeerConnectionJs(pc); + + StatsUnit stats = {FAKE_TIME_STAMP}; + stats.values["googAvailableSendBandwidth"] = "1000000"; + stats.values["googTargetEncBitrate"] = "1000"; + stats.values["googActualEncBitrate"] = "1000000"; + stats.values["googRetransmitBitrate"] = "10"; + stats.values["googTransmitBitrate"] = "1000000"; + const string stats_type = "bwe"; + const string stats_id = "videobwe"; + ExecuteAndVerifyAddStats(pc, stats_type, stats_id, stats); + + string graph_id = + pc.getIdString() + "-" + stats_id + "-bweCompound"; + bool result = false; + // Verify that the bweCompound graph exists. + ASSERT_TRUE(ExecuteScriptAndExtractBool( + shell()->web_contents(), + "window.domAutomationController.send(" + " graphViews['" + graph_id + "'] != null)", + &result)); + EXPECT_TRUE(result); + + // Verify that the bweCompound graph contains multiple dataSeries. + int count = 0; + ASSERT_TRUE(ExecuteScriptAndExtractInt( + shell()->web_contents(), + "window.domAutomationController.send(" + " graphViews['" + graph_id + "'].getDataSeriesCount())", + &count)); + EXPECT_EQ((int)stats.values.size(), count); +} + +// Tests that the total packet/byte count is converted to count per second, +// and the converted data is drawn. +IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, ConvertedGraphs) { + GURL url("chrome://webrtc-internals"); + NavigateToURL(shell(), url); + + PeerConnectionEntry pc(1, 0); + ExecuteAddPeerConnectionJs(pc); + + const string stats_type = "s"; + const string stats_id = "1"; + const int num_converted_stats = 4; + const string stats_names[] = + {"packetsSent", "bytesSent", "packetsReceived", "bytesReceived"}; + const string converted_names[] = + {"packetsSentPerSecond", "bitsSentPerSecond", + "packetsReceivedPerSecond", "bitsReceivedPerSecond"}; + const string first_value = "1000"; + const string second_value = "2000"; + const string converted_values[] = {"1000", "8000", "1000", "8000"}; + + // Send the first data point. + StatsUnit stats = {FAKE_TIME_STAMP}; + for (int i = 0; i < num_converted_stats; ++i) + stats.values[stats_names[i]] = first_value; + + ExecuteAndVerifyAddStats(pc, stats_type, stats_id, stats); + + // Send the second data point at 1000ms after the first data point. + stats.timestamp += 1000; + for (int i = 0; i < num_converted_stats; ++i) + stats.values[stats_names[i]] = second_value; + ExecuteAndVerifyAddStats(pc, stats_type, stats_id, stats); + + // Verifies the graph data matches converted_values. + for (int i = 0; i < num_converted_stats; ++i) { + VerifyGraphDataPoint(pc.getIdString(), stats_id + "-" + converted_names[i], + 1, converted_values[i]); + } +} + +// Timing out on ARM linux bot: http://crbug.com/238490 +// Disabling due to failure on Linux, Mac, Win: http://crbug.com/272413 +// Sanity check of the page content under a real PeerConnection call. +IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, + DISABLED_WithRealPeerConnectionCall) { + // Start a peerconnection call in the first window. + ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); + GURL url(embedded_test_server()->GetURL("/media/peerconnection-call.html")); + NavigateToURL(shell(), url); + ASSERT_TRUE(ExecuteJavascript("call({video:true});")); + ExpectTitle("OK"); + + // Open webrtc-internals in the second window. + GURL url2("chrome://webrtc-internals"); + Shell* shell2 = CreateBrowser(); + NavigateToURL(shell2, url2); + + const int NUMBER_OF_PEER_CONNECTIONS = 2; + + // Verifies the number of peerconnections. + int count = 0; + ASSERT_TRUE(ExecuteScriptAndExtractInt( + shell2->web_contents(), + "window.domAutomationController.send(" + "$('peer-connections-list').getElementsByTagName('li').length);", + &count)); + EXPECT_EQ(NUMBER_OF_PEER_CONNECTIONS, count); + + // Verifies the the event tables. + ASSERT_TRUE(ExecuteScriptAndExtractInt( + shell2->web_contents(), + "window.domAutomationController.send($('peer-connections-list')" + ".getElementsByClassName('update-log-table').length);", + &count)); + EXPECT_EQ(NUMBER_OF_PEER_CONNECTIONS, count); + + ASSERT_TRUE(ExecuteScriptAndExtractInt( + shell2->web_contents(), + "window.domAutomationController.send($('peer-connections-list')" + ".getElementsByClassName('update-log-table')[0].rows.length);", + &count)); + EXPECT_GT(count, 1); + + ASSERT_TRUE(ExecuteScriptAndExtractInt( + shell2->web_contents(), + "window.domAutomationController.send($('peer-connections-list')" + ".getElementsByClassName('update-log-table')[1].rows.length);", + &count)); + EXPECT_GT(count, 1); + + // Wait until the stats table containers are created. + count = 0; + while (count != NUMBER_OF_PEER_CONNECTIONS) { + ASSERT_TRUE(ExecuteScriptAndExtractInt( + shell2->web_contents(), + "window.domAutomationController.send(" + "$('peer-connections-list').getElementsByClassName(" + "'stats-table-container').length);", + &count)); + } + + // Verifies each stats table having more than one rows. + bool result = false; + ASSERT_TRUE(ExecuteScriptAndExtractBool( + shell2->web_contents(), + "var tableContainers = $('peer-connections-list')" + ".getElementsByClassName('stats-table-container');" + "var result = true;" + "for (var i = 0; i < tableContainers.length && result; ++i) {" + "var tables = tableContainers[i].getElementsByTagName('table');" + "for (var j = 0; j < tables.length && result; ++j) {" + "result = (tables[j].rows.length > 1);" + "}" + "if (!result) {" + "console.log(tableContainers[i].innerHTML);" + "}" + "}" + "window.domAutomationController.send(result);", + &result)); + + EXPECT_TRUE(result); + + count = GetSsrcInfoBlockCount(shell2); + EXPECT_GT(count, 0); +} + +IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, CreatePageDump) { + GURL url("chrome://webrtc-internals"); + NavigateToURL(shell(), url); + + PeerConnectionEntry pc_0(1, 0); + pc_0.AddEvent("e1", "v1"); + pc_0.AddEvent("e2", "v2"); + PeerConnectionEntry pc_1(1, 1); + pc_1.AddEvent("e3", "v3"); + pc_1.AddEvent("e4", "v4"); + string pc_array = + "[" + pc_0.getAllUpdateString() + ", " + pc_1.getAllUpdateString() + "]"; + EXPECT_TRUE(ExecuteJavascript("updateAllPeerConnections(" + pc_array + ");")); + + // Verifies the peer connection data store can be created without stats. + string dump_json; + ASSERT_TRUE(ExecuteScriptAndExtractString( + shell()->web_contents(), + "window.domAutomationController.send(" + "JSON.stringify(peerConnectionDataStore));", + &dump_json)); + scoped_ptr<base::Value> dump; + dump.reset(base::JSONReader::Read(dump_json)); + VerifyPageDumpStructure(dump.get(), + 2 /*peer_connection_number*/, + 2 /*update_number*/, + 0 /*stats_number*/); + + // Adds a stats report. + const string type = "dummy"; + const string id = "1234"; + StatsUnit stats = { FAKE_TIME_STAMP }; + stats.values["bitrate"] = "2000"; + stats.values["framerate"] = "30"; + ExecuteAndVerifyAddStats(pc_0, type, id, stats); + + ASSERT_TRUE(ExecuteScriptAndExtractString( + shell()->web_contents(), + "window.domAutomationController.send(" + "JSON.stringify(peerConnectionDataStore));", + &dump_json)); + dump.reset(base::JSONReader::Read(dump_json)); + VerifyStatsDump(dump.get(), pc_0, type, id, stats); +} + +} // namespace content diff --git a/chromium/content/browser/media/webrtc_internals_message_handler.cc b/chromium/content/browser/media/webrtc_internals_message_handler.cc new file mode 100644 index 00000000000..2ba75227380 --- /dev/null +++ b/chromium/content/browser/media/webrtc_internals_message_handler.cc @@ -0,0 +1,78 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/media/webrtc_internals_message_handler.h" + +#include "content/browser/media/webrtc_internals.h" +#include "content/common/media/peer_connection_tracker_messages.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/render_view_host.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" + +namespace content { + +WebRTCInternalsMessageHandler::WebRTCInternalsMessageHandler() { + WebRTCInternals::GetInstance()->AddObserver(this); +} + +WebRTCInternalsMessageHandler::~WebRTCInternalsMessageHandler() { + WebRTCInternals::GetInstance()->RemoveObserver(this); +} + +void WebRTCInternalsMessageHandler::RegisterMessages() { + web_ui()->RegisterMessageCallback("getAllUpdates", + base::Bind(&WebRTCInternalsMessageHandler::OnGetAllUpdates, + base::Unretained(this))); + + web_ui()->RegisterMessageCallback("getAllStats", + base::Bind(&WebRTCInternalsMessageHandler::OnGetAllStats, + base::Unretained(this))); + + web_ui()->RegisterMessageCallback("startRtpRecording", + base::Bind(&WebRTCInternalsMessageHandler::OnStartRtpRecording, + base::Unretained(this))); + + web_ui()->RegisterMessageCallback("stopRtpRecording", + base::Bind(&WebRTCInternalsMessageHandler::OnStopRtpRecording, + base::Unretained(this))); +} + +void WebRTCInternalsMessageHandler::OnGetAllUpdates( + const base::ListValue* list) { + WebRTCInternals::GetInstance()->SendAllUpdates(); +} + +void WebRTCInternalsMessageHandler::OnGetAllStats(const base::ListValue* list) { + for (RenderProcessHost::iterator i( + content::RenderProcessHost::AllHostsIterator()); + !i.IsAtEnd(); i.Advance()) { + i.GetCurrentValue()->Send(new PeerConnectionTracker_GetAllStats()); + } +} + +void WebRTCInternalsMessageHandler::OnStartRtpRecording( + const base::ListValue* list) { + WebRTCInternals::GetInstance()->StartRtpRecording(); +} + +void WebRTCInternalsMessageHandler::OnStopRtpRecording( + const base::ListValue* list) { + WebRTCInternals::GetInstance()->StopRtpRecording(); +} + +void WebRTCInternalsMessageHandler::OnUpdate(const std::string& command, + const base::Value* args) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + std::vector<const base::Value*> args_vector; + args_vector.push_back(args); + string16 update = WebUI::GetJavascriptCall(command, args_vector); + + RenderViewHost* host = web_ui()->GetWebContents()->GetRenderViewHost(); + if (host) + host->ExecuteJavascriptInWebFrame(string16(), update); +} + +} // namespace content diff --git a/chromium/content/browser/media/webrtc_internals_message_handler.h b/chromium/content/browser/media/webrtc_internals_message_handler.h new file mode 100644 index 00000000000..ee2059a646d --- /dev/null +++ b/chromium/content/browser/media/webrtc_internals_message_handler.h @@ -0,0 +1,45 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_MESSAGE_HANDLER_H_ +#define CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_MESSAGE_HANDLER_H_ + +#include "base/memory/ref_counted.h" +#include "content/browser/media/webrtc_internals_ui_observer.h" +#include "content/public/browser/web_ui_message_handler.h" + +namespace base { +class ListValue; +} // namespace base + +namespace content { + +// This class handles messages to and from WebRTCInternalsUI. +// It delegates all its work to WebRTCInternalsProxy on the IO thread. +class WebRTCInternalsMessageHandler : public WebUIMessageHandler, + public WebRTCInternalsUIObserver{ + public: + WebRTCInternalsMessageHandler(); + virtual ~WebRTCInternalsMessageHandler(); + + // WebUIMessageHandler implementation. + virtual void RegisterMessages() OVERRIDE; + + // WebRTCInternalsUIObserver override. + virtual void OnUpdate(const std::string& command, + const base::Value* args) OVERRIDE; + + // Javascript message handler. + void OnGetAllUpdates(const base::ListValue* list); + void OnGetAllStats(const base::ListValue* list); + void OnStartRtpRecording(const base::ListValue* list); + void OnStopRtpRecording(const base::ListValue* list); + + private: + DISALLOW_COPY_AND_ASSIGN(WebRTCInternalsMessageHandler); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_MESSAGE_HANDLER_H_ diff --git a/chromium/content/browser/media/webrtc_internals_ui.cc b/chromium/content/browser/media/webrtc_internals_ui.cc new file mode 100644 index 00000000000..5c4adc591a9 --- /dev/null +++ b/chromium/content/browser/media/webrtc_internals_ui.cc @@ -0,0 +1,44 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/media/webrtc_internals_ui.h" + +#include "content/browser/media/webrtc_internals_message_handler.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" +#include "content/public/browser/web_ui_data_source.h" +#include "content/public/common/url_constants.h" +#include "grit/content_resources.h" + +namespace content { +namespace { + +WebUIDataSource* CreateWebRTCInternalsHTMLSource() { + WebUIDataSource* source = + WebUIDataSource::Create(kChromeUIWebRTCInternalsHost); + + source->SetJsonPath("strings.js"); + source->AddResourcePath("webrtc_internals.js", IDR_WEBRTC_INTERNALS_JS); + source->SetDefaultResource(IDR_WEBRTC_INTERNALS_HTML); + return source; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +// +// WebRTCInternalsUI +// +//////////////////////////////////////////////////////////////////////////////// + +WebRTCInternalsUI::WebRTCInternalsUI(WebUI* web_ui) + : WebUIController(web_ui) { + web_ui->AddMessageHandler(new WebRTCInternalsMessageHandler()); + + BrowserContext* browser_context = + web_ui->GetWebContents()->GetBrowserContext(); + WebUIDataSource::Add(browser_context, CreateWebRTCInternalsHTMLSource()); +} + +} // namespace content diff --git a/chromium/content/browser/media/webrtc_internals_ui.h b/chromium/content/browser/media/webrtc_internals_ui.h new file mode 100644 index 00000000000..29745b18681 --- /dev/null +++ b/chromium/content/browser/media/webrtc_internals_ui.h @@ -0,0 +1,23 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_UI_H_ +#define CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_UI_H_ + +#include "content/public/browser/web_ui_controller.h" + +namespace content { + +// The implementation for the chrome://webrtc-internals page. +class WebRTCInternalsUI : public WebUIController { + public: + explicit WebRTCInternalsUI(WebUI* web_ui); + + private: + DISALLOW_COPY_AND_ASSIGN(WebRTCInternalsUI); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_UI_H_ diff --git a/chromium/content/browser/media/webrtc_internals_ui_observer.h b/chromium/content/browser/media/webrtc_internals_ui_observer.h new file mode 100644 index 00000000000..8d2c60e63b8 --- /dev/null +++ b/chromium/content/browser/media/webrtc_internals_ui_observer.h @@ -0,0 +1,28 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_UI_OBSERVER_H_ +#define CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_UI_OBSERVER_H_ + +#include <string> + +namespace base { +class Value; +} // namespace base + +namespace content { + +// Implement this interface to receive WebRTCInternals updates. +class WebRTCInternalsUIObserver { + public: + virtual ~WebRTCInternalsUIObserver() {} + + // This is called on the browser IO thread. + virtual void OnUpdate(const std::string& command, + const base::Value* args) = 0; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_MEDIA_WEBRTC_INTERNALS_UI_OBSERVER_H_ diff --git a/chromium/content/browser/media/webrtc_internals_unittest.cc b/chromium/content/browser/media/webrtc_internals_unittest.cc new file mode 100644 index 00000000000..a00ccb9877c --- /dev/null +++ b/chromium/content/browser/media/webrtc_internals_unittest.cc @@ -0,0 +1,158 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/values.h" +#include "content/browser/media/webrtc_internals.h" +#include "content/browser/media/webrtc_internals_ui_observer.h" +#include "content/public/test/test_browser_thread.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +namespace { + +static const std::string kContraints = "c"; +static const std::string kServers = "s"; +static const std::string kUrl = "u"; + +class MockWebRTCInternalsProxy : public WebRTCInternalsUIObserver { + public: + virtual void OnUpdate(const std::string& command, + const base::Value* value) OVERRIDE { + command_ = command; + value_.reset(value->DeepCopy()); + } + + std::string command() { + return command_; + } + + base::Value* value() { + return value_.get(); + } + + private: + std::string command_; + scoped_ptr<base::Value> value_; +}; + +class WebRTCInternalsTest : public testing::Test { + public: + WebRTCInternalsTest() : io_thread_(BrowserThread::UI, &io_loop_) {} + + protected: + std::string ExpectedInfo(std::string prefix, + std::string id, + std::string suffix) { + static const std::string kstatic_part1 = std::string( + "{\"constraints\":\"c\","); + static const std::string kstatic_part2 = std::string( + ",\"servers\":\"s\",\"url\":\"u\"}"); + return prefix + kstatic_part1 + id + kstatic_part2 + suffix; + } + + base::MessageLoop io_loop_; + TestBrowserThread io_thread_; +}; + +} // namespace + +TEST_F(WebRTCInternalsTest, AddRemoveObserver) { + scoped_ptr<MockWebRTCInternalsProxy> observer( + new MockWebRTCInternalsProxy()); + WebRTCInternals::GetInstance()->AddObserver(observer.get()); + WebRTCInternals::GetInstance()->RemoveObserver(observer.get()); + WebRTCInternals::GetInstance()->OnAddPeerConnection( + 0, 3, 4, kUrl, kServers, kContraints); + EXPECT_EQ("", observer->command()); + + WebRTCInternals::GetInstance()->OnRemovePeerConnection(3, 4); +} + +TEST_F(WebRTCInternalsTest, SendAddPeerConnectionUpdate) { + scoped_ptr<MockWebRTCInternalsProxy> observer( + new MockWebRTCInternalsProxy()); + WebRTCInternals::GetInstance()->AddObserver(observer.get()); + WebRTCInternals::GetInstance()->OnAddPeerConnection( + 0, 1, 2, kUrl, kServers, kContraints); + EXPECT_EQ("addPeerConnection", observer->command()); + + base::DictionaryValue* dict = NULL; + EXPECT_TRUE(observer->value()->GetAsDictionary(&dict)); + + int int_value; + EXPECT_TRUE(dict->GetInteger("pid", &int_value)); + EXPECT_EQ(1, int_value); + EXPECT_TRUE(dict->GetInteger("lid", &int_value)); + EXPECT_EQ(2, int_value); + + std::string value; + EXPECT_TRUE(dict->GetString("url", &value)); + EXPECT_EQ(kUrl, value); + EXPECT_TRUE(dict->GetString("servers", &value)); + EXPECT_EQ(kServers, value); + EXPECT_TRUE(dict->GetString("constraints", &value)); + EXPECT_EQ(kContraints, value); + + WebRTCInternals::GetInstance()->RemoveObserver(observer.get()); + WebRTCInternals::GetInstance()->OnRemovePeerConnection(1, 2); +} + +TEST_F(WebRTCInternalsTest, SendRemovePeerConnectionUpdate) { + scoped_ptr<MockWebRTCInternalsProxy> observer( + new MockWebRTCInternalsProxy()); + WebRTCInternals::GetInstance()->AddObserver(observer.get()); + WebRTCInternals::GetInstance()->OnAddPeerConnection( + 0, 1, 2, kUrl, kServers, kContraints); + WebRTCInternals::GetInstance()->OnRemovePeerConnection(1, 2); + EXPECT_EQ("removePeerConnection", observer->command()); + + base::DictionaryValue* dict = NULL; + EXPECT_TRUE(observer->value()->GetAsDictionary(&dict)); + + int int_value; + EXPECT_TRUE(dict->GetInteger("pid", &int_value)); + EXPECT_EQ(1, int_value); + EXPECT_TRUE(dict->GetInteger("lid", &int_value)); + EXPECT_EQ(2, int_value); + + WebRTCInternals::GetInstance()->RemoveObserver(observer.get()); +} + +TEST_F(WebRTCInternalsTest, SendUpdatePeerConnectionUpdate) { + scoped_ptr<MockWebRTCInternalsProxy> observer( + new MockWebRTCInternalsProxy()); + WebRTCInternals::GetInstance()->AddObserver(observer.get()); + WebRTCInternals::GetInstance()->OnAddPeerConnection( + 0, 1, 2, kUrl, kServers, kContraints); + + const std::string update_type = "fakeType"; + const std::string update_value = "fakeValue"; + WebRTCInternals::GetInstance()->OnUpdatePeerConnection( + 1, 2, update_type, update_value); + + EXPECT_EQ("updatePeerConnection", observer->command()); + + base::DictionaryValue* dict = NULL; + EXPECT_TRUE(observer->value()->GetAsDictionary(&dict)); + + int int_value; + EXPECT_TRUE(dict->GetInteger("pid", &int_value)); + EXPECT_EQ(1, int_value); + EXPECT_TRUE(dict->GetInteger("lid", &int_value)); + EXPECT_EQ(2, int_value); + + std::string value; + EXPECT_TRUE(dict->GetString("type", &value)); + EXPECT_EQ(update_type, value); + EXPECT_TRUE(dict->GetString("value", &value)); + EXPECT_EQ(update_value, value); + + WebRTCInternals::GetInstance()->OnRemovePeerConnection(1, 2); + WebRTCInternals::GetInstance()->RemoveObserver(observer.get()); +} + +} // namespace content |