diff options
Diffstat (limited to 'chromium/chrome/browser/media')
131 files changed, 33136 insertions, 0 deletions
diff --git a/chromium/chrome/browser/media/webrtc/DEPS b/chromium/chrome/browser/media/webrtc/DEPS new file mode 100644 index 00000000000..e1fc475bafa --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/DEPS @@ -0,0 +1,25 @@ +include_rules = [ + "+media/webrtc", + "+services/audio/public/cpp", + "+third_party/libyuv", + "+third_party/webrtc", +] + +specific_include_rules = { + # TODO(mash): Fix these. https://crbug.com/723880 + "desktop_capture_access_handler\.cc": [ + "+ash/shell.h", + ], + "desktop_media_list_ash\.cc": [ + "+ash/shell.h", + "+ash/wm/desks/desks_util.h", + ], + # TODO(mash): Fix. https://crbug.com/855147 + "media_capture_devices_dispatcher\.cc": [ + "+ash/shell.h", + ], + # TODO(mash): Remove. https://crbug.com/723880 + ".*_unittest\.cc": [ + "+ash/test/ash_test_base.h", + ] +} diff --git a/chromium/chrome/browser/media/webrtc/OWNERS b/chromium/chrome/browser/media/webrtc/OWNERS new file mode 100644 index 00000000000..1d63dd49b92 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/OWNERS @@ -0,0 +1,24 @@ +sergeyu@chromium.org +tommi@chromium.org +guidou@chromium.org + +# For WebRTC desktop capturer related changes only +braveyao@chromium.org + +# For WebRTC browser tests. +per-file *webrtc*browsertest*=hbos@chromium.org +per-file *webrtc*browsertest*=phoglund@chromium.org + +# For changes related to the tab media indicators. +per-file media_stream_capture_indicator*=miu@chromium.org + +# For permissions related code. +per-file media_stream_device*=raymes@chromium.org +per-file media_permission*=raymes@chromium.org + +# For WebRTC event logging code. +per-file webrtc_event_log_*=eladalon@chromium.org + +per-file *permission_context*=file://chrome/browser/permissions/PERMISSIONS_OWNERS + +# COMPONENT: Blink>WebRTC diff --git a/chromium/chrome/browser/media/webrtc/audio_debug_recordings_handler.cc b/chromium/chrome/browser/media/webrtc/audio_debug_recordings_handler.cc new file mode 100644 index 00000000000..44ee73b6ae7 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/audio_debug_recordings_handler.cc @@ -0,0 +1,168 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/audio_debug_recordings_handler.h" + +#include <string> +#include <utility> + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/files/file_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/task/post_task.h" +#include "base/time/time.h" +#include "components/webrtc_logging/browser/text_log_list.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/system_connector.h" +#include "media/audio/audio_debug_recording_session.h" +#include "services/audio/public/cpp/debug_recording_session_factory.h" +#include "services/service_manager/public/cpp/connector.h" + +using content::BrowserThread; + +// Keys used to attach handler to the RenderProcessHost +const char AudioDebugRecordingsHandler::kAudioDebugRecordingsHandlerKey[] = + "kAudioDebugRecordingsHandlerKey"; + +namespace { + +// Returns a path name to be used as prefix for audio debug recordings files. +base::FilePath GetAudioDebugRecordingsPrefixPath( + const base::FilePath& directory, + uint64_t audio_debug_recordings_id) { + static const char kAudioDebugRecordingsFilePrefix[] = "AudioDebugRecordings."; + return directory.AppendASCII(kAudioDebugRecordingsFilePrefix + + base::NumberToString(audio_debug_recordings_id)); +} + +base::FilePath GetLogDirectoryAndEnsureExists( + content::BrowserContext* browser_context) { + base::FilePath log_dir_path = + webrtc_logging::TextLogList::GetWebRtcLogDirectoryForBrowserContextPath( + browser_context->GetPath()); + base::File::Error error; + if (!base::CreateDirectoryAndGetError(log_dir_path, &error)) { + DLOG(ERROR) << "Could not create WebRTC log directory, error: " << error; + return base::FilePath(); + } + return log_dir_path; +} + +} // namespace + +AudioDebugRecordingsHandler::AudioDebugRecordingsHandler( + content::BrowserContext* browser_context) + : browser_context_(browser_context), current_audio_debug_recordings_id_(0) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(browser_context_); +} + +AudioDebugRecordingsHandler::~AudioDebugRecordingsHandler() {} + +void AudioDebugRecordingsHandler::StartAudioDebugRecordings( + content::RenderProcessHost* host, + base::TimeDelta delay, + const RecordingDoneCallback& callback, + const RecordingErrorCallback& error_callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + base::PostTaskAndReplyWithResult( + FROM_HERE, + {base::ThreadPool(), base::MayBlock(), base::TaskPriority::BEST_EFFORT}, + base::BindOnce(&GetLogDirectoryAndEnsureExists, browser_context_), + base::BindOnce(&AudioDebugRecordingsHandler::DoStartAudioDebugRecordings, + this, host, delay, callback, error_callback)); +} + +void AudioDebugRecordingsHandler::StopAudioDebugRecordings( + content::RenderProcessHost* host, + const RecordingDoneCallback& callback, + const RecordingErrorCallback& error_callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + const bool is_manual_stop = true; + base::PostTaskAndReplyWithResult( + FROM_HERE, + {base::ThreadPool(), base::MayBlock(), base::TaskPriority::BEST_EFFORT}, + base::BindOnce(&GetLogDirectoryAndEnsureExists, browser_context_), + base::BindOnce(&AudioDebugRecordingsHandler::DoStopAudioDebugRecordings, + this, host, is_manual_stop, + current_audio_debug_recordings_id_, callback, + error_callback)); +} + +void AudioDebugRecordingsHandler::DoStartAudioDebugRecordings( + content::RenderProcessHost* host, + base::TimeDelta delay, + const RecordingDoneCallback& callback, + const RecordingErrorCallback& error_callback, + const base::FilePath& log_directory) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (audio_debug_recording_session_) { + error_callback.Run("Audio debug recordings already in progress"); + return; + } + + base::FilePath prefix_path = GetAudioDebugRecordingsPrefixPath( + log_directory, ++current_audio_debug_recordings_id_); + host->EnableAudioDebugRecordings(prefix_path); + + audio_debug_recording_session_ = audio::CreateAudioDebugRecordingSession( + prefix_path, content::GetSystemConnector()->Clone()); + + if (delay.is_zero()) { + const bool is_stopped = false, is_manual_stop = false; + callback.Run(prefix_path.AsUTF8Unsafe(), is_stopped, is_manual_stop); + return; + } + + const bool is_manual_stop = false; + base::PostDelayedTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce(&AudioDebugRecordingsHandler::DoStopAudioDebugRecordings, + this, host, is_manual_stop, + current_audio_debug_recordings_id_, callback, + error_callback, log_directory), + delay); +} + +void AudioDebugRecordingsHandler::DoStopAudioDebugRecordings( + content::RenderProcessHost* host, + bool is_manual_stop, + uint64_t audio_debug_recordings_id, + const RecordingDoneCallback& callback, + const RecordingErrorCallback& error_callback, + const base::FilePath& log_directory) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK_LE(audio_debug_recordings_id, current_audio_debug_recordings_id_); + + base::FilePath prefix_path = GetAudioDebugRecordingsPrefixPath( + log_directory, audio_debug_recordings_id); + // Prevent an old posted StopAudioDebugRecordings() call to stop a newer dump. + // This could happen in a sequence like: + // Start(10); // Start dump 1. Post Stop() to run after 10 seconds. + // Stop(); // Manually stop dump 1 before 10 seconds; + // Start(20); // Start dump 2. Posted Stop() for 1 should not stop dump 2. + if (audio_debug_recordings_id < current_audio_debug_recordings_id_) { + const bool is_stopped = false; + callback.Run(prefix_path.AsUTF8Unsafe(), is_stopped, is_manual_stop); + return; + } + + if (!audio_debug_recording_session_) { + error_callback.Run("No audio debug recording in progress"); + return; + } + + audio_debug_recording_session_.reset(); + + host->DisableAudioDebugRecordings(); + + const bool is_stopped = true; + callback.Run(prefix_path.AsUTF8Unsafe(), is_stopped, is_manual_stop); +} diff --git a/chromium/chrome/browser/media/webrtc/audio_debug_recordings_handler.h b/chromium/chrome/browser/media/webrtc/audio_debug_recordings_handler.h new file mode 100644 index 00000000000..4725e565594 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/audio_debug_recordings_handler.h @@ -0,0 +1,96 @@ +// Copyright 2016 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 CHROME_BROWSER_MEDIA_WEBRTC_AUDIO_DEBUG_RECORDINGS_HANDLER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_AUDIO_DEBUG_RECORDINGS_HANDLER_H_ + +#include <stddef.h> +#include <stdint.h> + +#include <memory> +#include <string> + +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/memory/ref_counted.h" +#include "base/time/time.h" + +namespace content { +class BrowserContext; +class RenderProcessHost; +} + +namespace media { +class AudioDebugRecordingSession; +} + +// AudioDebugRecordingsHandler provides an interface to start and stop +// AudioDebugRecordings, including WebRTC AEC dumps. Lives on the UI thread. +class AudioDebugRecordingsHandler + : public base::RefCountedThreadSafe<AudioDebugRecordingsHandler> { + public: + typedef base::Callback<void(bool, const std::string&)> GenericDoneCallback; + typedef base::Callback<void(const std::string&)> RecordingErrorCallback; + typedef base::Callback<void(const std::string&, bool, bool)> + RecordingDoneCallback; + + // Key used to attach the handler to the RenderProcessHost + static const char kAudioDebugRecordingsHandlerKey[]; + + explicit AudioDebugRecordingsHandler( + content::BrowserContext* browser_context); + + // Starts an audio debug recording. The recording lasts the given |delay|, + // unless |delay| is zero, in which case recording will continue until + // StopAudioDebugRecordings() is explicitly invoked. + // |callback| is invoked once recording stops. If |delay| is zero + // |callback| is invoked once recording starts. + // If a recording was already in progress, |error_callback| is invoked instead + // of |callback|. + void StartAudioDebugRecordings(content::RenderProcessHost* host, + base::TimeDelta delay, + const RecordingDoneCallback& callback, + const RecordingErrorCallback& error_callback); + + // Stops an audio debug recording. |callback| is invoked once recording + // stops. If no recording was in progress, |error_callback| is invoked instead + // of |callback|. + void StopAudioDebugRecordings(content::RenderProcessHost* host, + const RecordingDoneCallback& callback, + const RecordingErrorCallback& error_callback); + + private: + friend class base::RefCountedThreadSafe<AudioDebugRecordingsHandler>; + + virtual ~AudioDebugRecordingsHandler(); + + // Helper for starting audio debug recordings. + void DoStartAudioDebugRecordings(content::RenderProcessHost* host, + base::TimeDelta delay, + const RecordingDoneCallback& callback, + const RecordingErrorCallback& error_callback, + const base::FilePath& log_directory); + + // Helper for stopping audio debug recordings. + void DoStopAudioDebugRecordings(content::RenderProcessHost* host, + bool is_manual_stop, + uint64_t audio_debug_recordings_id, + const RecordingDoneCallback& callback, + const RecordingErrorCallback& error_callback, + const base::FilePath& log_directory); + + // The browser context associated with our renderer process. + content::BrowserContext* const browser_context_; + + // This counter allows saving each debug recording in separate files. + uint64_t current_audio_debug_recordings_id_; + + // Used for controlling debug recordings. + std::unique_ptr<media::AudioDebugRecordingSession> + audio_debug_recording_session_; + + DISALLOW_COPY_AND_ASSIGN(AudioDebugRecordingsHandler); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_AUDIO_DEBUG_RECORDINGS_HANDLER_H_ diff --git a/chromium/chrome/browser/media/webrtc/desktop_capture_access_handler.cc b/chromium/chrome/browser/media/webrtc/desktop_capture_access_handler.cc new file mode 100644 index 00000000000..7e52d84fa42 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_capture_access_handler.cc @@ -0,0 +1,598 @@ +// Copyright 2015 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 "chrome/browser/media/webrtc/desktop_capture_access_handler.h" + +#include <memory> +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/desktop_capture_devices_util.h" +#include "chrome/browser/media/webrtc/desktop_media_picker_factory_impl.h" +#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" +#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h" +#include "chrome/browser/media/webrtc/native_desktop_media_list.h" +#include "chrome/browser/media/webrtc/tab_desktop_media_list.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_window.h" +#include "chrome/browser/ui/screen_capture_notification_ui.h" +#include "chrome/browser/ui/simple_message_box.h" +#include "chrome/common/chrome_features.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/grit/generated_resources.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/desktop_capture.h" +#include "content/public/browser/desktop_streams_registry.h" +#include "content/public/browser/media_stream_request.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_switches.h" +#include "content/public/common/origin_util.h" +#include "extensions/browser/app_window/app_window.h" +#include "extensions/browser/app_window/app_window_registry.h" +#include "extensions/common/constants.h" +#include "extensions/common/extension.h" +#include "extensions/common/switches.h" +#include "net/base/url_util.h" +#include "third_party/blink/public/common/mediastream/media_stream_request.h" +#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h" +#include "ui/base/l10n/l10n_util.h" +#include "url/origin.h" + +#if defined(OS_CHROMEOS) +#include "ash/shell.h" +#include "ui/base/ui_base_features.h" +#endif // defined(OS_CHROMEOS) + +#if defined(OS_MACOSX) +#include "chrome/browser/media/webrtc/system_media_capture_permissions_mac.h" +#endif // defined(OS_MACOSX) + +using content::BrowserThread; + +namespace { + +// Helper to get title of the calling application shown in the screen capture +// notification. +base::string16 GetApplicationTitle(content::WebContents* web_contents, + const extensions::Extension* extension) { + // Use extension name as title for extensions and host/origin for drive-by + // web. + std::string title; + if (extension) { + title = extension->name(); + return base::UTF8ToUTF16(title); + } + GURL url = web_contents->GetURL(); + title = content::IsOriginSecure(url) ? net::GetHostAndOptionalPort(url) + : url.GetOrigin().spec(); + return base::UTF8ToUTF16(title); +} + +// Returns whether an on-screen notification should appear after desktop capture +// is approved for |extension|. Component extensions do not display a +// notification. +bool ShouldDisplayNotification(const extensions::Extension* extension) { + return !(extension && + (extension->location() == extensions::Manifest::COMPONENT || + extension->location() == extensions::Manifest::EXTERNAL_COMPONENT)); +} + +#if !defined(OS_ANDROID) +// Find browser or app window from a given |web_contents|. +gfx::NativeWindow FindParentWindowForWebContents( + content::WebContents* web_contents) { + Browser* browser = chrome::FindBrowserWithWebContents(web_contents); + if (browser && browser->window()) + return browser->window()->GetNativeWindow(); + + const extensions::AppWindowRegistry::AppWindowList& window_list = + extensions::AppWindowRegistry::Get(web_contents->GetBrowserContext()) + ->app_windows(); + for (auto iter = window_list.begin(); iter != window_list.end(); ++iter) { + if ((*iter)->web_contents() == web_contents) + return (*iter)->GetNativeWindow(); + } + + return NULL; +} +#endif + +} // namespace + +// Holds pending request information so that we display one picker UI at a time +// for each content::WebContents. +struct DesktopCaptureAccessHandler::PendingAccessRequest { + PendingAccessRequest(std::unique_ptr<DesktopMediaPicker> picker, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) + : picker(std::move(picker)), + request(request), + callback(std::move(callback)), + extension(extension) {} + ~PendingAccessRequest() = default; + + std::unique_ptr<DesktopMediaPicker> picker; + content::MediaStreamRequest request; + content::MediaResponseCallback callback; + const extensions::Extension* extension; +}; + +DesktopCaptureAccessHandler::DesktopCaptureAccessHandler() + : picker_factory_(new DesktopMediaPickerFactoryImpl()), + display_notification_(true) { + AddNotificationObserver(); +} + +DesktopCaptureAccessHandler::DesktopCaptureAccessHandler( + std::unique_ptr<DesktopMediaPickerFactory> picker_factory) + : picker_factory_(std::move(picker_factory)), display_notification_(false) { + AddNotificationObserver(); +} + +DesktopCaptureAccessHandler::~DesktopCaptureAccessHandler() = default; + +void DesktopCaptureAccessHandler::ProcessScreenCaptureAccessRequest( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) { + blink::MediaStreamDevices devices; + std::unique_ptr<content::MediaStreamUI> ui; + + DCHECK_EQ(request.video_type, + blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE); + + UpdateExtensionTrusted(request, extension); + + bool loopback_audio_supported = false; +#if defined(USE_CRAS) || defined(OS_WIN) + // Currently loopback audio capture is supported only on Windows and ChromeOS. + loopback_audio_supported = true; +#endif + + bool screen_capture_enabled = + base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kEnableUserMediaScreenCapturing) || + MediaCaptureDevicesDispatcher::IsOriginForCasting( + request.security_origin) || + IsExtensionWhitelistedForScreenCapture(extension) || + IsBuiltInExtension(request.security_origin); + + const bool origin_is_secure = + content::IsOriginSecure(request.security_origin) || + base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kAllowHttpScreenCapture); + + // If basic conditions (screen capturing is enabled and origin is secure) + // aren't fulfilled, we'll use "invalid state" as result. Otherwise, we set + // it after checking permission. + // TODO(grunell): It would be good to change this result for something else, + // probably a new one. + blink::mojom::MediaStreamRequestResult result = + blink::mojom::MediaStreamRequestResult::INVALID_STATE; + + // Approve request only when the following conditions are met: + // 1. Screen capturing is enabled via command line switch or white-listed for + // the given origin. + // 2. Request comes from a page with a secure origin or from an extension. + if (screen_capture_enabled && origin_is_secure) { + // Get title of the calling application prior to showing the message box. + // chrome::ShowQuestionMessageBox() starts a nested run loop which may + // allow |web_contents| to be destroyed on the UI thread before the messag + // box is closed. See http://crbug.com/326690. + base::string16 application_title = + GetApplicationTitle(web_contents, extension); +#if !defined(OS_ANDROID) + gfx::NativeWindow parent_window = + FindParentWindowForWebContents(web_contents); +#else + gfx::NativeWindow parent_window = NULL; +#endif + web_contents = NULL; + + // Some extensions do not require user approval, because they provide their + // own user approval UI. + bool is_approved = IsDefaultApproved(extension); + if (!is_approved) { + base::string16 application_name = + base::UTF8ToUTF16(request.security_origin.spec()); + if (extension) + application_name = base::UTF8ToUTF16(extension->name()); + base::string16 confirmation_text = l10n_util::GetStringFUTF16( + request.audio_type == blink::mojom::MediaStreamType::NO_SERVICE + ? IDS_MEDIA_SCREEN_CAPTURE_CONFIRMATION_TEXT + : IDS_MEDIA_SCREEN_AND_AUDIO_CAPTURE_CONFIRMATION_TEXT, + application_name); + chrome::MessageBoxResult result = chrome::ShowQuestionMessageBox( + parent_window, + l10n_util::GetStringFUTF16( + IDS_MEDIA_SCREEN_CAPTURE_CONFIRMATION_TITLE, application_name), + confirmation_text); + is_approved = (result == chrome::MESSAGE_BOX_RESULT_YES); + } + + if (is_approved) { + content::DesktopMediaID screen_id; +#if defined(OS_CHROMEOS) + screen_id = content::DesktopMediaID::RegisterNativeWindow( + content::DesktopMediaID::TYPE_SCREEN, + ash::Shell::Get()->GetPrimaryRootWindow()); +#else // defined(OS_CHROMEOS) + screen_id = content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, + webrtc::kFullDesktopScreenId); +#endif // !defined(OS_CHROMEOS) + + bool capture_audio = + (request.audio_type == + blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE && + loopback_audio_supported); + + // Determine if the extension is required to display a notification. + const bool display_notification = + display_notification_ && ShouldDisplayNotification(extension); + + ui = GetDevicesForDesktopCapture( + web_contents, &devices, screen_id, + blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, + blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE, + capture_audio, request.disable_local_echo, display_notification, + application_title, application_title); + DCHECK(!devices.empty()); + } + + // The only case when devices can be empty is if the user has denied + // permission. + result = devices.empty() + ? blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED + : blink::mojom::MediaStreamRequestResult::OK; + } + + std::move(callback).Run(devices, result, std::move(ui)); +} + +bool DesktopCaptureAccessHandler::IsDefaultApproved( + const extensions::Extension* extension) { + return extension && + (extension->location() == extensions::Manifest::COMPONENT || + extension->location() == extensions::Manifest::EXTERNAL_COMPONENT || + IsExtensionWhitelistedForScreenCapture(extension)); +} + +bool DesktopCaptureAccessHandler::SupportsStreamType( + content::WebContents* web_contents, + const blink::mojom::MediaStreamType type, + const extensions::Extension* extension) { + return type == blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE || + type == blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE; +} + +bool DesktopCaptureAccessHandler::CheckMediaAccessPermission( + content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type, + const extensions::Extension* extension) { + return false; +} + +void DesktopCaptureAccessHandler::HandleRequest( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) { + blink::MediaStreamDevices devices; + std::unique_ptr<content::MediaStreamUI> ui; + + if (request.video_type != + blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE) { + std::move(callback).Run( + devices, blink::mojom::MediaStreamRequestResult::INVALID_STATE, + std::move(ui)); + return; + } + + if (request.request_type == blink::MEDIA_DEVICE_UPDATE) { + ProcessChangeSourceRequest(web_contents, request, std::move(callback), + extension); + return; + } + + // If the device id wasn't specified then this is a screen capture request + // (i.e. chooseDesktopMedia() API wasn't used to generate device id). + if (request.requested_video_device_id.empty()) { +#if defined(OS_MACOSX) + if (system_media_permissions::CheckSystemScreenCapturePermission() != + system_media_permissions::SystemPermission::kAllowed) { + std::move(callback).Run( + blink::MediaStreamDevices(), + blink::mojom::MediaStreamRequestResult::SYSTEM_PERMISSION_DENIED, + nullptr); + return; + } +#endif + ProcessScreenCaptureAccessRequest(web_contents, request, + std::move(callback), extension); + return; + } + + // Resolve DesktopMediaID for the specified device id. + content::DesktopMediaID media_id; + // TODO(miu): Replace "main RenderFrame" IDs with the request's actual + // RenderFrame IDs once the desktop capture extension API implementation is + // fixed. http://crbug.com/304341 + content::WebContents* const web_contents_for_stream = + content::WebContents::FromRenderFrameHost( + content::RenderFrameHost::FromID(request.render_process_id, + request.render_frame_id)); + content::RenderFrameHost* const main_frame = + web_contents_for_stream ? web_contents_for_stream->GetMainFrame() : NULL; + if (main_frame) { + media_id = + content::DesktopStreamsRegistry::GetInstance()->RequestMediaForStreamId( + request.requested_video_device_id, + main_frame->GetProcess()->GetID(), main_frame->GetRoutingID(), + url::Origin::Create(request.security_origin), nullptr, + content::kRegistryStreamTypeDesktop); + } + + // Received invalid device id. + if (media_id.type == content::DesktopMediaID::TYPE_NONE) { + std::move(callback).Run( + devices, blink::mojom::MediaStreamRequestResult::INVALID_STATE, + std::move(ui)); + return; + } +#if defined(OS_MACOSX) + if (media_id.type != content::DesktopMediaID::TYPE_WEB_CONTENTS && + system_media_permissions::CheckSystemScreenCapturePermission() != + system_media_permissions::SystemPermission::kAllowed) { + std::move(callback).Run( + blink::MediaStreamDevices(), + blink::mojom::MediaStreamRequestResult::SYSTEM_PERMISSION_DENIED, + nullptr); + return; + } +#endif + + bool loopback_audio_supported = false; +#if defined(USE_CRAS) || defined(OS_WIN) + // Currently loopback audio capture is supported only on Windows and ChromeOS. + loopback_audio_supported = true; +#endif + + // This value essentially from the checkbox on picker window, so it + // corresponds to user permission. + const bool audio_permitted = media_id.audio_share; + + // This value essentially from whether getUserMedia requests audio stream. + const bool audio_requested = + request.audio_type == + blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE; + + // This value shows for a given capture type, whether the system or our code + // can support audio sharing. Currently audio is only supported for screen and + // tab/webcontents capture streams. + const bool audio_supported = + (media_id.type == content::DesktopMediaID::TYPE_SCREEN && + loopback_audio_supported) || + media_id.type == content::DesktopMediaID::TYPE_WEB_CONTENTS; + + const bool check_audio_permission = + !base::CommandLine::ForCurrentProcess()->HasSwitch( + extensions::switches::kDisableDesktopCaptureAudio); + const bool capture_audio = + (check_audio_permission ? audio_permitted : true) && audio_requested && + audio_supported; + + // Determine if the extension is required to display a notification. + const bool display_notification = + display_notification_ && ShouldDisplayNotification(extension); + + ui = GetDevicesForDesktopCapture( + web_contents, &devices, media_id, + blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, + blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE, capture_audio, + request.disable_local_echo, display_notification, + GetApplicationTitle(web_contents, extension), + GetApplicationTitle(web_contents, extension)); + UpdateExtensionTrusted(request, extension); + std::move(callback).Run(devices, blink::mojom::MediaStreamRequestResult::OK, + std::move(ui)); +} + +void DesktopCaptureAccessHandler::ProcessChangeSourceRequest( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + std::unique_ptr<DesktopMediaPicker> picker; + + if (!base::FeatureList::IsEnabled( + features::kDesktopCaptureTabSharingInfobar) || + request.requested_video_device_id.empty()) { + picker = picker_factory_->CreatePicker(); + if (!picker) { + std::move(callback).Run( + blink::MediaStreamDevices(), + blink::mojom::MediaStreamRequestResult::INVALID_STATE, nullptr); + return; + } + } + + RequestsQueue& queue = pending_requests_[web_contents]; + queue.push_back(std::make_unique<PendingAccessRequest>( + std::move(picker), request, std::move(callback), extension)); + // If this is the only request then pop picker UI. + if (queue.size() == 1) + ProcessQueuedAccessRequest(queue, web_contents); +} + +void DesktopCaptureAccessHandler::UpdateMediaRequestState( + int render_process_id, + int render_frame_id, + int page_request_id, + blink::mojom::MediaStreamType stream_type, + content::MediaRequestState state) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + if (state != content::MEDIA_REQUEST_STATE_DONE && + state != content::MEDIA_REQUEST_STATE_CLOSING) { + return; + } + + if (state == content::MEDIA_REQUEST_STATE_CLOSING) { + DeletePendingAccessRequest(render_process_id, render_frame_id, + page_request_id); + } + CaptureAccessHandlerBase::UpdateMediaRequestState( + render_process_id, render_frame_id, page_request_id, stream_type, state); + + // This method only gets called with the above checked states when all + // requests are to be canceled. Therefore, we don't need to process the + // next queued request. +} + +void DesktopCaptureAccessHandler::ProcessQueuedAccessRequest( + const RequestsQueue& queue, + content::WebContents* web_contents) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + const PendingAccessRequest& pending_request = *queue.front(); + + if (!pending_request.picker) { + DCHECK(!pending_request.request.requested_video_device_id.empty()); + content::WebContentsMediaCaptureId web_contents_id; + if (content::WebContentsMediaCaptureId::Parse( + pending_request.request.requested_video_device_id, + &web_contents_id)) { + content::DesktopMediaID media_id( + content::DesktopMediaID::TYPE_WEB_CONTENTS, + content::DesktopMediaID::kNullId, web_contents_id); + media_id.audio_share = pending_request.request.audio_type != + blink::mojom::MediaStreamType::NO_SERVICE; + OnPickerDialogResults(web_contents, media_id); + return; + } + } + + std::vector<content::DesktopMediaID::Type> media_types = { + content::DesktopMediaID::TYPE_WEB_CONTENTS}; + auto source_lists = picker_factory_->CreateMediaList(media_types); + + DesktopMediaPicker::DoneCallback done_callback = + base::BindOnce(&DesktopCaptureAccessHandler::OnPickerDialogResults, + base::Unretained(this), web_contents); + DesktopMediaPicker::Params picker_params; + picker_params.web_contents = web_contents; + gfx::NativeWindow parent_window = web_contents->GetTopLevelNativeWindow(); + picker_params.context = parent_window; + picker_params.parent = parent_window; + picker_params.app_name = + GetApplicationTitle(web_contents, pending_request.extension); + picker_params.target_name = picker_params.app_name; + picker_params.request_audio = (pending_request.request.audio_type == + blink::mojom::MediaStreamType::NO_SERVICE) + ? false + : true; + pending_request.picker->Show(picker_params, std::move(source_lists), + std::move(done_callback)); + + // Focus on the tab with the picker for easy access. + if (auto* delegate = web_contents->GetDelegate()) + delegate->ActivateContents(web_contents); +} + +void DesktopCaptureAccessHandler::OnPickerDialogResults( + content::WebContents* web_contents, + content::DesktopMediaID media_id) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + DCHECK(web_contents); + + auto it = pending_requests_.find(web_contents); + if (it == pending_requests_.end()) + return; + RequestsQueue& queue = it->second; + if (queue.empty()) { + // UpdateMediaRequestState() called with MEDIA_REQUEST_STATE_CLOSING. Don't + // need to do anything. + return; + } + + PendingAccessRequest& pending_request = *queue.front(); + blink::MediaStreamDevices devices; + blink::mojom::MediaStreamRequestResult request_result = + blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED; + const extensions::Extension* extension = pending_request.extension; + std::unique_ptr<content::MediaStreamUI> ui; + if (media_id.is_null()) { + request_result = blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED; + } else { + request_result = blink::mojom::MediaStreamRequestResult::OK; + // Determine if the extension is required to display a notification. + const bool display_notification = + display_notification_ && ShouldDisplayNotification(extension); + ui = GetDevicesForDesktopCapture( + web_contents, &devices, media_id, pending_request.request.video_type, + pending_request.request.audio_type, media_id.audio_share, + pending_request.request.disable_local_echo, display_notification, + GetApplicationTitle(web_contents, extension), + GetApplicationTitle(web_contents, extension)); + } + + std::move(pending_request.callback) + .Run(devices, request_result, std::move(ui)); + queue.pop_front(); + + if (!queue.empty()) + ProcessQueuedAccessRequest(queue, web_contents); +} + +void DesktopCaptureAccessHandler::AddNotificationObserver() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + notifications_registrar_.Add(this, + content::NOTIFICATION_WEB_CONTENTS_DESTROYED, + content::NotificationService::AllSources()); +} + +void DesktopCaptureAccessHandler::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + DCHECK_EQ(content::NOTIFICATION_WEB_CONTENTS_DESTROYED, type); + + pending_requests_.erase(content::Source<content::WebContents>(source).ptr()); +} + +void DesktopCaptureAccessHandler::DeletePendingAccessRequest( + int render_process_id, + int render_frame_id, + int page_request_id) { + for (auto& queue_it : pending_requests_) { + RequestsQueue& queue = queue_it.second; + for (auto it = queue.begin(); it != queue.end(); ++it) { + const PendingAccessRequest& pending_request = **it; + if (pending_request.request.render_process_id == render_process_id && + pending_request.request.render_frame_id == render_frame_id && + pending_request.request.page_request_id == page_request_id) { + queue.erase(it); + return; + } + } + } +} diff --git a/chromium/chrome/browser/media/webrtc/desktop_capture_access_handler.h b/chromium/chrome/browser/media/webrtc/desktop_capture_access_handler.h new file mode 100644 index 00000000000..e97d6318179 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_capture_access_handler.h @@ -0,0 +1,105 @@ +// Copyright 2015 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 CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_CAPTURE_ACCESS_HANDLER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_CAPTURE_ACCESS_HANDLER_H_ + +#include <list> +#include <memory> +#include <string> +#include <utility> + +#include "base/containers/flat_map.h" +#include "base/macros.h" +#include "chrome/browser/media/capture_access_handler_base.h" +#include "chrome/browser/media/media_access_handler.h" +#include "chrome/browser/media/webrtc/desktop_media_list.h" +#include "chrome/browser/media/webrtc/desktop_media_picker.h" +#include "chrome/browser/media/webrtc/desktop_media_picker_factory.h" +#include "content/public/browser/desktop_media_id.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" + +namespace extensions { +class Extension; +} + +// MediaAccessHandler for DesktopCapture API requests that originate from +// getUserMedia() calls. Note that getDisplayMedia() calls are handled in +// DisplayMediaAccessHandler. +class DesktopCaptureAccessHandler : public CaptureAccessHandlerBase, + public content::NotificationObserver { + public: + DesktopCaptureAccessHandler(); + explicit DesktopCaptureAccessHandler( + std::unique_ptr<DesktopMediaPickerFactory> picker_factory); + ~DesktopCaptureAccessHandler() override; + + // MediaAccessHandler implementation. + bool SupportsStreamType(content::WebContents* web_contents, + const blink::mojom::MediaStreamType type, + const extensions::Extension* extension) override; + bool CheckMediaAccessPermission( + content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type, + const extensions::Extension* extension) override; + void HandleRequest(content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) override; + void UpdateMediaRequestState(int render_process_id, + int render_frame_id, + int page_request_id, + blink::mojom::MediaStreamType stream_type, + content::MediaRequestState state) override; + + private: + friend class DesktopCaptureAccessHandlerTest; + + struct PendingAccessRequest; + using RequestsQueue = + base::circular_deque<std::unique_ptr<PendingAccessRequest>>; + using RequestsQueues = base::flat_map<content::WebContents*, RequestsQueue>; + + void ProcessScreenCaptureAccessRequest( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension); + + // Returns whether desktop capture is always approved for |extension|. + // Currently component extensions and some whitelisted extensions are default + // approved. + static bool IsDefaultApproved(const extensions::Extension* extension); + + // content::NotificationObserver implementation. + void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) override; + void AddNotificationObserver(); + + // Methods for handling source change request, e.g. bringing up the picker to + // select a new source within the current desktop sharing session. + void ProcessChangeSourceRequest(content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension); + void ProcessQueuedAccessRequest(const RequestsQueue& queue, + content::WebContents* web_contents); + void OnPickerDialogResults(content::WebContents* web_contents, + content::DesktopMediaID source); + void DeletePendingAccessRequest(int render_process_id, + int render_frame_id, + int page_request_id); + + std::unique_ptr<DesktopMediaPickerFactory> picker_factory_; + bool display_notification_; + RequestsQueues pending_requests_; + content::NotificationRegistrar notifications_registrar_; + + DISALLOW_COPY_AND_ASSIGN(DesktopCaptureAccessHandler); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_CAPTURE_ACCESS_HANDLER_H_ diff --git a/chromium/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc b/chromium/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc new file mode 100644 index 00000000000..70ed3a0c4c8 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc @@ -0,0 +1,254 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/desktop_capture_access_handler.h" + +#include <memory> +#include <string> +#include <utility> + +#include "base/bind.h" +#include "base/macros.h" +#include "base/run_loop.h" +#include "chrome/browser/media/webrtc/fake_desktop_media_picker_factory.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/desktop_media_id.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/web_contents.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/common/mediastream/media_stream_request.h" +#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" + +class DesktopCaptureAccessHandlerTest : public ChromeRenderViewHostTestHarness { + public: + DesktopCaptureAccessHandlerTest() {} + ~DesktopCaptureAccessHandlerTest() override {} + + void SetUp() override { + ChromeRenderViewHostTestHarness::SetUp(); + auto picker_factory = std::make_unique<FakeDesktopMediaPickerFactory>(); + picker_factory_ = picker_factory.get(); + access_handler_ = std::make_unique<DesktopCaptureAccessHandler>( + std::move(picker_factory)); + } + + void ProcessRequest( + const content::DesktopMediaID& fake_desktop_media_id_response, + blink::mojom::MediaStreamRequestResult* request_result, + blink::MediaStreamDevices* devices_result, + blink::MediaStreamRequestType request_type, + bool request_audio) { + FakeDesktopMediaPickerFactory::TestFlags test_flags[] = { + {false /* expect_screens */, false /* expect_windows*/, + true /* expect_tabs */, request_audio /* expect_audio */, + fake_desktop_media_id_response /* selected_source */}}; + picker_factory_->SetTestFlags(test_flags, base::size(test_flags)); + blink::mojom::MediaStreamType audio_type = + request_audio ? blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE + : blink::mojom::MediaStreamType::NO_SERVICE; + content::MediaStreamRequest request( + 0, 0, 0, GURL("http://origin/"), false, request_type, std::string(), + std::string(), audio_type, + blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, false); + + base::RunLoop wait_loop; + content::MediaResponseCallback callback = base::BindOnce( + [](base::RunLoop* wait_loop, + blink::mojom::MediaStreamRequestResult* request_result, + blink::MediaStreamDevices* devices_result, + const blink::MediaStreamDevices& devices, + blink::mojom::MediaStreamRequestResult result, + std::unique_ptr<content::MediaStreamUI> ui) { + *request_result = result; + *devices_result = devices; + wait_loop->Quit(); + }, + &wait_loop, request_result, devices_result); + access_handler_->HandleRequest(web_contents(), request, std::move(callback), + nullptr /* extension */); + wait_loop.Run(); + EXPECT_TRUE(test_flags[0].picker_created); + + access_handler_.reset(); + EXPECT_TRUE(test_flags[0].picker_deleted); + } + + void NotifyWebContentsDestroyed() { + access_handler_->Observe( + content::NOTIFICATION_WEB_CONTENTS_DESTROYED, + content::Source<content::WebContents>(web_contents()), + content::NotificationDetails()); + } + + const DesktopCaptureAccessHandler::RequestsQueues& GetRequestQueues() { + return access_handler_->pending_requests_; + } + + protected: + FakeDesktopMediaPickerFactory* picker_factory_; + std::unique_ptr<DesktopCaptureAccessHandler> access_handler_; +}; + +TEST_F(DesktopCaptureAccessHandlerTest, + ChangeSourceWithoutAudioRequestPermissionGiven) { + blink::mojom::MediaStreamRequestResult result; + blink::MediaStreamDevices devices; + ProcessRequest(content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, + content::DesktopMediaID::kFakeId), + &result, &devices, blink::MEDIA_DEVICE_UPDATE, + false /*request_audio*/); + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, result); + EXPECT_EQ(1u, devices.size()); + EXPECT_EQ(blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, + devices[0].type); +} + +TEST_F(DesktopCaptureAccessHandlerTest, + ChangeSourceWithAudioRequestPermissionGiven) { + blink::mojom::MediaStreamRequestResult result; + blink::MediaStreamDevices devices; + ProcessRequest(content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, + content::DesktopMediaID::kFakeId, + true /* audio_share */), + &result, &devices, blink::MEDIA_DEVICE_UPDATE, + true /* request_audio */); + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, result); + EXPECT_EQ(2u, devices.size()); + EXPECT_EQ(blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, + devices[0].type); + EXPECT_EQ(blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE, + devices[1].type); +} + +TEST_F(DesktopCaptureAccessHandlerTest, ChangeSourcePermissionDenied) { + blink::mojom::MediaStreamRequestResult result; + blink::MediaStreamDevices devices; + ProcessRequest(content::DesktopMediaID(), &result, &devices, + blink::MEDIA_DEVICE_UPDATE, false /*request audio*/); + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED, result); + EXPECT_EQ(0u, devices.size()); +} + +TEST_F(DesktopCaptureAccessHandlerTest, + ChangeSourceUpdateMediaRequestStateWithClosing) { + const int render_process_id = 0; + const int render_frame_id = 0; + const int page_request_id = 0; + const blink::mojom::MediaStreamType stream_type = + blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE; + FakeDesktopMediaPickerFactory::TestFlags test_flags[] = { + {false /* expect_screens */, false /* expect_windows*/, + true /* expect_tabs */, false /* expect_audio */, + content::DesktopMediaID(), true /* cancelled */}}; + picker_factory_->SetTestFlags(test_flags, base::size(test_flags)); + content::MediaStreamRequest request( + render_process_id, render_frame_id, page_request_id, + GURL("http://origin/"), false, blink::MEDIA_DEVICE_UPDATE, std::string(), + std::string(), blink::mojom::MediaStreamType::NO_SERVICE, stream_type, + false); + content::MediaResponseCallback callback; + access_handler_->HandleRequest(web_contents(), request, std::move(callback), + nullptr /* extension */); + EXPECT_TRUE(test_flags[0].picker_created); + EXPECT_EQ(1u, GetRequestQueues().size()); + auto queue_it = GetRequestQueues().find(web_contents()); + EXPECT_TRUE(queue_it != GetRequestQueues().end()); + EXPECT_EQ(1u, queue_it->second.size()); + + access_handler_->UpdateMediaRequestState( + render_process_id, render_frame_id, page_request_id, stream_type, + content::MEDIA_REQUEST_STATE_CLOSING); + EXPECT_EQ(1u, GetRequestQueues().size()); + queue_it = GetRequestQueues().find(web_contents()); + EXPECT_TRUE(queue_it != GetRequestQueues().end()); + EXPECT_EQ(0u, queue_it->second.size()); + EXPECT_TRUE(test_flags[0].picker_deleted); + access_handler_.reset(); +} + +TEST_F(DesktopCaptureAccessHandlerTest, ChangeSourceWebContentsDestroyed) { + FakeDesktopMediaPickerFactory::TestFlags test_flags[] = { + {false /* expect_screens */, false /* expect_windows*/, + true /* expect_tabs */, false /* expect_audio */, + content::DesktopMediaID(), true /* cancelled */}}; + picker_factory_->SetTestFlags(test_flags, base::size(test_flags)); + content::MediaStreamRequest request( + 0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_DEVICE_UPDATE, + std::string(), std::string(), blink::mojom::MediaStreamType::NO_SERVICE, + blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, false); + content::MediaResponseCallback callback; + access_handler_->HandleRequest(web_contents(), request, std::move(callback), + nullptr /* extension */); + EXPECT_TRUE(test_flags[0].picker_created); + EXPECT_EQ(1u, GetRequestQueues().size()); + auto queue_it = GetRequestQueues().find(web_contents()); + EXPECT_TRUE(queue_it != GetRequestQueues().end()); + EXPECT_EQ(1u, queue_it->second.size()); + + NotifyWebContentsDestroyed(); + EXPECT_EQ(0u, GetRequestQueues().size()); + access_handler_.reset(); +} + +TEST_F(DesktopCaptureAccessHandlerTest, ChangeSourceMultipleRequests) { + FakeDesktopMediaPickerFactory::TestFlags test_flags[] = { + {false /* expect_screens */, false /* expect_windows*/, + true /* expect_tabs */, false /* expect_audio */, + content::DesktopMediaID( + content::DesktopMediaID::TYPE_SCREEN, + content::DesktopMediaID::kFakeId) /* selected_source */}, + {false /* expect_screens */, false /* expect_windows*/, + true /* expect_tabs */, false /* expect_audio */, + content::DesktopMediaID( + content::DesktopMediaID::TYPE_WINDOW, + content::DesktopMediaID::kNullId) /* selected_source */}}; + const size_t kTestFlagCount = 2; + picker_factory_->SetTestFlags(test_flags, kTestFlagCount); + + blink::mojom::MediaStreamRequestResult result; + blink::MediaStreamDevices devices; + base::RunLoop wait_loop[kTestFlagCount]; + for (size_t i = 0; i < kTestFlagCount; ++i) { + content::MediaStreamRequest request( + 0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_DEVICE_UPDATE, + std::string(), std::string(), blink::mojom::MediaStreamType::NO_SERVICE, + blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, false); + content::MediaResponseCallback callback = base::BindOnce( + [](base::RunLoop* wait_loop, + blink::mojom::MediaStreamRequestResult* request_result, + blink::MediaStreamDevices* devices_result, + const blink::MediaStreamDevices& devices, + blink::mojom::MediaStreamRequestResult result, + std::unique_ptr<content::MediaStreamUI> ui) { + *request_result = result; + *devices_result = devices; + wait_loop->Quit(); + }, + &wait_loop[i], &result, &devices); + access_handler_->HandleRequest(web_contents(), request, std::move(callback), + nullptr /* extension */); + } + wait_loop[0].Run(); + EXPECT_TRUE(test_flags[0].picker_created); + EXPECT_TRUE(test_flags[0].picker_deleted); + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, result); + EXPECT_EQ(1u, devices.size()); + EXPECT_EQ(blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, + devices[0].type); + + blink::MediaStreamDevice first_device = devices[0]; + EXPECT_TRUE(test_flags[1].picker_created); + EXPECT_FALSE(test_flags[1].picker_deleted); + wait_loop[1].Run(); + EXPECT_TRUE(test_flags[1].picker_deleted); + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, result); + EXPECT_EQ(1u, devices.size()); + EXPECT_EQ(blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, + devices[0].type); + EXPECT_FALSE(devices[0].IsSameDevice(first_device)); + + access_handler_.reset(); +} diff --git a/chromium/chrome/browser/media/webrtc/desktop_capture_devices_util.cc b/chromium/chrome/browser/media/webrtc/desktop_capture_devices_util.cc new file mode 100644 index 00000000000..76fede3233e --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_capture_devices_util.cc @@ -0,0 +1,198 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/desktop_capture_devices_util.h" + +#include <string> +#include <utility> + +#include "base/strings/string_util.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" +#include "chrome/browser/ui/screen_capture_notification_ui.h" +#include "chrome/browser/ui/tab_sharing/tab_sharing_ui.h" +#include "chrome/common/chrome_features.h" +#include "chrome/grit/generated_resources.h" +#include "content/public/browser/browser_thread.h" +#include "media/audio/audio_device_description.h" +#include "media/mojo/mojom/display_media_information.mojom.h" +#include "ui/base/l10n/l10n_util.h" + +namespace { + +media::mojom::DisplayMediaInformationPtr +DesktopMediaIDToDisplayMediaInformation( + const content::DesktopMediaID& media_id) { + media::mojom::DisplayCaptureSurfaceType display_surface = + media::mojom::DisplayCaptureSurfaceType::MONITOR; + bool logical_surface = true; + media::mojom::CursorCaptureType cursor = + media::mojom::CursorCaptureType::NEVER; +#if defined(USE_AURA) + const bool uses_aura = + media_id.window_id != content::DesktopMediaID::kNullId ? true : false; +#else + const bool uses_aura = false; +#endif // defined(USE_AURA) + switch (media_id.type) { + case content::DesktopMediaID::TYPE_SCREEN: + display_surface = media::mojom::DisplayCaptureSurfaceType::MONITOR; + cursor = uses_aura ? media::mojom::CursorCaptureType::MOTION + : media::mojom::CursorCaptureType::ALWAYS; + break; + case content::DesktopMediaID::TYPE_WINDOW: + display_surface = media::mojom::DisplayCaptureSurfaceType::WINDOW; + cursor = uses_aura ? media::mojom::CursorCaptureType::MOTION + : media::mojom::CursorCaptureType::ALWAYS; + break; + case content::DesktopMediaID::TYPE_WEB_CONTENTS: + display_surface = media::mojom::DisplayCaptureSurfaceType::BROWSER; + cursor = media::mojom::CursorCaptureType::MOTION; + break; + case content::DesktopMediaID::TYPE_NONE: + break; + } + + return media::mojom::DisplayMediaInformation::New(display_surface, + logical_surface, cursor); +} + +base::string16 GetStopSharingUIString( + const base::string16& application_title, + const base::string16& registered_extension_name, + bool capture_audio, + content::DesktopMediaID::Type capture_type) { + if (!capture_audio) { + if (application_title == registered_extension_name) { + switch (capture_type) { + case content::DesktopMediaID::TYPE_SCREEN: + return l10n_util::GetStringFUTF16( + IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_TEXT, application_title); + case content::DesktopMediaID::TYPE_WINDOW: + return l10n_util::GetStringFUTF16( + IDS_MEDIA_WINDOW_CAPTURE_NOTIFICATION_TEXT, application_title); + case content::DesktopMediaID::TYPE_WEB_CONTENTS: + return l10n_util::GetStringFUTF16( + IDS_MEDIA_TAB_CAPTURE_NOTIFICATION_TEXT, application_title); + case content::DesktopMediaID::TYPE_NONE: + NOTREACHED(); + } + } else { + switch (capture_type) { + case content::DesktopMediaID::TYPE_SCREEN: + return l10n_util::GetStringFUTF16( + IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_TEXT_DELEGATED, + registered_extension_name, application_title); + case content::DesktopMediaID::TYPE_WINDOW: + return l10n_util::GetStringFUTF16( + IDS_MEDIA_WINDOW_CAPTURE_NOTIFICATION_TEXT_DELEGATED, + registered_extension_name, application_title); + case content::DesktopMediaID::TYPE_WEB_CONTENTS: + return l10n_util::GetStringFUTF16( + IDS_MEDIA_TAB_CAPTURE_NOTIFICATION_TEXT_DELEGATED, + registered_extension_name, application_title); + case content::DesktopMediaID::TYPE_NONE: + NOTREACHED(); + } + } + } else { // The case with audio + if (application_title == registered_extension_name) { + switch (capture_type) { + case content::DesktopMediaID::TYPE_SCREEN: + return l10n_util::GetStringFUTF16( + IDS_MEDIA_SCREEN_CAPTURE_WITH_AUDIO_NOTIFICATION_TEXT, + application_title); + case content::DesktopMediaID::TYPE_WEB_CONTENTS: + return l10n_util::GetStringFUTF16( + IDS_MEDIA_TAB_CAPTURE_WITH_AUDIO_NOTIFICATION_TEXT, + application_title); + case content::DesktopMediaID::TYPE_NONE: + case content::DesktopMediaID::TYPE_WINDOW: + NOTREACHED(); + } + } else { + switch (capture_type) { + case content::DesktopMediaID::TYPE_SCREEN: + return l10n_util::GetStringFUTF16( + IDS_MEDIA_SCREEN_CAPTURE_WITH_AUDIO_NOTIFICATION_TEXT_DELEGATED, + registered_extension_name, application_title); + case content::DesktopMediaID::TYPE_WEB_CONTENTS: + return l10n_util::GetStringFUTF16( + IDS_MEDIA_TAB_CAPTURE_WITH_AUDIO_NOTIFICATION_TEXT_DELEGATED, + registered_extension_name, application_title); + case content::DesktopMediaID::TYPE_NONE: + case content::DesktopMediaID::TYPE_WINDOW: + NOTREACHED(); + } + } + } + return base::string16(); +} + +} // namespace + +std::unique_ptr<content::MediaStreamUI> GetDevicesForDesktopCapture( + content::WebContents* web_contents, + blink::MediaStreamDevices* devices, + const content::DesktopMediaID& media_id, + blink::mojom::MediaStreamType devices_video_type, + blink::mojom::MediaStreamType devices_audio_type, + bool capture_audio, + bool disable_local_echo, + bool display_notification, + const base::string16& application_title, + const base::string16& registered_extension_name) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + DVLOG(2) << __func__ << ": media_id " << media_id.ToString() + << ", capture_audio " << capture_audio << ", disable_local_echo " + << disable_local_echo << ", display_notification " + << display_notification << ", application_title " + << application_title << ", extension_name " + << registered_extension_name; + + // Add selected desktop source to the list. + auto device = blink::MediaStreamDevice( + devices_video_type, media_id.ToString(), media_id.ToString()); + device.display_media_info = DesktopMediaIDToDisplayMediaInformation(media_id); + devices->push_back(device); + if (capture_audio) { + if (media_id.type == content::DesktopMediaID::TYPE_WEB_CONTENTS) { + content::WebContentsMediaCaptureId web_id = media_id.web_contents_id; + web_id.disable_local_echo = disable_local_echo; + devices->push_back(blink::MediaStreamDevice( + devices_audio_type, web_id.ToString(), "Tab audio")); + } else if (disable_local_echo) { + // Use the special loopback device ID for system audio capture. + devices->push_back(blink::MediaStreamDevice( + devices_audio_type, + media::AudioDeviceDescription::kLoopbackWithMuteDeviceId, + "System Audio")); + } else { + // Use the special loopback device ID for system audio capture. + devices->push_back(blink::MediaStreamDevice( + devices_audio_type, + media::AudioDeviceDescription::kLoopbackInputDeviceId, + "System Audio")); + } + } + + // If required, register to display the notification for stream capture. + std::unique_ptr<MediaStreamUI> notification_ui; + if (display_notification) { + if (media_id.type == content::DesktopMediaID::TYPE_WEB_CONTENTS && + base::FeatureList::IsEnabled( + features::kDesktopCaptureTabSharingInfobar)) { + notification_ui = TabSharingUI::Create(media_id, application_title); + } else { + notification_ui = ScreenCaptureNotificationUI::Create( + GetStopSharingUIString(application_title, registered_extension_name, + capture_audio, media_id.type)); + } + } + + return MediaCaptureDevicesDispatcher::GetInstance() + ->GetMediaStreamCaptureIndicator() + ->RegisterMediaStream(web_contents, *devices, std::move(notification_ui)); +} diff --git a/chromium/chrome/browser/media/webrtc/desktop_capture_devices_util.h b/chromium/chrome/browser/media/webrtc/desktop_capture_devices_util.h new file mode 100644 index 00000000000..2bdfe18cd21 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_capture_devices_util.h @@ -0,0 +1,31 @@ +// Copyright 2018 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 CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_CAPTURE_DEVICES_UTIL_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_CAPTURE_DEVICES_UTIL_H_ + +#include <memory> + +#include "base/strings/string_util.h" +#include "content/public/browser/desktop_media_id.h" +#include "content/public/browser/media_stream_request.h" +#include "content/public/browser/web_contents.h" +#include "third_party/blink/public/common/mediastream/media_stream_request.h" + +// Helper to get list of media stream devices for desktop capture in |devices|. +// Registers to display notification if |display_notification| is true. +// Returns an instance of MediaStreamUI to be passed to content layer. +std::unique_ptr<content::MediaStreamUI> GetDevicesForDesktopCapture( + content::WebContents* web_contents, + blink::MediaStreamDevices* devices, + const content::DesktopMediaID& media_id, + blink::mojom::MediaStreamType devices_video_type, + blink::mojom::MediaStreamType devices_audio_type, + bool capture_audio, + bool disable_local_echo, + bool display_notification, + const base::string16& application_title, + const base::string16& registered_extension_name); + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_CAPTURE_DEVICES_UTIL_H_ diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_list_ash.cc b/chromium/chrome/browser/media/webrtc/desktop_media_list_ash.cc new file mode 100644 index 00000000000..8f69f01efaa --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_list_ash.cc @@ -0,0 +1,166 @@ +// 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 "chrome/browser/media/webrtc/desktop_media_list_ash.h" + +#include <utility> + +#include "ash/public/cpp/shell_window_ids.h" +#include "ash/shell.h" +#include "ash/wm/desks/desks_util.h" +#include "base/bind.h" +#include "chrome/grit/generated_resources.h" +#include "media/base/video_util.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/gfx/image/image.h" +#include "ui/snapshot/snapshot.h" + +using content::DesktopMediaID; + +namespace { + +// Update the list twice per second. +const int kDefaultDesktopMediaListUpdatePeriod = 500; + +} // namespace + +DesktopMediaListAsh::DesktopMediaListAsh(content::DesktopMediaID::Type type) + : DesktopMediaListBase(base::TimeDelta::FromMilliseconds( + kDefaultDesktopMediaListUpdatePeriod)) { + DCHECK(type == content::DesktopMediaID::TYPE_SCREEN || + type == content::DesktopMediaID::TYPE_WINDOW); + type_ = type; +} + +DesktopMediaListAsh::~DesktopMediaListAsh() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); +} + +void DesktopMediaListAsh::Refresh(bool update_thumnails) { + DCHECK(can_refresh()); + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK_EQ(pending_window_capture_requests_, 0); + + std::vector<SourceDescription> new_sources; + EnumerateSources(&new_sources, update_thumnails); + UpdateSourcesList(new_sources); + OnRefreshMaybeComplete(); +} + +void DesktopMediaListAsh::EnumerateWindowsForRoot( + std::vector<DesktopMediaListAsh::SourceDescription>* sources, + bool update_thumnails, + aura::Window* root_window, + int container_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + aura::Window* container = ash::Shell::GetContainer(root_window, container_id); + if (!container) + return; + // The |container| has all the top-level windows in reverse order, e.g. the + // most top-level window is at the end. So iterate children reversely to make + // sure |sources| is in the expected order. + for (aura::Window::Windows::const_reverse_iterator it = + container->children().rbegin(); + it != container->children().rend(); ++it) { + if (!(*it)->IsVisible() || !(*it)->CanFocus()) + continue; + content::DesktopMediaID id = content::DesktopMediaID::RegisterNativeWindow( + content::DesktopMediaID::TYPE_WINDOW, *it); + if (id.window_id == view_dialog_id_.window_id) + continue; + SourceDescription window_source(id, (*it)->GetTitle()); + sources->push_back(window_source); + + if (update_thumnails) + CaptureThumbnail(window_source.id, *it); + } +} + +void DesktopMediaListAsh::EnumerateSources( + std::vector<DesktopMediaListAsh::SourceDescription>* sources, + bool update_thumnails) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + aura::Window::Windows root_windows = ash::Shell::GetAllRootWindows(); + + for (size_t i = 0; i < root_windows.size(); ++i) { + if (type_ == content::DesktopMediaID::TYPE_SCREEN) { + SourceDescription screen_source( + content::DesktopMediaID::RegisterNativeWindow( + content::DesktopMediaID::TYPE_SCREEN, root_windows[i]), + root_windows[i]->GetTitle()); + + if (root_windows[i] == ash::Shell::GetPrimaryRootWindow()) + sources->insert(sources->begin(), screen_source); + else + sources->push_back(screen_source); + + if (screen_source.name.empty()) { + if (root_windows.size() > 1) { + // 'Screen' in 'Screen 1, Screen 2, etc ' might be inflected in some + // languages depending on the number although rather unlikely. To be + // safe, use the plural format. + // TODO(jshin): Revert to GetStringFUTF16Int (with native digits) + // if none of UI languages inflects 'Screen' in this context. + screen_source.name = l10n_util::GetPluralStringFUTF16( + IDS_DESKTOP_MEDIA_PICKER_MULTIPLE_SCREEN_NAME, + static_cast<int>(i + 1)); + } else { + screen_source.name = l10n_util::GetStringUTF16( + IDS_DESKTOP_MEDIA_PICKER_SINGLE_SCREEN_NAME); + } + } + + if (update_thumnails) + CaptureThumbnail(screen_source.id, root_windows[i]); + } else { + // The list of desks containers depends on whether the Virtual Desks + // feature is enabled or not. + for (int desk_id : ash::desks_util::GetDesksContainersIds()) + EnumerateWindowsForRoot(sources, update_thumnails, root_windows[i], + desk_id); + + EnumerateWindowsForRoot(sources, update_thumnails, root_windows[i], + ash::kShellWindowId_AlwaysOnTopContainer); + EnumerateWindowsForRoot(sources, update_thumnails, root_windows[i], + ash::kShellWindowId_PipContainer); + } + } +} + +void DesktopMediaListAsh::CaptureThumbnail(content::DesktopMediaID id, + aura::Window* window) { + gfx::Rect window_rect(window->bounds().width(), window->bounds().height()); + gfx::Rect scaled_rect = media::ComputeLetterboxRegion( + gfx::Rect(thumbnail_size_), window_rect.size()); + + ++pending_window_capture_requests_; + ui::GrabWindowSnapshotAndScaleAsync( + window, window_rect, scaled_rect.size(), + base::Bind(&DesktopMediaListAsh::OnThumbnailCaptured, + weak_factory_.GetWeakPtr(), id)); +} + +void DesktopMediaListAsh::OnThumbnailCaptured(content::DesktopMediaID id, + gfx::Image image) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + UpdateSourceThumbnail(id, image.AsImageSkia()); + + --pending_window_capture_requests_; + DCHECK_GE(pending_window_capture_requests_, 0); + + OnRefreshMaybeComplete(); +} + +void DesktopMediaListAsh::OnRefreshMaybeComplete() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (pending_window_capture_requests_ == 0) { + // Once we've finished capturing all windows, notify the caller, which will + // post a task for the next list update if necessary. + OnRefreshComplete(); + } +} diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_list_ash.h b/chromium/chrome/browser/media/webrtc/desktop_media_list_ash.h new file mode 100644 index 00000000000..e8450edc102 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_list_ash.h @@ -0,0 +1,54 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_LIST_ASH_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_LIST_ASH_H_ + +#include <vector> + +#include "base/memory/weak_ptr.h" +#include "base/sequence_checker.h" +#include "chrome/browser/media/webrtc/desktop_media_list_base.h" +#include "content/public/browser/desktop_media_id.h" + +namespace aura { +class Window; +} + +namespace gfx { +class Image; +} + +// Implementation of DesktopMediaList that shows native screens and +// native windows. +class DesktopMediaListAsh : public DesktopMediaListBase { + public: + explicit DesktopMediaListAsh(content::DesktopMediaID::Type type); + ~DesktopMediaListAsh() override; + + private: + // Override from DesktopMediaListBase. + void Refresh(bool update_thumnails) override; + void EnumerateWindowsForRoot( + std::vector<DesktopMediaListAsh::SourceDescription>* windows, + bool update_thumnails, + aura::Window* root_window, + int container_id); + void EnumerateSources( + std::vector<DesktopMediaListAsh::SourceDescription>* windows, + bool update_thumnails); + void CaptureThumbnail(content::DesktopMediaID id, aura::Window* window); + void OnThumbnailCaptured(content::DesktopMediaID id, gfx::Image image); + void OnRefreshMaybeComplete(); + + int pending_window_capture_requests_ = 0; + + SEQUENCE_CHECKER(sequence_checker_); + + base::WeakPtrFactory<DesktopMediaListAsh> weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(DesktopMediaListAsh); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_LIST_ASH_H_ diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_list_ash_unittest.cc b/chromium/chrome/browser/media/webrtc/desktop_media_list_ash_unittest.cc new file mode 100644 index 00000000000..dfc973b1989 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_list_ash_unittest.cc @@ -0,0 +1,96 @@ +// 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 "chrome/browser/media/webrtc/desktop_media_list_ash.h" + +#include "base/location.h" +#include "base/macros.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/single_thread_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/desktop_media_list_observer.h" +#include "chrome/test/base/chrome_ash_test_base.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/aura/window.h" + +int kThumbnailSize = 100; + +using testing::AtLeast; +using testing::DoDefault; + +class MockDesktopMediaListObserver : public DesktopMediaListObserver { + public: + MOCK_METHOD2(OnSourceAdded, void(DesktopMediaList* list, int index)); + MOCK_METHOD2(OnSourceRemoved, void(DesktopMediaList* list, int index)); + MOCK_METHOD3(OnSourceMoved, + void(DesktopMediaList* list, int old_index, int new_index)); + MOCK_METHOD2(OnSourceNameChanged, void(DesktopMediaList* list, int index)); + MOCK_METHOD2(OnSourceThumbnailChanged, + void(DesktopMediaList* list, int index)); +}; + +class DesktopMediaListAshTest : public ChromeAshTestBase { + public: + DesktopMediaListAshTest() {} + ~DesktopMediaListAshTest() override {} + + void TearDown() override { + // Reset the unique_ptr so the list stops refreshing. + list_.reset(); + ChromeAshTestBase::TearDown(); + } + + void CreateList(content::DesktopMediaID::Type type) { + list_.reset(new DesktopMediaListAsh(type)); + list_->SetThumbnailSize(gfx::Size(kThumbnailSize, kThumbnailSize)); + + // Set update period to reduce the time it takes to run tests. + list_->SetUpdatePeriod(base::TimeDelta::FromMilliseconds(1)); + } + + protected: + MockDesktopMediaListObserver observer_; + std::unique_ptr<DesktopMediaListAsh> list_; + DISALLOW_COPY_AND_ASSIGN(DesktopMediaListAshTest); +}; + +ACTION(QuitMessageLoop) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::RunLoop::QuitCurrentWhenIdleClosureDeprecated()); +} + +TEST_F(DesktopMediaListAshTest, ScreenOnly) { + CreateList(content::DesktopMediaID::TYPE_SCREEN); + + std::unique_ptr<aura::Window> window(CreateTestWindowInShellWithId(0)); + + EXPECT_CALL(observer_, OnSourceAdded(list_.get(), 0)); + EXPECT_CALL(observer_, OnSourceThumbnailChanged(list_.get(), 0)) + .WillOnce(QuitMessageLoop()) + .WillRepeatedly(DoDefault()); + + list_->StartUpdating(&observer_); + base::RunLoop().Run(); +} + +TEST_F(DesktopMediaListAshTest, WindowOnly) { + CreateList(content::DesktopMediaID::TYPE_WINDOW); + + std::unique_ptr<aura::Window> window(CreateTestWindowInShellWithId(0)); + + EXPECT_CALL(observer_, OnSourceAdded(list_.get(), 0)); + EXPECT_CALL(observer_, OnSourceThumbnailChanged(list_.get(), 0)) + .WillOnce(QuitMessageLoop()) + .WillRepeatedly(DoDefault()); + EXPECT_CALL(observer_, OnSourceRemoved(list_.get(), 0)) + .WillOnce(QuitMessageLoop()); + + list_->StartUpdating(&observer_); + base::RunLoop().Run(); + window.reset(); + base::RunLoop().Run(); +} diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_list_base.cc b/chromium/chrome/browser/media/webrtc/desktop_media_list_base.cc new file mode 100644 index 00000000000..9d9b7435044 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_list_base.cc @@ -0,0 +1,186 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/desktop_media_list_base.h" + +#include <set> +#include <utility> + +#include "base/bind.h" +#include "base/task/post_task.h" +#include "chrome/browser/media/webrtc/desktop_media_list.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "ui/gfx/image/image.h" + +using content::BrowserThread; +using content::DesktopMediaID; + +DesktopMediaListBase::DesktopMediaListBase(base::TimeDelta update_period) + : update_period_(update_period) {} + +DesktopMediaListBase::~DesktopMediaListBase() {} + +void DesktopMediaListBase::SetUpdatePeriod(base::TimeDelta period) { + DCHECK(!observer_); + update_period_ = period; +} + +void DesktopMediaListBase::SetThumbnailSize(const gfx::Size& thumbnail_size) { + thumbnail_size_ = thumbnail_size; +} + +void DesktopMediaListBase::SetViewDialogWindowId(DesktopMediaID dialog_id) { + view_dialog_id_ = dialog_id; +} + +void DesktopMediaListBase::StartUpdating(DesktopMediaListObserver* observer) { + DCHECK(!observer_); + observer_ = observer; + + // Process sources previously discovered by a call to Update(). + if (observer_) { + for (size_t i = 0; i < sources_.size(); i++) { + observer_->OnSourceAdded(this, i); + } + } + + DCHECK(!refresh_callback_); + refresh_callback_ = base::BindOnce(&DesktopMediaListBase::ScheduleNextRefresh, + weak_factory_.GetWeakPtr()); + Refresh(true); +} + +void DesktopMediaListBase::Update(UpdateCallback callback) { + DCHECK(sources_.empty()); + DCHECK(!refresh_callback_); + refresh_callback_ = std::move(callback); + Refresh(false); +} + +int DesktopMediaListBase::GetSourceCount() const { + return sources_.size(); +} + +const DesktopMediaList::Source& DesktopMediaListBase::GetSource( + int index) const { + DCHECK_GE(index, 0); + DCHECK_LT(index, static_cast<int>(sources_.size())); + return sources_[index]; +} + +DesktopMediaID::Type DesktopMediaListBase::GetMediaListType() const { + return type_; +} + +DesktopMediaListBase::SourceDescription::SourceDescription( + DesktopMediaID id, + const base::string16& name) + : id(id), name(name) {} + +void DesktopMediaListBase::UpdateSourcesList( + const std::vector<SourceDescription>& new_sources) { + typedef std::set<DesktopMediaID> SourceSet; + SourceSet new_source_set; + for (size_t i = 0; i < new_sources.size(); ++i) { + new_source_set.insert(new_sources[i].id); + } + // Iterate through the old sources to find the removed sources. + for (size_t i = 0; i < sources_.size(); ++i) { + if (new_source_set.find(sources_[i].id) == new_source_set.end()) { + sources_.erase(sources_.begin() + i); + if (observer_) + observer_->OnSourceRemoved(this, i); + --i; + } + } + // Iterate through the new sources to find the added sources. + if (new_sources.size() > sources_.size()) { + SourceSet old_source_set; + for (size_t i = 0; i < sources_.size(); ++i) { + old_source_set.insert(sources_[i].id); + } + + for (size_t i = 0; i < new_sources.size(); ++i) { + if (old_source_set.find(new_sources[i].id) == old_source_set.end()) { + sources_.insert(sources_.begin() + i, Source()); + sources_[i].id = new_sources[i].id; + sources_[i].name = new_sources[i].name; + if (observer_) + observer_->OnSourceAdded(this, i); + } + } + } + DCHECK_EQ(new_sources.size(), sources_.size()); + + // Find the moved/changed sources. + size_t pos = 0; + while (pos < sources_.size()) { + if (!(sources_[pos].id == new_sources[pos].id)) { + // Find the source that should be moved to |pos|, starting from |pos + 1| + // of |sources_|, because entries before |pos| should have been sorted. + size_t old_pos = pos + 1; + for (; old_pos < sources_.size(); ++old_pos) { + if (sources_[old_pos].id == new_sources[pos].id) + break; + } + DCHECK(sources_[old_pos].id == new_sources[pos].id); + + // Move the source from |old_pos| to |pos|. + Source temp = sources_[old_pos]; + sources_.erase(sources_.begin() + old_pos); + sources_.insert(sources_.begin() + pos, temp); + + if (observer_) + observer_->OnSourceMoved(this, old_pos, pos); + } + + if (sources_[pos].name != new_sources[pos].name) { + sources_[pos].name = new_sources[pos].name; + if (observer_) + observer_->OnSourceNameChanged(this, pos); + } + ++pos; + } +} + +void DesktopMediaListBase::UpdateSourceThumbnail(DesktopMediaID id, + const gfx::ImageSkia& image) { + // Unlike other methods that check can_refresh(), this one won't cause + // OnRefreshComplete() to be called, but the caller is expected to schedule a + // call to OnRefreshComplete() after this method has been called as many times + // as needed, so the check is still valid. + DCHECK(can_refresh()); + + for (size_t i = 0; i < sources_.size(); ++i) { + if (sources_[i].id == id) { + sources_[i].thumbnail = image; + if (observer_) + observer_->OnSourceThumbnailChanged(this, i); + break; + } + } +} + +// static +uint32_t DesktopMediaListBase::GetImageHash(const gfx::Image& image) { + SkBitmap bitmap = image.AsBitmap(); + uint32_t value = base::Hash(bitmap.getPixels(), bitmap.computeByteSize()); + return value; +} + +void DesktopMediaListBase::OnRefreshComplete() { + DCHECK(refresh_callback_); + std::move(refresh_callback_).Run(); +} + +void DesktopMediaListBase::ScheduleNextRefresh() { + DCHECK(!refresh_callback_); + refresh_callback_ = base::BindOnce(&DesktopMediaListBase::ScheduleNextRefresh, + weak_factory_.GetWeakPtr()); + base::PostDelayedTask(FROM_HERE, {BrowserThread::UI}, + base::BindOnce(&DesktopMediaListBase::Refresh, + weak_factory_.GetWeakPtr(), true), + update_period_); +} diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_list_base.h b/chromium/chrome/browser/media/webrtc/desktop_media_list_base.h new file mode 100644 index 00000000000..3c09ec3111d --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_list_base.h @@ -0,0 +1,102 @@ +// Copyright 2016 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 CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_LIST_BASE_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_LIST_BASE_H_ + +#include "chrome/browser/media/webrtc/desktop_media_list.h" +#include "chrome/browser/media/webrtc/desktop_media_list_observer.h" +#include "content/public/browser/desktop_media_id.h" + +namespace gfx { +class Image; +} + +// Thumbnail size is 100*100 pixels +static const int kDefaultThumbnailSize = 100; + +// Base class for DesktopMediaList implementations. Implements logic shared +// between implementations. Specifically it's responsible for keeping current +// list of sources and calling the observer when the list changes. +// +// TODO(crbug.com/987001): Consider renaming this class. +class DesktopMediaListBase : public DesktopMediaList { + public: + explicit DesktopMediaListBase(base::TimeDelta update_period); + ~DesktopMediaListBase() override; + + // DesktopMediaList interface. + void SetUpdatePeriod(base::TimeDelta period) override; + void SetThumbnailSize(const gfx::Size& thumbnail_size) override; + void SetViewDialogWindowId(content::DesktopMediaID dialog_id) override; + void StartUpdating(DesktopMediaListObserver* observer) override; + void Update(UpdateCallback callback) override; + int GetSourceCount() const override; + const Source& GetSource(int index) const override; + content::DesktopMediaID::Type GetMediaListType() const override; + + static uint32_t GetImageHash(const gfx::Image& image); + + protected: + using RefreshCallback = UpdateCallback; + + struct SourceDescription { + SourceDescription(content::DesktopMediaID id, const base::string16& name); + + content::DesktopMediaID id; + base::string16 name; + }; + + // Before this method is called, |refresh_callback_| must be non-null, and + // after it completes (usually asychnonrously), |refresh_callback_| must be + // null. Since |refresh_callback_| is private, subclasses can check this + // condition by calling can_refresh(). + virtual void Refresh(bool update_thumnails) = 0; + + // Update source media list to observer. + void UpdateSourcesList(const std::vector<SourceDescription>& new_sources); + + // Update a thumbnail to observer. + void UpdateSourceThumbnail(content::DesktopMediaID id, + const gfx::ImageSkia& image); + + // Called when a refresh is complete. Invokes |refresh_callback_| unless it + // is null. Postcondition: |refresh_callback_| is null. + void OnRefreshComplete(); + + bool can_refresh() const { return !refresh_callback_.is_null(); } + + // Size of thumbnails generated by the model. + gfx::Size thumbnail_size_ = + gfx::Size(kDefaultThumbnailSize, kDefaultThumbnailSize); + + // ID of the hosting dialog. + content::DesktopMediaID view_dialog_id_ = + content::DesktopMediaID(content::DesktopMediaID::TYPE_NONE, -1); + + // Desktop media type of the list. + content::DesktopMediaID::Type type_ = content::DesktopMediaID::TYPE_NONE; + + private: + // Post a task for next list update. + void ScheduleNextRefresh(); + + // Time interval between mode updates. + base::TimeDelta update_period_; + + // Current list of sources. + std::vector<Source> sources_; + + // The observer passed to StartUpdating(). + DesktopMediaListObserver* observer_ = nullptr; + + // Called when a refresh operation completes. + RefreshCallback refresh_callback_; + + base::WeakPtrFactory<DesktopMediaListBase> weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(DesktopMediaListBase); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_LIST_BASE_H_ diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_list_observer.h b/chromium/chrome/browser/media/webrtc/desktop_media_list_observer.h new file mode 100644 index 00000000000..ad7f766a36b --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_list_observer.h @@ -0,0 +1,28 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_LIST_OBSERVER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_LIST_OBSERVER_H_ + +class DesktopMediaList; + +// Interface implemented by the desktop media picker dialog to receive +// notifications about changes in DesktopMediaList. +class DesktopMediaListObserver { + public: + // TODO(jrw): None of the |list| parameters below seem to be used. Consider + // removing them. + virtual void OnSourceAdded(DesktopMediaList* list, int index) = 0; + virtual void OnSourceRemoved(DesktopMediaList* list, int index) = 0; + virtual void OnSourceMoved(DesktopMediaList* list, + int old_index, + int new_index) = 0; + virtual void OnSourceNameChanged(DesktopMediaList* list, int index) = 0; + virtual void OnSourceThumbnailChanged(DesktopMediaList* list, int index) = 0; + + protected: + virtual ~DesktopMediaListObserver() {} +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_LIST_OBSERVER_H_ diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_picker.cc b/chromium/chrome/browser/media/webrtc/desktop_media_picker.cc new file mode 100644 index 00000000000..32b31d5f579 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_picker.cc @@ -0,0 +1,10 @@ +// Copyright 2018 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 "desktop_media_picker.h" + +#include "chrome/browser/media/webrtc/desktop_media_picker.h" + +DesktopMediaPicker::Params::Params() = default; +DesktopMediaPicker::Params::~Params() = default; diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_picker.h b/chromium/chrome/browser/media/webrtc/desktop_media_picker.h new file mode 100644 index 00000000000..afcb60351dd --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_picker.h @@ -0,0 +1,88 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_H_ + +#include <memory> +#include <string> +#include <utility> + +#include "base/callback_forward.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/optional.h" +#include "base/strings/string16.h" +#include "content/public/browser/desktop_media_id.h" +#include "content/public/browser/web_contents_observer.h" +#include "ui/base/ui_base_types.h" +#include "ui/gfx/native_widget_types.h" + +class DesktopMediaList; + +namespace content { +class WebContents; +} + +// Abstract interface for desktop media picker UI. It's used by Desktop Media +// API and by ARC to let user choose a desktop media source. +// +// TODO(crbug.com/987001): Rename this class. +class DesktopMediaPicker { + public: + using DoneCallback = base::OnceCallback<void(content::DesktopMediaID id)>; + + struct Params { + Params(); + ~Params(); + + // WebContents this picker is relative to, can be null. + content::WebContents* web_contents = nullptr; + // The context whose root window is used for dialog placement, cannot be + // null for Aura. + gfx::NativeWindow context = nullptr; + // Parent window the dialog is relative to, only used on Mac. + gfx::NativeWindow parent = nullptr; + // The modality used for showing the dialog. + ui::ModalType modality = ui::ModalType::MODAL_TYPE_CHILD; + // The name used in the dialog for what is requesting the picker to be + // shown. + base::string16 app_name; + // Can be the same as target_name. If it is not then this is used in the + // dialog for what is specific target within the app_name is requesting the + // picker. + base::string16 target_name; + // Whether audio capture should be shown as an option in the picker. + bool request_audio = false; + // Whether audio capture option should be approved by default if shown. + bool approve_audio_by_default = true; + // This flag controls the behvior in the case where the picker is invoked to + // select a screen and there is only one screen available. If true, the + // dialog is bypassed entirely and the screen is automatically selected. + // This behavior is disabled by default because in addition to letting the + // user select a desktop, the desktop picker also serves to prevent the + // screen screen from being shared without the user's explicit consent. + bool select_only_screen = false; + }; + + // Creates default implementation of DesktopMediaPicker for the current + // platform. + static std::unique_ptr<DesktopMediaPicker> Create(); + + DesktopMediaPicker() {} + virtual ~DesktopMediaPicker() {} + + // Shows dialog with list of desktop media sources (screens, windows, tabs) + // provided by |sources_lists|. + // Dialog window will call |done_callback| when user chooses one of the + // sources or closes the dialog. + virtual void Show(const Params& params, + std::vector<std::unique_ptr<DesktopMediaList>> source_lists, + DoneCallback done_callback) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(DesktopMediaPicker); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_H_ diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_picker_controller.cc b/chromium/chrome/browser/media/webrtc/desktop_media_picker_controller.cc new file mode 100644 index 00000000000..e026c204534 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_picker_controller.cc @@ -0,0 +1,109 @@ +// Copyright 2019 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 "chrome/browser/media/webrtc/desktop_media_picker_controller.h" + +#include <memory> +#include <tuple> +#include <utility> + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/desktop_media_list_ash.h" +#include "chrome/browser/media/webrtc/desktop_media_picker.h" +#include "chrome/browser/media/webrtc/desktop_media_picker_factory_impl.h" +#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" +#include "chrome/browser/media/webrtc/native_desktop_media_list.h" +#include "chrome/browser/media/webrtc/tab_desktop_media_list.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_window.h" +#include "chrome/grit/chromium_strings.h" +#include "content/public/browser/desktop_capture.h" +#include "content/public/browser/desktop_streams_registry.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/web_contents.h" +#include "desktop_media_picker.h" +#include "extensions/common/manifest.h" +#include "extensions/common/switches.h" +#include "ui/base/l10n/l10n_util.h" + +DesktopMediaPickerController::DesktopMediaPickerController( + DesktopMediaPickerFactory* picker_factory) + : picker_factory_(picker_factory + ? picker_factory + : DesktopMediaPickerFactoryImpl::GetInstance()) {} + +DesktopMediaPickerController::~DesktopMediaPickerController() = default; + +void DesktopMediaPickerController::Show( + const Params& params, + const std::vector<content::DesktopMediaID::Type>& sources, + DoneCallback done_callback) { + DCHECK(!base::Contains(sources, content::DesktopMediaID::TYPE_NONE)); + DCHECK(!done_callback_); + + done_callback_ = std::move(done_callback); + params_ = params; + + Observe(params.web_contents); + + // Keep same order as the input |sources| and avoid duplicates. + source_lists_ = picker_factory_->CreateMediaList(sources); + if (source_lists_.empty()) { + OnPickerDialogResults("At least one source type must be specified.", {}); + return; + } + + if (params.select_only_screen && sources.size() == 1 && + sources[0] == content::DesktopMediaID::TYPE_SCREEN) { + // Try to bypass the picker dialog if possible. + DCHECK(source_lists_.size() == 1); + auto* source_list = source_lists_[0].get(); + source_list->Update( + base::BindOnce(&DesktopMediaPickerController::OnInitialMediaListFound, + base::Unretained(this))); + } else { + ShowPickerDialog(); + } +} + +void DesktopMediaPickerController::WebContentsDestroyed() { + OnPickerDialogResults(std::string(), content::DesktopMediaID()); +} + +void DesktopMediaPickerController::OnInitialMediaListFound() { + DCHECK(params_.select_only_screen); + DCHECK(source_lists_.size() == 1); + auto* source_list = source_lists_[0].get(); + if (source_list->GetSourceCount() == 1) { + OnPickerDialogResults({}, source_list->GetSource(0).id); + return; + } + + ShowPickerDialog(); +} + +void DesktopMediaPickerController::ShowPickerDialog() { + picker_ = picker_factory_->CreatePicker(); + if (!picker_) { + OnPickerDialogResults( + "Desktop Capture API is not yet implemented for this platform.", {}); + return; + } + + picker_->Show(params_, std::move(source_lists_), + base::Bind(&DesktopMediaPickerController::OnPickerDialogResults, + base::Unretained(this), std::string())); +} + +void DesktopMediaPickerController::OnPickerDialogResults( + const std::string& err, + content::DesktopMediaID source) { + DCHECK(done_callback_); + std::move(done_callback_).Run(err, source); +} diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_picker_controller.h b/chromium/chrome/browser/media/webrtc/desktop_media_picker_controller.h new file mode 100644 index 00000000000..5bdd0ecdf82 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_picker_controller.h @@ -0,0 +1,91 @@ +// Copyright 2019 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 CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_CONTROLLER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_CONTROLLER_H_ + +#include <memory> +#include <string> +#include <utility> + +#include "base/macros.h" +#include "chrome/browser/media/webrtc/desktop_media_picker.h" +#include "content/public/browser/desktop_media_id.h" +#include "content/public/browser/web_contents_observer.h" +#include "ui/base/ui_base_types.h" + +class DesktopMediaList; +class DesktopMediaPickerFactory; + +// The main entry point for the desktop picker dialog box, which prompts the +// user to select a desktop or an application window whose content will be made +// available as a video stream. +// +// TODO(crbug.com/987001): Rename this class. Consider merging with +// DesktopMediaPickerViews and naming the merged class just DesktopMediaPicker. +class DesktopMediaPickerController : private content::WebContentsObserver { + public: + using Params = DesktopMediaPicker::Params; + + // Callback for desktop selection results. There are three possible cases: + // + // - If |err| is non-empty, it contains an error message regarding why the + // dialog could not be displayed, and the value of |id| should not be used. + // + // - If |err| is empty and id.is_null() is true, the user canceled the dialog. + // + // - Otherwise, |id| represents the user's selection. + using DoneCallback = base::OnceCallback<void(const std::string& err, + content::DesktopMediaID id)>; + + explicit DesktopMediaPickerController( + DesktopMediaPickerFactory* picker_factory = nullptr); + DesktopMediaPickerController(const DesktopMediaPickerController&) = delete; + DesktopMediaPickerController& operator=(const DesktopMediaPickerController&) = + delete; + ~DesktopMediaPickerController() override; + + // Show the desktop picker dialog using the parameters specified by |params|, + // with the possible selections restricted to those included in |sources|. If + // an error is detected synchronously, it is reported by returning an error + // string. Otherwise, the return value is nullopt, and the closure passed as + // |done_callback| is called when the dialog is closed. If the dialog is + // canceled, the argument to |done_callback| will be an instance of + // DesktopMediaID whose is_null() method returns true. + // + // As a special case, if |params.select_only_screen| is true, and the only + // selection type is TYPE_SCREEN, and there is only one screen, + // |done_callback| is called immediately with the screen's ID, and the dialog + // is not shown. This option must be used with care, because even when the + // dialog has only one option to select, the dialog itself helps prevent the + // user for accidentally sharing their screen and gives them the option to + // prevent their screen from being shared. + // + // Note that |done_callback| is called only if the dialog completes normally. + // If an instance of this class is destroyed while the dialog is visible, the + // dialog will be cleaned up, but |done_callback| will not be invoked. + void Show(const Params& params, + const std::vector<content::DesktopMediaID::Type>& sources, + DoneCallback done_callback); + + // content::WebContentsObserver overrides. + void WebContentsDestroyed() override; + + private: + void OnInitialMediaListFound(); + void ShowPickerDialog(); + // This function is responsible to call |done_callback_| and after running the + // callback |this| might be destroyed. Do **not** access fields after calling + // this function. + void OnPickerDialogResults(const std::string& err, + content::DesktopMediaID source); + + Params params_; + DoneCallback done_callback_; + std::vector<std::unique_ptr<DesktopMediaList>> source_lists_; + std::unique_ptr<DesktopMediaPicker> picker_; + DesktopMediaPickerFactory* picker_factory_; +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_CONTROLLER_H_ diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_picker_controller_unittest.cc b/chromium/chrome/browser/media/webrtc/desktop_media_picker_controller_unittest.cc new file mode 100644 index 00000000000..545d149d069 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_picker_controller_unittest.cc @@ -0,0 +1,154 @@ +// Copyright 2019 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 <memory> +#include <tuple> +#include <utility> +#include <vector> + +#include "base/strings/utf_string_conversions.h" +#include "base/test/mock_callback.h" +#include "chrome/browser/media/webrtc/desktop_media_picker.h" +#include "chrome/browser/media/webrtc/desktop_media_picker_controller.h" +#include "chrome/browser/media/webrtc/desktop_media_picker_factory_impl.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::AnyNumber; +using testing::ByMove; +using testing::InvokeArgument; +using testing::Ne; +using testing::Return; +using testing::ReturnRef; +using testing::WithArg; + +class MockDesktopMediaPicker : public DesktopMediaPicker { + public: + MOCK_METHOD3(Show, + void(const Params& params, + std::vector<std::unique_ptr<DesktopMediaList>> source_lists, + DoneCallback done_callback)); +}; + +class MockDesktopMediaList : public DesktopMediaList { + public: + MOCK_METHOD1(SetUpdatePeriod, void(base::TimeDelta period)); + MOCK_METHOD1(SetThumbnailSize, void(const gfx::Size& thumbnail_size)); + MOCK_METHOD1(SetViewDialogWindowId, void(content::DesktopMediaID dialog_id)); + MOCK_METHOD1(StartUpdating, void(DesktopMediaListObserver* observer)); + MOCK_METHOD1(Update, void(UpdateCallback callback)); + MOCK_CONST_METHOD0(GetSourceCount, int()); + MOCK_CONST_METHOD1(GetSource, Source&(int index)); + MOCK_CONST_METHOD0(GetMediaListType, content::DesktopMediaID::Type()); +}; + +class MockDesktopMediaPickerFactory : public DesktopMediaPickerFactory { + public: + MOCK_METHOD0(CreatePicker, std::unique_ptr<DesktopMediaPicker>()); + MOCK_METHOD1(CreateMediaList, + std::vector<std::unique_ptr<DesktopMediaList>>( + const std::vector<content::DesktopMediaID::Type>& types)); +}; + +class DesktopMediaPickerControllerTest : public testing::Test { + public: + void SetUp() override { + ON_CALL(factory_, CreatePicker).WillByDefault([this]() { + return std::unique_ptr<DesktopMediaPicker>(std::move(picker_)); + }); + ON_CALL(factory_, CreateMediaList).WillByDefault([this](const auto& types) { + std::vector<std::unique_ptr<DesktopMediaList>> lists; + lists.push_back(std::move(media_list_)); + return lists; + }); + } + + protected: + DesktopMediaPickerController::Params picker_params_; + base::MockCallback<DesktopMediaPickerController::DoneCallback> done_; + std::vector<content::DesktopMediaID::Type> source_types_{ + content::DesktopMediaID::TYPE_SCREEN}; + content::DesktopMediaID media_id_{content::DesktopMediaID::TYPE_SCREEN, 42}; + std::unique_ptr<MockDesktopMediaPicker> picker_ = + std::make_unique<MockDesktopMediaPicker>(); + std::unique_ptr<MockDesktopMediaList> media_list_ = + std::make_unique<MockDesktopMediaList>(); + MockDesktopMediaPickerFactory factory_; +}; + +// Test that the picker dialog is shown and the selected media ID is returned. +TEST_F(DesktopMediaPickerControllerTest, ShowPicker) { + EXPECT_CALL(factory_, CreatePicker()); + EXPECT_CALL(factory_, CreateMediaList(source_types_)); + EXPECT_CALL(done_, Run("", media_id_)); + EXPECT_CALL(*picker_, Show) + .WillOnce(WithArg<2>([&](DesktopMediaPicker::DoneCallback cb) { + std::move(cb).Run(media_id_); + })); + EXPECT_CALL(*media_list_, Update).Times(0); + + DesktopMediaPickerController controller(&factory_); + controller.Show(picker_params_, source_types_, done_.Get()); +} + +// Test that a null result is returned in response to WebContentsDestroyed(). +TEST_F(DesktopMediaPickerControllerTest, WebContentsDestroyed) { + EXPECT_CALL(factory_, CreatePicker()); + EXPECT_CALL(factory_, CreateMediaList(source_types_)); + EXPECT_CALL(done_, Run("", content::DesktopMediaID())); + EXPECT_CALL(*picker_, Show); + + DesktopMediaPickerController controller(&factory_); + controller.Show(picker_params_, source_types_, done_.Get()); + controller.WebContentsDestroyed(); +} + +// Test that the picker dialog can be bypassed. +TEST_F(DesktopMediaPickerControllerTest, ShowSingleScreen) { + picker_params_.select_only_screen = true; + + DesktopMediaList::Source source; + source.id = media_id_; + source.name = base::ASCIIToUTF16("fake name"); + + EXPECT_CALL(factory_, CreatePicker()).Times(0); + EXPECT_CALL(factory_, CreateMediaList(source_types_)); + EXPECT_CALL(done_, Run("", source.id)); + EXPECT_CALL(*picker_, Show).Times(0); + EXPECT_CALL(*media_list_, Update) + .WillOnce( + [](DesktopMediaList::UpdateCallback cb) { std::move(cb).Run(); }); + EXPECT_CALL(*media_list_, GetSourceCount) + .Times(AnyNumber()) + .WillRepeatedly(Return(1)); + EXPECT_CALL(*media_list_, GetSource(0)) + .Times(AnyNumber()) + .WillRepeatedly(ReturnRef(source)); + + DesktopMediaPickerController controller(&factory_); + controller.Show(picker_params_, source_types_, done_.Get()); +} + +// Test that an error is reported when no sources are found. +TEST_F(DesktopMediaPickerControllerTest, EmptySourceList) { + EXPECT_CALL(factory_, CreateMediaList) + .WillOnce( + Return(ByMove(std::vector<std::unique_ptr<DesktopMediaList>>()))); + EXPECT_CALL(done_, Run(Ne(""), content::DesktopMediaID())); + + DesktopMediaPickerController controller(&factory_); + controller.Show(picker_params_, source_types_, done_.Get()); +} + +// Test that an error is reported when no picker can be created. +TEST_F(DesktopMediaPickerControllerTest, NoPicker) { + EXPECT_CALL(factory_, CreatePicker) + .WillOnce(Return(ByMove(std::unique_ptr<DesktopMediaPicker>()))); + EXPECT_CALL(done_, Run(Ne(""), content::DesktopMediaID())); + EXPECT_CALL(factory_, CreateMediaList).Times(AnyNumber()); + + DesktopMediaPickerController controller(&factory_); + controller.Show(picker_params_, source_types_, done_.Get()); +} diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory.cc b/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory.cc new file mode 100644 index 00000000000..cbb3e722374 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory.cc @@ -0,0 +1,9 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/desktop_media_picker_factory.h" + +DesktopMediaPickerFactory::DesktopMediaPickerFactory() = default; + +DesktopMediaPickerFactory::~DesktopMediaPickerFactory() = default; diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory.h b/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory.h new file mode 100644 index 00000000000..0e393e91a4d --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory.h @@ -0,0 +1,33 @@ +// Copyright 2018 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 CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_FACTORY_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_FACTORY_H_ + +#include <memory> +#include <vector> + +#include "base/optional.h" +#include "chrome/browser/media/webrtc/desktop_media_list.h" +#include "chrome/browser/media/webrtc/desktop_media_picker.h" +#include "content/public/browser/desktop_media_id.h" + +// Interface for factory creating DesktopMediaList and DesktopMediaPicker +// instances. +class DesktopMediaPickerFactory { + public: + virtual ~DesktopMediaPickerFactory(); + + virtual std::unique_ptr<DesktopMediaPicker> CreatePicker() = 0; + virtual std::vector<std::unique_ptr<DesktopMediaList>> CreateMediaList( + const std::vector<content::DesktopMediaID::Type>& types) = 0; + + protected: + DesktopMediaPickerFactory(); + + private: + DISALLOW_COPY_AND_ASSIGN(DesktopMediaPickerFactory); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_FACTORY_H_ diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory_impl.cc b/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory_impl.cc new file mode 100644 index 00000000000..fa7d872810a --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory_impl.cc @@ -0,0 +1,91 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/desktop_media_picker_factory_impl.h" + +#include "base/no_destructor.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/desktop_media_list_ash.h" +#include "chrome/browser/media/webrtc/native_desktop_media_list.h" +#include "chrome/browser/media/webrtc/tab_desktop_media_list.h" +#include "content/public/browser/desktop_capture.h" + +DesktopMediaPickerFactoryImpl::DesktopMediaPickerFactoryImpl() = default; + +DesktopMediaPickerFactoryImpl::~DesktopMediaPickerFactoryImpl() = default; + +// static +DesktopMediaPickerFactoryImpl* DesktopMediaPickerFactoryImpl::GetInstance() { + static base::NoDestructor<DesktopMediaPickerFactoryImpl> impl; + return impl.get(); +} + +std::unique_ptr<DesktopMediaPicker> +DesktopMediaPickerFactoryImpl::CreatePicker() { +// DesktopMediaPicker is implemented only for Windows, OSX and Aura Linux +// builds. +#if defined(TOOLKIT_VIEWS) || defined(OS_MACOSX) + return DesktopMediaPicker::Create(); +#else + return nullptr; +#endif +} + +std::vector<std::unique_ptr<DesktopMediaList>> +DesktopMediaPickerFactoryImpl::CreateMediaList( + const std::vector<content::DesktopMediaID::Type>& types) { + // Keep same order as the input |sources| and avoid duplicates. + std::vector<std::unique_ptr<DesktopMediaList>> source_lists; + bool have_screen_list = false; + bool have_window_list = false; + bool have_tab_list = false; + for (auto source_type : types) { + switch (source_type) { + case content::DesktopMediaID::TYPE_NONE: + break; + case content::DesktopMediaID::TYPE_SCREEN: { + if (have_screen_list) + continue; + std::unique_ptr<DesktopMediaList> screen_list; +#if defined(OS_CHROMEOS) + screen_list = std::make_unique<DesktopMediaListAsh>( + content::DesktopMediaID::TYPE_SCREEN); +#else // !defined(OS_CHROMEOS) + screen_list = std::make_unique<NativeDesktopMediaList>( + content::DesktopMediaID::TYPE_SCREEN, + content::desktop_capture::CreateScreenCapturer()); +#endif // !defined(OS_CHROMEOS) + have_screen_list = true; + source_lists.push_back(std::move(screen_list)); + break; + } + case content::DesktopMediaID::TYPE_WINDOW: { + if (have_window_list) + continue; + std::unique_ptr<DesktopMediaList> window_list; +#if defined(OS_CHROMEOS) + window_list = std::make_unique<DesktopMediaListAsh>( + content::DesktopMediaID::TYPE_WINDOW); +#else // !defined(OS_CHROMEOS) + window_list = std::make_unique<NativeDesktopMediaList>( + content::DesktopMediaID::TYPE_WINDOW, + content::desktop_capture::CreateWindowCapturer()); +#endif // !defined(OS_CHROMEOS) + have_window_list = true; + source_lists.push_back(std::move(window_list)); + break; + } + case content::DesktopMediaID::TYPE_WEB_CONTENTS: { + if (have_tab_list) + continue; + std::unique_ptr<DesktopMediaList> tab_list = + std::make_unique<TabDesktopMediaList>(); + have_tab_list = true; + source_lists.push_back(std::move(tab_list)); + break; + } + } + } + return source_lists; +} diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory_impl.h b/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory_impl.h new file mode 100644 index 00000000000..4888b591445 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_picker_factory_impl.h @@ -0,0 +1,35 @@ +// Copyright 2018 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 CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_FACTORY_IMPL_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_FACTORY_IMPL_H_ + +#include <memory> +#include <vector> + +#include "chrome/browser/media/webrtc/desktop_media_list.h" +#include "chrome/browser/media/webrtc/desktop_media_picker.h" +#include "chrome/browser/media/webrtc/desktop_media_picker_factory.h" +#include "content/public/browser/desktop_media_id.h" + +// Factory creating DesktopMediaList and DesktopMediaPicker instances. +class DesktopMediaPickerFactoryImpl : public DesktopMediaPickerFactory { + public: + DesktopMediaPickerFactoryImpl(); + ~DesktopMediaPickerFactoryImpl() override; + + // Get the lazy initialized instance of the factory. + static DesktopMediaPickerFactoryImpl* GetInstance(); + + // DesktopMediaPickerFactory implementation + // Can return |nullptr| if platform doesn't support DesktopMediaPicker. + std::unique_ptr<DesktopMediaPicker> CreatePicker() override; + std::vector<std::unique_ptr<DesktopMediaList>> CreateMediaList( + const std::vector<content::DesktopMediaID::Type>& types) override; + + private: + DISALLOW_COPY_AND_ASSIGN(DesktopMediaPickerFactoryImpl); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_FACTORY_IMPL_H_ diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_picker_manager.cc b/chromium/chrome/browser/media/webrtc/desktop_media_picker_manager.cc new file mode 100644 index 00000000000..1f3a18f8268 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_picker_manager.cc @@ -0,0 +1,34 @@ +// Copyright 2019 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 "chrome/browser/media/webrtc/desktop_media_picker_manager.h" + +#include "base/no_destructor.h" + +// static +DesktopMediaPickerManager* DesktopMediaPickerManager::Get() { + static base::NoDestructor<DesktopMediaPickerManager> instance; + return instance.get(); +} + +DesktopMediaPickerManager::DesktopMediaPickerManager() = default; +DesktopMediaPickerManager::~DesktopMediaPickerManager() = default; + +void DesktopMediaPickerManager::AddObserver(DialogObserver* observer) { + observers_.AddObserver(observer); +} + +void DesktopMediaPickerManager::RemoveObserver(DialogObserver* observer) { + observers_.RemoveObserver(observer); +} + +void DesktopMediaPickerManager::OnShowDialog() { + for (auto& observer : observers_) + observer.OnDialogOpened(); +} + +void DesktopMediaPickerManager::OnHideDialog() { + for (auto& observer : observers_) + observer.OnDialogClosed(); +} diff --git a/chromium/chrome/browser/media/webrtc/desktop_media_picker_manager.h b/chromium/chrome/browser/media/webrtc/desktop_media_picker_manager.h new file mode 100644 index 00000000000..391994156e6 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/desktop_media_picker_manager.h @@ -0,0 +1,51 @@ +// Copyright 2019 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 CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_MANAGER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_MANAGER_H_ + +#include "base/macros.h" +#include "base/observer_list.h" + +namespace base { +template <typename T> +class NoDestructor; +} + +// A singleton that acts as a rendezvous for dialog observers to register and +// the dialog managers/delegates to post their activities. +// TODO(crbug/953495): Merge this into DesktopMediaPickerFactoryImpl. +class DesktopMediaPickerManager { + public: + class DialogObserver : public base::CheckedObserver { + public: + // Called when a media dialog is opened/shown. + virtual void OnDialogOpened() = 0; + + // Called when a media dialog is closed/hidden. + virtual void OnDialogClosed() = 0; + }; + + static DesktopMediaPickerManager* Get(); + + // For the observers + void AddObserver(DialogObserver* observer); + void RemoveObserver(DialogObserver* observer); + + // For the notifiers + void OnShowDialog(); + void OnHideDialog(); + + private: + friend base::NoDestructor<DesktopMediaPickerManager>; + + DesktopMediaPickerManager(); + ~DesktopMediaPickerManager(); // Never called. + + base::ObserverList<DesktopMediaPickerManager::DialogObserver> observers_; + + DISALLOW_COPY_AND_ASSIGN(DesktopMediaPickerManager); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_DESKTOP_MEDIA_PICKER_MANAGER_H_ diff --git a/chromium/chrome/browser/media/webrtc/display_media_access_handler.cc b/chromium/chrome/browser/media/webrtc/display_media_access_handler.cc new file mode 100644 index 00000000000..7e63e08ccf9 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/display_media_access_handler.cc @@ -0,0 +1,270 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/display_media_access_handler.h" + +#include <string> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/desktop_capture_devices_util.h" +#include "chrome/browser/media/webrtc/desktop_media_picker_factory_impl.h" +#include "chrome/browser/media/webrtc/native_desktop_media_list.h" +#include "chrome/browser/media/webrtc/tab_desktop_media_list.h" +#include "components/url_formatter/elide_url.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/desktop_capture.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/web_contents.h" +#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" + +#if defined(OS_MACOSX) +#include "chrome/browser/media/webrtc/system_media_capture_permissions_mac.h" +#endif + +// Holds pending request information so that we display one picker UI at a time +// for each content::WebContents. +struct DisplayMediaAccessHandler::PendingAccessRequest { + PendingAccessRequest(std::unique_ptr<DesktopMediaPicker> picker, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback) + : picker(std::move(picker)), + request(request), + callback(std::move(callback)) {} + ~PendingAccessRequest() = default; + + std::unique_ptr<DesktopMediaPicker> picker; + content::MediaStreamRequest request; + content::MediaResponseCallback callback; +}; + +DisplayMediaAccessHandler::DisplayMediaAccessHandler() + : picker_factory_(new DesktopMediaPickerFactoryImpl()) { + AddNotificationObserver(); +} + +DisplayMediaAccessHandler::DisplayMediaAccessHandler( + std::unique_ptr<DesktopMediaPickerFactory> picker_factory, + bool display_notification) + : display_notification_(display_notification), + picker_factory_(std::move(picker_factory)) { + AddNotificationObserver(); +} + +DisplayMediaAccessHandler::~DisplayMediaAccessHandler() = default; + +bool DisplayMediaAccessHandler::SupportsStreamType( + content::WebContents* web_contents, + const blink::mojom::MediaStreamType stream_type, + const extensions::Extension* extension) { + return stream_type == blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE; + // This class handles MEDIA_DISPLAY_AUDIO_CAPTURE as well, but only if it is + // accompanied by MEDIA_DISPLAY_VIDEO_CAPTURE request as per spec. + // https://w3c.github.io/mediacapture-screen-share/#mediadevices-additions + // 5.1 MediaDevices Additions + // "The user agent MUST reject audio-only requests." +} + +bool DisplayMediaAccessHandler::CheckMediaAccessPermission( + content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type, + const extensions::Extension* extension) { + return false; +} + +void DisplayMediaAccessHandler::HandleRequest( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + +#if defined(OS_MACOSX) + // Do not allow picker UI to be shown on a page that isn't in the foreground + // in Mac, because the UI implementation in Mac pops a window over any content + // which might be confusing for the users. See https://crbug.com/1407733 for + // details. + // TODO(emircan): Remove this once Mac UI doesn't use a window. + if (web_contents->GetVisibility() != content::Visibility::VISIBLE) { + LOG(ERROR) << "Do not allow getDisplayMedia() on a backgrounded page."; + std::move(callback).Run( + blink::MediaStreamDevices(), + blink::mojom::MediaStreamRequestResult::INVALID_STATE, nullptr); + return; + } +#endif // defined(OS_MACOSX) + + std::unique_ptr<DesktopMediaPicker> picker = picker_factory_->CreatePicker(); + if (!picker) { + std::move(callback).Run( + blink::MediaStreamDevices(), + blink::mojom::MediaStreamRequestResult::INVALID_STATE, nullptr); + return; + } + + RequestsQueue& queue = pending_requests_[web_contents]; + queue.push_back(std::make_unique<PendingAccessRequest>( + std::move(picker), request, std::move(callback))); + // If this is the only request then pop picker UI. + if (queue.size() == 1) + ProcessQueuedAccessRequest(queue, web_contents); +} + +void DisplayMediaAccessHandler::UpdateMediaRequestState( + int render_process_id, + int render_frame_id, + int page_request_id, + blink::mojom::MediaStreamType stream_type, + content::MediaRequestState state) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + if (state != content::MEDIA_REQUEST_STATE_DONE && + state != content::MEDIA_REQUEST_STATE_CLOSING) { + return; + } + + if (state == content::MEDIA_REQUEST_STATE_CLOSING) { + DeletePendingAccessRequest(render_process_id, render_frame_id, + page_request_id); + } + CaptureAccessHandlerBase::UpdateMediaRequestState( + render_process_id, render_frame_id, page_request_id, stream_type, state); + + // This method only gets called with the above checked states when all + // requests are to be canceled. Therefore, we don't need to process the + // next queued request. +} + +void DisplayMediaAccessHandler::ProcessQueuedAccessRequest( + const RequestsQueue& queue, + content::WebContents* web_contents) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + const PendingAccessRequest& pending_request = *queue.front(); + UpdateTrusted(pending_request.request, false /* is_trusted */); + + std::vector<content::DesktopMediaID::Type> media_types = { + content::DesktopMediaID::TYPE_SCREEN, + content::DesktopMediaID::TYPE_WINDOW, + content::DesktopMediaID::TYPE_WEB_CONTENTS}; + auto source_lists = picker_factory_->CreateMediaList(media_types); + + DesktopMediaPicker::DoneCallback done_callback = + base::BindOnce(&DisplayMediaAccessHandler::OnPickerDialogResults, + base::Unretained(this), web_contents); + DesktopMediaPicker::Params picker_params; + picker_params.web_contents = web_contents; + gfx::NativeWindow parent_window = web_contents->GetTopLevelNativeWindow(); + picker_params.context = parent_window; + picker_params.parent = parent_window; + picker_params.app_name = url_formatter::FormatUrlForSecurityDisplay( + web_contents->GetLastCommittedURL(), + url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC); + picker_params.target_name = picker_params.app_name; + picker_params.request_audio = + pending_request.request.audio_type == + blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE; + picker_params.approve_audio_by_default = false; + pending_request.picker->Show(picker_params, std::move(source_lists), + std::move(done_callback)); +} + +void DisplayMediaAccessHandler::OnPickerDialogResults( + content::WebContents* web_contents, + content::DesktopMediaID media_id) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + DCHECK(web_contents); + + auto it = pending_requests_.find(web_contents); + if (it == pending_requests_.end()) + return; + RequestsQueue& queue = it->second; + if (queue.empty()) { + // UpdateMediaRequestState() called with MEDIA_REQUEST_STATE_CLOSING. Don't + // need to do anything. + return; + } + + PendingAccessRequest& pending_request = *queue.front(); + blink::MediaStreamDevices devices; + blink::mojom::MediaStreamRequestResult request_result = + blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED; + std::unique_ptr<content::MediaStreamUI> ui; + if (media_id.is_null()) { + request_result = blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED; + } else { + request_result = blink::mojom::MediaStreamRequestResult::OK; +#if defined(OS_MACOSX) + // Check screen capture permissions on Mac if necessary. + if ((media_id.type == content::DesktopMediaID::TYPE_SCREEN || + media_id.type == content::DesktopMediaID::TYPE_WINDOW) && + system_media_permissions::CheckSystemScreenCapturePermission() != + system_media_permissions::SystemPermission::kAllowed) { + request_result = + blink::mojom::MediaStreamRequestResult::SYSTEM_PERMISSION_DENIED; + } +#endif + if (request_result == blink::mojom::MediaStreamRequestResult::OK) { + const auto& visible_url = url_formatter::FormatUrlForSecurityDisplay( + web_contents->GetLastCommittedURL(), + url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC); + ui = GetDevicesForDesktopCapture( + web_contents, &devices, media_id, + blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, + blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE, + media_id.audio_share, false /* disable_local_echo */, + display_notification_, visible_url, visible_url); + } + } + + std::move(pending_request.callback) + .Run(devices, request_result, std::move(ui)); + queue.pop_front(); + + if (!queue.empty()) + ProcessQueuedAccessRequest(queue, web_contents); +} + +void DisplayMediaAccessHandler::AddNotificationObserver() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + notifications_registrar_.Add(this, + content::NOTIFICATION_WEB_CONTENTS_DESTROYED, + content::NotificationService::AllSources()); +} + +void DisplayMediaAccessHandler::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + DCHECK_EQ(content::NOTIFICATION_WEB_CONTENTS_DESTROYED, type); + + pending_requests_.erase(content::Source<content::WebContents>(source).ptr()); +} + +void DisplayMediaAccessHandler::DeletePendingAccessRequest( + int render_process_id, + int render_frame_id, + int page_request_id) { + for (auto& queue_it : pending_requests_) { + RequestsQueue& queue = queue_it.second; + for (auto it = queue.begin(); it != queue.end(); ++it) { + const PendingAccessRequest& pending_request = **it; + if (pending_request.request.render_process_id == render_process_id && + pending_request.request.render_frame_id == render_frame_id && + pending_request.request.page_request_id == page_request_id) { + queue.erase(it); + return; + } + } + } +} diff --git a/chromium/chrome/browser/media/webrtc/display_media_access_handler.h b/chromium/chrome/browser/media/webrtc/display_media_access_handler.h new file mode 100644 index 00000000000..8ae226f2324 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/display_media_access_handler.h @@ -0,0 +1,89 @@ +// Copyright 2018 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 CHROME_BROWSER_MEDIA_WEBRTC_DISPLAY_MEDIA_ACCESS_HANDLER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_DISPLAY_MEDIA_ACCESS_HANDLER_H_ + +#include <memory> +#include <string> + +#include "base/containers/flat_map.h" +#include "base/macros.h" +#include "chrome/browser/media/capture_access_handler_base.h" +#include "chrome/browser/media/media_access_handler.h" +#include "chrome/browser/media/webrtc/desktop_media_list.h" +#include "chrome/browser/media/webrtc/desktop_media_picker.h" +#include "chrome/browser/media/webrtc/desktop_media_picker_factory.h" +#include "content/public/browser/desktop_media_id.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" + +namespace extensions { +class Extension; +} + +// MediaAccessHandler for getDisplayMedia API, see +// https://w3c.github.io/mediacapture-screen-share. +class DisplayMediaAccessHandler : public CaptureAccessHandlerBase, + public content::NotificationObserver { + public: + DisplayMediaAccessHandler(); + DisplayMediaAccessHandler( + std::unique_ptr<DesktopMediaPickerFactory> picker_factory, + bool display_notification); + ~DisplayMediaAccessHandler() override; + + // MediaAccessHandler implementation. + bool SupportsStreamType(content::WebContents* web_contents, + const blink::mojom::MediaStreamType stream_type, + const extensions::Extension* extension) override; + bool CheckMediaAccessPermission( + content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type, + const extensions::Extension* extension) override; + void HandleRequest(content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) override; + void UpdateMediaRequestState(int render_process_id, + int render_frame_id, + int page_request_id, + blink::mojom::MediaStreamType stream_type, + content::MediaRequestState state) override; + + private: + friend class DisplayMediaAccessHandlerTest; + + struct PendingAccessRequest; + using RequestsQueue = + base::circular_deque<std::unique_ptr<PendingAccessRequest>>; + using RequestsQueues = base::flat_map<content::WebContents*, RequestsQueue>; + + void ProcessQueuedAccessRequest(const RequestsQueue& queue, + content::WebContents* web_contents); + + void OnPickerDialogResults(content::WebContents* web_contents, + content::DesktopMediaID source); + + void AddNotificationObserver(); + + // content::NotificationObserver implementation. + void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) override; + + void DeletePendingAccessRequest(int render_process_id, + int render_frame_id, + int page_request_id); + + bool display_notification_ = true; + std::unique_ptr<DesktopMediaPickerFactory> picker_factory_; + RequestsQueues pending_requests_; + content::NotificationRegistrar notifications_registrar_; + + DISALLOW_COPY_AND_ASSIGN(DisplayMediaAccessHandler); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_DISPLAY_MEDIA_ACCESS_HANDLER_H_ diff --git a/chromium/chrome/browser/media/webrtc/display_media_access_handler_unittest.cc b/chromium/chrome/browser/media/webrtc/display_media_access_handler_unittest.cc new file mode 100644 index 00000000000..0fa5aa3bfbe --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/display_media_access_handler_unittest.cc @@ -0,0 +1,286 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/display_media_access_handler.h" + +#include <memory> +#include <string> +#include <utility> + +#include "base/bind.h" +#include "base/macros.h" +#include "base/run_loop.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/fake_desktop_media_picker_factory.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/desktop_media_id.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/web_contents.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/common/mediastream/media_stream_request.h" +#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" + +#if defined(OS_MACOSX) +#include "base/mac/mac_util.h" +#endif + +class DisplayMediaAccessHandlerTest : public ChromeRenderViewHostTestHarness { + public: + DisplayMediaAccessHandlerTest() {} + ~DisplayMediaAccessHandlerTest() override {} + + void SetUp() override { + ChromeRenderViewHostTestHarness::SetUp(); + auto picker_factory = std::make_unique<FakeDesktopMediaPickerFactory>(); + picker_factory_ = picker_factory.get(); + access_handler_ = std::make_unique<DisplayMediaAccessHandler>( + std::move(picker_factory), false /* display_notification */); + } + + void ProcessRequest( + const content::DesktopMediaID& fake_desktop_media_id_response, + blink::mojom::MediaStreamRequestResult* request_result, + blink::MediaStreamDevices* devices_result, + bool request_audio) { + FakeDesktopMediaPickerFactory::TestFlags test_flags[] = { + {true /* expect_screens */, true /* expect_windows*/, + true /* expect_tabs */, request_audio, + fake_desktop_media_id_response /* selected_source */}}; + picker_factory_->SetTestFlags(test_flags, base::size(test_flags)); + content::MediaStreamRequest request( + 0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM, + std::string(), std::string(), + request_audio ? blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE + : blink::mojom::MediaStreamType::NO_SERVICE, + blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, false); + + base::RunLoop wait_loop; + content::MediaResponseCallback callback = base::BindOnce( + [](base::RunLoop* wait_loop, + blink::mojom::MediaStreamRequestResult* request_result, + blink::MediaStreamDevices* devices_result, + const blink::MediaStreamDevices& devices, + blink::mojom::MediaStreamRequestResult result, + std::unique_ptr<content::MediaStreamUI> ui) { + *request_result = result; + *devices_result = devices; + wait_loop->Quit(); + }, + &wait_loop, request_result, devices_result); + access_handler_->HandleRequest(web_contents(), request, std::move(callback), + nullptr /* extension */); + wait_loop.Run(); + EXPECT_TRUE(test_flags[0].picker_created); + + access_handler_.reset(); + EXPECT_TRUE(test_flags[0].picker_deleted); + } + + void NotifyWebContentsDestroyed() { + access_handler_->Observe( + content::NOTIFICATION_WEB_CONTENTS_DESTROYED, + content::Source<content::WebContents>(web_contents()), + content::NotificationDetails()); + } + + const DisplayMediaAccessHandler::RequestsQueues& GetRequestQueues() { + return access_handler_->pending_requests_; + } + + protected: + FakeDesktopMediaPickerFactory* picker_factory_; + std::unique_ptr<DisplayMediaAccessHandler> access_handler_; +}; + +TEST_F(DisplayMediaAccessHandlerTest, PermissionGiven) { + blink::mojom::MediaStreamRequestResult result; + blink::MediaStreamDevices devices; + ProcessRequest(content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, + content::DesktopMediaID::kFakeId), + &result, &devices, false /* request_audio */); +#if defined(OS_MACOSX) + // Starting from macOS 10.15, screen capture requires system permissions + // that are disabled by default. + if (base::mac::IsAtLeastOS10_15()) { + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED, + result); + return; + } +#endif + + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, result); + EXPECT_EQ(1u, devices.size()); + EXPECT_EQ(blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, + devices[0].type); + EXPECT_TRUE(devices[0].display_media_info.has_value()); +} + +TEST_F(DisplayMediaAccessHandlerTest, PermissionGivenToRequestWithAudio) { + blink::mojom::MediaStreamRequestResult result; + blink::MediaStreamDevices devices; + content::DesktopMediaID fake_media_id(content::DesktopMediaID::TYPE_SCREEN, + content::DesktopMediaID::kFakeId, + true /* audio_share */); + ProcessRequest(fake_media_id, &result, &devices, true /* request_audio */); +#if defined(OS_MACOSX) + // Starting from macOS 10.15, screen capture requires system permissions + // that are disabled by default. + if (base::mac::IsAtLeastOS10_15()) { + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED, + result); + return; + } +#endif + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, result); + EXPECT_EQ(2u, devices.size()); + EXPECT_EQ(blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, + devices[0].type); + EXPECT_TRUE(devices[0].display_media_info.has_value()); + EXPECT_EQ(blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE, + devices[1].type); + EXPECT_TRUE(devices[1].input.IsValid()); +} + +TEST_F(DisplayMediaAccessHandlerTest, PermissionDenied) { + blink::mojom::MediaStreamRequestResult result; + blink::MediaStreamDevices devices; + ProcessRequest(content::DesktopMediaID(), &result, &devices, + true /* request_audio */); + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED, result); + EXPECT_EQ(0u, devices.size()); +} + +TEST_F(DisplayMediaAccessHandlerTest, UpdateMediaRequestStateWithClosing) { + const int render_process_id = 0; + const int render_frame_id = 0; + const int page_request_id = 0; + const blink::mojom::MediaStreamType video_stream_type = + blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE; + const blink::mojom::MediaStreamType audio_stream_type = + blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE; + FakeDesktopMediaPickerFactory::TestFlags test_flags[] = { + {true /* expect_screens */, true /* expect_windows*/, + true /* expect_tabs */, true /* expect_audio */, + content::DesktopMediaID(), true /* cancelled */}}; + picker_factory_->SetTestFlags(test_flags, base::size(test_flags)); + content::MediaStreamRequest request( + render_process_id, render_frame_id, page_request_id, + GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM, + std::string(), std::string(), audio_stream_type, video_stream_type, + false); + content::MediaResponseCallback callback; + access_handler_->HandleRequest(web_contents(), request, std::move(callback), + nullptr /* extension */); + EXPECT_TRUE(test_flags[0].picker_created); + EXPECT_EQ(1u, GetRequestQueues().size()); + auto queue_it = GetRequestQueues().find(web_contents()); + EXPECT_TRUE(queue_it != GetRequestQueues().end()); + EXPECT_EQ(1u, queue_it->second.size()); + + access_handler_->UpdateMediaRequestState( + render_process_id, render_frame_id, page_request_id, video_stream_type, + content::MEDIA_REQUEST_STATE_CLOSING); + EXPECT_EQ(1u, GetRequestQueues().size()); + queue_it = GetRequestQueues().find(web_contents()); + EXPECT_TRUE(queue_it != GetRequestQueues().end()); + EXPECT_EQ(0u, queue_it->second.size()); + EXPECT_TRUE(test_flags[0].picker_deleted); + access_handler_.reset(); +} + +TEST_F(DisplayMediaAccessHandlerTest, WebContentsDestroyed) { + FakeDesktopMediaPickerFactory::TestFlags test_flags[] = { + {true /* expect_screens */, true /* expect_windows*/, + true /* expect_tabs */, false /* expect_audio */, + content::DesktopMediaID(), true /* cancelled */}}; + picker_factory_->SetTestFlags(test_flags, base::size(test_flags)); + content::MediaStreamRequest request( + 0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM, + std::string(), std::string(), blink::mojom::MediaStreamType::NO_SERVICE, + blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, false); + content::MediaResponseCallback callback; + access_handler_->HandleRequest(web_contents(), request, std::move(callback), + nullptr /* extension */); + EXPECT_TRUE(test_flags[0].picker_created); + EXPECT_EQ(1u, GetRequestQueues().size()); + auto queue_it = GetRequestQueues().find(web_contents()); + EXPECT_TRUE(queue_it != GetRequestQueues().end()); + EXPECT_EQ(1u, queue_it->second.size()); + + NotifyWebContentsDestroyed(); + EXPECT_EQ(0u, GetRequestQueues().size()); + access_handler_.reset(); +} + +TEST_F(DisplayMediaAccessHandlerTest, MultipleRequests) { + FakeDesktopMediaPickerFactory::TestFlags test_flags[] = { + {true /* expect_screens */, true /* expect_windows*/, + true /* expect_tabs */, false /* expect_audio */, + content::DesktopMediaID( + content::DesktopMediaID::TYPE_SCREEN, + content::DesktopMediaID::kFakeId) /* selected_source */}, + {true /* expect_screens */, true /* expect_windows*/, + true /* expect_tabs */, false /* expect_audio */, + content::DesktopMediaID( + content::DesktopMediaID::TYPE_WINDOW, + content::DesktopMediaID::kNullId) /* selected_source */}}; + const size_t kTestFlagCount = 2; + picker_factory_->SetTestFlags(test_flags, kTestFlagCount); + + blink::mojom::MediaStreamRequestResult result; + blink::MediaStreamDevices devices; + base::RunLoop wait_loop[kTestFlagCount]; + for (size_t i = 0; i < kTestFlagCount; ++i) { + content::MediaStreamRequest request( + 0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM, + std::string(), std::string(), blink::mojom::MediaStreamType::NO_SERVICE, + blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, false); + content::MediaResponseCallback callback = base::BindOnce( + [](base::RunLoop* wait_loop, + blink::mojom::MediaStreamRequestResult* request_result, + blink::MediaStreamDevices* devices_result, + const blink::MediaStreamDevices& devices, + blink::mojom::MediaStreamRequestResult result, + std::unique_ptr<content::MediaStreamUI> ui) { + *request_result = result; + *devices_result = devices; + wait_loop->Quit(); + }, + &wait_loop[i], &result, &devices); + access_handler_->HandleRequest(web_contents(), request, std::move(callback), + nullptr /* extension */); + } + wait_loop[0].Run(); + EXPECT_TRUE(test_flags[0].picker_created); + EXPECT_TRUE(test_flags[0].picker_deleted); +#if defined(OS_MACOSX) + // Starting from macOS 10.15, screen capture requires system permissions + // that are disabled by default. + if (base::mac::IsAtLeastOS10_15()) { + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED, + result); + access_handler_.reset(); + return; + } +#endif + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, result); + EXPECT_EQ(1u, devices.size()); + EXPECT_EQ(blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, + devices[0].type); + + blink::MediaStreamDevice first_device = devices[0]; + EXPECT_TRUE(test_flags[1].picker_created); + EXPECT_FALSE(test_flags[1].picker_deleted); + wait_loop[1].Run(); + EXPECT_TRUE(test_flags[1].picker_deleted); + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, result); + EXPECT_EQ(1u, devices.size()); + EXPECT_EQ(blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, + devices[0].type); + EXPECT_FALSE(devices[0].IsSameDevice(first_device)); + + access_handler_.reset(); +} diff --git a/chromium/chrome/browser/media/webrtc/fake_desktop_media_list.cc b/chromium/chrome/browser/media/webrtc/fake_desktop_media_list.cc new file mode 100644 index 00000000000..ee71126dc3e --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/fake_desktop_media_list.cc @@ -0,0 +1,88 @@ +// 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 "chrome/browser/media/webrtc/fake_desktop_media_list.h" + +#include <utility> + +#include "base/strings/string_number_conversions.h" +#include "chrome/browser/media/webrtc/desktop_media_list.h" +#include "chrome/browser/media/webrtc/desktop_media_list_observer.h" +#include "ui/gfx/skia_util.h" + +using content::DesktopMediaID; + +FakeDesktopMediaList::FakeDesktopMediaList(DesktopMediaID::Type type) + : observer_(NULL), type_(type) {} +FakeDesktopMediaList::~FakeDesktopMediaList() {} + +void FakeDesktopMediaList::AddSource(int id) { + AddSourceByFullMediaID( + content::DesktopMediaID(DesktopMediaID::TYPE_WINDOW, id)); +} + +void FakeDesktopMediaList::AddSourceByFullMediaID( + content::DesktopMediaID media_id) { + Source source; + source.id = media_id; + source.name = base::NumberToString16(media_id.id); + + sources_.push_back(source); + observer_->OnSourceAdded(this, sources_.size() - 1); +} + +void FakeDesktopMediaList::RemoveSource(int index) { + sources_.erase(sources_.begin() + index); + observer_->OnSourceRemoved(this, index); +} + +void FakeDesktopMediaList::MoveSource(int old_index, int new_index) { + Source source = sources_[old_index]; + sources_.erase(sources_.begin() + old_index); + sources_.insert(sources_.begin() + new_index, source); + observer_->OnSourceMoved(this, old_index, new_index); +} + +void FakeDesktopMediaList::SetSourceThumbnail(int index) { + sources_[index].thumbnail = thumbnail_; + observer_->OnSourceThumbnailChanged(this, index); +} + +void FakeDesktopMediaList::SetSourceName(int index, base::string16 name) { + sources_[index].name = name; + observer_->OnSourceNameChanged(this, index); +} + +void FakeDesktopMediaList::SetUpdatePeriod(base::TimeDelta period) {} + +void FakeDesktopMediaList::SetThumbnailSize(const gfx::Size& thumbnail_size) {} + +void FakeDesktopMediaList::SetViewDialogWindowId( + content::DesktopMediaID dialog_id) {} + +void FakeDesktopMediaList::StartUpdating(DesktopMediaListObserver* observer) { + observer_ = observer; + + SkBitmap bitmap; + bitmap.allocN32Pixels(150, 150); + bitmap.eraseARGB(255, 0, 255, 0); + thumbnail_ = gfx::ImageSkia::CreateFrom1xBitmap(bitmap); +} + +void FakeDesktopMediaList::Update(UpdateCallback callback) { + std::move(callback).Run(); +} + +int FakeDesktopMediaList::GetSourceCount() const { + return sources_.size(); +} + +const DesktopMediaList::Source& FakeDesktopMediaList::GetSource( + int index) const { + return sources_[index]; +} + +DesktopMediaID::Type FakeDesktopMediaList::GetMediaListType() const { + return type_; +} diff --git a/chromium/chrome/browser/media/webrtc/fake_desktop_media_list.h b/chromium/chrome/browser/media/webrtc/fake_desktop_media_list.h new file mode 100644 index 00000000000..d9ed43891a3 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/fake_desktop_media_list.h @@ -0,0 +1,43 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_FAKE_DESKTOP_MEDIA_LIST_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_FAKE_DESKTOP_MEDIA_LIST_H_ + +#include <vector> + +#include "chrome/browser/media/webrtc/desktop_media_list.h" + +class FakeDesktopMediaList : public DesktopMediaList { + public: + explicit FakeDesktopMediaList(content::DesktopMediaID::Type type); + ~FakeDesktopMediaList() override; + + void AddSource(int id); + void AddSourceByFullMediaID(content::DesktopMediaID media_id); + void RemoveSource(int index); + void MoveSource(int old_index, int new_index); + void SetSourceThumbnail(int index); + void SetSourceName(int index, base::string16 name); + + // DesktopMediaList implementation: + void SetUpdatePeriod(base::TimeDelta period) override; + void SetThumbnailSize(const gfx::Size& thumbnail_size) override; + void SetViewDialogWindowId(content::DesktopMediaID dialog_id) override; + void StartUpdating(DesktopMediaListObserver* observer) override; + void Update(UpdateCallback callback) override; + int GetSourceCount() const override; + const Source& GetSource(int index) const override; + content::DesktopMediaID::Type GetMediaListType() const override; + + private: + std::vector<Source> sources_; + DesktopMediaListObserver* observer_; + gfx::ImageSkia thumbnail_; + const content::DesktopMediaID::Type type_; + + DISALLOW_COPY_AND_ASSIGN(FakeDesktopMediaList); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_FAKE_DESKTOP_MEDIA_LIST_H_ diff --git a/chromium/chrome/browser/media/webrtc/fake_desktop_media_picker_factory.cc b/chromium/chrome/browser/media/webrtc/fake_desktop_media_picker_factory.cc new file mode 100644 index 00000000000..99a474ce324 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/fake_desktop_media_picker_factory.cc @@ -0,0 +1,108 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/fake_desktop_media_picker_factory.h" + +#include <utility> + +#include "base/bind.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_task_runner_handle.h" +#include "chrome/browser/media/webrtc/fake_desktop_media_list.h" +#include "testing/gtest/include/gtest/gtest.h" + +class FakeDesktopMediaPicker : public DesktopMediaPicker { + public: + explicit FakeDesktopMediaPicker( + FakeDesktopMediaPickerFactory::TestFlags* expectation) + : expectation_(expectation) { + expectation_->picker_created = true; + } + ~FakeDesktopMediaPicker() override { expectation_->picker_deleted = true; } + + // DesktopMediaPicker interface. + void Show(const DesktopMediaPicker::Params& params, + std::vector<std::unique_ptr<DesktopMediaList>> source_lists, + DoneCallback done_callback) override { + bool show_screens = false; + bool show_windows = false; + bool show_tabs = false; + + for (auto& source_list : source_lists) { + switch (source_list->GetMediaListType()) { + case content::DesktopMediaID::TYPE_NONE: + break; + case content::DesktopMediaID::TYPE_SCREEN: + show_screens = true; + break; + case content::DesktopMediaID::TYPE_WINDOW: + show_windows = true; + break; + case content::DesktopMediaID::TYPE_WEB_CONTENTS: + show_tabs = true; + break; + } + } + EXPECT_EQ(expectation_->expect_screens, show_screens); + EXPECT_EQ(expectation_->expect_windows, show_windows); + EXPECT_EQ(expectation_->expect_tabs, show_tabs); + EXPECT_EQ(expectation_->expect_audio, params.request_audio); + EXPECT_EQ(params.modality, ui::ModalType::MODAL_TYPE_CHILD); + + if (!expectation_->cancelled) { + // Post a task to call the callback asynchronously. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&FakeDesktopMediaPicker::CallCallback, + weak_factory_.GetWeakPtr(), std::move(done_callback))); + } else { + // If we expect the dialog to be cancelled then store the callback to + // retain reference to the callback handler. + done_callback_ = std::move(done_callback); + } + } + + private: + void CallCallback(DoneCallback done_callback) { + std::move(done_callback).Run(expectation_->selected_source); + } + + FakeDesktopMediaPickerFactory::TestFlags* expectation_; + DoneCallback done_callback_; + + base::WeakPtrFactory<FakeDesktopMediaPicker> weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(FakeDesktopMediaPicker); +}; + +FakeDesktopMediaPickerFactory::FakeDesktopMediaPickerFactory() = default; + +FakeDesktopMediaPickerFactory::~FakeDesktopMediaPickerFactory() = default; + +void FakeDesktopMediaPickerFactory::SetTestFlags(TestFlags* test_flags, + int tests_count) { + test_flags_ = test_flags; + tests_count_ = tests_count; + current_test_ = 0; +} + +std::unique_ptr<DesktopMediaPicker> +FakeDesktopMediaPickerFactory::CreatePicker() { + EXPECT_LE(current_test_, tests_count_); + if (current_test_ >= tests_count_) + return std::unique_ptr<DesktopMediaPicker>(); + ++current_test_; + return std::unique_ptr<DesktopMediaPicker>( + new FakeDesktopMediaPicker(test_flags_ + current_test_ - 1)); +} + +std::vector<std::unique_ptr<DesktopMediaList>> +FakeDesktopMediaPickerFactory::CreateMediaList( + const std::vector<content::DesktopMediaID::Type>& types) { + EXPECT_LE(current_test_, tests_count_); + std::vector<std::unique_ptr<DesktopMediaList>> media_lists; + for (auto source_type : types) + media_lists.emplace_back(new FakeDesktopMediaList(source_type)); + return media_lists; +} diff --git a/chromium/chrome/browser/media/webrtc/fake_desktop_media_picker_factory.h b/chromium/chrome/browser/media/webrtc/fake_desktop_media_picker_factory.h new file mode 100644 index 00000000000..57ace04ddcc --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/fake_desktop_media_picker_factory.h @@ -0,0 +1,53 @@ +// Copyright 2018 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 CHROME_BROWSER_MEDIA_WEBRTC_FAKE_DESKTOP_MEDIA_PICKER_FACTORY_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_FAKE_DESKTOP_MEDIA_PICKER_FACTORY_H_ + +#include <memory> +#include <vector> + +#include "base/optional.h" +#include "chrome/browser/media/webrtc/desktop_media_list.h" +#include "chrome/browser/media/webrtc/desktop_media_picker.h" +#include "chrome/browser/media/webrtc/desktop_media_picker_factory.h" +#include "content/public/browser/desktop_media_id.h" + +// Used in tests to supply fake picker. +class FakeDesktopMediaPickerFactory : public DesktopMediaPickerFactory { + public: + struct TestFlags { + bool expect_screens = false; + bool expect_windows = false; + bool expect_tabs = false; + bool expect_audio = false; + content::DesktopMediaID selected_source; + bool cancelled = false; + + // Following flags are set by FakeDesktopMediaPicker when it's created and + // deleted. + bool picker_created = false; + bool picker_deleted = false; + }; + + FakeDesktopMediaPickerFactory(); + ~FakeDesktopMediaPickerFactory() override; + + // |test_flags| are expected to outlive the factory. + void SetTestFlags(TestFlags* test_flags, int tests_count); + + // DesktopMediaPickerFactory implementation + std::unique_ptr<DesktopMediaPicker> CreatePicker() override; + std::vector<std::unique_ptr<DesktopMediaList>> CreateMediaList( + const std::vector<content::DesktopMediaID::Type>& types) override; + + private: + TestFlags* test_flags_; + int tests_count_; + int current_test_; + + DISALLOW_COPY_AND_ASSIGN(FakeDesktopMediaPickerFactory); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_FAKE_DESKTOP_MEDIA_PICKER_FACTORY_H_ diff --git a/chromium/chrome/browser/media/webrtc/media_authorization_wrapper_mac.h b/chromium/chrome/browser/media/webrtc/media_authorization_wrapper_mac.h new file mode 100644 index 00000000000..cd5c578d604 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_authorization_wrapper_mac.h @@ -0,0 +1,26 @@ +// Copyright 2019 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 CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_AUTHORIZATION_WRAPPER_MAC_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_AUTHORIZATION_WRAPPER_MAC_H_ + +#import <Foundation/NSString.h> + +#include "base/callback_forward.h" + +namespace system_media_permissions { + +class MediaAuthorizationWrapper { + public: + virtual ~MediaAuthorizationWrapper() {} + + virtual NSInteger AuthorizationStatusForMediaType(NSString* media_type) = 0; + virtual void RequestAccessForMediaType(NSString* media_type, + base::RepeatingClosure callback, + const base::TaskTraits& traits) = 0; +}; + +} // namespace system_media_permissions + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_AUTHORIZATION_WRAPPER_MAC_H_ diff --git a/chromium/chrome/browser/media/webrtc/media_capture_devices_dispatcher.cc b/chromium/chrome/browser/media/webrtc/media_capture_devices_dispatcher.cc new file mode 100644 index 00000000000..954ce697768 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_capture_devices_dispatcher.cc @@ -0,0 +1,493 @@ +// 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 "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" + +#include <memory> +#include <utility> + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/logging.h" +#include "base/metrics/field_trial.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/task/post_task.h" +#include "build/build_config.h" +#include "chrome/browser/media/media_access_handler.h" +#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h" +#include "chrome/browser/media/webrtc/permission_bubble_media_access_handler.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/pref_names.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/scoped_user_pref_update.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/media_capture_devices.h" +#include "content/public/browser/notification_source.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/web_contents.h" +#include "extensions/buildflags/buildflags.h" +#include "extensions/common/constants.h" +#include "media/base/media_switches.h" +#include "third_party/blink/public/common/features.h" +#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" + +#if !defined(OS_ANDROID) +#include "chrome/browser/media/webrtc/display_media_access_handler.h" +#endif // defined(OS_ANDROID) + +#if defined(OS_CHROMEOS) +#include "ash/shell.h" +#include "chrome/browser/media/chromeos_login_media_access_handler.h" +#include "chrome/browser/media/public_session_media_access_handler.h" +#include "chrome/browser/media/public_session_tab_capture_access_handler.h" +#endif // defined(OS_CHROMEOS) + +#if BUILDFLAG(ENABLE_EXTENSIONS) +#include "chrome/browser/media/extension_media_access_handler.h" +#include "chrome/browser/media/webrtc/desktop_capture_access_handler.h" +#include "chrome/browser/media/webrtc/tab_capture_access_handler.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/common/extension.h" +#include "extensions/common/permissions/permissions_data.h" +#endif // BUILDFLAG(ENABLE_EXTENSIONS) + +using blink::MediaStreamDevices; +using content::BrowserThread; +using content::MediaCaptureDevices; + +namespace { + +// Finds a device in |devices| that has |device_id|, or NULL if not found. +const blink::MediaStreamDevice* FindDeviceWithId( + const blink::MediaStreamDevices& devices, + const std::string& device_id) { + auto iter = devices.begin(); + for (; iter != devices.end(); ++iter) { + if (iter->id == device_id) { + return &(*iter); + } + } + return NULL; +} + +content::WebContents* WebContentsFromIds(int render_process_id, + int render_frame_id) { + content::WebContents* web_contents = + content::WebContents::FromRenderFrameHost( + content::RenderFrameHost::FromID(render_process_id, render_frame_id)); + return web_contents; +} + +} // namespace + +MediaCaptureDevicesDispatcher* MediaCaptureDevicesDispatcher::GetInstance() { + return base::Singleton<MediaCaptureDevicesDispatcher>::get(); +} + +MediaCaptureDevicesDispatcher::MediaCaptureDevicesDispatcher() + : is_device_enumeration_disabled_(false), + media_stream_capture_indicator_(new MediaStreamCaptureIndicator()) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + +#if !defined(OS_ANDROID) + media_access_handlers_.push_back( + std::make_unique<DisplayMediaAccessHandler>()); +#endif // defined(OS_ANDROID) + +#if BUILDFLAG(ENABLE_EXTENSIONS) +#if defined(OS_CHROMEOS) + media_access_handlers_.push_back( + std::make_unique<ChromeOSLoginMediaAccessHandler>()); + // Wrapper around ExtensionMediaAccessHandler used in Public Sessions. + media_access_handlers_.push_back( + std::make_unique<PublicSessionMediaAccessHandler>()); +#else + media_access_handlers_.push_back( + std::make_unique<ExtensionMediaAccessHandler>()); +#endif + media_access_handlers_.push_back( + std::make_unique<DesktopCaptureAccessHandler>()); +#if defined(OS_CHROMEOS) + // Wrapper around TabCaptureAccessHandler used in Public Sessions. + media_access_handlers_.push_back( + std::make_unique<PublicSessionTabCaptureAccessHandler>()); +#else + media_access_handlers_.push_back(std::make_unique<TabCaptureAccessHandler>()); +#endif +#endif + media_access_handlers_.push_back( + std::make_unique<PermissionBubbleMediaAccessHandler>()); +} + +MediaCaptureDevicesDispatcher::~MediaCaptureDevicesDispatcher() {} + +void MediaCaptureDevicesDispatcher::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterStringPref(prefs::kDefaultAudioCaptureDevice, + std::string()); + registry->RegisterStringPref(prefs::kDefaultVideoCaptureDevice, + std::string()); +} + +bool MediaCaptureDevicesDispatcher::IsOriginForCasting(const GURL& origin) { + // Whitelisted tab casting extensions. + return + // Media Router Dev + origin.spec() == "chrome-extension://enhhojjnijigcajfphajepfemndkmdlo/" || + // Media Router Stable + origin.spec() == "chrome-extension://pkedcjkdefgpdelpbcmbmeomcjbeemfm/"; +} + +void MediaCaptureDevicesDispatcher::AddObserver(Observer* observer) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (!observers_.HasObserver(observer)) + observers_.AddObserver(observer); +} + +void MediaCaptureDevicesDispatcher::RemoveObserver(Observer* observer) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + observers_.RemoveObserver(observer); +} + +const MediaStreamDevices& +MediaCaptureDevicesDispatcher::GetAudioCaptureDevices() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (is_device_enumeration_disabled_ || !test_audio_devices_.empty()) + return test_audio_devices_; + + return MediaCaptureDevices::GetInstance()->GetAudioCaptureDevices(); +} + +const MediaStreamDevices& +MediaCaptureDevicesDispatcher::GetVideoCaptureDevices() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (is_device_enumeration_disabled_ || !test_video_devices_.empty()) + return test_video_devices_; + + return MediaCaptureDevices::GetInstance()->GetVideoCaptureDevices(); +} + +void MediaCaptureDevicesDispatcher::ProcessMediaAccessRequest( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + // Kill switch for getDisplayMedia() on browser side to prevent renderer from + // bypassing blink side checks. + if (request.video_type == + blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE && + !base::FeatureList::IsEnabled(blink::features::kRTCGetDisplayMedia)) { + std::move(callback).Run( + blink::MediaStreamDevices(), + blink::mojom::MediaStreamRequestResult::NOT_SUPPORTED, nullptr); + return; + } + + for (const auto& handler : media_access_handlers_) { + if (handler->SupportsStreamType(web_contents, request.video_type, + extension) || + handler->SupportsStreamType(web_contents, request.audio_type, + extension)) { + handler->HandleRequest(web_contents, request, std::move(callback), + extension); + return; + } + } + std::move(callback).Run(blink::MediaStreamDevices(), + blink::mojom::MediaStreamRequestResult::NOT_SUPPORTED, + nullptr); +} + +bool MediaCaptureDevicesDispatcher::CheckMediaAccessPermission( + content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + return CheckMediaAccessPermission(render_frame_host, security_origin, type, + nullptr); +} + +bool MediaCaptureDevicesDispatcher::CheckMediaAccessPermission( + content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type, + const extensions::Extension* extension) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + for (const auto& handler : media_access_handlers_) { + if (handler->SupportsStreamType( + content::WebContents::FromRenderFrameHost(render_frame_host), type, + extension)) { + return handler->CheckMediaAccessPermission( + render_frame_host, security_origin, type, extension); + } + } + return false; +} + +void MediaCaptureDevicesDispatcher::GetDefaultDevicesForProfile( + Profile* profile, + bool audio, + bool video, + blink::MediaStreamDevices* devices) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(audio || video); + + PrefService* prefs = profile->GetPrefs(); + std::string default_device; + if (audio) { + default_device = prefs->GetString(prefs::kDefaultAudioCaptureDevice); + const blink::MediaStreamDevice* device = + GetRequestedAudioDevice(default_device); + if (!device) + device = GetFirstAvailableAudioDevice(); + if (device) + devices->push_back(*device); + } + + if (video) { + default_device = prefs->GetString(prefs::kDefaultVideoCaptureDevice); + const blink::MediaStreamDevice* device = + GetRequestedVideoDevice(default_device); + if (!device) + device = GetFirstAvailableVideoDevice(); + if (device) + devices->push_back(*device); + } +} + +std::string MediaCaptureDevicesDispatcher::GetDefaultDeviceIDForProfile( + Profile* profile, + blink::mojom::MediaStreamType type) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + PrefService* prefs = profile->GetPrefs(); + if (type == blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE) + return prefs->GetString(prefs::kDefaultAudioCaptureDevice); + else if (type == blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE) + return prefs->GetString(prefs::kDefaultVideoCaptureDevice); + else + return std::string(); +} + +const blink::MediaStreamDevice* +MediaCaptureDevicesDispatcher::GetRequestedAudioDevice( + const std::string& requested_audio_device_id) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + const blink::MediaStreamDevices& audio_devices = GetAudioCaptureDevices(); + const blink::MediaStreamDevice* const device = + FindDeviceWithId(audio_devices, requested_audio_device_id); + return device; +} + +const blink::MediaStreamDevice* +MediaCaptureDevicesDispatcher::GetFirstAvailableAudioDevice() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + const blink::MediaStreamDevices& audio_devices = GetAudioCaptureDevices(); + if (audio_devices.empty()) + return NULL; + return &(*audio_devices.begin()); +} + +const blink::MediaStreamDevice* +MediaCaptureDevicesDispatcher::GetRequestedVideoDevice( + const std::string& requested_video_device_id) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + const blink::MediaStreamDevices& video_devices = GetVideoCaptureDevices(); + const blink::MediaStreamDevice* const device = + FindDeviceWithId(video_devices, requested_video_device_id); + return device; +} + +const blink::MediaStreamDevice* +MediaCaptureDevicesDispatcher::GetFirstAvailableVideoDevice() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + const blink::MediaStreamDevices& video_devices = GetVideoCaptureDevices(); + if (video_devices.empty()) + return NULL; + return &(*video_devices.begin()); +} + +void MediaCaptureDevicesDispatcher::DisableDeviceEnumerationForTesting() { + is_device_enumeration_disabled_ = true; +} + +scoped_refptr<MediaStreamCaptureIndicator> +MediaCaptureDevicesDispatcher::GetMediaStreamCaptureIndicator() { + return media_stream_capture_indicator_; +} + +void MediaCaptureDevicesDispatcher::OnAudioCaptureDevicesChanged() { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce( + &MediaCaptureDevicesDispatcher::NotifyAudioDevicesChangedOnUIThread, + base::Unretained(this))); +} + +void MediaCaptureDevicesDispatcher::OnVideoCaptureDevicesChanged() { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce( + &MediaCaptureDevicesDispatcher::NotifyVideoDevicesChangedOnUIThread, + base::Unretained(this))); +} + +void MediaCaptureDevicesDispatcher::OnMediaRequestStateChanged( + int render_process_id, + int render_frame_id, + int page_request_id, + const GURL& security_origin, + blink::mojom::MediaStreamType stream_type, + content::MediaRequestState state) { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce( + &MediaCaptureDevicesDispatcher::UpdateMediaRequestStateOnUIThread, + base::Unretained(this), render_process_id, render_frame_id, + page_request_id, security_origin, stream_type, state)); +} + +void MediaCaptureDevicesDispatcher::OnCreatingAudioStream(int render_process_id, + int render_frame_id) { + // TODO(https://crbug.com/837606): Figure out how to simplify threading here. + // Currently, this will either always be called on the UI thread, or always + // on the IO thread, depending on how far along the work to migrate to the + // audio service has progressed. The rest of the methods of the + // content::MediaObserver are always called on the IO thread. + if (BrowserThread::CurrentlyOn(BrowserThread::UI)) { + OnCreatingAudioStreamOnUIThread(render_process_id, render_frame_id); + return; + } + + DCHECK_CURRENTLY_ON(BrowserThread::IO); + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce( + &MediaCaptureDevicesDispatcher::OnCreatingAudioStreamOnUIThread, + base::Unretained(this), render_process_id, render_frame_id)); +} + +void MediaCaptureDevicesDispatcher::NotifyAudioDevicesChangedOnUIThread() { + MediaStreamDevices devices = GetAudioCaptureDevices(); + for (auto& observer : observers_) + observer.OnUpdateAudioDevices(devices); +} + +void MediaCaptureDevicesDispatcher::NotifyVideoDevicesChangedOnUIThread() { + MediaStreamDevices devices = GetVideoCaptureDevices(); + for (auto& observer : observers_) + observer.OnUpdateVideoDevices(devices); +} + +void MediaCaptureDevicesDispatcher::UpdateMediaRequestStateOnUIThread( + int render_process_id, + int render_frame_id, + int page_request_id, + const GURL& security_origin, + blink::mojom::MediaStreamType stream_type, + content::MediaRequestState state) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + for (const auto& handler : media_access_handlers_) { + if (handler->SupportsStreamType( + WebContentsFromIds(render_process_id, render_frame_id), stream_type, + nullptr)) { + handler->UpdateMediaRequestState(render_process_id, render_frame_id, + page_request_id, stream_type, state); + break; + } + } + +#if defined(OS_CHROMEOS) + if (IsOriginForCasting(security_origin) && + blink::IsVideoInputMediaType(stream_type)) { + // Notify ash that casting state has changed. + if (state == content::MEDIA_REQUEST_STATE_DONE) { + ash::Shell::Get()->OnCastingSessionStartedOrStopped(true); + } else if (state == content::MEDIA_REQUEST_STATE_CLOSING) { + ash::Shell::Get()->OnCastingSessionStartedOrStopped(false); + } + } +#endif + + for (auto& observer : observers_) { + observer.OnRequestUpdate(render_process_id, render_frame_id, stream_type, + state); + } +} + +void MediaCaptureDevicesDispatcher::OnCreatingAudioStreamOnUIThread( + int render_process_id, + int render_frame_id) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + for (auto& observer : observers_) + observer.OnCreatingAudioStream(render_process_id, render_frame_id); +} + +bool MediaCaptureDevicesDispatcher::IsInsecureCapturingInProgress( + int render_process_id, + int render_frame_id) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + for (const auto& handler : media_access_handlers_) { + if (handler->IsInsecureCapturingInProgress(render_process_id, + render_frame_id)) + return true; + } + return false; +} + +void MediaCaptureDevicesDispatcher::SetTestAudioCaptureDevices( + const MediaStreamDevices& devices) { + test_audio_devices_ = devices; +} + +void MediaCaptureDevicesDispatcher::SetTestVideoCaptureDevices( + const MediaStreamDevices& devices) { + test_video_devices_ = devices; +} + +void MediaCaptureDevicesDispatcher::OnSetCapturingLinkSecured( + int render_process_id, + int render_frame_id, + int page_request_id, + blink::mojom::MediaStreamType stream_type, + bool is_secure) { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + + if (!blink::IsVideoScreenCaptureMediaType(stream_type)) + return; + + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce( + &MediaCaptureDevicesDispatcher::UpdateVideoScreenCaptureStatus, + base::Unretained(this), render_process_id, render_frame_id, + page_request_id, stream_type, is_secure)); +} + +void MediaCaptureDevicesDispatcher::UpdateVideoScreenCaptureStatus( + int render_process_id, + int render_frame_id, + int page_request_id, + blink::mojom::MediaStreamType stream_type, + bool is_secure) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(blink::IsVideoScreenCaptureMediaType(stream_type)); + + for (const auto& handler : media_access_handlers_) { + handler->UpdateVideoScreenCaptureStatus(render_process_id, render_frame_id, + page_request_id, is_secure); + break; + } +} diff --git a/chromium/chrome/browser/media/webrtc/media_capture_devices_dispatcher.h b/chromium/chrome/browser/media/webrtc/media_capture_devices_dispatcher.h new file mode 100644 index 00000000000..51f9d9db2b3 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_capture_devices_dispatcher.h @@ -0,0 +1,209 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_CAPTURE_DEVICES_DISPATCHER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_CAPTURE_DEVICES_DISPATCHER_H_ + +#include <list> +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/singleton.h" +#include "base/observer_list.h" +#include "content/public/browser/media_observer.h" +#include "content/public/browser/media_stream_request.h" +#include "content/public/browser/web_contents_delegate.h" +#include "third_party/blink/public/common/mediastream/media_stream_request.h" + +class MediaAccessHandler; +class MediaStreamCaptureIndicator; +class Profile; + +namespace extensions { +class Extension; +} + +namespace user_prefs { +class PrefRegistrySyncable; +} + +// This singleton is used to receive updates about media events from the content +// layer. +class MediaCaptureDevicesDispatcher : public content::MediaObserver { + public: + class Observer { + public: + // Handle an information update consisting of a up-to-date audio capture + // device lists. This happens when a microphone is plugged in or unplugged. + virtual void OnUpdateAudioDevices( + const blink::MediaStreamDevices& devices) {} + + // Handle an information update consisting of a up-to-date video capture + // device lists. This happens when a camera is plugged in or unplugged. + virtual void OnUpdateVideoDevices( + const blink::MediaStreamDevices& devices) {} + + // Handle an information update related to a media stream request. + virtual void OnRequestUpdate(int render_process_id, + int render_frame_id, + blink::mojom::MediaStreamType stream_type, + const content::MediaRequestState state) {} + + // Handle an information update that a new stream is being created. + virtual void OnCreatingAudioStream(int render_process_id, + int render_frame_id) {} + + virtual ~Observer() {} + }; + + static MediaCaptureDevicesDispatcher* GetInstance(); + + // Registers the preferences related to Media Stream default devices. + static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + + // Returns true if the security origin is associated with casting. + static bool IsOriginForCasting(const GURL& origin); + + // Methods for observers. Called on UI thread. + // Observers should add themselves on construction and remove themselves + // on destruction. + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + const blink::MediaStreamDevices& GetAudioCaptureDevices(); + const blink::MediaStreamDevices& GetVideoCaptureDevices(); + + // Method called from WebCapturerDelegate implementations to process access + // requests. |extension| is set to NULL if request was made from a drive-by + // page. + void ProcessMediaAccessRequest(content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension); + + // Method called from WebCapturerDelegate implementations to check media + // access permission. Note that this does not query the user. + bool CheckMediaAccessPermission(content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type); + + // Same as above but for an |extension|, which may not be NULL. + bool CheckMediaAccessPermission(content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type, + const extensions::Extension* extension); + + // Helper to get the default devices which can be used by the media request. + // Uses the first available devices if the default devices are not available. + // If the return list is empty, it means there is no available device on the + // OS. + // Called on the UI thread. + void GetDefaultDevicesForProfile(Profile* profile, + bool audio, + bool video, + blink::MediaStreamDevices* devices); + + // Helper to get default device IDs. If the returned value is an empty string, + // it means that there is no default device for the given device |type|. The + // only supported |type| values are + // blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE and + // blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE. + // Must be called on the UI thread. + std::string GetDefaultDeviceIDForProfile(Profile* profile, + blink::mojom::MediaStreamType type); + + // Helpers for picking particular requested devices, identified by raw id. + // If the device requested is not available it will return NULL. + const blink::MediaStreamDevice* GetRequestedAudioDevice( + const std::string& requested_audio_device_id); + const blink::MediaStreamDevice* GetRequestedVideoDevice( + const std::string& requested_video_device_id); + + // Returns the first available audio or video device, or NULL if no devices + // are available. + const blink::MediaStreamDevice* GetFirstAvailableAudioDevice(); + const blink::MediaStreamDevice* GetFirstAvailableVideoDevice(); + + // Unittests that do not require actual device enumeration should call this + // API on the singleton. It is safe to call this multiple times on the + // signleton. + void DisableDeviceEnumerationForTesting(); + + // Overridden from content::MediaObserver: + void OnAudioCaptureDevicesChanged() override; + void OnVideoCaptureDevicesChanged() override; + void OnMediaRequestStateChanged(int render_process_id, + int render_frame_id, + int page_request_id, + const GURL& security_origin, + blink::mojom::MediaStreamType stream_type, + content::MediaRequestState state) override; + void OnCreatingAudioStream(int render_process_id, + int render_frame_id) override; + void OnSetCapturingLinkSecured(int render_process_id, + int render_frame_id, + int page_request_id, + blink::mojom::MediaStreamType stream_type, + bool is_secure) override; + + scoped_refptr<MediaStreamCaptureIndicator> GetMediaStreamCaptureIndicator(); + + // Return true if there is any ongoing insecured capturing. The capturing is + // deemed secure if all connected video sinks are reported secure and the + // extension is trusted. + bool IsInsecureCapturingInProgress(int render_process_id, + int render_frame_id); + + // Only for testing. + void SetTestAudioCaptureDevices(const blink::MediaStreamDevices& devices); + void SetTestVideoCaptureDevices(const blink::MediaStreamDevices& devices); + + private: + friend struct base::DefaultSingletonTraits<MediaCaptureDevicesDispatcher>; + + MediaCaptureDevicesDispatcher(); + ~MediaCaptureDevicesDispatcher() override; + + // Called by the MediaObserver() functions, executed on UI thread. + void NotifyAudioDevicesChangedOnUIThread(); + void NotifyVideoDevicesChangedOnUIThread(); + void UpdateMediaRequestStateOnUIThread( + int render_process_id, + int render_frame_id, + int page_request_id, + const GURL& security_origin, + blink::mojom::MediaStreamType stream_type, + content::MediaRequestState state); + void OnCreatingAudioStreamOnUIThread(int render_process_id, + int render_frame_id); + void UpdateVideoScreenCaptureStatus(int render_process_id, + int render_frame_id, + int page_request_id, + blink::mojom::MediaStreamType stream_type, + bool is_secure); + + // Only for testing, a list of cached audio capture devices. + blink::MediaStreamDevices test_audio_devices_; + + // Only for testing, a list of cached video capture devices. + blink::MediaStreamDevices test_video_devices_; + + // A list of observers for the device update notifications. + base::ObserverList<Observer>::Unchecked observers_; + + // Flag used by unittests to disable device enumeration. + bool is_device_enumeration_disabled_; + + scoped_refptr<MediaStreamCaptureIndicator> media_stream_capture_indicator_; + + // Handlers for processing media access requests. + std::vector<std::unique_ptr<MediaAccessHandler>> media_access_handlers_; + + DISALLOW_COPY_AND_ASSIGN(MediaCaptureDevicesDispatcher); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_CAPTURE_DEVICES_DISPATCHER_H_ diff --git a/chromium/chrome/browser/media/webrtc/media_stream_capture_indicator.cc b/chromium/chrome/browser/media/webrtc/media_stream_capture_indicator.cc new file mode 100644 index 00000000000..8cda295cb72 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_stream_capture_indicator.cc @@ -0,0 +1,470 @@ +// 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 "chrome/browser/media/webrtc/media_stream_capture_indicator.h" + +#include <stddef.h> + +#include <memory> +#include <string> +#include <utility> + +#include "base/logging.h" +#include "base/macros.h" +#include "build/build_config.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/content_settings/chrome_content_settings_utils.h" +#include "chrome/browser/status_icons/status_icon.h" +#include "chrome/browser/status_icons/status_tray.h" +#include "chrome/browser/tab_contents/tab_util.h" +#include "components/url_formatter/elide_url.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/content_browser_client.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_contents_delegate.h" +#include "content/public/browser/web_contents_observer.h" +#include "extensions/buildflags/buildflags.h" +#include "ui/gfx/image/image_skia.h" + +#if !defined(OS_ANDROID) +#include "chrome/grit/chromium_strings.h" +#include "components/vector_icons/vector_icons.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/gfx/color_palette.h" +#include "ui/gfx/paint_vector_icon.h" +#endif + +#if BUILDFLAG(ENABLE_EXTENSIONS) +#include "base/strings/utf_string_conversions.h" +#include "chrome/common/extensions/extension_constants.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/common/extension.h" +#endif + +using content::BrowserThread; +using content::WebContents; + +namespace { + +#if BUILDFLAG(ENABLE_EXTENSIONS) +const extensions::Extension* GetExtension(WebContents* web_contents) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (!web_contents) + return NULL; + + extensions::ExtensionRegistry* registry = + extensions::ExtensionRegistry::Get(web_contents->GetBrowserContext()); + return registry->enabled_extensions().GetExtensionOrAppByURL( + web_contents->GetURL()); +} + +#endif // BUILDFLAG(ENABLE_EXTENSIONS) + +base::string16 GetTitle(WebContents* web_contents) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (!web_contents) + return base::string16(); + +#if BUILDFLAG(ENABLE_EXTENSIONS) + const extensions::Extension* const extension = GetExtension(web_contents); + if (extension) + return base::UTF8ToUTF16(extension->name()); +#endif + + return url_formatter::FormatUrlForSecurityDisplay(web_contents->GetURL()); +} + +} // namespace + +// Stores usage counts for all the capture devices associated with a single +// WebContents instance. Instances of this class are owned by +// MediaStreamCaptureIndicator. They also observe for the destruction of their +// corresponding WebContents and trigger their own deletion from their +// MediaStreamCaptureIndicator. +class MediaStreamCaptureIndicator::WebContentsDeviceUsage + : public content::WebContentsObserver { + public: + WebContentsDeviceUsage(scoped_refptr<MediaStreamCaptureIndicator> indicator, + WebContents* web_contents) + : WebContentsObserver(web_contents), indicator_(std::move(indicator)) {} + + bool IsCapturingAudio() const { return audio_stream_count_ > 0; } + bool IsCapturingVideo() const { return video_stream_count_ > 0; } + bool IsMirroring() const { return mirroring_stream_count_ > 0; } + bool IsCapturingDesktop() const { return desktop_stream_count_ > 0; } + + std::unique_ptr<content::MediaStreamUI> RegisterMediaStream( + const blink::MediaStreamDevices& devices, + std::unique_ptr<MediaStreamUI> ui); + + // Increment ref-counts up based on the type of each device provided. + void AddDevices(const blink::MediaStreamDevices& devices, + base::OnceClosure stop_callback); + + // Decrement ref-counts up based on the type of each device provided. + void RemoveDevices(const blink::MediaStreamDevices& devices); + + // Helper to call |stop_callback_|. + void NotifyStopped(); + + private: + int& GetStreamCount(blink::mojom::MediaStreamType type); + + // content::WebContentsObserver overrides. + void WebContentsDestroyed() override { + indicator_->UnregisterWebContents(web_contents()); + } + + scoped_refptr<MediaStreamCaptureIndicator> indicator_; + int audio_stream_count_ = 0; + int video_stream_count_ = 0; + int mirroring_stream_count_ = 0; + int desktop_stream_count_ = 0; + + base::OnceClosure stop_callback_; + base::WeakPtrFactory<WebContentsDeviceUsage> weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(WebContentsDeviceUsage); +}; + +// Implements MediaStreamUI interface. Instances of this class are created for +// each MediaStream and their ownership is passed to MediaStream implementation +// in the content layer. Each UIDelegate keeps a weak pointer to the +// corresponding WebContentsDeviceUsage object to deliver updates about state of +// the stream. +class MediaStreamCaptureIndicator::UIDelegate : public content::MediaStreamUI { + public: + UIDelegate(base::WeakPtr<WebContentsDeviceUsage> device_usage, + const blink::MediaStreamDevices& devices, + std::unique_ptr<::MediaStreamUI> ui) + : device_usage_(device_usage), devices_(devices), ui_(std::move(ui)) { + DCHECK(!devices_.empty()); + } + + ~UIDelegate() override { + if (started_ && device_usage_) + device_usage_->RemoveDevices(devices_); + } + + private: + // content::MediaStreamUI interface. + gfx::NativeViewId OnStarted( + base::OnceClosure stop_callback, + content::MediaStreamUI::SourceCallback source_callback) override { + DCHECK(!started_); + started_ = true; + + if (device_usage_) { + // |device_usage_| handles |stop_callback| when |ui_| is unspecified. + device_usage_->AddDevices( + devices_, ui_ ? base::OnceClosure() : std::move(stop_callback)); + } + + // If a custom |ui_| is specified, notify it that the stream started and let + // it handle the |stop_callback| and |source_callback|. + if (ui_) + return ui_->OnStarted(std::move(stop_callback), + std::move(source_callback)); + + return 0; + } + + base::WeakPtr<WebContentsDeviceUsage> device_usage_; + const blink::MediaStreamDevices devices_; + const std::unique_ptr<::MediaStreamUI> ui_; + bool started_ = false; + + DISALLOW_COPY_AND_ASSIGN(UIDelegate); +}; + +std::unique_ptr<content::MediaStreamUI> +MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream( + const blink::MediaStreamDevices& devices, + std::unique_ptr<MediaStreamUI> ui) { + return std::make_unique<UIDelegate>(weak_factory_.GetWeakPtr(), devices, + std::move(ui)); +} + +void MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices( + const blink::MediaStreamDevices& devices, + base::OnceClosure stop_callback) { + for (const auto& device : devices) + ++GetStreamCount(device.type); + + if (web_contents()) { + stop_callback_ = std::move(stop_callback); + web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB); + } + + indicator_->UpdateNotificationUserInterface(); +} + +void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices( + const blink::MediaStreamDevices& devices) { + for (const auto& device : devices) { + int& stream_count = GetStreamCount(device.type); + --stream_count; + DCHECK_GE(stream_count, 0); + } + + if (web_contents()) { + web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB); + content_settings::UpdateLocationBarUiForWebContents(web_contents()); + } + + indicator_->UpdateNotificationUserInterface(); +} + +void MediaStreamCaptureIndicator::WebContentsDeviceUsage::NotifyStopped() { + if (stop_callback_) + std::move(stop_callback_).Run(); +} + +int& MediaStreamCaptureIndicator::WebContentsDeviceUsage::GetStreamCount( + blink::mojom::MediaStreamType type) { + switch (type) { + case blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE: + return audio_stream_count_; + + case blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE: + return video_stream_count_; + + case blink::mojom::MediaStreamType::GUM_TAB_AUDIO_CAPTURE: + case blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE: + return mirroring_stream_count_; + + case blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE: + case blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE: + case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE: + case blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE: + return desktop_stream_count_; + + case blink::mojom::MediaStreamType::NO_SERVICE: + case blink::mojom::MediaStreamType::NUM_MEDIA_TYPES: + NOTREACHED(); + return video_stream_count_; + } +} + +MediaStreamCaptureIndicator::MediaStreamCaptureIndicator() {} + +MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() { + // The user is responsible for cleaning up by reporting the closure of any + // opened devices. However, there exists a race condition at shutdown: The UI + // thread may be stopped before CaptureDevicesClosed() posts the task to + // invoke DoDevicesClosedOnUIThread(). In this case, usage_map_ won't be + // empty like it should. + DCHECK(usage_map_.empty() || + !BrowserThread::IsThreadInitialized(BrowserThread::UI)); +} + +std::unique_ptr<content::MediaStreamUI> +MediaStreamCaptureIndicator::RegisterMediaStream( + content::WebContents* web_contents, + const blink::MediaStreamDevices& devices, + std::unique_ptr<MediaStreamUI> ui) { + auto& usage = usage_map_[web_contents]; + if (!usage) + usage = std::make_unique<WebContentsDeviceUsage>(this, web_contents); + + return usage->RegisterMediaStream(devices, std::move(ui)); +} + +void MediaStreamCaptureIndicator::ExecuteCommand(int command_id, + int event_flags) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + const int index = + command_id - IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST; + DCHECK_LE(0, index); + DCHECK_GT(static_cast<int>(command_targets_.size()), index); + WebContents* web_contents = command_targets_[index]; + if (base::Contains(usage_map_, web_contents)) + web_contents->GetDelegate()->ActivateContents(web_contents); +} + +bool MediaStreamCaptureIndicator::IsCapturingUserMedia( + content::WebContents* web_contents) const { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + auto it = usage_map_.find(web_contents); + return it != usage_map_.end() && + (it->second->IsCapturingAudio() || it->second->IsCapturingVideo()); +} + +bool MediaStreamCaptureIndicator::IsCapturingVideo( + content::WebContents* web_contents) const { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + auto it = usage_map_.find(web_contents); + return it != usage_map_.end() && it->second->IsCapturingVideo(); +} + +bool MediaStreamCaptureIndicator::IsCapturingAudio( + content::WebContents* web_contents) const { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + auto it = usage_map_.find(web_contents); + return it != usage_map_.end() && it->second->IsCapturingAudio(); +} + +bool MediaStreamCaptureIndicator::IsBeingMirrored( + content::WebContents* web_contents) const { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + auto it = usage_map_.find(web_contents); + return it != usage_map_.end() && it->second->IsMirroring(); +} + +bool MediaStreamCaptureIndicator::IsCapturingDesktop( + content::WebContents* web_contents) const { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + auto it = usage_map_.find(web_contents); + return it != usage_map_.end() && it->second->IsCapturingDesktop(); +} + +void MediaStreamCaptureIndicator::NotifyStopped( + content::WebContents* web_contents) const { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + auto it = usage_map_.find(web_contents); + DCHECK(it != usage_map_.end()); + it->second->NotifyStopped(); +} + +void MediaStreamCaptureIndicator::UnregisterWebContents( + WebContents* web_contents) { + usage_map_.erase(web_contents); + UpdateNotificationUserInterface(); +} + +void MediaStreamCaptureIndicator::MaybeCreateStatusTrayIcon(bool audio, + bool video) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (status_icon_) + return; + + // If there is no browser process, we should not create the status tray. + if (!g_browser_process) + return; + + StatusTray* status_tray = g_browser_process->status_tray(); + if (!status_tray) + return; + + gfx::ImageSkia image; + base::string16 tool_tip; + GetStatusTrayIconInfo(audio, video, &image, &tool_tip); + DCHECK(!image.isNull()); + DCHECK(!tool_tip.empty()); + + status_icon_ = status_tray->CreateStatusIcon( + StatusTray::MEDIA_STREAM_CAPTURE_ICON, image, tool_tip); +} + +void MediaStreamCaptureIndicator::MaybeDestroyStatusTrayIcon() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (!status_icon_) + return; + + // If there is no browser process, we should not do anything. + if (!g_browser_process) + return; + + StatusTray* status_tray = g_browser_process->status_tray(); + if (status_tray != NULL) { + status_tray->RemoveStatusIcon(status_icon_); + status_icon_ = NULL; + } +} + +void MediaStreamCaptureIndicator::UpdateNotificationUserInterface() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + std::unique_ptr<StatusIconMenuModel> menu(new StatusIconMenuModel(this)); + bool audio = false; + bool video = false; + int command_id = IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST; + command_targets_.clear(); + + for (const auto& it : usage_map_) { + // Check if any audio and video devices have been used. + const WebContentsDeviceUsage& usage = *it.second; + if (!usage.IsCapturingAudio() && !usage.IsCapturingVideo()) + continue; + + WebContents* const web_contents = it.first; + + // The audio/video icon is shown only for non-whitelisted extensions or on + // Android. For regular tabs on desktop, we show an indicator in the tab + // icon. +#if BUILDFLAG(ENABLE_EXTENSIONS) + const extensions::Extension* extension = GetExtension(web_contents); + if (!extension) + continue; +#endif + + audio = audio || usage.IsCapturingAudio(); + video = video || usage.IsCapturingVideo(); + + command_targets_.push_back(web_contents); + menu->AddItem(command_id, GetTitle(web_contents)); + + // If the menu item is not a label, enable it. + menu->SetCommandIdEnabled(command_id, command_id != IDC_MinimumLabelValue); + + // If reaching the maximum number, no more item will be added to the menu. + if (command_id == IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST) + break; + ++command_id; + } + + if (command_targets_.empty()) { + MaybeDestroyStatusTrayIcon(); + return; + } + + // The icon will take the ownership of the passed context menu. + MaybeCreateStatusTrayIcon(audio, video); + if (status_icon_) { + status_icon_->SetContextMenu(std::move(menu)); + } +} + +void MediaStreamCaptureIndicator::GetStatusTrayIconInfo( + bool audio, + bool video, + gfx::ImageSkia* image, + base::string16* tool_tip) { +#if defined(OS_ANDROID) + NOTREACHED(); +#else // !defined(OS_ANDROID) + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(audio || video); + DCHECK(image); + DCHECK(tool_tip); + + int message_id = 0; + const gfx::VectorIcon* icon = nullptr; + if (audio && video) { + message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO; + icon = &vector_icons::kVideocamIcon; + } else if (audio && !video) { + message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY; + icon = &vector_icons::kMicIcon; + } else if (!audio && video) { + message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY; + icon = &vector_icons::kVideocamIcon; + } + + *tool_tip = l10n_util::GetStringUTF16(message_id); + *image = gfx::CreateVectorIcon(*icon, 16, gfx::kChromeIconGrey); +#endif // !defined(OS_ANDROID) +} diff --git a/chromium/chrome/browser/media/webrtc/media_stream_capture_indicator.h b/chromium/chrome/browser/media/webrtc/media_stream_capture_indicator.h new file mode 100644 index 00000000000..29591788429 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_stream_capture_indicator.h @@ -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. + +#ifndef CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_CAPTURE_INDICATOR_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_CAPTURE_INDICATOR_H_ + +#include <unordered_map> +#include <vector> + +#include "base/callback_forward.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "chrome/browser/status_icons/status_icon_menu_model.h" +#include "content/public/browser/media_stream_request.h" +#include "third_party/blink/public/common/mediastream/media_stream_request.h" +#include "ui/gfx/native_widget_types.h" + +namespace content { +class WebContents; +} // namespace content + +namespace gfx { +class ImageSkia; +} // namespace gfx + +class StatusIcon; + +// Interface to display custom UI during stream capture. +class MediaStreamUI { + public: + // Called when stream capture is stopped. + virtual ~MediaStreamUI() = default; + + // Called when stream capture starts. + // |stop_callback| is a callback to stop the stream. + // |source_callback| is a callback to change the desktop capture source. + // Returns the platform-dependent window ID for the UI, or 0 if not + // applicable. + virtual gfx::NativeViewId OnStarted( + base::OnceClosure stop_callback, + content::MediaStreamUI::SourceCallback source_callback) = 0; +}; + +// Keeps track of which WebContents are capturing media streams. Used to display +// indicators (e.g. in the tab strip, via notifications) and to make resource +// allocation decisions (e.g. WebContents capturing streams are not discarded). +// +// Owned by MediaCaptureDevicesDispatcher, which is a singleton. +class MediaStreamCaptureIndicator + : public base::RefCountedThreadSafe<MediaStreamCaptureIndicator>, + public StatusIconMenuModel::Delegate { + public: + MediaStreamCaptureIndicator(); + + // Registers a new media stream for |web_contents| and returns an object used + // by the content layer to notify about the state of the stream. Optionally, + // |ui| is used to display custom UI while the stream is captured. + std::unique_ptr<content::MediaStreamUI> RegisterMediaStream( + content::WebContents* web_contents, + const blink::MediaStreamDevices& devices, + std::unique_ptr<MediaStreamUI> ui = nullptr); + + // Overrides from StatusIconMenuModel::Delegate implementation. + void ExecuteCommand(int command_id, int event_flags) override; + + // Returns true if |web_contents| is capturing user media (e.g., webcam or + // microphone input). + bool IsCapturingUserMedia(content::WebContents* web_contents) const; + + // Returns true if |web_contents| is capturing video (e.g., webcam). + bool IsCapturingVideo(content::WebContents* web_contents) const; + + // Returns true if |web_contents| is capturing audio (e.g., microphone). + bool IsCapturingAudio(content::WebContents* web_contents) const; + + // Returns true if |web_contents| itself is being mirrored (e.g., a source of + // media for remote broadcast). + bool IsBeingMirrored(content::WebContents* web_contents) const; + + // Returns true if |web_contents| is capturing the desktop (screen, window, + // audio). + bool IsCapturingDesktop(content::WebContents* web_contents) const; + + // Called when STOP button in media capture notification is clicked. + void NotifyStopped(content::WebContents* web_contents) const; + + private: + class UIDelegate; + class WebContentsDeviceUsage; + friend class WebContentsDeviceUsage; + + friend class base::RefCountedThreadSafe<MediaStreamCaptureIndicator>; + ~MediaStreamCaptureIndicator() override; + + // Following functions/variables are executed/accessed only on UI thread. + + // Called by WebContentsDeviceUsage when it's about to destroy itself, i.e. + // when WebContents is being destroyed. + void UnregisterWebContents(content::WebContents* web_contents); + + // Updates the status tray menu. Called by WebContentsDeviceUsage. + void UpdateNotificationUserInterface(); + + // Helpers to create and destroy status tray icon. Called from + // UpdateNotificationUserInterface(). + void EnsureStatusTrayIconResources(); + void MaybeCreateStatusTrayIcon(bool audio, bool video); + void MaybeDestroyStatusTrayIcon(); + + // Gets the status icon image and the string to use as the tooltip. + void GetStatusTrayIconInfo(bool audio, + bool video, + gfx::ImageSkia* image, + base::string16* tool_tip); + + // Reference to our status icon - owned by the StatusTray. If null, + // the platform doesn't support status icons. + StatusIcon* status_icon_ = nullptr; + + // A map that contains the usage counts of the opened capture devices for each + // WebContents instance. + std::unordered_map<content::WebContents*, + std::unique_ptr<WebContentsDeviceUsage>> + usage_map_; + + // A vector which maps command IDs to their associated WebContents + // instance. This is rebuilt each time the status tray icon context menu is + // updated. + typedef std::vector<content::WebContents*> CommandTargets; + CommandTargets command_targets_; + + DISALLOW_COPY_AND_ASSIGN(MediaStreamCaptureIndicator); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_CAPTURE_INDICATOR_H_ diff --git a/chromium/chrome/browser/media/webrtc/media_stream_device_permission_context.cc b/chromium/chrome/browser/media/webrtc/media_stream_device_permission_context.cc new file mode 100644 index 00000000000..b64eea23ca6 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_stream_device_permission_context.cc @@ -0,0 +1,101 @@ +// Copyright 2015 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 "chrome/browser/media/webrtc/media_stream_device_permission_context.h" +#include "chrome/browser/media/webrtc/media_stream_device_permissions.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/pref_names.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings.h" +#include "content/public/common/content_features.h" +#include "content/public/common/url_constants.h" +#include "extensions/common/constants.h" + +namespace { + +blink::mojom::FeaturePolicyFeature GetFeaturePolicyFeature( + ContentSettingsType type) { + if (type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC) + return blink::mojom::FeaturePolicyFeature::kMicrophone; + + DCHECK_EQ(CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, type); + return blink::mojom::FeaturePolicyFeature::kCamera; +} + +} // namespace + +MediaStreamDevicePermissionContext::MediaStreamDevicePermissionContext( + Profile* profile, + const ContentSettingsType content_settings_type) + : PermissionContextBase(profile, + content_settings_type, + GetFeaturePolicyFeature(content_settings_type)), + content_settings_type_(content_settings_type) { + DCHECK(content_settings_type_ == CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC || + content_settings_type_ == CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA); +} + +MediaStreamDevicePermissionContext::~MediaStreamDevicePermissionContext() {} + +void MediaStreamDevicePermissionContext::DecidePermission( + content::WebContents* web_contents, + const PermissionRequestID& id, + const GURL& requesting_origin, + const GURL& embedding_origin, + bool user_gesture, + BrowserPermissionCallback callback) { + PermissionContextBase::DecidePermission(web_contents, id, requesting_origin, + embedding_origin, user_gesture, + std::move(callback)); +} + +ContentSetting MediaStreamDevicePermissionContext::GetPermissionStatusInternal( + content::RenderFrameHost* render_frame_host, + const GURL& requesting_origin, + const GURL& embedding_origin) const { + // TODO(raymes): Merge this policy check into content settings + // crbug.com/244389. + const char* policy_name = nullptr; + const char* urls_policy_name = nullptr; + if (content_settings_type_ == CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC) { + policy_name = prefs::kAudioCaptureAllowed; + urls_policy_name = prefs::kAudioCaptureAllowedUrls; + } else { + DCHECK(content_settings_type_ == CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA); + policy_name = prefs::kVideoCaptureAllowed; + urls_policy_name = prefs::kVideoCaptureAllowedUrls; + } + + MediaStreamDevicePolicy policy = GetDevicePolicy( + profile(), requesting_origin, policy_name, urls_policy_name); + + switch (policy) { + case ALWAYS_DENY: + return CONTENT_SETTING_BLOCK; + case ALWAYS_ALLOW: + return CONTENT_SETTING_ALLOW; + default: + DCHECK_EQ(POLICY_NOT_SET, policy); + } + + // Check the content setting. TODO(raymes): currently mic/camera permission + // doesn't consider the embedder. + ContentSetting setting = PermissionContextBase::GetPermissionStatusInternal( + render_frame_host, requesting_origin, requesting_origin); + + if (setting == CONTENT_SETTING_DEFAULT) + setting = CONTENT_SETTING_ASK; + + return setting; +} + +void MediaStreamDevicePermissionContext::ResetPermission( + const GURL& requesting_origin, + const GURL& embedding_origin) { + NOTREACHED() << "ResetPermission is not implemented"; +} + +bool MediaStreamDevicePermissionContext::IsRestrictedToSecureOrigins() const { + return true; +} diff --git a/chromium/chrome/browser/media/webrtc/media_stream_device_permission_context.h b/chromium/chrome/browser/media/webrtc/media_stream_device_permission_context.h new file mode 100644 index 00000000000..9a1983462d9 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_stream_device_permission_context.h @@ -0,0 +1,46 @@ +// Copyright 2015 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 CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_DEVICE_PERMISSION_CONTEXT_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_DEVICE_PERMISSION_CONTEXT_H_ + +#include "base/macros.h" +#include "chrome/browser/permissions/permission_context_base.h" +#include "components/content_settings/core/common/content_settings_types.h" + +// Common class which handles the mic and camera permissions. +class MediaStreamDevicePermissionContext : public PermissionContextBase { + public: + MediaStreamDevicePermissionContext(Profile* profile, + ContentSettingsType content_settings_type); + ~MediaStreamDevicePermissionContext() override; + + // PermissionContextBase: + void DecidePermission(content::WebContents* web_contents, + const PermissionRequestID& id, + const GURL& requesting_origin, + const GURL& embedding_origin, + bool user_gesture, + BrowserPermissionCallback callback) override; + + // TODO(xhwang): GURL.GetOrigin() shouldn't be used as the origin. Need to + // refactor to use url::Origin. crbug.com/527149 is filed for this. + ContentSetting GetPermissionStatusInternal( + content::RenderFrameHost* render_frame_host, + const GURL& requesting_origin, + const GURL& embedding_origin) const override; + + void ResetPermission(const GURL& requesting_origin, + const GURL& embedding_origin) override; + + private: + // PermissionContextBase: + bool IsRestrictedToSecureOrigins() const override; + + ContentSettingsType content_settings_type_; + + DISALLOW_COPY_AND_ASSIGN(MediaStreamDevicePermissionContext); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_DEVICE_PERMISSION_CONTEXT_H_ diff --git a/chromium/chrome/browser/media/webrtc/media_stream_device_permission_context_unittest.cc b/chromium/chrome/browser/media/webrtc/media_stream_device_permission_context_unittest.cc new file mode 100644 index 00000000000..7dbaaf09cbb --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_stream_device_permission_context_unittest.cc @@ -0,0 +1,136 @@ +// Copyright 2015 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 "chrome/browser/media/webrtc/media_stream_device_permission_context.h" + +#include "base/bind.h" +#include "base/macros.h" +#include "base/test/scoped_feature_list.h" +#include "build/build_config.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/infobars/infobar_service.h" +#include "chrome/browser/permissions/permission_request_id.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "chrome/test/base/testing_profile.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings.h" +#include "components/content_settings/core/common/content_settings_types.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_features.h" +#include "content/public/test/mock_render_process_host.h" +#include "content/public/test/web_contents_tester.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if !defined(OS_ANDROID) +#include "chrome/browser/permissions/permission_request_manager.h" +#endif + +namespace { +class TestPermissionContext : public MediaStreamDevicePermissionContext { + public: + TestPermissionContext(Profile* profile, + const ContentSettingsType content_settings_type) + : MediaStreamDevicePermissionContext(profile, content_settings_type) {} + + ~TestPermissionContext() override {} +}; + +} // anonymous namespace + +// TODO(raymes): many tests in MediaStreamDevicesControllerTest should be +// converted to tests in this file. +class MediaStreamDevicePermissionContextTests + : public ChromeRenderViewHostTestHarness { + protected: + MediaStreamDevicePermissionContextTests() = default; + + void TestInsecureQueryingUrl(ContentSettingsType content_settings_type) { + TestPermissionContext permission_context(profile(), content_settings_type); + GURL insecure_url("http://www.example.com"); + GURL secure_url("https://www.example.com"); + + // Check that there is no saved content settings. + EXPECT_EQ(CONTENT_SETTING_ASK, + HostContentSettingsMapFactory::GetForProfile(profile()) + ->GetContentSetting(insecure_url.GetOrigin(), + insecure_url.GetOrigin(), + content_settings_type, std::string())); + EXPECT_EQ(CONTENT_SETTING_ASK, + HostContentSettingsMapFactory::GetForProfile(profile()) + ->GetContentSetting(secure_url.GetOrigin(), + insecure_url.GetOrigin(), + content_settings_type, std::string())); + EXPECT_EQ(CONTENT_SETTING_ASK, + HostContentSettingsMapFactory::GetForProfile(profile()) + ->GetContentSetting(insecure_url.GetOrigin(), + secure_url.GetOrigin(), + content_settings_type, std::string())); + + EXPECT_EQ(CONTENT_SETTING_BLOCK, + permission_context + .GetPermissionStatus(nullptr /* render_frame_host */, + insecure_url, insecure_url) + .content_setting); + + EXPECT_EQ(CONTENT_SETTING_BLOCK, + permission_context + .GetPermissionStatus(nullptr /* render_frame_host */, + insecure_url, secure_url) + .content_setting); + } + + void TestSecureQueryingUrl(ContentSettingsType content_settings_type) { + TestPermissionContext permission_context(profile(), content_settings_type); + GURL secure_url("https://www.example.com"); + + // Check that there is no saved content settings. + EXPECT_EQ(CONTENT_SETTING_ASK, + HostContentSettingsMapFactory::GetForProfile(profile()) + ->GetContentSetting(secure_url.GetOrigin(), + secure_url.GetOrigin(), + content_settings_type, + std::string())); + + EXPECT_EQ(CONTENT_SETTING_ASK, + permission_context + .GetPermissionStatus(nullptr /* render_frame_host */, + secure_url, secure_url) + .content_setting); + } + + private: + // ChromeRenderViewHostTestHarness: + void SetUp() override { + ChromeRenderViewHostTestHarness::SetUp(); +#if defined(OS_ANDROID) + InfoBarService::CreateForWebContents(web_contents()); +#else + PermissionRequestManager::CreateForWebContents(web_contents()); +#endif + } + + DISALLOW_COPY_AND_ASSIGN(MediaStreamDevicePermissionContextTests); +}; + +// MEDIASTREAM_MIC permission status should be ask for insecure origin to +// accommodate the usage case of Flash. +TEST_F(MediaStreamDevicePermissionContextTests, TestMicInsecureQueryingUrl) { + TestInsecureQueryingUrl(CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC); +} + +// MEDIASTREAM_CAMERA permission status should be ask for insecure origin to +// accommodate the usage case of Flash. +TEST_F(MediaStreamDevicePermissionContextTests, TestCameraInsecureQueryingUrl) { + TestInsecureQueryingUrl(CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA); +} + +// MEDIASTREAM_MIC permission status should be ask for Secure origin. +TEST_F(MediaStreamDevicePermissionContextTests, TestMicSecureQueryingUrl) { + TestSecureQueryingUrl(CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC); +} + +// MEDIASTREAM_CAMERA permission status should be ask for Secure origin. +TEST_F(MediaStreamDevicePermissionContextTests, TestCameraSecureQueryingUrl) { + TestSecureQueryingUrl(CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA); +} diff --git a/chromium/chrome/browser/media/webrtc/media_stream_device_permissions.cc b/chromium/chrome/browser/media/webrtc/media_stream_device_permissions.cc new file mode 100644 index 00000000000..a1aaed06c81 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_stream_device_permissions.cc @@ -0,0 +1,54 @@ +// Copyright 2014 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 "chrome/browser/media/webrtc/media_stream_device_permissions.h" + +#include <stddef.h> + +#include "base/values.h" +#include "chrome/browser/profiles/profile.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings_pattern.h" +#include "components/prefs/pref_service.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/common/origin_util.h" +#include "extensions/common/constants.h" +#include "url/gurl.h" + +MediaStreamDevicePolicy GetDevicePolicy(const Profile* profile, + const GURL& security_origin, + const char* policy_name, + const char* whitelist_policy_name) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + // If the security origin policy matches a value in the whitelist, allow it. + // Otherwise, check the |policy_name| master switch for the default behavior. + + const PrefService* prefs = profile->GetPrefs(); + + const base::ListValue* list = prefs->GetList(whitelist_policy_name); + std::string value; + for (size_t i = 0; i < list->GetSize(); ++i) { + if (list->GetString(i, &value)) { + ContentSettingsPattern pattern = + ContentSettingsPattern::FromString(value); + if (pattern == ContentSettingsPattern::Wildcard()) { + DLOG(WARNING) << "Ignoring wildcard URL pattern: " << value; + continue; + } + DLOG_IF(ERROR, !pattern.IsValid()) << "Invalid URL pattern: " << value; + if (pattern.IsValid() && pattern.Matches(security_origin)) + return ALWAYS_ALLOW; + } + } + + // If a match was not found, check if audio capture is otherwise disallowed + // or if the user should be prompted. Setting the policy value to "true" + // is equal to not setting it at all, so from hereon out, we will return + // either POLICY_NOT_SET (prompt) or ALWAYS_DENY (no prompt, no access). + if (!prefs->GetBoolean(policy_name)) + return ALWAYS_DENY; + + return POLICY_NOT_SET; +} diff --git a/chromium/chrome/browser/media/webrtc/media_stream_device_permissions.h b/chromium/chrome/browser/media/webrtc/media_stream_device_permissions.h new file mode 100644 index 00000000000..5bfbb9e26a5 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_stream_device_permissions.h @@ -0,0 +1,26 @@ +// Copyright 2014 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 CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_DEVICE_PERMISSIONS_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_DEVICE_PERMISSIONS_H_ + +#include "components/content_settings/core/common/content_settings.h" +#include "components/content_settings/core/common/content_settings_types.h" + +class GURL; +class Profile; + +enum MediaStreamDevicePolicy { + POLICY_NOT_SET, + ALWAYS_DENY, + ALWAYS_ALLOW, +}; + +// Get the device policy for |security_origin| and |profile|. +MediaStreamDevicePolicy GetDevicePolicy(const Profile* profile, + const GURL& security_origin, + const char* policy_name, + const char* whitelist_policy_name); + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_DEVICE_PERMISSIONS_H_ diff --git a/chromium/chrome/browser/media/webrtc/media_stream_devices_controller.cc b/chromium/chrome/browser/media/webrtc/media_stream_devices_controller.cc new file mode 100644 index 00000000000..cddfd5b1b4d --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_stream_devices_controller.cc @@ -0,0 +1,605 @@ +// 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 "chrome/browser/media/webrtc/media_stream_devices_controller.h" + +#include <algorithm> +#include <memory> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/utf_string_conversions.h" +#include "base/values.h" +#include "chrome/browser/content_settings/tab_specific_content_settings.h" +#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" +#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h" +#include "chrome/browser/media/webrtc/media_stream_device_permissions.h" +#include "chrome/browser/permissions/permission_manager.h" +#include "chrome/browser/permissions/permission_request_manager.h" +#include "chrome/browser/permissions/permission_result.h" +#include "chrome/browser/permissions/permission_uma_util.h" +#include "chrome/browser/permissions/permission_util.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/pref_names.h" +#include "components/content_settings/core/common/content_settings_pattern.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/scoped_user_pref_update.h" +#include "components/url_formatter/elide_url.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/render_widget_host_view.h" +#include "content/public/common/origin_util.h" +#include "extensions/common/constants.h" +#include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom.h" + +#if defined(OS_ANDROID) +#include "chrome/browser/android/android_theme_resources.h" +#include "chrome/browser/android/preferences/pref_service_bridge.h" +#include "chrome/browser/permissions/permission_dialog_delegate.h" +#include "chrome/browser/permissions/permission_update_infobar_delegate_android.h" +#include "ui/android/window_android.h" +#else // !defined(OS_ANDROID) +#include "components/vector_icons/vector_icons.h" +#endif + +using content::BrowserThread; + +namespace { + +// Returns true if the given ContentSettingsType is being requested in +// |request|. +bool ContentTypeIsRequested(ContentSettingsType type, + const content::MediaStreamRequest& request) { + if (type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC) + return request.audio_type == + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE; + + if (type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA) + return request.video_type == + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE; + + return false; +} + +bool HasAvailableDevices(ContentSettingsType content_type, + const std::string& device_id) { + const blink::MediaStreamDevices* devices = nullptr; + if (content_type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC) { + devices = + &MediaCaptureDevicesDispatcher::GetInstance()->GetAudioCaptureDevices(); + } else if (content_type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA) { + devices = + &MediaCaptureDevicesDispatcher::GetInstance()->GetVideoCaptureDevices(); + } else { + NOTREACHED(); + } + + // TODO(tommi): It's kind of strange to have this here since if we fail this + // test, there'll be a UI shown that indicates to the user that access to + // non-existing audio/video devices has been denied. The user won't have + // any way to change that but there will be a UI shown which indicates that + // access is blocked. + if (devices->empty()) + return false; + + // Note: we check device_id before dereferencing devices. If the requested + // device id is non-empty, then the corresponding device list must not be + // NULL. + if (!device_id.empty()) { + auto it = std::find_if(devices->begin(), devices->end(), + [device_id](const blink::MediaStreamDevice& device) { + return device.id == device_id; + }); + if (it == devices->end()) + return false; + } + + return true; +} + +} // namespace + +// static +void MediaStreamDevicesController::RequestPermissions( + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback) { + content::RenderFrameHost* rfh = content::RenderFrameHost::FromID( + request.render_process_id, request.render_frame_id); + // The RFH may have been destroyed by the time the request is processed. + if (!rfh) { + std::move(callback).Run( + blink::MediaStreamDevices(), + blink::mojom::MediaStreamRequestResult::FAILED_DUE_TO_SHUTDOWN, + std::unique_ptr<content::MediaStreamUI>()); + return; + } + content::WebContents* web_contents = + content::WebContents::FromRenderFrameHost(rfh); + std::unique_ptr<MediaStreamDevicesController> controller( + new MediaStreamDevicesController(web_contents, request, + std::move(callback))); + + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + std::vector<ContentSettingsType> content_settings_types; + + PermissionManager* permission_manager = PermissionManager::Get(profile); + bool will_prompt_for_audio = false; + bool will_prompt_for_video = false; + + if (controller->ShouldRequestAudio()) { + PermissionResult permission_status = + permission_manager->GetPermissionStatusForFrame( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC, rfh, + request.security_origin); + if (permission_status.content_setting == CONTENT_SETTING_BLOCK) { + controller->denial_reason_ = + blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED; + controller->RunCallback(permission_status.source == + PermissionStatusSource::FEATURE_POLICY); + return; + } + + content_settings_types.push_back(CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC); + will_prompt_for_audio = + permission_status.content_setting == CONTENT_SETTING_ASK; + } + if (controller->ShouldRequestVideo()) { + PermissionResult permission_status = + permission_manager->GetPermissionStatusForFrame( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, rfh, + request.security_origin); + if (permission_status.content_setting == CONTENT_SETTING_BLOCK) { + controller->denial_reason_ = + blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED; + controller->RunCallback(permission_status.source == + PermissionStatusSource::FEATURE_POLICY); + return; + } + + content_settings_types.push_back(CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA); + will_prompt_for_video = + permission_status.content_setting == CONTENT_SETTING_ASK; + } + + permission_manager->RequestPermissions( + content_settings_types, rfh, request.security_origin, + request.user_gesture, + base::Bind( + &MediaStreamDevicesController::RequestAndroidPermissionsIfNeeded, + web_contents, base::Passed(&controller), will_prompt_for_audio, + will_prompt_for_video)); +} + +void MediaStreamDevicesController::RequestAndroidPermissionsIfNeeded( + content::WebContents* web_contents, + std::unique_ptr<MediaStreamDevicesController> controller, + bool did_prompt_for_audio, + bool did_prompt_for_video, + const std::vector<ContentSetting>& responses) { +#if defined(OS_ANDROID) + // If either audio or video was previously allowed and Chrome no longer has + // the necessary permissions, show a infobar to attempt to address this + // mismatch. + std::vector<ContentSettingsType> content_settings_types; + // The audio setting will always be the first one in the vector, if it was + // requested. + // If the user was already prompted for mic (|did_prompt_for_audio| flag), we + // would have requested Android permission at that point. + if (!did_prompt_for_audio && + controller->ShouldRequestAudio() && + responses.front() == CONTENT_SETTING_ALLOW) { + content_settings_types.push_back(CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC); + } + + // If the user was already prompted for camera (|did_prompt_for_video| flag), + // we would have requested Android permission at that point. + if (!did_prompt_for_video && + controller->ShouldRequestVideo() && + responses.back() == CONTENT_SETTING_ALLOW) { + content_settings_types.push_back(CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA); + } + if (content_settings_types.empty()) { + controller->PromptAnsweredGroupedRequest(responses); + return; + } + + ShowPermissionInfoBarState show_permission_infobar_state = + PermissionUpdateInfoBarDelegate::ShouldShowPermissionInfoBar( + web_contents, content_settings_types); + switch (show_permission_infobar_state) { + case ShowPermissionInfoBarState::NO_NEED_TO_SHOW_PERMISSION_INFOBAR: + controller->PromptAnsweredGroupedRequest(responses); + return; + case ShowPermissionInfoBarState::SHOW_PERMISSION_INFOBAR: + PermissionUpdateInfoBarDelegate::Create( + web_contents, content_settings_types, + base::BindOnce(&MediaStreamDevicesController::AndroidOSPromptAnswered, + base::Passed(&controller), responses)); + return; + case ShowPermissionInfoBarState::CANNOT_SHOW_PERMISSION_INFOBAR: { + std::vector<ContentSetting> blocked_responses(responses.size(), + CONTENT_SETTING_BLOCK); + controller->PromptAnsweredGroupedRequest(blocked_responses); + return; + } + } + + NOTREACHED() << "Unknown show permission infobar state."; +#else + controller->PromptAnsweredGroupedRequest(responses); +#endif +} + +#if defined(OS_ANDROID) +// static +void MediaStreamDevicesController::AndroidOSPromptAnswered( + std::unique_ptr<MediaStreamDevicesController> controller, + std::vector<ContentSetting> responses, + bool android_prompt_granted) { + if (!android_prompt_granted) { + // Only permissions that were previously ALLOW for a site will have had + // their android permissions requested. It's only in that case that we need + // to change the setting to BLOCK to reflect that it wasn't allowed. + for (size_t i = 0; i < responses.size(); ++i) { + if (responses[i] == CONTENT_SETTING_ALLOW) + responses[i] = CONTENT_SETTING_BLOCK; + } + } + + controller->PromptAnsweredGroupedRequest(responses); +} +#endif // defined(OS_ANDROID) + +// static +void MediaStreamDevicesController::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* prefs) { + prefs->RegisterBooleanPref(prefs::kVideoCaptureAllowed, true); + prefs->RegisterBooleanPref(prefs::kAudioCaptureAllowed, true); + prefs->RegisterListPref(prefs::kVideoCaptureAllowedUrls); + prefs->RegisterListPref(prefs::kAudioCaptureAllowedUrls); +} + +MediaStreamDevicesController::~MediaStreamDevicesController() { + if (!callback_.is_null()) { + std::move(callback_).Run( + blink::MediaStreamDevices(), + blink::mojom::MediaStreamRequestResult::FAILED_DUE_TO_SHUTDOWN, + std::unique_ptr<content::MediaStreamUI>()); + } +} + +void MediaStreamDevicesController::PromptAnsweredGroupedRequest( + const std::vector<ContentSetting>& responses) { + // The audio setting will always be the first one in the vector, if it was + // requested. + bool blocked_by_feature_policy = ShouldRequestAudio() || ShouldRequestVideo(); + if (ShouldRequestAudio()) { + audio_setting_ = responses.front(); + blocked_by_feature_policy &= + audio_setting_ == CONTENT_SETTING_BLOCK && + PermissionIsBlockedForReason(CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC, + PermissionStatusSource::FEATURE_POLICY); + } + + if (ShouldRequestVideo()) { + video_setting_ = responses.back(); + blocked_by_feature_policy &= + video_setting_ == CONTENT_SETTING_BLOCK && + PermissionIsBlockedForReason(CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, + PermissionStatusSource::FEATURE_POLICY); + } + + for (ContentSetting response : responses) { + if (response == CONTENT_SETTING_BLOCK) + denial_reason_ = + blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED; + else if (response == CONTENT_SETTING_ASK) + denial_reason_ = + blink::mojom::MediaStreamRequestResult::PERMISSION_DISMISSED; + } + + RunCallback(blocked_by_feature_policy); +} + +MediaStreamDevicesController::MediaStreamDevicesController( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback) + : web_contents_(web_contents), + request_(request), + callback_(std::move(callback)) { + DCHECK(content::IsOriginSecure(request_.security_origin) || + request_.request_type == blink::MEDIA_OPEN_DEVICE_PEPPER_ONLY); + + profile_ = Profile::FromBrowserContext(web_contents->GetBrowserContext()); + content_settings_ = TabSpecificContentSettings::FromWebContents(web_contents); + + denial_reason_ = blink::mojom::MediaStreamRequestResult::OK; + audio_setting_ = GetContentSetting(CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC, + request, &denial_reason_); + video_setting_ = GetContentSetting(CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, + request, &denial_reason_); +} + +bool MediaStreamDevicesController::ShouldRequestAudio() const { + return audio_setting_ == CONTENT_SETTING_ASK; +} + +bool MediaStreamDevicesController::ShouldRequestVideo() const { + return video_setting_ == CONTENT_SETTING_ASK; +} + +blink::MediaStreamDevices MediaStreamDevicesController::GetDevices( + ContentSetting audio_setting, + ContentSetting video_setting) { + bool audio_allowed = audio_setting == CONTENT_SETTING_ALLOW; + bool video_allowed = video_setting == CONTENT_SETTING_ALLOW; + + if (!audio_allowed && !video_allowed) + return blink::MediaStreamDevices(); + + blink::MediaStreamDevices devices; + switch (request_.request_type) { + case blink::MEDIA_OPEN_DEVICE_PEPPER_ONLY: { + const blink::MediaStreamDevice* device = NULL; + // For open device request, when requested device_id is empty, pick + // the first available of the given type. If requested device_id is + // not empty, return the desired device if it's available. Otherwise, + // return no device. + if (audio_allowed && + request_.audio_type == + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE) { + DCHECK_EQ(blink::mojom::MediaStreamType::NO_SERVICE, + request_.video_type); + if (!request_.requested_audio_device_id.empty()) { + device = + MediaCaptureDevicesDispatcher::GetInstance() + ->GetRequestedAudioDevice(request_.requested_audio_device_id); + } else { + device = MediaCaptureDevicesDispatcher::GetInstance() + ->GetFirstAvailableAudioDevice(); + } + } else if (video_allowed && + request_.video_type == + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE) { + DCHECK_EQ(blink::mojom::MediaStreamType::NO_SERVICE, + request_.audio_type); + // Pepper API opens only one device at a time. + if (!request_.requested_video_device_id.empty()) { + device = + MediaCaptureDevicesDispatcher::GetInstance() + ->GetRequestedVideoDevice(request_.requested_video_device_id); + } else { + device = MediaCaptureDevicesDispatcher::GetInstance() + ->GetFirstAvailableVideoDevice(); + } + } + if (device) + devices.push_back(*device); + break; + } + case blink::MEDIA_GENERATE_STREAM: { + bool get_default_audio_device = audio_allowed; + bool get_default_video_device = video_allowed; + + // Get the exact audio or video device if an id is specified. + if (audio_allowed && !request_.requested_audio_device_id.empty()) { + const blink::MediaStreamDevice* audio_device = + MediaCaptureDevicesDispatcher::GetInstance() + ->GetRequestedAudioDevice(request_.requested_audio_device_id); + if (audio_device) { + devices.push_back(*audio_device); + get_default_audio_device = false; + } + } + if (video_allowed && !request_.requested_video_device_id.empty()) { + const blink::MediaStreamDevice* video_device = + MediaCaptureDevicesDispatcher::GetInstance() + ->GetRequestedVideoDevice(request_.requested_video_device_id); + if (video_device) { + devices.push_back(*video_device); + get_default_video_device = false; + } + } + + // If either or both audio and video devices were requested but not + // specified by id, get the default devices. + if (get_default_audio_device || get_default_video_device) { + MediaCaptureDevicesDispatcher::GetInstance() + ->GetDefaultDevicesForProfile(profile_, get_default_audio_device, + get_default_video_device, &devices); + } + break; + } + case blink::MEDIA_DEVICE_ACCESS: { + // Get the default devices for the request. + MediaCaptureDevicesDispatcher::GetInstance()->GetDefaultDevicesForProfile( + profile_, audio_allowed, video_allowed, &devices); + break; + } + case blink::MEDIA_DEVICE_UPDATE: { + NOTREACHED(); + break; + } + } // switch + + return devices; +} + +void MediaStreamDevicesController::RunCallback(bool blocked_by_feature_policy) { + CHECK(!callback_.is_null()); + + // If the kill switch is, or the request was blocked because of feature + // policy we don't update the tab context. + if (denial_reason_ != + blink::mojom::MediaStreamRequestResult::KILL_SWITCH_ON && + !blocked_by_feature_policy) { + UpdateTabSpecificContentSettings(audio_setting_, video_setting_); + } + + blink::MediaStreamDevices devices; + + // If all requested permissions are allowed then the callback should report + // success, otherwise we report |denial_reason_|. + blink::mojom::MediaStreamRequestResult request_result = + blink::mojom::MediaStreamRequestResult::OK; + if ((audio_setting_ == CONTENT_SETTING_ALLOW || + audio_setting_ == CONTENT_SETTING_DEFAULT) && + (video_setting_ == CONTENT_SETTING_ALLOW || + video_setting_ == CONTENT_SETTING_DEFAULT)) { + devices = GetDevices(audio_setting_, video_setting_); + if (devices.empty()) { + // Even if all requested permissions are allowed, if there are no devices + // at this point we still report a failure. + request_result = blink::mojom::MediaStreamRequestResult::NO_HARDWARE; + } + } else { + DCHECK_NE(blink::mojom::MediaStreamRequestResult::OK, denial_reason_); + request_result = denial_reason_; + } + + std::unique_ptr<content::MediaStreamUI> ui; + if (!devices.empty()) { + ui = MediaCaptureDevicesDispatcher::GetInstance() + ->GetMediaStreamCaptureIndicator() + ->RegisterMediaStream(web_contents_, devices); + } + std::move(callback_).Run(devices, request_result, std::move(ui)); +} + +void MediaStreamDevicesController::UpdateTabSpecificContentSettings( + ContentSetting audio_setting, + ContentSetting video_setting) const { + if (!content_settings_) + return; + + TabSpecificContentSettings::MicrophoneCameraState microphone_camera_state = + TabSpecificContentSettings::MICROPHONE_CAMERA_NOT_ACCESSED; + std::string selected_audio_device; + std::string selected_video_device; + std::string requested_audio_device = request_.requested_audio_device_id; + std::string requested_video_device = request_.requested_video_device_id; + + // TODO(raymes): Why do we use the defaults here for the selected devices? + // Shouldn't we just use the devices that were actually selected? + PrefService* prefs = Profile::FromBrowserContext( + web_contents_->GetBrowserContext())->GetPrefs(); + if (audio_setting != CONTENT_SETTING_DEFAULT) { + selected_audio_device = + requested_audio_device.empty() + ? prefs->GetString(prefs::kDefaultAudioCaptureDevice) + : requested_audio_device; + microphone_camera_state |= + TabSpecificContentSettings::MICROPHONE_ACCESSED | + (audio_setting == CONTENT_SETTING_ALLOW + ? 0 + : TabSpecificContentSettings::MICROPHONE_BLOCKED); + } + + if (video_setting != CONTENT_SETTING_DEFAULT) { + selected_video_device = + requested_video_device.empty() + ? prefs->GetString(prefs::kDefaultVideoCaptureDevice) + : requested_video_device; + microphone_camera_state |= + TabSpecificContentSettings::CAMERA_ACCESSED | + (video_setting == CONTENT_SETTING_ALLOW + ? 0 + : TabSpecificContentSettings::CAMERA_BLOCKED); + } + + content_settings_->OnMediaStreamPermissionSet( + PermissionManager::Get(profile_)->GetCanonicalOrigin( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, request_.security_origin, + web_contents_->GetLastCommittedURL()), + microphone_camera_state, selected_audio_device, selected_video_device, + requested_audio_device, requested_video_device); +} + +ContentSetting MediaStreamDevicesController::GetContentSetting( + ContentSettingsType content_type, + const content::MediaStreamRequest& request, + blink::mojom::MediaStreamRequestResult* denial_reason) const { + DCHECK(content_type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC || + content_type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA); + DCHECK(!request_.security_origin.is_empty()); + DCHECK(content::IsOriginSecure(request_.security_origin) || + request_.request_type == blink::MEDIA_OPEN_DEVICE_PEPPER_ONLY); + if (!ContentTypeIsRequested(content_type, request)) { + // No denial reason set as it will have been previously set. + return CONTENT_SETTING_DEFAULT; + } + + std::string device_id; + if (content_type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC) + device_id = request.requested_audio_device_id; + else + device_id = request.requested_video_device_id; + if (!HasAvailableDevices(content_type, device_id)) { + *denial_reason = blink::mojom::MediaStreamRequestResult::NO_HARDWARE; + return CONTENT_SETTING_BLOCK; + } + + if (!IsUserAcceptAllowed(content_type)) { + *denial_reason = blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED; + return CONTENT_SETTING_BLOCK; + } + + // Don't request if the kill switch is on. + if (PermissionIsBlockedForReason(content_type, + PermissionStatusSource::KILL_SWITCH)) { + *denial_reason = blink::mojom::MediaStreamRequestResult::KILL_SWITCH_ON; + return CONTENT_SETTING_BLOCK; + } + + return CONTENT_SETTING_ASK; +} + +bool MediaStreamDevicesController::IsUserAcceptAllowed( + ContentSettingsType content_type) const { +#if defined(OS_ANDROID) + ui::WindowAndroid* window_android = + web_contents_->GetNativeView()->GetWindowAndroid(); + if (!window_android) + return false; + + std::vector<std::string> android_permissions; + PrefServiceBridge::GetAndroidPermissionsForContentSetting( + content_type, &android_permissions); + for (const auto& android_permission : android_permissions) { + if (!window_android->HasPermission(android_permission) && + !window_android->CanRequestPermission(android_permission)) { + return false; + } + } + + // Don't approve device requests if the tab was hidden. + // TODO(qinmin): Add a test for this. http://crbug.com/396869. + // TODO(raymes): Shouldn't this apply to all permissions not just audio/video? + return web_contents_->GetRenderWidgetHostView()->IsShowing(); +#endif + return true; +} + +bool MediaStreamDevicesController::PermissionIsBlockedForReason( + ContentSettingsType content_type, + PermissionStatusSource reason) const { + // TODO(raymes): This function wouldn't be needed if + // PermissionManager::RequestPermissions returned a denial reason. + content::RenderFrameHost* rfh = content::RenderFrameHost::FromID( + request_.render_process_id, request_.render_frame_id); + PermissionResult result = + PermissionManager::Get(profile_)->GetPermissionStatusForFrame( + content_type, rfh, request_.security_origin); + if (result.source == reason) { + DCHECK_EQ(CONTENT_SETTING_BLOCK, result.content_setting); + return true; + } + return false; +} diff --git a/chromium/chrome/browser/media/webrtc/media_stream_devices_controller.h b/chromium/chrome/browser/media/webrtc/media_stream_devices_controller.h new file mode 100644 index 00000000000..4b5920a2c9b --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_stream_devices_controller.h @@ -0,0 +1,138 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_DEVICES_CONTROLLER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_DEVICES_CONTROLLER_H_ + +#include <map> +#include <string> + +#include "base/callback.h" +#include "base/macros.h" +#include "build/build_config.h" +#include "components/content_settings/core/common/content_settings.h" +#include "content/public/browser/media_stream_request.h" +#include "third_party/blink/public/common/mediastream/media_stream_request.h" +#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" + +class MediaStreamDevicesController; +class Profile; +class TabSpecificContentSettings; +enum class PermissionStatusSource; + +namespace content { +class WebContents; +} + +namespace user_prefs { +class PrefRegistrySyncable; +} + +namespace policy { +class MediaStreamDevicesControllerBrowserTest; +} + +namespace test { +class MediaStreamDevicesControllerTestApi; +} + +class MediaStreamDevicesController { + public: + static void RequestPermissions(const content::MediaStreamRequest& request, + content::MediaResponseCallback callback); + + static void RequestAndroidPermissionsIfNeeded( + content::WebContents* web_contents, + std::unique_ptr<MediaStreamDevicesController> controller, + bool did_prompt_for_audio, + bool did_prompt_for_video, + const std::vector<ContentSetting>& responses); + +#if defined(OS_ANDROID) + // Called when the Android OS-level prompt is answered. + static void AndroidOSPromptAnswered( + std::unique_ptr<MediaStreamDevicesController> controller, + std::vector<ContentSetting> responses, + bool android_prompt_granted); +#endif // defined(OS_ANDROID) + + // Registers the prefs backing the audio and video policies. + static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + + ~MediaStreamDevicesController(); + + // Called when a permission prompt is answered through the PermissionManager. + void PromptAnsweredGroupedRequest( + const std::vector<ContentSetting>& responses); + + private: + friend class MediaStreamDevicesControllerTest; + friend class test::MediaStreamDevicesControllerTestApi; + friend class policy::MediaStreamDevicesControllerBrowserTest; + + MediaStreamDevicesController(content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback); + + // Returns true if audio/video should be requested through the + // PermissionManager. We won't try to request permission if the request is + // already blocked for some other reason, e.g. there are no devices available. + bool ShouldRequestAudio() const; + bool ShouldRequestVideo() const; + + // Returns a list of devices available for the request for the given + // audio/video permission settings. + blink::MediaStreamDevices GetDevices(ContentSetting audio_setting, + ContentSetting video_setting); + + // Runs |callback_| with the current audio/video permission settings. + void RunCallback(bool blocked_by_feature_policy); + + // Called when the permission has been set to update the + // TabSpecificContentSettings. + void UpdateTabSpecificContentSettings(ContentSetting audio_setting, + ContentSetting video_setting) const; + + // Returns the content settings for the given content type and request. + ContentSetting GetContentSetting( + ContentSettingsType content_type, + const content::MediaStreamRequest& request, + blink::mojom::MediaStreamRequestResult* denial_reason) const; + + // Returns true if clicking allow on the dialog should give access to the + // requested devices. + bool IsUserAcceptAllowed(ContentSettingsType content_type) const; + + bool PermissionIsBlockedForReason(ContentSettingsType content_type, + PermissionStatusSource reason) const; + + // The current state of the audio/video content settings which may be updated + // through the lifetime of the request. + ContentSetting audio_setting_; + ContentSetting video_setting_; + blink::mojom::MediaStreamRequestResult denial_reason_; + + content::WebContents* web_contents_; + + // The owner of this class needs to make sure it does not outlive the profile. + Profile* profile_; + + // Weak pointer to the tab specific content settings of the tab for which the + // MediaStreamDevicesController was created. The tab specific content + // settings are associated with a the web contents of the tab. The + // MediaStreamDeviceController must not outlive the web contents for which it + // was created. + TabSpecificContentSettings* content_settings_; + + // The original request for access to devices. + const content::MediaStreamRequest request_; + + // The callback that needs to be Run to notify WebRTC of whether access to + // audio/video devices was granted or not. + content::MediaResponseCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(MediaStreamDevicesController); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_MEDIA_STREAM_DEVICES_CONTROLLER_H_ diff --git a/chromium/chrome/browser/media/webrtc/media_stream_devices_controller_browsertest.cc b/chromium/chrome/browser/media/webrtc/media_stream_devices_controller_browsertest.cc new file mode 100644 index 00000000000..ed67e419527 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_stream_devices_controller_browsertest.cc @@ -0,0 +1,963 @@ +// Copyright 2014 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 <string> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/metrics/field_trial.h" +#include "base/run_loop.h" +#include "base/stl_util.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/content_settings/tab_specific_content_settings.h" +#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" +#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h" +#include "chrome/browser/media/webrtc/media_stream_device_permissions.h" +#include "chrome/browser/media/webrtc/media_stream_devices_controller.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/permissions/permission_context_base.h" +#include "chrome/browser/permissions/permission_request.h" +#include "chrome/browser/permissions/permission_request_manager.h" +#include "chrome/browser/permissions/permission_util.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/permission_bubble/mock_permission_prompt_factory.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/webui_url_constants.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/prefs/pref_service.h" +#include "components/variations/variations_associated_data.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/test/browser_test_utils.h" +#include "content/public/test/mock_render_process_host.h" +#include "extensions/common/constants.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/common/mediastream/media_stream_request.h" + +class MediaStreamDevicesControllerTest : public WebRtcTestBase { + public: + MediaStreamDevicesControllerTest() + : example_audio_id_("fake_audio_dev"), + example_video_id_("fake_video_dev"), + media_stream_result_( + blink::mojom::MediaStreamRequestResult::NUM_MEDIA_REQUEST_RESULTS) { + } + + // Dummy callback for when we deny the current request directly. + void OnMediaStreamResponse(const blink::MediaStreamDevices& devices, + blink::mojom::MediaStreamRequestResult result, + std::unique_ptr<content::MediaStreamUI> ui) { + media_stream_devices_ = devices; + media_stream_result_ = result; + quit_closure_.Run(); + quit_closure_ = base::Closure(); + } + + protected: + enum DeviceType { DEVICE_TYPE_AUDIO, DEVICE_TYPE_VIDEO }; + enum Access { ACCESS_ALLOWED, ACCESS_DENIED }; + + const GURL& example_url() const { return example_url_; } + + TabSpecificContentSettings* GetContentSettings() { + return TabSpecificContentSettings::FromWebContents(GetWebContents()); + } + + const std::string& example_audio_id() const { return example_audio_id_; } + const std::string& example_video_id() const { return example_video_id_; } + + blink::mojom::MediaStreamRequestResult media_stream_result() const { + return media_stream_result_; + } + + void RequestPermissions(content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback) { + base::RunLoop run_loop; + ASSERT_TRUE(quit_closure_.is_null()); + quit_closure_ = run_loop.QuitClosure(); + MediaStreamDevicesController::RequestPermissions(request, + std::move(callback)); + run_loop.Run(); + } + + // Sets the device policy-controlled |access| for |example_url_| to be for the + // selected |device_type|. + void SetDevicePolicy(DeviceType device_type, Access access) { + PrefService* prefs = Profile::FromBrowserContext( + GetWebContents()->GetBrowserContext())->GetPrefs(); + const char* policy_name = NULL; + switch (device_type) { + case DEVICE_TYPE_AUDIO: + policy_name = prefs::kAudioCaptureAllowed; + break; + case DEVICE_TYPE_VIDEO: + policy_name = prefs::kVideoCaptureAllowed; + break; + } + prefs->SetBoolean(policy_name, access == ACCESS_ALLOWED); + } + + // Set the content settings for mic/cam. + void SetContentSettings(ContentSetting mic_setting, + ContentSetting cam_setting) { + HostContentSettingsMap* content_settings = + HostContentSettingsMapFactory::GetForProfile( + Profile::FromBrowserContext(GetWebContents()->GetBrowserContext())); + content_settings->SetContentSettingDefaultScope( + example_url_, GURL(), CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC, + std::string(), mic_setting); + content_settings->SetContentSettingDefaultScope( + example_url_, GURL(), CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, + std::string(), cam_setting); + } + + // Checks whether the devices returned in OnMediaStreamResponse contains a + // microphone and/or camera device. + bool CheckDevicesListContains(blink::mojom::MediaStreamType type) { + for (const auto& device : media_stream_devices_) { + if (device.type == type) { + return true; + } + } + return false; + } + + content::WebContents* GetWebContents() { + return browser()->tab_strip_model()->GetActiveWebContents(); + } + + // Creates a MediaStreamRequest, asking for those media types, which have a + // non-empty id string. + content::MediaStreamRequest CreateRequestWithType( + const std::string& audio_id, + const std::string& video_id, + blink::MediaStreamRequestType request_type) { + blink::mojom::MediaStreamType audio_type = + audio_id.empty() ? blink::mojom::MediaStreamType::NO_SERVICE + : blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE; + blink::mojom::MediaStreamType video_type = + video_id.empty() ? blink::mojom::MediaStreamType::NO_SERVICE + : blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE; + EXPECT_EQ(example_url(), + GetWebContents()->GetMainFrame()->GetLastCommittedURL()); + int render_process_id = + GetWebContents()->GetMainFrame()->GetProcess()->GetID(); + int render_frame_id = GetWebContents()->GetMainFrame()->GetRoutingID(); + return content::MediaStreamRequest( + render_process_id, render_frame_id, 0, example_url(), false, + request_type, audio_id, video_id, audio_type, video_type, false); + } + + content::MediaStreamRequest CreateRequest(const std::string& audio_id, + const std::string& video_id) { + return CreateRequestWithType(audio_id, video_id, + blink::MEDIA_DEVICE_ACCESS); + } + + void InitWithUrl(const GURL& url) { + DCHECK(example_url_.is_empty()); + example_url_ = url; + ui_test_utils::NavigateToURL(browser(), example_url_); + EXPECT_EQ(TabSpecificContentSettings::MICROPHONE_CAMERA_NOT_ACCESSED, + GetContentSettings()->GetMicrophoneCameraState()); + } + + MockPermissionPromptFactory* prompt_factory() { + return prompt_factory_.get(); + } + + private: + void SetUpOnMainThread() override { + WebRtcTestBase::SetUpOnMainThread(); + + ASSERT_TRUE(embedded_test_server()->Start()); + + PermissionRequestManager* manager = + PermissionRequestManager::FromWebContents( + browser()->tab_strip_model()->GetActiveWebContents()); + prompt_factory_.reset(new MockPermissionPromptFactory(manager)); + + // Cleanup. + media_stream_devices_.clear(); + media_stream_result_ = + blink::mojom::MediaStreamRequestResult::NUM_MEDIA_REQUEST_RESULTS; + + blink::MediaStreamDevices audio_devices; + blink::MediaStreamDevice fake_audio_device( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE, example_audio_id_, + "Fake Audio Device"); + audio_devices.push_back(fake_audio_device); + MediaCaptureDevicesDispatcher::GetInstance()->SetTestAudioCaptureDevices( + audio_devices); + + blink::MediaStreamDevices video_devices; + blink::MediaStreamDevice fake_video_device( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, example_video_id_, + "Fake Video Device"); + video_devices.push_back(fake_video_device); + MediaCaptureDevicesDispatcher::GetInstance()->SetTestVideoCaptureDevices( + video_devices); + } + + void TearDownOnMainThread() override { + prompt_factory_.reset(); + + WebRtcTestBase::TearDownOnMainThread(); + } + + GURL example_url_; + const std::string example_audio_id_; + const std::string example_video_id_; + + blink::MediaStreamDevices media_stream_devices_; + blink::mojom::MediaStreamRequestResult media_stream_result_; + + base::Closure quit_closure_; + + std::unique_ptr<MockPermissionPromptFactory> prompt_factory_; +}; + +// Request and allow microphone access. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, RequestAndAllowMic) { + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + SetDevicePolicy(DEVICE_TYPE_AUDIO, ACCESS_ALLOWED); + // Ensure the prompt is accepted if necessary such that tab specific content + // settings are updated. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), std::string()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + EXPECT_TRUE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_FALSE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_EQ(TabSpecificContentSettings::MICROPHONE_ACCESSED, + GetContentSettings()->GetMicrophoneCameraState()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_requested_audio_device()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_selected_audio_device()); + EXPECT_EQ(std::string(), + GetContentSettings()->media_stream_requested_video_device()); + EXPECT_EQ(std::string(), + GetContentSettings()->media_stream_selected_video_device()); +} + +// Request and allow camera access. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, RequestAndAllowCam) { + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + SetDevicePolicy(DEVICE_TYPE_VIDEO, ACCESS_ALLOWED); + // Ensure the prompt is accepted if necessary such that tab specific content + // settings are updated. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(std::string(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + EXPECT_TRUE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_FALSE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_EQ(TabSpecificContentSettings::CAMERA_ACCESSED, + GetContentSettings()->GetMicrophoneCameraState()); + EXPECT_EQ(std::string(), + GetContentSettings()->media_stream_requested_audio_device()); + EXPECT_EQ(std::string(), + GetContentSettings()->media_stream_selected_audio_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_requested_video_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_selected_video_device()); +} + +// Request and block microphone access. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, RequestAndBlockMic) { + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + SetDevicePolicy(DEVICE_TYPE_AUDIO, ACCESS_DENIED); + // Ensure the prompt is accepted if necessary such that tab specific content + // settings are updated. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), std::string()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + EXPECT_FALSE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_TRUE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_EQ(TabSpecificContentSettings::MICROPHONE_ACCESSED | + TabSpecificContentSettings::MICROPHONE_BLOCKED, + GetContentSettings()->GetMicrophoneCameraState()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_requested_audio_device()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_selected_audio_device()); + EXPECT_EQ(std::string(), + GetContentSettings()->media_stream_requested_video_device()); + EXPECT_EQ(std::string(), + GetContentSettings()->media_stream_selected_video_device()); +} + +// Request and block camera access. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, RequestAndBlockCam) { + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + SetDevicePolicy(DEVICE_TYPE_VIDEO, ACCESS_DENIED); + // Ensure the prompt is accepted if necessary such that tab specific content + // settings are updated. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(std::string(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + EXPECT_FALSE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_TRUE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_EQ(TabSpecificContentSettings::CAMERA_ACCESSED | + TabSpecificContentSettings::CAMERA_BLOCKED, + GetContentSettings()->GetMicrophoneCameraState()); + EXPECT_EQ(std::string(), + GetContentSettings()->media_stream_requested_audio_device()); + EXPECT_EQ(std::string(), + GetContentSettings()->media_stream_selected_audio_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_requested_video_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_selected_video_device()); +} + +// Request and allow microphone and camera access. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + RequestAndAllowMicCam) { + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + SetDevicePolicy(DEVICE_TYPE_AUDIO, ACCESS_ALLOWED); + SetDevicePolicy(DEVICE_TYPE_VIDEO, ACCESS_ALLOWED); + // Ensure the prompt is accepted if necessary such that tab specific content + // settings are updated. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + EXPECT_TRUE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_FALSE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_TRUE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_FALSE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_EQ(TabSpecificContentSettings::MICROPHONE_ACCESSED | + TabSpecificContentSettings::CAMERA_ACCESSED, + GetContentSettings()->GetMicrophoneCameraState()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_requested_audio_device()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_selected_audio_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_requested_video_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_selected_video_device()); +} + +// Request and block microphone and camera access. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + RequestAndBlockMicCam) { + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + SetDevicePolicy(DEVICE_TYPE_AUDIO, ACCESS_DENIED); + SetDevicePolicy(DEVICE_TYPE_VIDEO, ACCESS_DENIED); + // Ensure the prompt is accepted if necessary such that tab specific content + // settings are updated. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + EXPECT_FALSE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_TRUE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_FALSE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_TRUE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_EQ(TabSpecificContentSettings::MICROPHONE_ACCESSED | + TabSpecificContentSettings::MICROPHONE_BLOCKED | + TabSpecificContentSettings::CAMERA_ACCESSED | + TabSpecificContentSettings::CAMERA_BLOCKED, + GetContentSettings()->GetMicrophoneCameraState()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_requested_audio_device()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_selected_audio_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_requested_video_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_selected_video_device()); +} + +// Request microphone and camera access. Camera is denied, thus everything +// must be denied. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + RequestMicCamBlockCam) { + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + SetDevicePolicy(DEVICE_TYPE_AUDIO, ACCESS_ALLOWED); + SetDevicePolicy(DEVICE_TYPE_VIDEO, ACCESS_DENIED); + // Ensure the prompt is accepted if necessary such that tab specific content + // settings are updated. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + EXPECT_FALSE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_TRUE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_FALSE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_TRUE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_EQ(TabSpecificContentSettings::MICROPHONE_ACCESSED | + TabSpecificContentSettings::MICROPHONE_BLOCKED | + TabSpecificContentSettings::CAMERA_ACCESSED | + TabSpecificContentSettings::CAMERA_BLOCKED, + GetContentSettings()->GetMicrophoneCameraState()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_requested_audio_device()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_selected_audio_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_requested_video_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_selected_video_device()); +} + +// Request microphone and camera access. Microphone is denied, thus everything +// must be denied. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + RequestMicCamBlockMic) { + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + SetDevicePolicy(DEVICE_TYPE_AUDIO, ACCESS_DENIED); + SetDevicePolicy(DEVICE_TYPE_VIDEO, ACCESS_ALLOWED); + // Ensure the prompt is accepted if necessary such that tab specific content + // settings are updated. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + EXPECT_FALSE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_TRUE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_FALSE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_TRUE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_EQ(TabSpecificContentSettings::MICROPHONE_ACCESSED | + TabSpecificContentSettings::MICROPHONE_BLOCKED | + TabSpecificContentSettings::CAMERA_ACCESSED | + TabSpecificContentSettings::CAMERA_BLOCKED, + GetContentSettings()->GetMicrophoneCameraState()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_requested_audio_device()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_selected_audio_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_requested_video_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_selected_video_device()); +} + +// Request microphone access. Requesting camera should not change microphone +// state. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + RequestCamDoesNotChangeMic) { + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + // Request mic and deny. + SetDevicePolicy(DEVICE_TYPE_AUDIO, ACCESS_DENIED); + // Ensure the prompt is accepted if necessary such that tab specific content + // settings are updated. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), std::string()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + EXPECT_FALSE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_TRUE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_requested_audio_device()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_selected_audio_device()); + + // Request cam and allow + SetDevicePolicy(DEVICE_TYPE_VIDEO, ACCESS_ALLOWED); + RequestPermissions( + GetWebContents(), CreateRequest(std::string(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + EXPECT_TRUE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_FALSE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_requested_video_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_selected_video_device()); + + // Mic state should not have changed. + EXPECT_FALSE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_TRUE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_requested_audio_device()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_selected_audio_device()); +} + +// Denying mic access after camera access should still show the camera as state. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + DenyMicDoesNotChangeCam) { + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + // Request cam and allow + SetDevicePolicy(DEVICE_TYPE_VIDEO, ACCESS_ALLOWED); + // Ensure the prompt is accepted if necessary such that tab specific content + // settings are updated. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(std::string(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + EXPECT_TRUE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_FALSE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_requested_video_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_selected_video_device()); + EXPECT_EQ(TabSpecificContentSettings::CAMERA_ACCESSED, + GetContentSettings()->GetMicrophoneCameraState()); + + // Simulate that an a video stream is now being captured. + blink::MediaStreamDevices video_devices(1); + video_devices[0] = blink::MediaStreamDevice( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, example_video_id(), + example_video_id()); + MediaCaptureDevicesDispatcher* dispatcher = + MediaCaptureDevicesDispatcher::GetInstance(); + dispatcher->SetTestVideoCaptureDevices(video_devices); + std::unique_ptr<content::MediaStreamUI> video_stream_ui = + dispatcher->GetMediaStreamCaptureIndicator()->RegisterMediaStream( + GetWebContents(), video_devices); + video_stream_ui->OnStarted(base::OnceClosure(), + content::MediaStreamUI::SourceCallback()); + + // Request mic and deny. + SetDevicePolicy(DEVICE_TYPE_AUDIO, ACCESS_DENIED); + // Ensure the prompt is accepted if necessary such that tab specific content + // settings are updated. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), std::string()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + EXPECT_FALSE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_TRUE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_requested_audio_device()); + EXPECT_EQ(example_audio_id(), + GetContentSettings()->media_stream_selected_audio_device()); + + // Cam should still be included in the state. + EXPECT_TRUE(GetContentSettings()->IsContentAllowed( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_FALSE(GetContentSettings()->IsContentBlocked( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_requested_video_device()); + EXPECT_EQ(example_video_id(), + GetContentSettings()->media_stream_selected_video_device()); + EXPECT_EQ(TabSpecificContentSettings::MICROPHONE_ACCESSED | + TabSpecificContentSettings::MICROPHONE_BLOCKED | + TabSpecificContentSettings::CAMERA_ACCESSED, + GetContentSettings()->GetMicrophoneCameraState()); + + // After ending the camera capture, the camera permission is no longer + // relevant, so it should no be included in the mic/cam state. + video_stream_ui.reset(); + EXPECT_EQ(TabSpecificContentSettings::MICROPHONE_ACCESSED | + TabSpecificContentSettings::MICROPHONE_BLOCKED, + GetContentSettings()->GetMicrophoneCameraState()); +} + +// Stores the ContentSettings inputs for a particular test and has functions +// which return the expected outputs for that test. +struct ContentSettingsTestData { + // The initial value of the mic/cam content settings. + ContentSetting mic; + ContentSetting cam; + // Whether the infobar should be accepted if it's shown. + bool accept_infobar; + + // Whether the infobar should be displayed to request mic/cam for the given + // content settings inputs. + bool ExpectMicInfobar() const { + return mic == CONTENT_SETTING_ASK && cam != CONTENT_SETTING_BLOCK; + } + bool ExpectCamInfobar() const { + return cam == CONTENT_SETTING_ASK && mic != CONTENT_SETTING_BLOCK; + } + + // Whether or not the mic/cam should be allowed after clicking accept/deny for + // the given inputs. + bool ExpectMicAllowed() const { + return mic == CONTENT_SETTING_ALLOW || + (mic == CONTENT_SETTING_ASK && accept_infobar); + } + bool ExpectCamAllowed() const { + return cam == CONTENT_SETTING_ALLOW || + (cam == CONTENT_SETTING_ASK && accept_infobar); + } + + // The expected media stream result after clicking accept/deny for the given + // inputs. + blink::mojom::MediaStreamRequestResult ExpectedMediaStreamResult() const { + if (ExpectMicAllowed() && ExpectCamAllowed()) + return blink::mojom::MediaStreamRequestResult::OK; + return blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED; + } +}; + +// Test all combinations of cam/mic content settings. Then tests the result of +// clicking both accept/deny on the infobar. Both cam/mic are requested. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, ContentSettings) { + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + static const ContentSettingsTestData tests[] = { + // Settings that won't result in an infobar. + {CONTENT_SETTING_ALLOW, CONTENT_SETTING_ALLOW, false}, + {CONTENT_SETTING_ALLOW, CONTENT_SETTING_BLOCK, false}, + {CONTENT_SETTING_BLOCK, CONTENT_SETTING_ALLOW, false}, + {CONTENT_SETTING_BLOCK, CONTENT_SETTING_BLOCK, false}, + {CONTENT_SETTING_BLOCK, CONTENT_SETTING_ASK, false}, + {CONTENT_SETTING_ASK, CONTENT_SETTING_BLOCK, false}, + + // Settings that will result in an infobar. Test both accept and deny. + {CONTENT_SETTING_ALLOW, CONTENT_SETTING_ASK, false}, + {CONTENT_SETTING_ALLOW, CONTENT_SETTING_ASK, true}, + + {CONTENT_SETTING_ASK, CONTENT_SETTING_ASK, false}, + {CONTENT_SETTING_ASK, CONTENT_SETTING_ASK, true}, + + {CONTENT_SETTING_ASK, CONTENT_SETTING_ALLOW, false}, + {CONTENT_SETTING_ASK, CONTENT_SETTING_ALLOW, true}, + }; + + for (auto& test : tests) { + SetContentSettings(test.mic, test.cam); + + prompt_factory()->ResetCounts(); + + // Accept or deny the infobar if it's showing. + if (test.ExpectMicInfobar() || test.ExpectCamInfobar()) { + if (test.accept_infobar) { + prompt_factory()->set_response_type( + PermissionRequestManager::ACCEPT_ALL); + } else { + prompt_factory()->set_response_type(PermissionRequestManager::DENY_ALL); + } + } else { + prompt_factory()->set_response_type(PermissionRequestManager::NONE); + } + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + ASSERT_LE(prompt_factory()->TotalRequestCount(), 2); + ASSERT_EQ(test.ExpectMicInfobar(), + prompt_factory()->RequestTypeSeen( + PermissionRequestType::PERMISSION_MEDIASTREAM_MIC)); + ASSERT_EQ(test.ExpectCamInfobar(), + prompt_factory()->RequestTypeSeen( + PermissionRequestType::PERMISSION_MEDIASTREAM_CAMERA)); + + // Check the media stream result is expected and the devices returned are + // expected; + ASSERT_EQ(test.ExpectedMediaStreamResult(), media_stream_result()); + ASSERT_EQ(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE), + test.ExpectMicAllowed() && test.ExpectCamAllowed()); + ASSERT_EQ(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE), + test.ExpectMicAllowed() && test.ExpectCamAllowed()); + } +} + +// Request and allow camera access on WebUI pages without prompting. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + WebUIRequestAndAllowCam) { + InitWithUrl(GURL(chrome::kChromeUIVersionURL)); + RequestPermissions( + GetWebContents(), CreateRequest(std::string(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + ASSERT_EQ(0, prompt_factory()->TotalRequestCount()); + + ASSERT_EQ(blink::mojom::MediaStreamRequestResult::OK, media_stream_result()); + ASSERT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)); + ASSERT_TRUE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + ExtensionRequestMicCam) { + std::string pdf_extension_page = std::string(extensions::kExtensionScheme) + + "://" + extension_misc::kPdfExtensionId + + "/index.html"; + InitWithUrl(GURL(pdf_extension_page)); + // Test that a prompt is required. + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + ASSERT_EQ(2, prompt_factory()->TotalRequestCount()); + ASSERT_TRUE(prompt_factory()->RequestTypeSeen( + PermissionRequestType::PERMISSION_MEDIASTREAM_CAMERA)); + ASSERT_TRUE(prompt_factory()->RequestTypeSeen( + PermissionRequestType::PERMISSION_MEDIASTREAM_MIC)); + + // Accept the prompt. + ASSERT_EQ(blink::mojom::MediaStreamRequestResult::OK, media_stream_result()); + ASSERT_TRUE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)); + ASSERT_TRUE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)); + + // Check that re-requesting allows without prompting. + prompt_factory()->ResetCounts(); + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + ASSERT_EQ(0, prompt_factory()->TotalRequestCount()); + + ASSERT_EQ(blink::mojom::MediaStreamRequestResult::OK, media_stream_result()); + ASSERT_TRUE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)); + ASSERT_TRUE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + PepperRequestInsecure) { + InitWithUrl(GURL("http://www.example.com")); + + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + + RequestPermissions( + GetWebContents(), + CreateRequestWithType(example_audio_id(), example_video_id(), + blink::MEDIA_OPEN_DEVICE_PEPPER_ONLY), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + ASSERT_EQ(0, prompt_factory()->TotalRequestCount()); + + ASSERT_EQ(blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED, + media_stream_result()); + ASSERT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)); + ASSERT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, WebContentsDestroyed) { + InitWithUrl(GURL("http://www.example.com")); + + prompt_factory()->set_response_type(PermissionRequestManager::ACCEPT_ALL); + + content::MediaStreamRequest request = + CreateRequest(example_audio_id(), example_video_id()); + // Simulate a destroyed RenderFrameHost. + request.render_frame_id = 0; + request.render_process_id = 0; + + RequestPermissions( + nullptr, request, + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + ASSERT_EQ(0, prompt_factory()->TotalRequestCount()); + + ASSERT_EQ(blink::mojom::MediaStreamRequestResult::FAILED_DUE_TO_SHUTDOWN, + media_stream_result()); + ASSERT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)); + ASSERT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)); +} + +// Request and block microphone and camera access with kill switch. +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + RequestAndKillSwitchMicCam) { + std::map<std::string, std::string> params; + params[PermissionUtil::GetPermissionString( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC)] = + PermissionContextBase::kPermissionsKillSwitchBlockedValue; + params[PermissionUtil::GetPermissionString( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA)] = + PermissionContextBase::kPermissionsKillSwitchBlockedValue; + variations::AssociateVariationParams( + PermissionContextBase::kPermissionsKillSwitchFieldStudy, + "TestGroup", params); + base::FieldTrialList::CreateFieldTrial( + PermissionContextBase::kPermissionsKillSwitchFieldStudy, + "TestGroup"); + InitWithUrl(embedded_test_server()->GetURL("/simple.html")); + SetDevicePolicy(DEVICE_TYPE_AUDIO, ACCESS_ALLOWED); + SetDevicePolicy(DEVICE_TYPE_VIDEO, ACCESS_ALLOWED); + RequestPermissions( + GetWebContents(), CreateRequest(example_audio_id(), example_video_id()), + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + ASSERT_EQ(0, prompt_factory()->TotalRequestCount()); + + ASSERT_EQ(blink::mojom::MediaStreamRequestResult::KILL_SWITCH_ON, + media_stream_result()); + ASSERT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)); + ASSERT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + RequestCamAndMicBlockedByFeaturePolicy) { + InitWithUrl(embedded_test_server()->GetURL("/iframe_blank.html")); + + // Create a cross-origin request by using localhost as the iframe origin. + GURL::Replacements replace_host; + replace_host.SetHostStr("localhost"); + GURL cross_origin_url = embedded_test_server() + ->GetURL("/simple.html") + .ReplaceComponents(replace_host); + content::NavigateIframeToURL(GetWebContents(), "test", + GURL(cross_origin_url)); + content::RenderFrameHost* child_frame = + ChildFrameAt(GetWebContents()->GetMainFrame(), 0); + + content::MediaStreamRequest request = + CreateRequest(example_audio_id(), example_video_id()); + // Make the child frame the source of the request. + request.render_process_id = child_frame->GetProcess()->GetID(); + request.render_frame_id = child_frame->GetRoutingID(); + RequestPermissions( + GetWebContents(), request, + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + ASSERT_EQ(0, prompt_factory()->TotalRequestCount()); + + ASSERT_EQ(blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED, + media_stream_result()); + ASSERT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)); + ASSERT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)); + EXPECT_EQ(TabSpecificContentSettings::MICROPHONE_CAMERA_NOT_ACCESSED, + GetContentSettings()->GetMicrophoneCameraState()); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + RequestCamBlockedByFeaturePolicy) { + InitWithUrl(embedded_test_server()->GetURL("/iframe_blank.html")); + + // Create a cross-origin request by using localhost as the iframe origin. + GURL::Replacements replace_host; + replace_host.SetHostStr("localhost"); + GURL cross_origin_url = embedded_test_server() + ->GetURL("/simple.html") + .ReplaceComponents(replace_host); + content::NavigateIframeToURL(GetWebContents(), "test", + GURL(cross_origin_url)); + content::RenderFrameHost* child_frame = + ChildFrameAt(GetWebContents()->GetMainFrame(), 0); + + content::MediaStreamRequest request = + CreateRequest(std::string(), example_video_id()); + // Make the child frame the source of the request. + request.render_process_id = child_frame->GetProcess()->GetID(); + request.render_frame_id = child_frame->GetRoutingID(); + RequestPermissions( + GetWebContents(), request, + base::Bind(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + ASSERT_EQ(0, prompt_factory()->TotalRequestCount()); + + ASSERT_EQ(blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED, + media_stream_result()); + ASSERT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)); + ASSERT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)); + EXPECT_EQ(TabSpecificContentSettings::MICROPHONE_CAMERA_NOT_ACCESSED, + GetContentSettings()->GetMicrophoneCameraState()); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + PepperAudioRequestNoCamera) { + MediaCaptureDevicesDispatcher::GetInstance()->SetTestVideoCaptureDevices({}); + InitWithUrl(GURL(chrome::kChromeUIVersionURL)); + RequestPermissions( + GetWebContents(), + CreateRequestWithType(example_audio_id(), std::string(), + blink::MEDIA_OPEN_DEVICE_PEPPER_ONLY), + base::BindOnce(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, media_stream_result()); + EXPECT_TRUE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)); + EXPECT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamDevicesControllerTest, + PepperVideoRequestNoMic) { + MediaCaptureDevicesDispatcher::GetInstance()->SetTestAudioCaptureDevices({}); + InitWithUrl(GURL(chrome::kChromeUIVersionURL)); + RequestPermissions( + GetWebContents(), + CreateRequestWithType(std::string(), example_video_id(), + blink::MEDIA_OPEN_DEVICE_PEPPER_ONLY), + base::BindOnce(&MediaStreamDevicesControllerTest::OnMediaStreamResponse, + base::Unretained(this))); + + EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, media_stream_result()); + EXPECT_FALSE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)); + EXPECT_TRUE(CheckDevicesListContains( + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)); +} diff --git a/chromium/chrome/browser/media/webrtc/media_stream_infobar_browsertest.cc b/chromium/chrome/browser/media/webrtc/media_stream_infobar_browsertest.cc new file mode 100644 index 00000000000..651c8025892 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/media_stream_infobar_browsertest.cc @@ -0,0 +1,168 @@ +// 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/command_line.h" +#include "base/files/file_util.h" +#include "base/macros.h" +#include "chrome/browser/chrome_notification_types.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/media/webrtc/media_stream_devices_controller.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/test_switches.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings_types.h" +#include "content/public/browser/notification_service.h" +#include "content/public/common/content_switches.h" +#include "content/public/common/origin_util.h" +#include "content/public/test/browser_test_utils.h" +#include "media/base/media_switches.h" +#include "net/dns/mock_host_resolver.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "third_party/blink/public/common/mediastream/media_stream_request.h" +#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" + +// MediaStreamPermissionTest --------------------------------------------------- + +class MediaStreamPermissionTest : public WebRtcTestBase { + public: + MediaStreamPermissionTest() {} + ~MediaStreamPermissionTest() override {} + + // InProcessBrowserTest: + void SetUpCommandLine(base::CommandLine* command_line) override { + // This test expects to run with fake devices but real UI. + command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); + EXPECT_FALSE(command_line->HasSwitch(switches::kUseFakeUIForMediaStream)) + << "Since this test tests the UI we want the real UI!"; + } + + protected: + content::WebContents* LoadTestPageInTab() { + return LoadTestPageInBrowser(browser()); + } + + content::WebContents* LoadTestPageInIncognitoTab() { + return LoadTestPageInBrowser(CreateIncognitoBrowser()); + } + + // Returns the URL of the main test page. + GURL test_page_url() const { + const char kMainWebrtcTestHtmlPage[] = "/webrtc/webrtc_jsep01_test.html"; + return embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage); + } + + private: + content::WebContents* LoadTestPageInBrowser(Browser* browser) { + EXPECT_TRUE(embedded_test_server()->Start()); + + // Uses the default server. + GURL url = test_page_url(); + + EXPECT_TRUE(content::IsOriginSecure(url)); + + ui_test_utils::NavigateToURL(browser, url); + return browser->tab_strip_model()->GetActiveWebContents(); + } + + // Dummy callback for when we deny the current request directly. + static void OnMediaStreamResponse( + const blink::MediaStreamDevices& devices, + blink::mojom::MediaStreamRequestResult result, + std::unique_ptr<content::MediaStreamUI> ui) {} + + DISALLOW_COPY_AND_ASSIGN(MediaStreamPermissionTest); +}; + +// Actual tests --------------------------------------------------------------- + +IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest, TestAllowingUserMedia) { + content::WebContents* tab_contents = LoadTestPageInTab(); + EXPECT_TRUE(GetUserMediaAndAccept(tab_contents)); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest, TestDenyingUserMedia) { + content::WebContents* tab_contents = LoadTestPageInTab(); + GetUserMediaAndDeny(tab_contents); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest, TestDismissingRequest) { + content::WebContents* tab_contents = LoadTestPageInTab(); + GetUserMediaAndDismiss(tab_contents); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest, + TestDenyingUserMediaIncognito) { + content::WebContents* tab_contents = LoadTestPageInIncognitoTab(); + GetUserMediaAndDeny(tab_contents); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest, + TestSecureOriginDenyIsSticky) { + content::WebContents* tab_contents = LoadTestPageInTab(); + EXPECT_TRUE(content::IsOriginSecure(tab_contents->GetLastCommittedURL())); + + GetUserMediaAndDeny(tab_contents); + GetUserMediaAndExpectAutoDenyWithoutPrompt(tab_contents); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest, + TestSecureOriginAcceptIsSticky) { + content::WebContents* tab_contents = LoadTestPageInTab(); + EXPECT_TRUE(content::IsOriginSecure(tab_contents->GetLastCommittedURL())); + + EXPECT_TRUE(GetUserMediaAndAccept(tab_contents)); + GetUserMediaAndExpectAutoAcceptWithoutPrompt(tab_contents); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest, TestDismissIsNotSticky) { + content::WebContents* tab_contents = LoadTestPageInTab(); + + GetUserMediaAndDismiss(tab_contents); + GetUserMediaAndDismiss(tab_contents); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest, + TestDenyingThenClearingStickyException) { + content::WebContents* tab_contents = LoadTestPageInTab(); + + GetUserMediaAndDeny(tab_contents); + GetUserMediaAndExpectAutoDenyWithoutPrompt(tab_contents); + + HostContentSettingsMap* settings_map = + HostContentSettingsMapFactory::GetForProfile(browser()->profile()); + + settings_map->ClearSettingsForOneType(CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC); + settings_map->ClearSettingsForOneType( + CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA); + + GetUserMediaAndDeny(tab_contents); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest, + DenyingMicDoesNotCauseStickyDenyForCameras) { + content::WebContents* tab_contents = LoadTestPageInTab(); + + GetUserMediaWithSpecificConstraintsAndDeny(tab_contents, + kAudioOnlyCallConstraints); + EXPECT_TRUE(GetUserMediaWithSpecificConstraintsAndAccept( + tab_contents, kVideoOnlyCallConstraints)); +} + +IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest, + DenyingCameraDoesNotCauseStickyDenyForMics) { + content::WebContents* tab_contents = LoadTestPageInTab(); + + GetUserMediaWithSpecificConstraintsAndDeny(tab_contents, + kVideoOnlyCallConstraints); + EXPECT_TRUE(GetUserMediaWithSpecificConstraintsAndAccept( + tab_contents, kAudioOnlyCallConstraints)); +} diff --git a/chromium/chrome/browser/media/webrtc/native_desktop_media_list.cc b/chromium/chrome/browser/media/webrtc/native_desktop_media_list.cc new file mode 100644 index 00000000000..2b109bd1dea --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/native_desktop_media_list.cc @@ -0,0 +1,407 @@ +// 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 "chrome/browser/media/webrtc/native_desktop_media_list.h" + +#include <utility> + +#include "base/bind.h" +#include "base/hash/hash.h" +#include "base/message_loop/message_pump_type.h" +#include "base/single_thread_task_runner.h" +#include "base/strings/utf_string_conversions.h" +#include "base/task/post_task.h" +#include "base/threading/thread_restrictions.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/desktop_media_list.h" +#include "chrome/grit/generated_resources.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "media/base/video_util.h" +#include "third_party/libyuv/include/libyuv/scale_argb.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/gfx/native_widget_types.h" +#include "ui/snapshot/snapshot.h" + +#if defined(USE_AURA) +#include "ui/aura/window.h" +#include "ui/aura/window_tree_host.h" +#include "ui/snapshot/snapshot_aura.h" +#endif + +using content::BrowserThread; +using content::DesktopMediaID; + +namespace { + +// Update the list every second. +const int kDefaultNativeDesktopMediaListUpdatePeriod = 1000; + +// Returns a hash of a DesktopFrame content to detect when image for a desktop +// media source has changed. +uint32_t GetFrameHash(webrtc::DesktopFrame* frame) { + int data_size = frame->stride() * frame->size().height(); + return base::Hash(frame->data(), data_size); +} + +gfx::ImageSkia ScaleDesktopFrame(std::unique_ptr<webrtc::DesktopFrame> frame, + gfx::Size size) { + gfx::Rect scaled_rect = media::ComputeLetterboxRegion( + gfx::Rect(0, 0, size.width(), size.height()), + gfx::Size(frame->size().width(), frame->size().height())); + + SkBitmap result; + result.allocN32Pixels(scaled_rect.width(), scaled_rect.height(), true); + + uint8_t* pixels_data = reinterpret_cast<uint8_t*>(result.getPixels()); + libyuv::ARGBScale(frame->data(), frame->stride(), frame->size().width(), + frame->size().height(), pixels_data, result.rowBytes(), + scaled_rect.width(), scaled_rect.height(), + libyuv::kFilterBilinear); + + // Set alpha channel values to 255 for all pixels. + // TODO(sergeyu): Fix screen/window capturers to capture alpha channel and + // remove this code. Currently screen/window capturers (at least some + // implementations) only capture R, G and B channels and set Alpha to 0. + // crbug.com/264424 + for (int y = 0; y < result.height(); ++y) { + for (int x = 0; x < result.width(); ++x) { + pixels_data[result.rowBytes() * y + x * result.bytesPerPixel() + 3] = + 0xff; + } + } + + return gfx::ImageSkia::CreateFrom1xBitmap(result); +} + +} // namespace + +class NativeDesktopMediaList::Worker + : public webrtc::DesktopCapturer::Callback { + public: + Worker(scoped_refptr<base::SingleThreadTaskRunner> task_runner, + base::WeakPtr<NativeDesktopMediaList> media_list, + DesktopMediaID::Type type, + std::unique_ptr<webrtc::DesktopCapturer> capturer); + ~Worker() override; + + void Start(); + void Refresh(const DesktopMediaID::Id& view_dialog_id, bool update_thumnails); + + void RefreshThumbnails(const std::vector<DesktopMediaID>& native_ids, + const gfx::Size& thumbnail_size); + + private: + typedef std::map<DesktopMediaID, uint32_t> ImageHashesMap; + + // webrtc::DesktopCapturer::Callback interface. + void OnCaptureResult(webrtc::DesktopCapturer::Result result, + std::unique_ptr<webrtc::DesktopFrame> frame) override; + + // Task runner used for capturing operations. + scoped_refptr<base::SingleThreadTaskRunner> task_runner_; + + base::WeakPtr<NativeDesktopMediaList> media_list_; + + DesktopMediaID::Type type_; + std::unique_ptr<webrtc::DesktopCapturer> capturer_; + + std::unique_ptr<webrtc::DesktopFrame> current_frame_; + + ImageHashesMap image_hashes_; + + DISALLOW_COPY_AND_ASSIGN(Worker); +}; + +NativeDesktopMediaList::Worker::Worker( + scoped_refptr<base::SingleThreadTaskRunner> task_runner, + base::WeakPtr<NativeDesktopMediaList> media_list, + DesktopMediaID::Type type, + std::unique_ptr<webrtc::DesktopCapturer> capturer) + : task_runner_(task_runner), + media_list_(media_list), + type_(type), + capturer_(std::move(capturer)) {} + +NativeDesktopMediaList::Worker::~Worker() { + DCHECK(task_runner_->BelongsToCurrentThread()); +} + +void NativeDesktopMediaList::Worker::Start() { + DCHECK(task_runner_->BelongsToCurrentThread()); + capturer_->Start(this); +} + +void NativeDesktopMediaList::Worker::Refresh( + const DesktopMediaID::Id& view_dialog_id, + bool update_thumnails) { + DCHECK(task_runner_->BelongsToCurrentThread()); + std::vector<SourceDescription> result; + + webrtc::DesktopCapturer::SourceList sources; + if (!capturer_->GetSourceList(&sources)) { + // Will pass empty results list to RefreshForAuraWindows(). + sources.clear(); + } + + bool mutiple_sources = sources.size() > 1; + base::string16 title; + for (size_t i = 0; i < sources.size(); ++i) { + switch (type_) { + case DesktopMediaID::TYPE_SCREEN: + // Just in case 'Screen' is inflected depending on the screen number, + // use plural formatter. + title = mutiple_sources + ? l10n_util::GetPluralStringFUTF16( + IDS_DESKTOP_MEDIA_PICKER_MULTIPLE_SCREEN_NAME, + static_cast<int>(i + 1)) + : l10n_util::GetStringUTF16( + IDS_DESKTOP_MEDIA_PICKER_SINGLE_SCREEN_NAME); + break; + + case DesktopMediaID::TYPE_WINDOW: + // Skip the picker dialog window. + if (sources[i].id == view_dialog_id) + continue; + title = base::UTF8ToUTF16(sources[i].title); + break; + + default: + NOTREACHED(); + } + result.push_back( + SourceDescription(DesktopMediaID(type_, sources[i].id), title)); + } + + base::PostTask(FROM_HERE, {BrowserThread::UI}, + base::BindOnce(&NativeDesktopMediaList::RefreshForAuraWindows, + media_list_, result, update_thumnails)); +} + +void NativeDesktopMediaList::Worker::RefreshThumbnails( + const std::vector<DesktopMediaID>& native_ids, + const gfx::Size& thumbnail_size) { + DCHECK(task_runner_->BelongsToCurrentThread()); + ImageHashesMap new_image_hashes; + + // Get a thumbnail for each native source. + for (const auto& id : native_ids) { + if (!capturer_->SelectSource(id.id)) + continue; + capturer_->CaptureFrame(); + + // Expect that DesktopCapturer to always captures frames synchronously. + // |current_frame_| may be NULL if capture failed (e.g. because window has + // been closed). + if (current_frame_) { + uint32_t frame_hash = GetFrameHash(current_frame_.get()); + new_image_hashes[id] = frame_hash; + + // Scale the image only if it has changed. + auto it = image_hashes_.find(id); + if (it == image_hashes_.end() || it->second != frame_hash) { + gfx::ImageSkia thumbnail = + ScaleDesktopFrame(std::move(current_frame_), thumbnail_size); + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce(&NativeDesktopMediaList::UpdateSourceThumbnail, + media_list_, id, thumbnail)); + } + } + } + + image_hashes_.swap(new_image_hashes); + + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce(&NativeDesktopMediaList::UpdateNativeThumbnailsFinished, + media_list_)); +} + +void NativeDesktopMediaList::Worker::OnCaptureResult( + webrtc::DesktopCapturer::Result result, + std::unique_ptr<webrtc::DesktopFrame> frame) { + current_frame_ = std::move(frame); +} + +NativeDesktopMediaList::NativeDesktopMediaList( + DesktopMediaID::Type type, + std::unique_ptr<webrtc::DesktopCapturer> capturer) + : DesktopMediaListBase(base::TimeDelta::FromMilliseconds( + kDefaultNativeDesktopMediaListUpdatePeriod)), + thread_("DesktopMediaListCaptureThread") { + type_ = type; + +#if defined(OS_WIN) || defined(OS_MACOSX) + // On Windows/OSX the thread must be a UI thread. + base::MessagePumpType thread_type = base::MessagePumpType::UI; +#else + base::MessagePumpType thread_type = base::MessagePumpType::DEFAULT; +#endif + thread_.StartWithOptions(base::Thread::Options(thread_type, 0)); + + worker_.reset(new Worker(thread_.task_runner(), weak_factory_.GetWeakPtr(), + type, std::move(capturer))); + + thread_.task_runner()->PostTask( + FROM_HERE, + base::BindOnce(&Worker::Start, base::Unretained(worker_.get()))); +} + +NativeDesktopMediaList::~NativeDesktopMediaList() { + // This thread should mostly be an idle observer. Stopping it should be fast. + base::ScopedAllowBaseSyncPrimitivesOutsideBlockingScope allow_thread_join; + thread_.task_runner()->DeleteSoon(FROM_HERE, worker_.release()); + thread_.Stop(); +} + +void NativeDesktopMediaList::Refresh(bool update_thumnails) { + DCHECK(can_refresh()); + +#if defined(USE_AURA) + DCHECK_EQ(pending_aura_capture_requests_, 0); + DCHECK(!pending_native_thumbnail_capture_); + new_aura_thumbnail_hashes_.clear(); +#endif + + thread_.task_runner()->PostTask( + FROM_HERE, + base::BindOnce(&Worker::Refresh, base::Unretained(worker_.get()), + view_dialog_id_.id, update_thumnails)); +} + +void NativeDesktopMediaList::RefreshForAuraWindows( + std::vector<SourceDescription> sources, + bool update_thumnails) { + DCHECK(can_refresh()); + +#if defined(USE_AURA) + // Associate aura id with native id. + for (auto& source : sources) { + if (source.id.type != DesktopMediaID::TYPE_WINDOW) + continue; + + aura::WindowTreeHost* const host = + aura::WindowTreeHost::GetForAcceleratedWidget( + *reinterpret_cast<gfx::AcceleratedWidget*>(&source.id.id)); + aura::Window* const aura_window = host ? host->window() : nullptr; + if (aura_window) { + DesktopMediaID aura_id = DesktopMediaID::RegisterNativeWindow( + DesktopMediaID::TYPE_WINDOW, aura_window); + source.id.window_id = aura_id.window_id; + } + } +#endif // defined(USE_AURA) + + UpdateSourcesList(sources); + + if (!update_thumnails) { + OnRefreshComplete(); + return; + } + + if (thumbnail_size_.IsEmpty()) { +#if defined(USE_AURA) + pending_native_thumbnail_capture_ = true; +#endif + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce(&NativeDesktopMediaList::UpdateNativeThumbnailsFinished, + weak_factory_.GetWeakPtr())); + return; + } + + // OnAuraThumbnailCaptured() and UpdateNativeThumbnailsFinished() are + // guaranteed to be executed after RefreshForAuraWindows() and + // CaptureAuraWindowThumbnail() in the browser UI thread. + // Therefore pending_aura_capture_requests_ will be set the number of aura + // windows to be captured and pending_native_thumbnail_capture_ will be set + // true if native thumbnail capture is needed before OnAuraThumbnailCaptured() + // or UpdateNativeThumbnailsFinished() are called. + std::vector<DesktopMediaID> native_ids; + for (const auto& source : sources) { +#if defined(USE_AURA) + if (source.id.window_id > DesktopMediaID::kNullId) { + CaptureAuraWindowThumbnail(source.id); + continue; + } +#endif // defined(USE_AURA) + native_ids.push_back(source.id); + } + + if (!native_ids.empty()) { +#if defined(USE_AURA) + pending_native_thumbnail_capture_ = true; +#endif + thread_.task_runner()->PostTask( + FROM_HERE, base::BindOnce(&Worker::RefreshThumbnails, + base::Unretained(worker_.get()), native_ids, + thumbnail_size_)); + } +} + +void NativeDesktopMediaList::UpdateNativeThumbnailsFinished() { +#if defined(USE_AURA) + DCHECK(pending_native_thumbnail_capture_); + pending_native_thumbnail_capture_ = false; + // If native thumbnail captures finished after aura thumbnail captures, + // execute |done_callback| to let the caller know the update process is + // finished. If necessary, this will schedule the next refresh. + if (pending_aura_capture_requests_ == 0) + OnRefreshComplete(); +#else + OnRefreshComplete(); +#endif // defined(USE_AURA) +} + +#if defined(USE_AURA) + +void NativeDesktopMediaList::CaptureAuraWindowThumbnail( + const DesktopMediaID& id) { + DCHECK(can_refresh()); + + gfx::NativeWindow window = DesktopMediaID::GetNativeWindowById(id); + if (!window) + return; + + gfx::Rect window_rect(window->bounds().width(), window->bounds().height()); + gfx::Rect scaled_rect = media::ComputeLetterboxRegion( + gfx::Rect(thumbnail_size_), window_rect.size()); + + pending_aura_capture_requests_++; + ui::GrabWindowSnapshotAndScaleAsyncAura( + window, window_rect, scaled_rect.size(), + base::Bind(&NativeDesktopMediaList::OnAuraThumbnailCaptured, + weak_factory_.GetWeakPtr(), id)); +} + +void NativeDesktopMediaList::OnAuraThumbnailCaptured(const DesktopMediaID& id, + gfx::Image image) { + DCHECK(can_refresh()); + + if (!image.IsEmpty()) { + // Only new or changed thumbnail need update. + new_aura_thumbnail_hashes_[id] = GetImageHash(image); + if (!previous_aura_thumbnail_hashes_.count(id) || + previous_aura_thumbnail_hashes_[id] != new_aura_thumbnail_hashes_[id]) { + UpdateSourceThumbnail(id, image.AsImageSkia()); + } + } + + // After all aura windows are processed, schedule next refresh; + pending_aura_capture_requests_--; + DCHECK_GE(pending_aura_capture_requests_, 0); + if (pending_aura_capture_requests_ == 0) { + previous_aura_thumbnail_hashes_ = std::move(new_aura_thumbnail_hashes_); + // Schedule next refresh if aura thumbnail captures finished after native + // thumbnail captures. + if (!pending_native_thumbnail_capture_) + OnRefreshComplete(); + } +} + +#endif // defined(USE_AURA) diff --git a/chromium/chrome/browser/media/webrtc/native_desktop_media_list.h b/chromium/chrome/browser/media/webrtc/native_desktop_media_list.h new file mode 100644 index 00000000000..cfd8378fe93 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/native_desktop_media_list.h @@ -0,0 +1,67 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_NATIVE_DESKTOP_MEDIA_LIST_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_NATIVE_DESKTOP_MEDIA_LIST_H_ + +#include <memory> + +#include "base/memory/weak_ptr.h" +#include "base/threading/thread.h" +#include "chrome/browser/media/webrtc/desktop_media_list_base.h" +#include "content/public/browser/desktop_media_id.h" +#include "ui/gfx/image/image.h" + +namespace webrtc { +class DesktopCapturer; +} + +// Implementation of DesktopMediaList that shows native screens and +// native windows. +class NativeDesktopMediaList : public DesktopMediaListBase { + public: + NativeDesktopMediaList(content::DesktopMediaID::Type type, + std::unique_ptr<webrtc::DesktopCapturer> capturer); + ~NativeDesktopMediaList() override; + + private: + typedef std::map<content::DesktopMediaID, uint32_t> ImageHashesMap; + + class Worker; + friend class Worker; + + // Refresh() posts a task for the |worker_| to update list of windows, get + // thumbnails and schedules next refresh. + void Refresh(bool update_thumnails) override; + + void RefreshForAuraWindows(std::vector<SourceDescription> sources, + bool update_thumnails); + void UpdateNativeThumbnailsFinished(); + +#if defined(USE_AURA) + void CaptureAuraWindowThumbnail(const content::DesktopMediaID& id); + void OnAuraThumbnailCaptured(const content::DesktopMediaID& id, + gfx::Image image); +#endif + + base::Thread thread_; + std::unique_ptr<Worker> worker_; + +#if defined(USE_AURA) + // previous_aura_thumbnail_hashes_ holds thumbanil hash values of aura windows + // in the previous refresh. While new_aura_thumbnail_hashes_ has hash values + // of the ongoing refresh. Those two maps are used to detect new thumbnails + // and changed thumbnails from the previous refresh. + ImageHashesMap previous_aura_thumbnail_hashes_; + ImageHashesMap new_aura_thumbnail_hashes_; + + int pending_aura_capture_requests_ = 0; + bool pending_native_thumbnail_capture_ = false; +#endif + base::WeakPtrFactory<NativeDesktopMediaList> weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(NativeDesktopMediaList); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_NATIVE_DESKTOP_MEDIA_LIST_H_ diff --git a/chromium/chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc b/chromium/chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc new file mode 100644 index 00000000000..fe430e586e3 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc @@ -0,0 +1,554 @@ +// 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 "chrome/browser/media/webrtc/native_desktop_media_list.h" + +#include <stddef.h> +#include <stdint.h> +#include <string.h> + +#include <utility> +#include <vector> + +#include "base/location.h" +#include "base/macros.h" +#include "base/run_loop.h" +#include "base/single_thread_task_runner.h" +#include "base/strings/utf_string_conversions.h" +#include "base/synchronization/lock.h" +#include "base/threading/thread_task_runner_handle.h" +#include "chrome/browser/media/webrtc/desktop_media_list.h" +#include "chrome/test/views/chrome_views_test_base.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h" +#include "ui/views/widget/widget.h" + +#if defined(USE_AURA) +#include "ui/aura/window.h" +#include "ui/aura/window_tree_host.h" +#include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h" +#endif + +using content::DesktopMediaID; +using testing::_; +using testing::DoAll; + +namespace { + +// Aura window capture unit tests are not stable. crbug.com/602494 and +// crbug.com/603823. +// #define ENABLE_AURA_WINDOW_TESTS + +static const int kDefaultWindowCount = 2; +#if defined(ENABLE_AURA_WINDOW_TESTS) +static const int kDefaultAuraCount = 1; +#else +static const int kDefaultAuraCount = 0; +#endif + +class MockObserver : public DesktopMediaListObserver { + public: + MOCK_METHOD2(OnSourceAdded, void(DesktopMediaList* list, int index)); + MOCK_METHOD2(OnSourceRemoved, void(DesktopMediaList* list, int index)); + MOCK_METHOD3(OnSourceMoved, + void(DesktopMediaList* list, int old_index, int new_index)); + MOCK_METHOD2(OnSourceNameChanged, void(DesktopMediaList* list, int index)); + MOCK_METHOD2(OnSourceThumbnailChanged, + void(DesktopMediaList* list, int index)); + MOCK_METHOD1(OnAllSourcesFound, void(DesktopMediaList* list)); +}; + +class FakeScreenCapturer : public webrtc::DesktopCapturer { + public: + FakeScreenCapturer() {} + ~FakeScreenCapturer() override {} + + // webrtc::ScreenCapturer implementation. + void Start(Callback* callback) override { callback_ = callback; } + + void CaptureFrame() override { + DCHECK(callback_); + std::unique_ptr<webrtc::DesktopFrame> frame( + new webrtc::BasicDesktopFrame(webrtc::DesktopSize(10, 10))); + callback_->OnCaptureResult(webrtc::DesktopCapturer::Result::SUCCESS, + std::move(frame)); + } + + bool GetSourceList(SourceList* screens) override { + screens->push_back({0}); + return true; + } + + bool SelectSource(SourceId id) override { + EXPECT_EQ(0, id); + return true; + } + + protected: + Callback* callback_; + + DISALLOW_COPY_AND_ASSIGN(FakeScreenCapturer); +}; + +class FakeWindowCapturer : public webrtc::DesktopCapturer { + public: + FakeWindowCapturer() : callback_(nullptr) {} + ~FakeWindowCapturer() override {} + + void SetWindowList(const SourceList& list) { + base::AutoLock lock(window_list_lock_); + window_list_ = list; + } + + // Sets |value| thats going to be used to memset() content of the frames + // generated for |window_id|. By default generated frames are set to zeros. + void SetNextFrameValue(SourceId window_id, int8_t value) { + base::AutoLock lock(frame_values_lock_); + frame_values_[window_id] = value; + } + + // webrtc::WindowCapturer implementation. + void Start(Callback* callback) override { callback_ = callback; } + + void CaptureFrame() override { + DCHECK(callback_); + + base::AutoLock lock(frame_values_lock_); + + auto it = frame_values_.find(selected_window_id_); + int8_t value = (it != frame_values_.end()) ? it->second : 0; + std::unique_ptr<webrtc::DesktopFrame> frame( + new webrtc::BasicDesktopFrame(webrtc::DesktopSize(10, 10))); + memset(frame->data(), value, frame->stride() * frame->size().height()); + callback_->OnCaptureResult(webrtc::DesktopCapturer::Result::SUCCESS, + std::move(frame)); + } + + bool GetSourceList(SourceList* windows) override { + base::AutoLock lock(window_list_lock_); + *windows = window_list_; + return true; + } + + bool SelectSource(SourceId id) override { + selected_window_id_ = id; + return true; + } + + bool FocusOnSelectedSource() override { return true; } + + private: + Callback* callback_; + SourceList window_list_; + base::Lock window_list_lock_; + + SourceId selected_window_id_; + + // Frames to be captured per window. + std::map<SourceId, int8_t> frame_values_; + base::Lock frame_values_lock_; + + DISALLOW_COPY_AND_ASSIGN(FakeWindowCapturer); +}; + +} // namespace + +ACTION_P2(CheckListSize, model, expected_list_size) { + EXPECT_EQ(expected_list_size, model->GetSourceCount()); +} + +ACTION_P2(QuitRunLoop, task_runner, run_loop) { + task_runner->PostTask(FROM_HERE, run_loop->QuitWhenIdleClosure()); +} + +class NativeDesktopMediaListTest : public ChromeViewsTestBase { + public: + NativeDesktopMediaListTest() = default; + + void TearDown() override { + for (size_t i = 0; i < desktop_widgets_.size(); i++) + desktop_widgets_[i].reset(); + + ChromeViewsTestBase::TearDown(); + } + + void AddNativeWindow(int id) { + webrtc::DesktopCapturer::Source window; + window.id = id; + window.title = "Test window"; + window_list_.push_back(window); + } + +#if defined(USE_AURA) + std::unique_ptr<views::Widget> CreateDesktopWidget() { + std::unique_ptr<views::Widget> widget(new views::Widget); + views::Widget::InitParams params; + params.type = views::Widget::InitParams::TYPE_WINDOW_FRAMELESS; + params.accept_events = false; + params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + params.native_widget = new views::DesktopNativeWidgetAura(widget.get()); + params.bounds = gfx::Rect(0, 0, 20, 20); + widget->Init(std::move(params)); + widget->Show(); + return widget; + } + + void AddAuraWindow() { + webrtc::DesktopCapturer::Source window; + window.title = "Test window"; + + // Create a aura native widow through a widget. + desktop_widgets_.push_back(CreateDesktopWidget()); + aura::WindowTreeHost* const host = + desktop_widgets_.back()->GetNativeWindow()->GetHost(); + aura::Window* const aura_window = host->window(); + + // Get the native window's id. + gfx::AcceleratedWidget widget = host->GetAcceleratedWidget(); +#if defined(OS_WIN) + window.id = reinterpret_cast<DesktopMediaID::Id>(widget); +#else + window.id = widget; +#endif + + // Get the aura window's id. + DesktopMediaID aura_id = DesktopMediaID::RegisterNativeWindow( + DesktopMediaID::TYPE_WINDOW, aura_window); + native_aura_id_map_[window.id] = aura_id.window_id; + + window_list_.push_back(window); + } + + void RemoveAuraWindow(int index) { + DCHECK_LT(index, static_cast<int>(desktop_widgets_.size())); + + // Get the native window's id. + aura::Window* aura_window = desktop_widgets_[index]->GetNativeWindow(); + gfx::AcceleratedWidget widget = + aura_window->GetHost()->GetAcceleratedWidget(); +#if defined(OS_WIN) + int native_id = reinterpret_cast<DesktopMediaID::Id>(widget); +#else + int native_id = widget; +#endif + // Remove the widget and associated aura window. + desktop_widgets_.erase(desktop_widgets_.begin() + index); + // Remove the aura window from the window list. + size_t i; + for (i = 0; i < window_list_.size(); i++) { + if (window_list_[i].id == native_id) + break; + } + DCHECK_LT(i, window_list_.size()); + window_list_.erase(window_list_.begin() + i); + native_aura_id_map_.erase(native_id); + } + +#endif // defined(USE_AURA) + + void AddWindowsAndVerify(bool has_view_dialog) { + window_capturer_ = new FakeWindowCapturer(); + model_ = std::make_unique<NativeDesktopMediaList>( + DesktopMediaID::TYPE_WINDOW, base::WrapUnique(window_capturer_)); + + // Set update period to reduce the time it takes to run tests. + model_->SetUpdatePeriod(base::TimeDelta::FromMilliseconds(20)); + + // Set up widows. + size_t aura_window_first_index = kDefaultWindowCount - kDefaultAuraCount; + for (size_t i = 0; i < kDefaultWindowCount; ++i) { + if (i < aura_window_first_index) { + AddNativeWindow(i); + } else { +#if defined(USE_AURA) + AddAuraWindow(); +#endif + } + } + + if (window_capturer_) + window_capturer_->SetWindowList(window_list_); + + size_t window_count = kDefaultWindowCount; + + // Set view dialog window ID as the first window id. + if (has_view_dialog) { + DesktopMediaID dialog_window_id(DesktopMediaID::TYPE_WINDOW, + window_list_[0].id); + model_->SetViewDialogWindowId(dialog_window_id); + window_count--; + aura_window_first_index--; + } + + base::RunLoop run_loop; + + { + testing::InSequence dummy; + for (size_t i = 0; i < window_count; ++i) { + EXPECT_CALL(observer_, OnSourceAdded(model_.get(), i)) + .WillOnce(CheckListSize(model_.get(), static_cast<int>(i + 1))); + } + for (size_t i = 0; i < window_count - 1; ++i) { + EXPECT_CALL(observer_, OnSourceThumbnailChanged(model_.get(), i)); + } + EXPECT_CALL(observer_, + OnSourceThumbnailChanged(model_.get(), window_count - 1)) + .WillOnce( + QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop)); + } + model_->StartUpdating(&observer_); + run_loop.Run(); + + for (size_t i = 0; i < window_count; ++i) { + EXPECT_EQ(model_->GetSource(i).id.type, DesktopMediaID::TYPE_WINDOW); + EXPECT_EQ(model_->GetSource(i).name, base::UTF8ToUTF16("Test window")); + int index = has_view_dialog ? i + 1 : i; + int native_id = window_list_[index].id; + EXPECT_EQ(model_->GetSource(i).id.id, native_id); +#if defined(USE_AURA) + if (i >= aura_window_first_index) + EXPECT_EQ(model_->GetSource(i).id.window_id, + native_aura_id_map_[native_id]); +#endif + } + testing::Mock::VerifyAndClearExpectations(&observer_); + } + + protected: + // Must be listed before |model_|, so it's destroyed last. + MockObserver observer_; + + // Owned by |model_|; + FakeWindowCapturer* window_capturer_; + + webrtc::DesktopCapturer::SourceList window_list_; + std::vector<std::unique_ptr<views::Widget>> desktop_widgets_; + std::map<DesktopMediaID::Id, DesktopMediaID::Id> native_aura_id_map_; + std::unique_ptr<NativeDesktopMediaList> model_; + + DISALLOW_COPY_AND_ASSIGN(NativeDesktopMediaListTest); +}; + +TEST_F(NativeDesktopMediaListTest, Windows) { + AddWindowsAndVerify(false); +} + +TEST_F(NativeDesktopMediaListTest, ScreenOnly) { + model_ = std::make_unique<NativeDesktopMediaList>( + DesktopMediaID::TYPE_SCREEN, std::make_unique<FakeScreenCapturer>()); + + // Set update period to reduce the time it takes to run tests. + model_->SetUpdatePeriod(base::TimeDelta::FromMilliseconds(20)); + + base::RunLoop run_loop; + + { + testing::InSequence dummy; + EXPECT_CALL(observer_, OnSourceAdded(model_.get(), 0)) + .WillOnce(CheckListSize(model_.get(), 1)); + EXPECT_CALL(observer_, OnSourceThumbnailChanged(model_.get(), 0)) + .WillOnce(QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop)); + } + model_->StartUpdating(&observer_); + run_loop.Run(); + + EXPECT_EQ(model_->GetSource(0).id.type, DesktopMediaID::TYPE_SCREEN); + EXPECT_EQ(model_->GetSource(0).id.id, 0); +} + +// Verifies that the window specified with SetViewDialogWindowId() is filtered +// from the results. +TEST_F(NativeDesktopMediaListTest, WindowFiltering) { + AddWindowsAndVerify(true); +} + +TEST_F(NativeDesktopMediaListTest, AddNativeWindow) { + AddWindowsAndVerify(false); + + base::RunLoop run_loop; + + const int index = kDefaultWindowCount; + EXPECT_CALL(observer_, OnSourceAdded(model_.get(), index)) + .WillOnce( + DoAll(CheckListSize(model_.get(), kDefaultWindowCount + 1), + QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop))); + + AddNativeWindow(index); + window_capturer_->SetWindowList(window_list_); + + run_loop.Run(); + + EXPECT_EQ(model_->GetSource(index).id.type, DesktopMediaID::TYPE_WINDOW); + EXPECT_EQ(model_->GetSource(index).id.id, index); +} + +#if defined(ENABLE_AURA_WINDOW_TESTS) +TEST_F(NativeDesktopMediaListTest, AddAuraWindow) { + AddWindowsAndVerify(false); + + base::RunLoop run_loop; + + const int index = kDefaultWindowCount; + EXPECT_CALL(observer_, OnSourceAdded(model_.get(), index)) + .WillOnce( + DoAll(CheckListSize(model_.get(), kDefaultWindowCount + 1), + QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop))); + + AddAuraWindow(); + window_capturer_->SetWindowList(window_list_); + + run_loop.Run(); + + int native_id = window_list_.back().id; + EXPECT_EQ(model_->GetSource(index).id.type, DesktopMediaID::TYPE_WINDOW); + EXPECT_EQ(model_->GetSource(index).id.id, native_id); + EXPECT_EQ(model_->GetSource(index).id.window_id, + native_aura_id_map_[native_id]); +} +#endif // defined(ENABLE_AURA_WINDOW_TESTS) + +TEST_F(NativeDesktopMediaListTest, RemoveNativeWindow) { + AddWindowsAndVerify(false); + + base::RunLoop run_loop; + + EXPECT_CALL(observer_, OnSourceRemoved(model_.get(), 0)) + .WillOnce( + DoAll(CheckListSize(model_.get(), kDefaultWindowCount - 1), + QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop))); + + window_list_.erase(window_list_.begin()); + window_capturer_->SetWindowList(window_list_); + + run_loop.Run(); +} + +#if defined(ENABLE_AURA_WINDOW_TESTS) +TEST_F(NativeDesktopMediaListTest, RemoveAuraWindow) { + AddWindowsAndVerify(false); + + base::RunLoop run_loop; + + int aura_window_start_index = kDefaultWindowCount - kDefaultAuraCount; + EXPECT_CALL(observer_, OnSourceRemoved(model_.get(), aura_window_start_index)) + .WillOnce( + DoAll(CheckListSize(model_.get(), kDefaultWindowCount - 1), + QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop))); + + RemoveAuraWindow(0); + window_capturer_->SetWindowList(window_list_); + + run_loop.Run(); +} +#endif // defined(ENABLE_AURA_WINDOW_TESTS) + +TEST_F(NativeDesktopMediaListTest, RemoveAllWindows) { + AddWindowsAndVerify(false); + + base::RunLoop run_loop; + + testing::InSequence seq; + for (int i = 0; i < kDefaultWindowCount - 1; i++) { + EXPECT_CALL(observer_, OnSourceRemoved(model_.get(), 0)) + .WillOnce(CheckListSize(model_.get(), kDefaultWindowCount - i - 1)); + } + EXPECT_CALL(observer_, OnSourceRemoved(model_.get(), 0)) + .WillOnce( + DoAll(CheckListSize(model_.get(), 0), + QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop))); + + window_list_.clear(); + window_capturer_->SetWindowList(window_list_); + + run_loop.Run(); +} + +TEST_F(NativeDesktopMediaListTest, UpdateTitle) { + AddWindowsAndVerify(false); + + base::RunLoop run_loop; + + EXPECT_CALL(observer_, OnSourceNameChanged(model_.get(), 0)) + .WillOnce(QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop)); + + const std::string kTestTitle = "New Title"; + window_list_[0].title = kTestTitle; + window_capturer_->SetWindowList(window_list_); + + run_loop.Run(); + + EXPECT_EQ(model_->GetSource(0).name, base::UTF8ToUTF16(kTestTitle)); +} + +TEST_F(NativeDesktopMediaListTest, UpdateThumbnail) { + AddWindowsAndVerify(false); + + // Aura windows' thumbnails may unpredictably change over time. + for (size_t i = kDefaultWindowCount - kDefaultAuraCount; + i < kDefaultWindowCount; ++i) { + EXPECT_CALL(observer_, OnSourceThumbnailChanged(model_.get(), i)) + .Times(testing::AnyNumber()); + } + + base::RunLoop run_loop; + + EXPECT_CALL(observer_, OnSourceThumbnailChanged(model_.get(), 0)) + .WillOnce(QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop)); + + // Update frame for the window and verify that we get notification about it. + window_capturer_->SetNextFrameValue(0, 10); + + run_loop.Run(); +} + +TEST_F(NativeDesktopMediaListTest, MoveWindow) { + AddWindowsAndVerify(false); + + base::RunLoop run_loop; + + EXPECT_CALL(observer_, OnSourceMoved(model_.get(), 1, 0)) + .WillOnce( + DoAll(CheckListSize(model_.get(), kDefaultWindowCount), + QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop))); + + std::swap(window_list_[0], window_list_[1]); + window_capturer_->SetWindowList(window_list_); + + run_loop.Run(); +} + +// This test verifies that webrtc::DesktopCapturer::CaptureFrame() is not +// called when the thumbnail size is empty. +TEST_F(NativeDesktopMediaListTest, EmptyThumbnail) { + window_capturer_ = new FakeWindowCapturer(); + model_ = std::make_unique<NativeDesktopMediaList>( + DesktopMediaID::TYPE_WINDOW, base::WrapUnique(window_capturer_)); + model_->SetThumbnailSize(gfx::Size()); + + // Set update period to reduce the time it takes to run tests. + model_->SetUpdatePeriod(base::TimeDelta::FromMilliseconds(20)); + + base::RunLoop run_loop; + + EXPECT_CALL(observer_, OnSourceAdded(model_.get(), 0)) + .WillOnce( + DoAll(CheckListSize(model_.get(), 1), + QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop))); + // Called upon webrtc::DesktopCapturer::CaptureFrame() call. + ON_CALL(observer_, OnSourceThumbnailChanged(_, _)) + .WillByDefault(testing::InvokeWithoutArgs([]() { NOTREACHED(); })); + + model_->StartUpdating(&observer_); + + AddNativeWindow(0); + window_capturer_->SetWindowList(window_list_); + + run_loop.Run(); + + EXPECT_EQ(model_->GetSource(0).id.type, DesktopMediaID::TYPE_WINDOW); + EXPECT_EQ(model_->GetSource(0).id.id, 0); + EXPECT_EQ(model_->GetSource(0).thumbnail.size(), gfx::Size()); +} diff --git a/chromium/chrome/browser/media/webrtc/permission_bubble_media_access_handler.cc b/chromium/chrome/browser/media/webrtc/permission_bubble_media_access_handler.cc new file mode 100644 index 00000000000..c6303424cf2 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/permission_bubble_media_access_handler.cc @@ -0,0 +1,324 @@ +// Copyright 2015 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 "chrome/browser/media/webrtc/permission_bubble_media_access_handler.h" + +#include <memory> +#include <utility> + +#include "base/bind.h" +#include "base/metrics/field_trial.h" +#include "base/task/post_task.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/media_stream_device_permissions.h" +#include "chrome/browser/media/webrtc/media_stream_devices_controller.h" +#include "chrome/browser/permissions/permission_manager.h" +#include "chrome/browser/permissions/permission_result.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/pref_names.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/web_contents.h" + +#if defined(OS_ANDROID) +#include <vector> + +#include "chrome/browser/android/chrome_feature_list.h" +#include "chrome/browser/media/webrtc/screen_capture_infobar_delegate_android.h" +#include "chrome/browser/permissions/permission_uma_util.h" +#include "chrome/browser/permissions/permission_util.h" +#endif // defined(OS_ANDROID) + +#if defined(OS_MACOSX) +#include "base/metrics/histogram_macros.h" +#include "chrome/browser/content_settings/chrome_content_settings_utils.h" +#include "chrome/browser/media/webrtc/system_media_capture_permissions_mac.h" +#include "chrome/browser/media/webrtc/system_media_capture_permissions_stats_mac.h" +#endif + +using content::BrowserThread; + +using RepeatingMediaResponseCallback = + base::RepeatingCallback<void(const blink::MediaStreamDevices& devices, + blink::mojom::MediaStreamRequestResult result, + std::unique_ptr<content::MediaStreamUI> ui)>; + +#if defined(OS_MACOSX) +using system_media_permissions::SystemPermission; +#endif + +struct PermissionBubbleMediaAccessHandler::PendingAccessRequest { + PendingAccessRequest(const content::MediaStreamRequest& request, + RepeatingMediaResponseCallback callback) + : request(request), callback(callback) {} + ~PendingAccessRequest() {} + + // TODO(gbillock): make the MediaStreamDevicesController owned by + // this object when we're using bubbles. + content::MediaStreamRequest request; + RepeatingMediaResponseCallback callback; +}; + +PermissionBubbleMediaAccessHandler::PermissionBubbleMediaAccessHandler() { + // PermissionBubbleMediaAccessHandler should be created on UI thread. + // Otherwise, it will not receive + // content::NOTIFICATION_WEB_CONTENTS_DESTROYED, and that will result in + // possible use after free. + DCHECK_CURRENTLY_ON(BrowserThread::UI); + notifications_registrar_.Add(this, + content::NOTIFICATION_WEB_CONTENTS_DESTROYED, + content::NotificationService::AllSources()); +} + +PermissionBubbleMediaAccessHandler::~PermissionBubbleMediaAccessHandler() {} + +bool PermissionBubbleMediaAccessHandler::SupportsStreamType( + content::WebContents* web_contents, + const blink::mojom::MediaStreamType type, + const extensions::Extension* extension) { +#if defined(OS_ANDROID) + return type == blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE || + type == blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE || + type == blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE || + type == blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE; +#else + return type == blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE || + type == blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE; +#endif +} + +bool PermissionBubbleMediaAccessHandler::CheckMediaAccessPermission( + content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type, + const extensions::Extension* extension) { + content::WebContents* web_contents = + content::WebContents::FromRenderFrameHost(render_frame_host); + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + ContentSettingsType content_settings_type = + type == blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE + ? CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC + : CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA; + + DCHECK(!security_origin.is_empty()); + GURL embedding_origin = web_contents->GetLastCommittedURL().GetOrigin(); + PermissionManager* permission_manager = PermissionManager::Get(profile); + return permission_manager + ->GetPermissionStatusForFrame(content_settings_type, + render_frame_host, security_origin) + .content_setting == CONTENT_SETTING_ALLOW; +} + +void PermissionBubbleMediaAccessHandler::HandleRequest( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + +#if defined(OS_ANDROID) + if (blink::IsScreenCaptureMediaType(request.video_type) && + !base::FeatureList::IsEnabled( + chrome::android::kUserMediaScreenCapturing)) { + // If screen capturing isn't enabled on Android, we'll use "invalid state" + // as result, same as on desktop. + std::move(callback).Run( + blink::MediaStreamDevices(), + blink::mojom::MediaStreamRequestResult::INVALID_STATE, nullptr); + return; + } +#endif // defined(OS_ANDROID) + + RequestsMap& requests_map = pending_requests_[web_contents]; + requests_map.emplace( + next_request_id_++, + PendingAccessRequest( + request, base::AdaptCallbackForRepeating(std::move(callback)))); + + // If this is the only request then show the infobar. + if (requests_map.size() == 1) + ProcessQueuedAccessRequest(web_contents); +} + +void PermissionBubbleMediaAccessHandler::ProcessQueuedAccessRequest( + content::WebContents* web_contents) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + auto it = pending_requests_.find(web_contents); + + if (it == pending_requests_.end() || it->second.empty()) { + // Don't do anything if the tab was closed. + return; + } + + DCHECK(!it->second.empty()); + + const int request_id = it->second.begin()->first; + const content::MediaStreamRequest& request = + it->second.begin()->second.request; +#if defined(OS_ANDROID) + if (blink::IsScreenCaptureMediaType(request.video_type)) { + ScreenCaptureInfoBarDelegateAndroid::Create( + web_contents, request, + base::Bind(&PermissionBubbleMediaAccessHandler::OnAccessRequestResponse, + base::Unretained(this), web_contents, request_id)); + return; + } +#endif + + MediaStreamDevicesController::RequestPermissions( + request, + base::Bind(&PermissionBubbleMediaAccessHandler::OnAccessRequestResponse, + base::Unretained(this), web_contents, request_id)); +} + +void PermissionBubbleMediaAccessHandler::UpdateMediaRequestState( + int render_process_id, + int render_frame_id, + int page_request_id, + blink::mojom::MediaStreamType stream_type, + content::MediaRequestState state) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (state != content::MEDIA_REQUEST_STATE_CLOSING) + return; + + bool found = false; + for (auto requests_it = pending_requests_.begin(); + requests_it != pending_requests_.end(); ++requests_it) { + RequestsMap& requests_map = requests_it->second; + for (RequestsMap::iterator it = requests_map.begin(); + it != requests_map.end(); ++it) { + if (it->second.request.render_process_id == render_process_id && + it->second.request.render_frame_id == render_frame_id && + it->second.request.page_request_id == page_request_id) { + requests_map.erase(it); + found = true; + break; + } + } + if (found) + break; + } +} + +void PermissionBubbleMediaAccessHandler::OnAccessRequestResponse( + content::WebContents* web_contents, + int request_id, + const blink::MediaStreamDevices& devices, + blink::mojom::MediaStreamRequestResult result, + std::unique_ptr<content::MediaStreamUI> ui) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + auto request_maps_it = pending_requests_.find(web_contents); + if (request_maps_it == pending_requests_.end()) { + // WebContents has been destroyed. Don't need to do anything. + return; + } + + RequestsMap& requests_map(request_maps_it->second); + if (requests_map.empty()) + return; + + auto request_it = requests_map.find(request_id); + DCHECK(request_it != requests_map.end()); + if (request_it == requests_map.end()) + return; + + blink::mojom::MediaStreamRequestResult final_result = result; + +#if defined(OS_MACOSX) + // If the request was approved, ask for system permissions if needed, and run + // this function again when done. + if (result == blink::mojom::MediaStreamRequestResult::OK) { + const content::MediaStreamRequest& request = request_it->second.request; + if (request.audio_type == + blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE) { + const SystemPermission system_audio_permission = + system_media_permissions::CheckSystemAudioCapturePermission(); + UMA_HISTOGRAM_ENUMERATION( + "Media.Audio.Capture.Mac.MicSystemPermission.UserMedia", + system_audio_permission); + if (system_audio_permission == SystemPermission::kNotDetermined) { + // Using WeakPtr since callback can come at any time and we might be + // destroyed. + system_media_permissions::RequestSystemAudioCapturePermisson( + base::BindOnce( + &PermissionBubbleMediaAccessHandler::OnAccessRequestResponse, + weak_factory_.GetWeakPtr(), web_contents, request_id, devices, + result, std::move(ui)), + {content::BrowserThread::UI}); + return; + } else if (system_audio_permission == SystemPermission::kRestricted || + system_audio_permission == SystemPermission::kDenied) { + content_settings::UpdateLocationBarUiForWebContents(web_contents); + final_result = + blink::mojom::MediaStreamRequestResult::SYSTEM_PERMISSION_DENIED; + system_media_permissions::SystemAudioCapturePermissionBlocked(); + } else { + DCHECK_EQ(system_audio_permission, SystemPermission::kAllowed); + content_settings::UpdateLocationBarUiForWebContents(web_contents); + } + } + if (request.video_type == + blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE) { + const SystemPermission system_video_permission = + system_media_permissions::CheckSystemVideoCapturePermission(); + UMA_HISTOGRAM_ENUMERATION( + "Media.Video.Capture.Mac.CameraSystemPermission.UserMedia", + system_video_permission); + if (system_video_permission == SystemPermission::kNotDetermined) { + // Using WeakPtr since callback can come at any time and we might be + // destroyed. + system_media_permissions::RequestSystemVideoCapturePermisson( + base::BindOnce( + &PermissionBubbleMediaAccessHandler::OnAccessRequestResponse, + weak_factory_.GetWeakPtr(), web_contents, request_id, devices, + result, std::move(ui)), + {content::BrowserThread::UI}); + return; + } else if (system_video_permission == SystemPermission::kRestricted || + system_video_permission == SystemPermission::kDenied) { + content_settings::UpdateLocationBarUiForWebContents(web_contents); + final_result = + blink::mojom::MediaStreamRequestResult::SYSTEM_PERMISSION_DENIED; + system_media_permissions::SystemVideoCapturePermissionBlocked(); + } else { + DCHECK_EQ(system_video_permission, SystemPermission::kAllowed); + content_settings::UpdateLocationBarUiForWebContents(web_contents); + } + } + } +#endif // defined(OS_MACOSX) + + RepeatingMediaResponseCallback callback = + std::move(request_it->second.callback); + requests_map.erase(request_it); + + if (!requests_map.empty()) { + // Post a task to process next queued request. It has to be done + // asynchronously to make sure that calling infobar is not destroyed until + // after this function returns. + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce( + &PermissionBubbleMediaAccessHandler::ProcessQueuedAccessRequest, + base::Unretained(this), web_contents)); + } + + std::move(callback).Run(devices, final_result, std::move(ui)); +} + +void PermissionBubbleMediaAccessHandler::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK_EQ(content::NOTIFICATION_WEB_CONTENTS_DESTROYED, type); + + pending_requests_.erase(content::Source<content::WebContents>(source).ptr()); +} diff --git a/chromium/chrome/browser/media/webrtc/permission_bubble_media_access_handler.h b/chromium/chrome/browser/media/webrtc/permission_bubble_media_access_handler.h new file mode 100644 index 00000000000..53da4bb7a94 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/permission_bubble_media_access_handler.h @@ -0,0 +1,67 @@ +// Copyright 2015 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 CHROME_BROWSER_MEDIA_WEBRTC_PERMISSION_BUBBLE_MEDIA_ACCESS_HANDLER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_PERMISSION_BUBBLE_MEDIA_ACCESS_HANDLER_H_ + +#include <map> + +#include "base/memory/weak_ptr.h" +#include "chrome/browser/media/media_access_handler.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" +#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" + +// MediaAccessHandler for permission bubble requests. +class PermissionBubbleMediaAccessHandler + : public MediaAccessHandler, + public content::NotificationObserver { + public: + PermissionBubbleMediaAccessHandler(); + ~PermissionBubbleMediaAccessHandler() override; + + // MediaAccessHandler implementation. + bool SupportsStreamType(content::WebContents* web_contents, + const blink::mojom::MediaStreamType type, + const extensions::Extension* extension) override; + bool CheckMediaAccessPermission( + content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type, + const extensions::Extension* extension) override; + void HandleRequest(content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) override; + void UpdateMediaRequestState(int render_process_id, + int render_frame_id, + int page_request_id, + blink::mojom::MediaStreamType stream_type, + content::MediaRequestState state) override; + + private: + struct PendingAccessRequest; + using RequestsMap = std::map<int, PendingAccessRequest>; + using RequestsMaps = std::map<content::WebContents*, RequestsMap>; + + void ProcessQueuedAccessRequest(content::WebContents* web_contents); + void OnAccessRequestResponse(content::WebContents* web_contents, + int request_id, + const blink::MediaStreamDevices& devices, + blink::mojom::MediaStreamRequestResult result, + std::unique_ptr<content::MediaStreamUI> ui); + + // content::NotificationObserver implementation. + void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) override; + + int next_request_id_ = 0; + RequestsMaps pending_requests_; + content::NotificationRegistrar notifications_registrar_; + + base::WeakPtrFactory<PermissionBubbleMediaAccessHandler> weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_PERMISSION_BUBBLE_MEDIA_ACCESS_HANDLER_H_ diff --git a/chromium/chrome/browser/media/webrtc/rtp_dump_type.h b/chromium/chrome/browser/media/webrtc/rtp_dump_type.h new file mode 100644 index 00000000000..e4fc33dc8fd --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/rtp_dump_type.h @@ -0,0 +1,12 @@ +// Copyright 2014 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 CHROME_BROWSER_MEDIA_WEBRTC_RTP_DUMP_TYPE_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_RTP_DUMP_TYPE_H_ + +// The types of RTP header dumps: incoming packets only, outgoing packets only, +// and both incoming and outgoing packets. +enum RtpDumpType { RTP_DUMP_INCOMING, RTP_DUMP_OUTGOING, RTP_DUMP_BOTH }; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_RTP_DUMP_TYPE_H_ diff --git a/chromium/chrome/browser/media/webrtc/screen_capture_infobar_delegate_android.cc b/chromium/chrome/browser/media/webrtc/screen_capture_infobar_delegate_android.cc new file mode 100644 index 00000000000..d891933c061 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/screen_capture_infobar_delegate_android.cc @@ -0,0 +1,109 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/screen_capture_infobar_delegate_android.h" + +#include "base/callback_helpers.h" +#include "chrome/browser/android/android_theme_resources.h" +#include "chrome/browser/infobars/infobar_service.h" +#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" +#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h" +#include "chrome/grit/generated_resources.h" +#include "components/infobars/core/infobar.h" +#include "components/url_formatter/elide_url.h" +#include "content/public/browser/desktop_media_id.h" +#include "content/public/browser/web_contents.h" +#include "third_party/blink/public/common/mediastream/media_stream_request.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h" +#include "ui/base/l10n/l10n_util.h" + +// static +void ScreenCaptureInfoBarDelegateAndroid::Create( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback) { + InfoBarService* infobar_service = + InfoBarService::FromWebContents(web_contents); + + infobar_service->AddInfoBar(infobar_service->CreateConfirmInfoBar( + std::unique_ptr<ConfirmInfoBarDelegate>( + new ScreenCaptureInfoBarDelegateAndroid(web_contents, request, + std::move(callback))))); +} + +ScreenCaptureInfoBarDelegateAndroid::ScreenCaptureInfoBarDelegateAndroid( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback) + : web_contents_(web_contents), + request_(request), + callback_(std::move(callback)) { + DCHECK_EQ(blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, + request.video_type); +} + +ScreenCaptureInfoBarDelegateAndroid::~ScreenCaptureInfoBarDelegateAndroid() { + if (!callback_.is_null()) { + std::move(callback_).Run( + blink::MediaStreamDevices(), + blink::mojom::MediaStreamRequestResult::FAILED_DUE_TO_SHUTDOWN, + nullptr); + } +} + +infobars::InfoBarDelegate::InfoBarIdentifier +ScreenCaptureInfoBarDelegateAndroid::GetIdentifier() const { + return SCREEN_CAPTURE_INFOBAR_DELEGATE_ANDROID; +} + +base::string16 ScreenCaptureInfoBarDelegateAndroid::GetMessageText() const { + return l10n_util::GetStringFUTF16( + IDS_MEDIA_CAPTURE_SCREEN_INFOBAR_TEXT, + url_formatter::FormatUrlForSecurityDisplay(request_.security_origin)); +} + +int ScreenCaptureInfoBarDelegateAndroid::GetIconId() const { + return IDR_ANDROID_INFOBAR_MEDIA_STREAM_SCREEN; +} + +base::string16 ScreenCaptureInfoBarDelegateAndroid::GetButtonLabel( + InfoBarButton button) const { + return l10n_util::GetStringUTF16((button == BUTTON_OK) ? IDS_PERMISSION_ALLOW + : IDS_PERMISSION_DENY); +} + +bool ScreenCaptureInfoBarDelegateAndroid::Accept() { + RunCallback(blink::mojom::MediaStreamRequestResult::OK); + return true; +} + +bool ScreenCaptureInfoBarDelegateAndroid::Cancel() { + RunCallback(blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED); + return true; +} + +void ScreenCaptureInfoBarDelegateAndroid::InfoBarDismissed() { + RunCallback(blink::mojom::MediaStreamRequestResult::PERMISSION_DISMISSED); +} + +void ScreenCaptureInfoBarDelegateAndroid::RunCallback( + blink::mojom::MediaStreamRequestResult result) { + DCHECK(!callback_.is_null()); + + blink::MediaStreamDevices devices; + std::unique_ptr<content::MediaStreamUI> ui; + if (result == blink::mojom::MediaStreamRequestResult::OK) { + content::DesktopMediaID screen_id = content::DesktopMediaID( + content::DesktopMediaID::TYPE_SCREEN, webrtc::kFullDesktopScreenId); + devices.push_back(blink::MediaStreamDevice( + blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, + screen_id.ToString(), "Screen")); + + ui = MediaCaptureDevicesDispatcher::GetInstance() + ->GetMediaStreamCaptureIndicator() + ->RegisterMediaStream(web_contents_, devices); + } + + std::move(callback_).Run(devices, result, std::move(ui)); +} diff --git a/chromium/chrome/browser/media/webrtc/screen_capture_infobar_delegate_android.h b/chromium/chrome/browser/media/webrtc/screen_capture_infobar_delegate_android.h new file mode 100644 index 00000000000..829212ce022 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/screen_capture_infobar_delegate_android.h @@ -0,0 +1,52 @@ +// Copyright 2016 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 CHROME_BROWSER_MEDIA_WEBRTC_SCREEN_CAPTURE_INFOBAR_DELEGATE_ANDROID_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_SCREEN_CAPTURE_INFOBAR_DELEGATE_ANDROID_H_ + +#include "chrome/browser/media/media_access_handler.h" +#include "components/infobars/core/confirm_infobar_delegate.h" +#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" + +namespace content { +class WebContents; +} + +// An infobar that allows the user to share their screen with the current page. +class ScreenCaptureInfoBarDelegateAndroid : public ConfirmInfoBarDelegate { + public: + // Creates a screen capture infobar and delegate and adds the infobar to the + // InfoBarService associated with |web_contents|. + static void Create(content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback); + + private: + ScreenCaptureInfoBarDelegateAndroid( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback); + ~ScreenCaptureInfoBarDelegateAndroid() override; + + // ConfirmInfoBarDelegate: + infobars::InfoBarDelegate::InfoBarIdentifier GetIdentifier() const override; + base::string16 GetMessageText() const override; + int GetIconId() const override; + base::string16 GetButtonLabel(InfoBarButton button) const override; + bool Accept() override; + bool Cancel() override; + void InfoBarDismissed() override; + + // Runs |callback_|, passing it the |result|, and (if permission was granted) + // the appropriate stream device and UI object for video capture. + void RunCallback(blink::mojom::MediaStreamRequestResult result); + + content::WebContents* web_contents_; + const content::MediaStreamRequest request_; + content::MediaResponseCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(ScreenCaptureInfoBarDelegateAndroid); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_SCREEN_CAPTURE_INFOBAR_DELEGATE_ANDROID_H_ diff --git a/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_mac.h b/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_mac.h new file mode 100644 index 00000000000..73b04535322 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_mac.h @@ -0,0 +1,57 @@ +// Copyright 2019 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 CHROME_BROWSER_MEDIA_WEBRTC_SYSTEM_MEDIA_CAPTURE_PERMISSIONS_MAC_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_SYSTEM_MEDIA_CAPTURE_PERMISSIONS_MAC_H_ + +#include "base/callback_forward.h" + +namespace base { +class TaskTraits; +} + +namespace system_media_permissions { + +class MediaAuthorizationWrapper; + +// System permission state. These are also used in stats - do not remove or +// re-arrange the values. +enum class SystemPermission { + kNotDetermined = 0, + kRestricted = 1, + kDenied = 2, + kAllowed = 3, + kMaxValue = kAllowed +}; + +// On 10.14 and above: returns the system permission. +// On 10.13 and below: returns |SystemPermission::kAllowed|, since there are no +// system media capture permissions. +SystemPermission CheckSystemAudioCapturePermission(); +SystemPermission CheckSystemVideoCapturePermission(); + +// On 10.15 and above: returns the system permission. +// On 10.14 and below: returns |SystemPermission::kAllowed|, since there are no +// system screen capture permissions. +SystemPermission CheckSystemScreenCapturePermission(); + +// On 10.14 and above: requests system permission and returns. When requesting +// permission, the OS will show a user dialog and respond asynchronously. At the +// response, |callback| is posted with |traits|. +// On 10.13 and below: posts |callback| with |traits|, since there are no system +// media capture permissions. +// Note: these functions should really never be called for pre-10.14 since one +// would normally check the permission first, and only call this if it's not +// determined. +void RequestSystemAudioCapturePermisson(base::OnceClosure callback, + const base::TaskTraits& traits); +void RequestSystemVideoCapturePermisson(base::OnceClosure callback, + const base::TaskTraits& traits); + +// Sets the wrapper object for OS calls. For test mocking purposes. +void SetMediaAuthorizationWrapperForTesting(MediaAuthorizationWrapper* wrapper); + +} // namespace system_media_permissions + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_SYSTEM_MEDIA_CAPTURE_PERMISSIONS_MAC_H_ diff --git a/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_mac.mm b/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_mac.mm new file mode 100644 index 00000000000..66db2533c01 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_mac.mm @@ -0,0 +1,246 @@ +// Copyright 2019 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. +// +// Authorization functions and types are available on 10.14+. +// To avoid availability compile errors, use performSelector invocation of +// functions, NSInteger instead of AVAuthorizationStatus, and NSString* instead +// of AVMediaType. +// The AVAuthorizationStatus enum is defined as follows (10.14 SDK): +// AVAuthorizationStatusNotDetermined = 0, +// AVAuthorizationStatusRestricted = 1, +// AVAuthorizationStatusDenied = 2, +// AVAuthorizationStatusAuthorized = 3, +// TODO(grunell): Call functions directly and use AVAuthorizationStatus once +// we use the 10.14 SDK. + +#include "chrome/browser/media/webrtc/system_media_capture_permissions_mac.h" + +#import <AVFoundation/AVFoundation.h> + +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/command_line.h" +#include "base/feature_list.h" +#include "base/logging.h" +#include "base/mac/foundation_util.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/macros.h" +#include "base/no_destructor.h" +#include "base/task/post_task.h" +#include "base/task/task_traits.h" +#include "chrome/browser/media/webrtc/media_authorization_wrapper_mac.h" +#include "chrome/common/chrome_features.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "media/base/media_switches.h" + +namespace system_media_permissions { + +namespace { + +bool UsingFakeMediaDevices() { + return base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kUseFakeDeviceForMediaStream); +} + +// Pointer to OS call wrapper that tests can set. +MediaAuthorizationWrapper* g_media_authorization_wrapper_for_tests = nullptr; + +// Implementation of OS call wrapper that does the actual OS calls. +class MediaAuthorizationWrapperImpl : public MediaAuthorizationWrapper { + public: + MediaAuthorizationWrapperImpl() = default; + ~MediaAuthorizationWrapperImpl() final = default; + + NSInteger AuthorizationStatusForMediaType(NSString* media_type) final { + if (@available(macOS 10.14, *)) { + AVCaptureDevice* target = [AVCaptureDevice class]; + SEL selector = @selector(authorizationStatusForMediaType:); + NSInteger auth_status = 0; + if ([target respondsToSelector:selector]) { + auth_status = + (NSInteger)[target performSelector:selector withObject:media_type]; + } else { + DLOG(WARNING) + << "authorizationStatusForMediaType could not be executed"; + } + return auth_status; + } + + NOTREACHED(); + return 0; + } + + void RequestAccessForMediaType(NSString* media_type, + base::RepeatingClosure callback, + const base::TaskTraits& traits) final { + if (@available(macOS 10.14, *)) { + AVCaptureDevice* target = [AVCaptureDevice class]; + SEL selector = @selector(requestAccessForMediaType:completionHandler:); + if ([target respondsToSelector:selector]) { + [target performSelector:selector + withObject:media_type + withObject:^(BOOL granted) { + base::PostTask(FROM_HERE, traits, std::move(callback)); + }]; + } else { + DLOG(WARNING) << "requestAccessForMediaType could not be executed"; + base::PostTask(FROM_HERE, traits, std::move(callback)); + } + } else { + NOTREACHED(); + base::PostTask(FROM_HERE, traits, std::move(callback)); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(MediaAuthorizationWrapperImpl); +}; + +MediaAuthorizationWrapper& GetMediaAuthorizationWrapper() { + if (g_media_authorization_wrapper_for_tests) + return *g_media_authorization_wrapper_for_tests; + + static base::NoDestructor<MediaAuthorizationWrapperImpl> + media_authorization_wrapper; + return *media_authorization_wrapper; +} + +NSInteger MediaAuthorizationStatus(NSString* media_type) { + if (@available(macOS 10.14, *)) { + return GetMediaAuthorizationWrapper().AuthorizationStatusForMediaType( + media_type); + } + + NOTREACHED(); + return 0; +} + +SystemPermission CheckSystemMediaCapturePermission(NSString* media_type) { + if (UsingFakeMediaDevices()) + return SystemPermission::kAllowed; + + if (@available(macOS 10.14, *)) { + NSInteger auth_status = MediaAuthorizationStatus(media_type); + switch (auth_status) { + case 0: + return SystemPermission::kNotDetermined; + case 1: + return SystemPermission::kRestricted; + case 2: + return SystemPermission::kDenied; + case 3: + return SystemPermission::kAllowed; + default: + NOTREACHED(); + return SystemPermission::kAllowed; + } + } + + // On pre-10.14, there are no system permissions, so we return allowed. + return SystemPermission::kAllowed; +} + +// Use RepeatingCallback since it must be copyable for use in the block. It's +// only called once though. +void RequestSystemMediaCapturePermission(NSString* media_type, + base::RepeatingClosure callback, + const base::TaskTraits& traits) { + if (UsingFakeMediaDevices()) { + base::PostTask(FROM_HERE, traits, std::move(callback)); + return; + } + + if (@available(macOS 10.14, *)) { + GetMediaAuthorizationWrapper().RequestAccessForMediaType( + media_type, std::move(callback), traits); + } else { + NOTREACHED(); + // Should never happen since for pre-10.14 system permissions don't exist + // and checking them in CheckSystemAudioCapturePermission() will always + // return allowed, and this function should not be called. + base::PostTask(FROM_HERE, traits, std::move(callback)); + } +} + +// Heuristic to check screen capture permission on macOS 10.15. +// Screen Capture is considered allowed if the name of at least one normal +// or dock window running on another process is visible. +// See https://crbug.com/993692. +bool IsScreenCaptureAllowed() { + if (@available(macOS 10.15, *)) { + if (!base::FeatureList::IsEnabled( + features::kMacSystemScreenCapturePermissionCheck)) { + return true; + } + + base::ScopedCFTypeRef<CFArrayRef> window_list( + CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID)); + int current_pid = [[NSProcessInfo processInfo] processIdentifier]; + for (NSDictionary* window in base::mac::CFToNSCast(window_list.get())) { + NSNumber* window_pid = + [window objectForKey:base::mac::CFToNSCast(kCGWindowOwnerPID)]; + if (!window_pid || [window_pid integerValue] == current_pid) + continue; + + NSString* window_name = + [window objectForKey:base::mac::CFToNSCast(kCGWindowName)]; + if (!window_name) + continue; + + NSNumber* layer = + [window objectForKey:base::mac::CFToNSCast(kCGWindowLayer)]; + if (!layer) + continue; + + NSInteger layer_integer = [layer integerValue]; + if (layer_integer == CGWindowLevelForKey(kCGNormalWindowLevelKey) || + layer_integer == CGWindowLevelForKey(kCGDockWindowLevelKey)) { + return true; + } + } + return false; + } + + // Screen capture is always allowed in older macOS versions. + return true; +} + +} // namespace + +SystemPermission CheckSystemAudioCapturePermission() { + return CheckSystemMediaCapturePermission(AVMediaTypeAudio); +} + +SystemPermission CheckSystemVideoCapturePermission() { + return CheckSystemMediaCapturePermission(AVMediaTypeVideo); +} + +SystemPermission CheckSystemScreenCapturePermission() { + return IsScreenCaptureAllowed() ? SystemPermission::kAllowed + : SystemPermission::kDenied; +} + +void RequestSystemAudioCapturePermisson(base::OnceClosure callback, + const base::TaskTraits& traits) { + RequestSystemMediaCapturePermission( + AVMediaTypeAudio, base::AdaptCallbackForRepeating(std::move(callback)), + traits); +} + +void RequestSystemVideoCapturePermisson(base::OnceClosure callback, + const base::TaskTraits& traits) { + RequestSystemMediaCapturePermission( + AVMediaTypeVideo, base::AdaptCallbackForRepeating(std::move(callback)), + traits); +} + +void SetMediaAuthorizationWrapperForTesting( + MediaAuthorizationWrapper* wrapper) { + CHECK(!g_media_authorization_wrapper_for_tests); + g_media_authorization_wrapper_for_tests = wrapper; +} + +} // namespace system_media_permissions diff --git a/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_stats_mac.h b/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_stats_mac.h new file mode 100644 index 00000000000..9e1269beb47 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_stats_mac.h @@ -0,0 +1,36 @@ +// Copyright 2019 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. +// +// Functions for handling stats for system media permissions (camera, +// microphone). + +#ifndef CHROME_BROWSER_MEDIA_WEBRTC_SYSTEM_MEDIA_CAPTURE_PERMISSIONS_STATS_MAC_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_SYSTEM_MEDIA_CAPTURE_PERMISSIONS_STATS_MAC_H_ + +#include "chrome/browser/media/webrtc/system_media_capture_permissions_mac.h" + +class PrefRegistrySimple; + +namespace system_media_permissions { + +// Registers preferences used for system media permissions stats. +void RegisterSystemMediaPermissionStatesPrefs(PrefRegistrySimple* registry); + +// Logs stats for system media permissions. Called once per browser session, at +// browser start. +void LogSystemMediaPermissionsStartupStats(); + +// Called when a system permission goes from "not determined" to another state. +// The new permission is logged as startup state. +void SystemAudioCapturePermissionDetermined(SystemPermission permission); +void SystemVideoCapturePermissionDetermined(SystemPermission permission); + +// Called when a system permission was requested but was blocked. Information +// stored is later used when logging stats at startup. +void SystemAudioCapturePermissionBlocked(); +void SystemVideoCapturePermissionBlocked(); + +} // namespace system_media_permissions + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_SYSTEM_MEDIA_CAPTURE_PERMISSIONS_STATS_MAC_H_ diff --git a/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_stats_mac.mm b/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_stats_mac.mm new file mode 100644 index 00000000000..8c85e68cb36 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/system_media_capture_permissions_stats_mac.mm @@ -0,0 +1,196 @@ +// Copyright 2019 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 "chrome/browser/media/webrtc/system_media_capture_permissions_stats_mac.h" + +#include "base/metrics/histogram_functions.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/media/webrtc/system_media_capture_permissions_mac.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/pref_service.h" + +namespace system_media_permissions { + +namespace { + +const char kSystemPermissionMicFirstBlockedTimePref[] = + "system_permission.mic.first_blocked_time"; +const char kSystemPermissionMicLastBlockedTimePref[] = + "system_permission.mic.last_blocked_time"; +const char kSystemPermissionCameraFirstBlockedTimePref[] = + "system_permission.camera.first_blocked_time"; +const char kSystemPermissionCameraLastBlockedTimePref[] = + "system_permission.camera.last_blocked_time"; + +void LogStartupMicSystemPermission(SystemPermission permission) { + base::UmaHistogramEnumeration( + "Media.Audio.Capture.Mac.MicSystemPermission.Startup", permission); +} + +void LogStartupCameraSystemPermission(SystemPermission permission) { + base::UmaHistogramEnumeration( + "Media.Video.Capture.Mac.CameraSystemPermission.Startup", permission); +} + +void MaybeLogAdditionalMicSystemPermissionStats(SystemPermission permission) { + PrefService* prefs = g_browser_process->local_state(); + + if (!prefs->HasPrefPath(kSystemPermissionMicFirstBlockedTimePref)) { + DCHECK(!prefs->HasPrefPath(kSystemPermissionMicLastBlockedTimePref)); + return; + } + + // A pref exists, so there was a failure accessing the mic due to blocked + // system permission before the last restart. Log additional stats. + + DCHECK(prefs->HasPrefPath(kSystemPermissionMicLastBlockedTimePref)); + base::UmaHistogramEnumeration("Media.Audio.Capture.Mac.MicSystemPermission." + "StartupAfterFailure", + permission); + + // If the state has changed to allowed, log the time it took since first + // and last failure before restart. Check for positive time delta, since + // the system clock may change at any time. + if (permission == SystemPermission::kAllowed) { + base::Time stored_time = + prefs->GetTime(kSystemPermissionMicFirstBlockedTimePref); + base::TimeDelta time_delta = base::Time::Now() - stored_time; + if (time_delta > base::TimeDelta()) { + base::UmaHistogramCustomTimes( + "Media.Audio.Capture.Mac.MicSystemPermission." + "FixedTime.SinceFirstFailure", + time_delta, base::TimeDelta::FromSeconds(1), + base::TimeDelta::FromHours(1), 50); + } + + stored_time = prefs->GetTime(kSystemPermissionMicLastBlockedTimePref); + time_delta = base::Time::Now() - stored_time; + if (time_delta > base::TimeDelta()) { + base::UmaHistogramCustomTimes( + "Media.Audio.Capture.Mac.MicSystemPermission." + "FixedTime.SinceLastFailure", + time_delta, base::TimeDelta::FromSeconds(1), + base::TimeDelta::FromHours(1), 50); + } + } + + prefs->ClearPref(kSystemPermissionMicFirstBlockedTimePref); + prefs->ClearPref(kSystemPermissionMicLastBlockedTimePref); +} + +void MaybeLogAdditionalCameraSystemPermissionStats( + SystemPermission permission) { + PrefService* prefs = g_browser_process->local_state(); + + if (!prefs->HasPrefPath(kSystemPermissionCameraFirstBlockedTimePref)) { + DCHECK(!prefs->HasPrefPath(kSystemPermissionCameraLastBlockedTimePref)); + return; + } + + // A pref exists, so there was a failure accessing the camera due to blocked + // system permission before the last restart. Log additional stats. + + DCHECK(prefs->HasPrefPath(kSystemPermissionCameraLastBlockedTimePref)); + base::UmaHistogramEnumeration( + "Media.Video.Capture.Mac.CameraSystemPermission." + "StartupAfterFailure", + permission); + + // If the state has changed to allowed, log the time it took since first + // and last failure before restart. Check for positive time delta, since + // the system clock may change at any time. + if (permission == SystemPermission::kAllowed) { + base::Time stored_time = + prefs->GetTime(kSystemPermissionCameraFirstBlockedTimePref); + base::TimeDelta time_delta = base::Time::Now() - stored_time; + if (time_delta > base::TimeDelta()) { + base::UmaHistogramCustomTimes( + "Media.Video.Capture.Mac.CameraSystemPermission.FixedTime." + "SinceFirstFailure", + time_delta, base::TimeDelta::FromSeconds(1), + base::TimeDelta::FromHours(1), 50); + } + + stored_time = prefs->GetTime(kSystemPermissionCameraLastBlockedTimePref); + time_delta = base::Time::Now() - stored_time; + if (time_delta > base::TimeDelta()) { + base::UmaHistogramCustomTimes( + "Media.Video.Capture.Mac.CameraSystemPermission.FixedTime." + "SinceLastFailure", + time_delta, base::TimeDelta::FromSeconds(1), + base::TimeDelta::FromHours(1), 50); + } + } + + prefs->ClearPref(kSystemPermissionCameraFirstBlockedTimePref); + prefs->ClearPref(kSystemPermissionCameraLastBlockedTimePref); +} + +} // namespace + +void RegisterSystemMediaPermissionStatesPrefs(PrefRegistrySimple* registry) { + if (@available(macOS 10.14, *)) { + registry->RegisterTimePref(kSystemPermissionMicFirstBlockedTimePref, + base::Time()); + registry->RegisterTimePref(kSystemPermissionMicLastBlockedTimePref, + base::Time()); + registry->RegisterTimePref(kSystemPermissionCameraFirstBlockedTimePref, + base::Time()); + registry->RegisterTimePref(kSystemPermissionCameraLastBlockedTimePref, + base::Time()); + } +} + +void LogSystemMediaPermissionsStartupStats() { + if (@available(macOS 10.14, *)) { + const SystemPermission audio_permission = + CheckSystemAudioCapturePermission(); + LogStartupMicSystemPermission(audio_permission); + MaybeLogAdditionalMicSystemPermissionStats(audio_permission); + + const SystemPermission video_permission = + CheckSystemVideoCapturePermission(); + LogStartupCameraSystemPermission(video_permission); + MaybeLogAdditionalCameraSystemPermissionStats(video_permission); + } // (@available(macOS 10.14, *)) +} + +void SystemAudioCapturePermissionDetermined(SystemPermission permission) { + if (@available(macOS 10.14, *)) { + DCHECK_NE(permission, SystemPermission::kNotDetermined); + LogStartupMicSystemPermission(permission); + } +} + +void SystemVideoCapturePermissionDetermined(SystemPermission permission) { + if (@available(macOS 10.14, *)) { + DCHECK_NE(permission, SystemPermission::kNotDetermined); + LogStartupCameraSystemPermission(permission); + } +} + +void SystemAudioCapturePermissionBlocked() { + if (@available(macOS 10.14, *)) { + PrefService* prefs = g_browser_process->local_state(); + if (!prefs->HasPrefPath(kSystemPermissionMicFirstBlockedTimePref)) { + prefs->SetTime(kSystemPermissionMicFirstBlockedTimePref, + base::Time::Now()); + } + prefs->SetTime(kSystemPermissionMicLastBlockedTimePref, base::Time::Now()); + } +} + +void SystemVideoCapturePermissionBlocked() { + if (@available(macOS 10.14, *)) { + PrefService* prefs = g_browser_process->local_state(); + if (!prefs->HasPrefPath(kSystemPermissionCameraFirstBlockedTimePref)) { + prefs->SetTime(kSystemPermissionCameraFirstBlockedTimePref, + base::Time::Now()); + } + prefs->SetTime(kSystemPermissionCameraLastBlockedTimePref, + base::Time::Now()); + } +} + +} // namespace system_media_permissions diff --git a/chromium/chrome/browser/media/webrtc/tab_capture_access_handler.cc b/chromium/chrome/browser/media/webrtc/tab_capture_access_handler.cc new file mode 100644 index 00000000000..8a3fe7e1918 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/tab_capture_access_handler.cc @@ -0,0 +1,92 @@ +// Copyright 2015 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 "chrome/browser/media/webrtc/tab_capture_access_handler.h" + +#include <utility> + +#include "chrome/browser/extensions/api/tab_capture/tab_capture_registry.h" +#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" +#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h" +#include "chrome/browser/profiles/profile.h" +#include "content/public/browser/web_contents.h" +#include "extensions/common/permissions/permissions_data.h" +#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" + +TabCaptureAccessHandler::TabCaptureAccessHandler() { +} + +TabCaptureAccessHandler::~TabCaptureAccessHandler() { +} + +bool TabCaptureAccessHandler::SupportsStreamType( + content::WebContents* web_contents, + const blink::mojom::MediaStreamType type, + const extensions::Extension* extension) { + return type == blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE || + type == blink::mojom::MediaStreamType::GUM_TAB_AUDIO_CAPTURE; +} + +bool TabCaptureAccessHandler::CheckMediaAccessPermission( + content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type, + const extensions::Extension* extension) { + return false; +} + +void TabCaptureAccessHandler::HandleRequest( + content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) { + blink::MediaStreamDevices devices; + std::unique_ptr<content::MediaStreamUI> ui; + + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + extensions::TabCaptureRegistry* tab_capture_registry = + extensions::TabCaptureRegistry::Get(profile); + if (!tab_capture_registry) { + NOTREACHED(); + std::move(callback).Run( + devices, blink::mojom::MediaStreamRequestResult::INVALID_STATE, + std::move(ui)); + return; + } + // |extension| may be null if the tabCapture starts with + // tabCapture.getMediaStreamId(). + // TODO(crbug.com/831722): Deprecate tabCaptureRegistry soon. + const std::string extension_id = extension ? extension->id() : ""; + const bool tab_capture_allowed = tab_capture_registry->VerifyRequest( + request.render_process_id, request.render_frame_id, extension_id); + + if (request.audio_type == + blink::mojom::MediaStreamType::GUM_TAB_AUDIO_CAPTURE && + tab_capture_allowed) { + devices.push_back(blink::MediaStreamDevice( + blink::mojom::MediaStreamType::GUM_TAB_AUDIO_CAPTURE, std::string(), + std::string())); + } + + if (request.video_type == + blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE && + tab_capture_allowed) { + devices.push_back(blink::MediaStreamDevice( + blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE, std::string(), + std::string())); + } + + if (!devices.empty()) { + ui = MediaCaptureDevicesDispatcher::GetInstance() + ->GetMediaStreamCaptureIndicator() + ->RegisterMediaStream(web_contents, devices); + } + UpdateExtensionTrusted(request, extension); + std::move(callback).Run( + devices, + devices.empty() ? blink::mojom::MediaStreamRequestResult::INVALID_STATE + : blink::mojom::MediaStreamRequestResult::OK, + std::move(ui)); +} diff --git a/chromium/chrome/browser/media/webrtc/tab_capture_access_handler.h b/chromium/chrome/browser/media/webrtc/tab_capture_access_handler.h new file mode 100644 index 00000000000..26fd89ad892 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/tab_capture_access_handler.h @@ -0,0 +1,31 @@ +// Copyright 2015 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 CHROME_BROWSER_MEDIA_WEBRTC_TAB_CAPTURE_ACCESS_HANDLER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_TAB_CAPTURE_ACCESS_HANDLER_H_ + +#include "chrome/browser/media/capture_access_handler_base.h" + +// MediaAccessHandler for TabCapture API. +class TabCaptureAccessHandler : public CaptureAccessHandlerBase { + public: + TabCaptureAccessHandler(); + ~TabCaptureAccessHandler() override; + + // MediaAccessHandler implementation. + bool SupportsStreamType(content::WebContents* web_contents, + const blink::mojom::MediaStreamType type, + const extensions::Extension* extension) override; + bool CheckMediaAccessPermission( + content::RenderFrameHost* render_frame_host, + const GURL& security_origin, + blink::mojom::MediaStreamType type, + const extensions::Extension* extension) override; + void HandleRequest(content::WebContents* web_contents, + const content::MediaStreamRequest& request, + content::MediaResponseCallback callback, + const extensions::Extension* extension) override; +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_TAB_CAPTURE_ACCESS_HANDLER_H_ diff --git a/chromium/chrome/browser/media/webrtc/tab_desktop_media_list.cc b/chromium/chrome/browser/media/webrtc/tab_desktop_media_list.cc new file mode 100644 index 00000000000..00df7fa7d40 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/tab_desktop_media_list.cc @@ -0,0 +1,163 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/tab_desktop_media_list.h" + +#include <utility> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/hash/hash.h" +#include "base/task/post_task.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "components/favicon/content/content_favicon_driver.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/render_process_host.h" +#include "media/base/video_util.h" +#include "third_party/skia/include/core/SkCanvas.h" +#include "third_party/skia/include/core/SkImage.h" +#include "ui/gfx/favicon_size.h" +#include "ui/gfx/image/image.h" + +using content::BrowserThread; +using content::DesktopMediaID; + +namespace { + +gfx::ImageSkia CreateEnclosedFaviconImage(gfx::Size size, + const gfx::ImageSkia& favicon) { + DCHECK_GE(size.width(), gfx::kFaviconSize); + DCHECK_GE(size.height(), gfx::kFaviconSize); + + // Create a bitmap. + SkBitmap result; + result.allocN32Pixels(size.width(), size.height(), false); + SkCanvas canvas(result); + canvas.clear(SK_ColorTRANSPARENT); + + // Draw the favicon image into the center of result image. If the favicon is + // too big, scale it down. + gfx::Size fill_size = favicon.size(); + if (result.width() < favicon.width() || result.height() < favicon.height()) + fill_size = media::ScaleSizeToFitWithinTarget(favicon.size(), size); + + gfx::Rect center_rect(result.width(), result.height()); + center_rect.ClampToCenteredSize(fill_size); + SkRect dest_rect = + SkRect::MakeLTRB(center_rect.x(), center_rect.y(), center_rect.right(), + center_rect.bottom()); + canvas.drawBitmapRect(*favicon.bitmap(), dest_rect, nullptr); + + return gfx::ImageSkia::CreateFrom1xBitmap(result); +} + +// Update the list once per second. +const int kDefaultTabDesktopMediaListUpdatePeriod = 1000; + +} // namespace + +TabDesktopMediaList::TabDesktopMediaList() + : DesktopMediaListBase(base::TimeDelta::FromMilliseconds( + kDefaultTabDesktopMediaListUpdatePeriod)) { + type_ = DesktopMediaID::TYPE_WEB_CONTENTS; + thumbnail_task_runner_ = base::CreateSequencedTaskRunner( + {base::ThreadPool(), base::MayBlock(), base::TaskPriority::USER_VISIBLE}); +} + +TabDesktopMediaList::~TabDesktopMediaList() {} + +void TabDesktopMediaList::Refresh(bool update_thumnails) { + DCHECK(can_refresh()); + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + Profile* profile = ProfileManager::GetLastUsedProfileAllowedByPolicy(); + if (!profile) { + OnRefreshComplete(); + return; + } + + std::vector<Browser*> browsers; + for (auto* browser : *BrowserList::GetInstance()) { + if (browser->profile()->GetOriginalProfile() == + profile->GetOriginalProfile()) { + browsers.push_back(browser); + } + } + + ImageHashesMap new_favicon_hashes; + std::vector<SourceDescription> sources; + std::map<base::TimeTicks, SourceDescription> tab_map; + std::vector<std::pair<DesktopMediaID, gfx::ImageSkia>> favicon_pairs; + + // Enumerate all tabs with their titles and favicons for a user profile. + for (auto* browser : browsers) { + const TabStripModel* tab_strip_model = browser->tab_strip_model(); + DCHECK(tab_strip_model); + + for (int i = 0; i < tab_strip_model->count(); i++) { + // Create id for tab. + content::WebContents* contents = tab_strip_model->GetWebContentsAt(i); + DCHECK(contents); + content::RenderFrameHost* main_frame = contents->GetMainFrame(); + DCHECK(main_frame); + DesktopMediaID media_id( + DesktopMediaID::TYPE_WEB_CONTENTS, DesktopMediaID::kNullId, + content::WebContentsMediaCaptureId(main_frame->GetProcess()->GetID(), + main_frame->GetRoutingID())); + + // Get tab's last active time stamp. + const base::TimeTicks t = contents->GetLastActiveTime(); + tab_map.insert( + std::make_pair(t, SourceDescription(media_id, contents->GetTitle()))); + + // Get favicon for tab. + favicon::FaviconDriver* favicon_driver = + favicon::ContentFaviconDriver::FromWebContents(contents); + if (!favicon_driver) + continue; + + gfx::Image favicon = favicon_driver->GetFavicon(); + if (favicon.IsEmpty()) + continue; + + // Only new or changed favicon need update. + new_favicon_hashes[media_id] = GetImageHash(favicon); + if (!favicon_hashes_.count(media_id) || + (favicon_hashes_[media_id] != new_favicon_hashes[media_id])) { + gfx::ImageSkia image = favicon.AsImageSkia(); + image.MakeThreadSafe(); + favicon_pairs.push_back(std::make_pair(media_id, image)); + } + } + } + favicon_hashes_ = new_favicon_hashes; + + // Sort tab sources by time. Most recent one first. Then update sources list. + for (auto it = tab_map.rbegin(); it != tab_map.rend(); ++it) + sources.push_back(it->second); + + UpdateSourcesList(sources); + + for (const auto& it : favicon_pairs) { + // Create a thumbail in a different thread and update the thumbnail in + // current thread. + base::PostTaskAndReplyWithResult( + thumbnail_task_runner_.get(), FROM_HERE, + base::Bind(&CreateEnclosedFaviconImage, thumbnail_size_, it.second), + base::Bind(&TabDesktopMediaList::UpdateSourceThumbnail, + weak_factory_.GetWeakPtr(), it.first)); + } + + // OnRefreshComplete() needs to be called after all calls for + // UpdateSourceThumbnail() have done. Therefore, a DoNothing task is posted to + // the same sequenced task runner that CreateEnlargedFaviconImag() is posted. + thumbnail_task_runner_.get()->PostTaskAndReply( + FROM_HERE, base::DoNothing(), + base::BindOnce(&TabDesktopMediaList::OnRefreshComplete, + weak_factory_.GetWeakPtr())); +} diff --git a/chromium/chrome/browser/media/webrtc/tab_desktop_media_list.h b/chromium/chrome/browser/media/webrtc/tab_desktop_media_list.h new file mode 100644 index 00000000000..7454d5c364a --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/tab_desktop_media_list.h @@ -0,0 +1,31 @@ +// Copyright 2016 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 CHROME_BROWSER_MEDIA_WEBRTC_TAB_DESKTOP_MEDIA_LIST_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_TAB_DESKTOP_MEDIA_LIST_H_ + +#include "chrome/browser/media/webrtc/desktop_media_list_base.h" + +// Implementation of DesktopMediaList that shows tab/WebContents. +class TabDesktopMediaList : public DesktopMediaListBase { + public: + TabDesktopMediaList(); + ~TabDesktopMediaList() override; + + private: + typedef std::map<content::DesktopMediaID, uint32_t> ImageHashesMap; + + void Refresh(bool update_thumnails) override; + + ImageHashesMap favicon_hashes_; + + // Task runner used for the |worker_|. + scoped_refptr<base::SequencedTaskRunner> thumbnail_task_runner_; + + base::WeakPtrFactory<TabDesktopMediaList> weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(TabDesktopMediaList); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_TAB_DESKTOP_MEDIA_LIST_H_ diff --git a/chromium/chrome/browser/media/webrtc/tab_desktop_media_list_unittest.cc b/chromium/chrome/browser/media/webrtc/tab_desktop_media_list_unittest.cc new file mode 100644 index 00000000000..b2d850b6c42 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/tab_desktop_media_list_unittest.cc @@ -0,0 +1,372 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/tab_desktop_media_list.h" + +#include "base/command_line.h" +#include "base/files/file_util.h" +#include "base/location.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/single_thread_task_runner.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "chrome/browser/media/webrtc/desktop_media_list.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/test/base/scoped_testing_local_state.h" +#include "chrome/test/base/test_browser_window.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/browser/favicon_status.h" +#include "content/public/browser/navigation_entry.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_task_environment.h" +#include "content/public/test/test_renderer_host.h" +#include "content/public/test/web_contents_tester.h" +#include "testing/gmock/include/gmock/gmock.h" + +#if defined(OS_CHROMEOS) +#include "chrome/browser/chromeos/login/users/scoped_test_user_manager.h" +#include "chrome/browser/chromeos/settings/scoped_cros_settings_test_helper.h" +#endif // defined(OS_CHROMEOS) + +using content::WebContents; +using content::WebContentsTester; + +namespace { + +constexpr int kDefaultSourceCount = 2; +constexpr int kThumbnailSize = 50; + +class UnittestProfileManager : public ::ProfileManagerWithoutInit { + public: + explicit UnittestProfileManager(const base::FilePath& user_data_dir) + : ::ProfileManagerWithoutInit(user_data_dir) {} + + protected: + std::unique_ptr<Profile> CreateProfileHelper( + const base::FilePath& path) override { + if (!base::PathExists(path) && !base::CreateDirectory(path)) + return nullptr; + return std::make_unique<TestingProfile>(path); + } +}; + +// Create a greyscale image with certain size and grayscale value. +gfx::Image CreateGrayscaleImage(gfx::Size size, uint8_t greyscale_value) { + SkBitmap result; + result.allocN32Pixels(size.width(), size.height(), true); + + uint8_t* pixels_data = reinterpret_cast<uint8_t*>(result.getPixels()); + + // Set greyscale value for all pixels. + for (int y = 0; y < result.height(); ++y) { + for (int x = 0; x < result.width(); ++x) { + pixels_data[result.rowBytes() * y + x * result.bytesPerPixel()] = + greyscale_value; + pixels_data[result.rowBytes() * y + x * result.bytesPerPixel() + 1] = + greyscale_value; + pixels_data[result.rowBytes() * y + x * result.bytesPerPixel() + 2] = + greyscale_value; + pixels_data[result.rowBytes() * y + x * result.bytesPerPixel() + 3] = + 0xff; + } + } + + return gfx::Image::CreateFrom1xBitmap(result); +} + +} // namespace + +class MockObserver : public DesktopMediaListObserver { + public: + MOCK_METHOD2(OnSourceAdded, void(DesktopMediaList* list, int index)); + MOCK_METHOD2(OnSourceRemoved, void(DesktopMediaList* list, int index)); + MOCK_METHOD3(OnSourceMoved, + void(DesktopMediaList* list, int old_index, int new_index)); + MOCK_METHOD2(OnSourceNameChanged, void(DesktopMediaList* list, int index)); + MOCK_METHOD2(OnSourceThumbnailChanged, + void(DesktopMediaList* list, int index)); + MOCK_METHOD1(OnAllSourcesFound, void(DesktopMediaList* list)); + + void VerifyAndClearExpectations() { + testing::Mock::VerifyAndClearExpectations(this); + } +}; + +ACTION_P2(CheckListSize, list, expected_list_size) { + EXPECT_EQ(expected_list_size, list->GetSourceCount()); +} + +ACTION(QuitMessageLoop) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::RunLoop::QuitCurrentWhenIdleClosureDeprecated()); +} + +class TabDesktopMediaListTest : public testing::Test { + protected: + TabDesktopMediaListTest() + : local_state_(TestingBrowserProcess::GetGlobal()) {} + + void AddWebcontents(int favicon_greyscale) { + TabStripModel* tab_strip_model = browser_->tab_strip_model(); + ASSERT_TRUE(tab_strip_model); + std::unique_ptr<WebContents> contents( + content::WebContentsTester::CreateTestWebContents( + profile_, content::SiteInstance::Create(profile_))); + ASSERT_TRUE(contents); + + WebContentsTester::For(contents.get()) + ->SetLastActiveTime(base::TimeTicks::Now()); + + // Get or create the transient NavigationEntry and add a title and a + // favicon to it. + content::NavigationEntry* entry = + contents->GetController().GetTransientEntry(); + if (!entry) { + std::unique_ptr<content::NavigationEntry> entry_new = + content::NavigationController::CreateNavigationEntry( + GURL("chrome://blank"), content::Referrer(), base::nullopt, + ui::PAGE_TRANSITION_LINK, false, std::string(), profile_, + nullptr /* blob_url_loader_factory */); + + contents->GetController().SetTransientEntry(std::move(entry_new)); + entry = contents->GetController().GetTransientEntry(); + } + + contents->UpdateTitleForEntry(entry, base::ASCIIToUTF16("Test tab")); + + content::FaviconStatus favicon_info; + favicon_info.image = + CreateGrayscaleImage(gfx::Size(10, 10), favicon_greyscale); + entry->GetFavicon() = favicon_info; + + manually_added_web_contents_.push_back(contents.get()); + tab_strip_model->AppendWebContents(std::move(contents), true); + } + + void SetUp() override { + manually_added_web_contents_.clear(); + rvh_test_enabler_.reset(new content::RenderViewHostTestEnabler()); + // Create a new temporary directory, and store the path. + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + TestingBrowserProcess::GetGlobal()->SetProfileManager( + new UnittestProfileManager(temp_dir_.GetPath())); + +#if defined(OS_CHROMEOS) + base::CommandLine* cl = base::CommandLine::ForCurrentProcess(); + cl->AppendSwitch(switches::kTestType); +#endif + + // Create profile. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ASSERT_TRUE(profile_manager); + + profile_ = profile_manager->GetLastUsedProfileAllowedByPolicy(); + ASSERT_TRUE(profile_); + + // Create browser. + Browser::CreateParams profile_params(profile_, true); + browser_ = CreateBrowserWithTestWindowForParams(&profile_params); + ASSERT_TRUE(browser_); + for (int i = 0; i < kDefaultSourceCount; i++) { + AddWebcontents(i + 1); + } + } + + void TearDown() override { + // TODO(erikchen): Tearing down the TabStripModel should just delete all its + // owned WebContents. Then |manually_added_web_contents_| won't be + // necessary. https://crbug.com/832879. + TabStripModel* tab_strip_model = browser_->tab_strip_model(); + for (WebContents* contents : manually_added_web_contents_) { + tab_strip_model->DetachWebContentsAt( + tab_strip_model->GetIndexOfWebContents(contents)); + } + manually_added_web_contents_.clear(); + + browser_.reset(); + TestingBrowserProcess::GetGlobal()->SetProfileManager(NULL); + base::RunLoop().RunUntilIdle(); + rvh_test_enabler_.reset(); + } + + void CreateDefaultList() { + list_.reset(new TabDesktopMediaList()); + list_->SetThumbnailSize(gfx::Size(kThumbnailSize, kThumbnailSize)); + + // Set update period to reduce the time it takes to run tests. + // >0 to avoid unit test failure. + list_->SetUpdatePeriod(base::TimeDelta::FromMilliseconds(1)); + } + + void InitializeAndVerify() { + CreateDefaultList(); + + // The tabs in media source list are sorted in decreasing time order. The + // latest one is listed first. However, tabs are added to TabStripModel in + // increasing time order, the oldest one is added first. + { + testing::InSequence dummy; + + for (int i = 0; i < kDefaultSourceCount; i++) { + EXPECT_CALL(observer_, OnSourceAdded(list_.get(), i)) + .WillOnce(CheckListSize(list_.get(), i + 1)); + } + + for (int i = 0; i < kDefaultSourceCount - 1; i++) { + EXPECT_CALL(observer_, OnSourceThumbnailChanged( + list_.get(), kDefaultSourceCount - 1 - i)); + } + EXPECT_CALL(observer_, OnSourceThumbnailChanged(list_.get(), 0)) + .WillOnce(QuitMessageLoop()); + } + + list_->StartUpdating(&observer_); + base::RunLoop().Run(); + + for (int i = 0; i < kDefaultSourceCount; ++i) { + EXPECT_EQ(list_->GetSource(i).id.type, + content::DesktopMediaID::TYPE_WEB_CONTENTS); + } + + observer_.VerifyAndClearExpectations(); + } + + // The path to temporary directory used to contain the test operations. + base::ScopedTempDir temp_dir_; + ScopedTestingLocalState local_state_; + + std::unique_ptr<content::RenderViewHostTestEnabler> rvh_test_enabler_; + Profile* profile_; + std::unique_ptr<Browser> browser_; + + // Must be listed before |list_|, so it's destroyed last. + MockObserver observer_; + std::unique_ptr<TabDesktopMediaList> list_; + std::vector<WebContents*> manually_added_web_contents_; + + content::BrowserTaskEnvironment task_environment_; + +#if defined(OS_CHROMEOS) + chromeos::ScopedCrosSettingsTestHelper cros_settings_test_helper_; + chromeos::ScopedTestUserManager test_user_manager_; +#endif + + DISALLOW_COPY_AND_ASSIGN(TabDesktopMediaListTest); +}; + +TEST_F(TabDesktopMediaListTest, AddTab) { + InitializeAndVerify(); + + AddWebcontents(10); + + EXPECT_CALL(observer_, OnSourceAdded(list_.get(), 0)) + .WillOnce(CheckListSize(list_.get(), kDefaultSourceCount + 1)); + EXPECT_CALL(observer_, OnSourceThumbnailChanged(list_.get(), 0)) + .WillOnce(QuitMessageLoop()); + + base::RunLoop().Run(); + + list_.reset(); +} + +TEST_F(TabDesktopMediaListTest, RemoveTab) { + InitializeAndVerify(); + + TabStripModel* tab_strip_model = browser_->tab_strip_model(); + ASSERT_TRUE(tab_strip_model); + std::unique_ptr<WebContents> released_web_contents = + tab_strip_model->DetachWebContentsAt(kDefaultSourceCount - 1); + for (auto it = manually_added_web_contents_.begin(); + it != manually_added_web_contents_.end(); ++it) { + if (*it == released_web_contents.get()) { + manually_added_web_contents_.erase(it); + break; + } + } + + EXPECT_CALL(observer_, OnSourceRemoved(list_.get(), 0)) + .WillOnce( + testing::DoAll(CheckListSize(list_.get(), kDefaultSourceCount - 1), + QuitMessageLoop())); + + base::RunLoop().Run(); + + list_.reset(); +} + +TEST_F(TabDesktopMediaListTest, MoveTab) { + InitializeAndVerify(); + + // Swap the two media sources by swap their time stamps. + TabStripModel* tab_strip_model = browser_->tab_strip_model(); + ASSERT_TRUE(tab_strip_model); + + WebContents* contents0 = tab_strip_model->GetWebContentsAt(0); + ASSERT_TRUE(contents0); + base::TimeTicks t0 = contents0->GetLastActiveTime(); + WebContents* contents1 = tab_strip_model->GetWebContentsAt(1); + ASSERT_TRUE(contents1); + base::TimeTicks t1 = contents1->GetLastActiveTime(); + + WebContentsTester::For(contents0)->SetLastActiveTime(t1); + WebContentsTester::For(contents1)->SetLastActiveTime(t0); + + EXPECT_CALL(observer_, OnSourceMoved(list_.get(), 1, 0)) + .WillOnce(testing::DoAll(CheckListSize(list_.get(), kDefaultSourceCount), + QuitMessageLoop())); + + base::RunLoop().Run(); + + list_.reset(); +} + +TEST_F(TabDesktopMediaListTest, UpdateTitle) { + InitializeAndVerify(); + + // Change tab's title. + TabStripModel* tab_strip_model = browser_->tab_strip_model(); + ASSERT_TRUE(tab_strip_model); + WebContents* contents = + tab_strip_model->GetWebContentsAt(kDefaultSourceCount - 1); + ASSERT_TRUE(contents); + content::NavigationController& controller = contents->GetController(); + contents->UpdateTitleForEntry(controller.GetTransientEntry(), + base::ASCIIToUTF16("New test tab")); + + EXPECT_CALL(observer_, OnSourceNameChanged(list_.get(), 0)) + .WillOnce(QuitMessageLoop()); + + base::RunLoop().Run(); + + EXPECT_EQ(list_->GetSource(0).name, base::UTF8ToUTF16("New test tab")); + + list_.reset(); +} + +TEST_F(TabDesktopMediaListTest, UpdateThumbnail) { + InitializeAndVerify(); + + // Change tab's favicon. + TabStripModel* tab_strip_model = browser_->tab_strip_model(); + ASSERT_TRUE(tab_strip_model); + WebContents* contents = + tab_strip_model->GetWebContentsAt(kDefaultSourceCount - 1); + ASSERT_TRUE(contents); + + content::FaviconStatus favicon_info; + favicon_info.image = CreateGrayscaleImage(gfx::Size(10, 10), 100); + contents->GetController().GetTransientEntry()->GetFavicon() = favicon_info; + + EXPECT_CALL(observer_, OnSourceThumbnailChanged(list_.get(), 0)) + .WillOnce(QuitMessageLoop()); + + base::RunLoop().Run(); + + list_.reset(); +} diff --git a/chromium/chrome/browser/media/webrtc/test_stats_dictionary.cc b/chromium/chrome/browser/media/webrtc/test_stats_dictionary.cc new file mode 100644 index 00000000000..a02c385a068 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/test_stats_dictionary.cc @@ -0,0 +1,216 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/test_stats_dictionary.h" + +#include "base/json/json_writer.h" +#include "base/logging.h" + +namespace content { + +TestStatsReportDictionary::TestStatsReportDictionary( + std::unique_ptr<base::DictionaryValue> report) + : report_(std::move(report)) { + CHECK(report_); +} + +TestStatsReportDictionary::~TestStatsReportDictionary() { +} + +void TestStatsReportDictionary::ForEach( + std::function<void(const TestStatsDictionary&)> iteration) { + for (base::DictionaryValue::Iterator it(*report_); !it.IsAtEnd(); + it.Advance()) { + const base::DictionaryValue* it_value; + CHECK(it.value().GetAsDictionary(&it_value)); + iteration(TestStatsDictionary(this, it_value)); + } +} + +std::vector<TestStatsDictionary> TestStatsReportDictionary::Filter( + std::function<bool(const TestStatsDictionary&)> filter) { + std::vector<TestStatsDictionary> result; + ForEach([&result, &filter](const TestStatsDictionary& stats) { + if (filter(stats)) + result.push_back(stats); + }); + return result; +} + +std::unique_ptr<TestStatsDictionary> TestStatsReportDictionary::Get( + const std::string& id) { + const base::DictionaryValue* dictionary; + if (!report_->GetDictionary(id, &dictionary)) + return nullptr; + return std::unique_ptr<TestStatsDictionary>( + new TestStatsDictionary(this, dictionary)); +} + +std::vector<TestStatsDictionary> TestStatsReportDictionary::GetAll() { + return Filter([](const TestStatsDictionary&) { return true; }); +} + +std::vector<TestStatsDictionary> TestStatsReportDictionary::GetByType( + const std::string& type) { + return Filter([&type](const TestStatsDictionary& stats) { + return stats.GetString("type") == type; + }); +} + +TestStatsDictionary::TestStatsDictionary( + TestStatsReportDictionary* report, const base::DictionaryValue* stats) + : report_(report), stats_(stats) { + CHECK(report_); + CHECK(stats_); +} + +TestStatsDictionary::TestStatsDictionary( + const TestStatsDictionary& other) = default; + +TestStatsDictionary::~TestStatsDictionary() { +} + +bool TestStatsDictionary::IsBoolean(const std::string& key) const { + bool value; + return GetBoolean(key, &value); +} + +bool TestStatsDictionary::GetBoolean(const std::string& key) const { + bool value; + CHECK(GetBoolean(key, &value)); + return value; +} + +bool TestStatsDictionary::IsNumber(const std::string& key) const { + double value; + return GetNumber(key, &value); +} + +double TestStatsDictionary::GetNumber(const std::string& key) const { + double value; + CHECK(GetNumber(key, &value)); + return value; +} + +bool TestStatsDictionary::IsString(const std::string& key) const { + std::string value; + return GetString(key, &value); +} + +std::string TestStatsDictionary::GetString(const std::string& key) const { + std::string value; + CHECK(GetString(key, &value)); + return value; +} + +bool TestStatsDictionary::IsSequenceBoolean(const std::string& key) const { + std::vector<bool> value; + return GetSequenceBoolean(key, &value); +} + +std::vector<bool> TestStatsDictionary::GetSequenceBoolean( + const std::string& key) const { + std::vector<bool> value; + CHECK(GetSequenceBoolean(key, &value)); + return value; +} + +bool TestStatsDictionary::IsSequenceNumber(const std::string& key) const { + std::vector<double> value; + return GetSequenceNumber(key, &value); +} + +std::vector<double> TestStatsDictionary::GetSequenceNumber( + const std::string& key) const { + std::vector<double> value; + CHECK(GetSequenceNumber(key, &value)); + return value; +} + +bool TestStatsDictionary::IsSequenceString(const std::string& key) const { + std::vector<std::string> value; + return GetSequenceString(key, &value); +} + +std::vector<std::string> TestStatsDictionary::GetSequenceString( + const std::string& key) const { + std::vector<std::string> value; + CHECK(GetSequenceString(key, &value)); + return value; +} + +bool TestStatsDictionary::GetBoolean( + const std::string& key, bool* out) const { + return stats_->GetBoolean(key, out); +} + +bool TestStatsDictionary::GetNumber( + const std::string& key, double* out) const { + return stats_->GetDouble(key, out); +} + +bool TestStatsDictionary::GetString( + const std::string& key, std::string* out) const { + return stats_->GetString(key, out); +} + +bool TestStatsDictionary::GetSequenceBoolean( + const std::string& key, + std::vector<bool>* out) const { + const base::ListValue* list; + if (!stats_->GetList(key, &list)) + return false; + std::vector<bool> sequence; + bool element; + for (size_t i = 0; i < list->GetSize(); ++i) { + if (!list->GetBoolean(i, &element)) + return false; + sequence.push_back(element); + } + *out = std::move(sequence); + return true; +} + +bool TestStatsDictionary::GetSequenceNumber( + const std::string& key, + std::vector<double>* out) const { + const base::ListValue* list; + if (!stats_->GetList(key, &list)) + return false; + std::vector<double> sequence; + double element; + for (size_t i = 0; i < list->GetSize(); ++i) { + if (!list->GetDouble(i, &element)) + return false; + sequence.push_back(element); + } + *out = std::move(sequence); + return true; +} + +bool TestStatsDictionary::GetSequenceString( + const std::string& key, + std::vector<std::string>* out) const { + const base::ListValue* list; + if (!stats_->GetList(key, &list)) + return false; + std::vector<std::string> sequence; + std::string element; + for (size_t i = 0; i < list->GetSize(); ++i) { + if (!list->GetString(i, &element)) + return false; + sequence.push_back(element); + } + *out = std::move(sequence); + return true; +} + +std::string TestStatsDictionary::ToString() const { + std::string str; + CHECK(base::JSONWriter::WriteWithOptions( + *stats_, base::JSONWriter::OPTIONS_PRETTY_PRINT, &str)); + return str; +} + +} // namespace content diff --git a/chromium/chrome/browser/media/webrtc/test_stats_dictionary.h b/chromium/chrome/browser/media/webrtc/test_stats_dictionary.h new file mode 100644 index 00000000000..13fd7f78c6a --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/test_stats_dictionary.h @@ -0,0 +1,85 @@ +// Copyright 2016 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 CHROME_BROWSER_MEDIA_WEBRTC_TEST_STATS_DICTIONARY_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_TEST_STATS_DICTIONARY_H_ + +#include <functional> +#include <string> +#include <vector> + +#include "base/memory/ref_counted.h" +#include "base/values.h" + +namespace content { + +class TestStatsDictionary; + +class TestStatsReportDictionary + : public base::RefCounted<TestStatsReportDictionary> { + public: + explicit TestStatsReportDictionary( + std::unique_ptr<base::DictionaryValue> report); + + void ForEach(std::function<void(const TestStatsDictionary&)> iteration); + std::vector<TestStatsDictionary> Filter( + std::function<bool(const TestStatsDictionary&)> filter); + + std::unique_ptr<TestStatsDictionary> Get(const std::string& id); + std::vector<TestStatsDictionary> GetAll(); + std::vector<TestStatsDictionary> GetByType(const std::string& type); + + private: + friend class base::RefCounted<TestStatsReportDictionary>; + ~TestStatsReportDictionary(); + + std::unique_ptr<base::DictionaryValue> report_; +}; + +class TestStatsDictionary { + public: + TestStatsDictionary(TestStatsReportDictionary* report, + const base::DictionaryValue* stats); + TestStatsDictionary(const TestStatsDictionary& other); + ~TestStatsDictionary(); + + bool IsBoolean(const std::string& key) const; + bool GetBoolean(const std::string& key) const; + + bool IsNumber(const std::string& key) const; + double GetNumber(const std::string& key) const; + + bool IsString(const std::string& key) const; + std::string GetString(const std::string& key) const; + + bool IsSequenceBoolean(const std::string& key) const; + std::vector<bool> GetSequenceBoolean(const std::string& key) const; + + bool IsSequenceNumber(const std::string& key) const; + std::vector<double> GetSequenceNumber(const std::string& key) const; + + bool IsSequenceString(const std::string& key) const; + std::vector<std::string> GetSequenceString(const std::string& key) const; + + std::string ToString() const; + + private: + bool GetBoolean(const std::string& key, bool* out) const; + bool GetNumber(const std::string& key, double* out) const; + bool GetString(const std::string& key, std::string* out) const; + bool GetSequenceBoolean( + const std::string& key, std::vector<bool>* out) const; + bool GetSequenceNumber( + const std::string& key, std::vector<double>* out) const; + bool GetSequenceString( + const std::string& key, std::vector<std::string>* out) const; + + // The reference keeps the report alive which indirectly owns |stats_|. + scoped_refptr<TestStatsReportDictionary> report_; + const base::DictionaryValue* stats_; +}; + +} // namespace content + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_TEST_STATS_DICTIONARY_H_ diff --git a/chromium/chrome/browser/media/webrtc/test_stats_dictionary_unittest.cc b/chromium/chrome/browser/media/webrtc/test_stats_dictionary_unittest.cc new file mode 100644 index 00000000000..51bfa1630bc --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/test_stats_dictionary_unittest.cc @@ -0,0 +1,167 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/test_stats_dictionary.h" + +#include <memory> +#include <set> +#include <vector> + +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/values.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +namespace { + +const char kTestStatsReportJson[] = + R"({ + "GarbageA": { + "id": "GarbageA", + "timestamp": 0.0, + "type": "garbage" + }, + "RTCTestStatsID": { + "id": "RTCTestStatsID", + "timestamp": 13.37, + "type": "test", + "boolean": true, + "number": 42, + "string": "text", + "sequenceBoolean": [ true ], + "sequenceNumber": [ 42 ], + "sequenceString": [ "text" ] + }, + "GarbageB": { + "id": "GarbageB", + "timestamp": 0.0, + "type": "garbage" + } +})"; + +class TestStatsDictionaryTest : public testing::Test { + public: + TestStatsDictionaryTest() { + std::unique_ptr<base::Value> value = + base::JSONReader::ReadDeprecated(kTestStatsReportJson); + CHECK(value); + base::DictionaryValue* dictionary; + CHECK(value->GetAsDictionary(&dictionary)); + ignore_result(value.release()); + report_ = new TestStatsReportDictionary( + std::unique_ptr<base::DictionaryValue>(dictionary)); + } + + protected: + scoped_refptr<TestStatsReportDictionary> report_; +}; + +TEST_F(TestStatsDictionaryTest, ReportGetStats) { + EXPECT_FALSE(report_->Get("InvalidID")); + EXPECT_TRUE(report_->Get("GarbageA")); + EXPECT_TRUE(report_->Get("RTCTestStatsID")); + EXPECT_TRUE(report_->Get("GarbageB")); +} + +TEST_F(TestStatsDictionaryTest, ReportForEach) { + std::set<std::string> remaining; + remaining.insert("GarbageA"); + remaining.insert("RTCTestStatsID"); + remaining.insert("GarbageB"); + report_->ForEach([&remaining](const TestStatsDictionary& stats) { + remaining.erase(stats.GetString("id")); + }); + EXPECT_TRUE(remaining.empty()); +} + +TEST_F(TestStatsDictionaryTest, ReportFilterStats) { + std::vector<TestStatsDictionary> filtered_stats = report_->Filter( + [](const TestStatsDictionary& stats) -> bool { + return false; + }); + EXPECT_EQ(filtered_stats.size(), 0u); + + filtered_stats = report_->Filter( + [](const TestStatsDictionary& stats) -> bool { + return true; + }); + EXPECT_EQ(filtered_stats.size(), 3u); + + filtered_stats = report_->Filter( + [](const TestStatsDictionary& stats) -> bool { + return stats.GetString("id") == "RTCTestStatsID"; + }); + EXPECT_EQ(filtered_stats.size(), 1u); +} + +TEST_F(TestStatsDictionaryTest, ReportGetAll) { + std::set<std::string> remaining; + remaining.insert("GarbageA"); + remaining.insert("RTCTestStatsID"); + remaining.insert("GarbageB"); + for (const TestStatsDictionary& stats : report_->GetAll()) { + remaining.erase(stats.GetString("id")); + } + EXPECT_TRUE(remaining.empty()); +} + +TEST_F(TestStatsDictionaryTest, ReportGetByType) { + std::vector<TestStatsDictionary> stats = report_->GetByType("garbage"); + EXPECT_EQ(stats.size(), 2u); + std::set<std::string> remaining; + remaining.insert("GarbageA"); + remaining.insert("GarbageB"); + report_->ForEach([&remaining](const TestStatsDictionary& stats) { + remaining.erase(stats.GetString("id")); + }); + EXPECT_TRUE(remaining.empty()); +} + +TEST_F(TestStatsDictionaryTest, StatsVerifyMembers) { + std::unique_ptr<TestStatsDictionary> stats = report_->Get("RTCTestStatsID"); + EXPECT_TRUE(stats); + + EXPECT_FALSE(stats->IsBoolean("nonexistentMember")); + EXPECT_FALSE(stats->IsNumber("nonexistentMember")); + EXPECT_FALSE(stats->IsString("nonexistentMember")); + EXPECT_FALSE(stats->IsSequenceBoolean("nonexistentMember")); + EXPECT_FALSE(stats->IsSequenceNumber("nonexistentMember")); + EXPECT_FALSE(stats->IsSequenceString("nonexistentMember")); + + ASSERT_TRUE(stats->IsBoolean("boolean")); + EXPECT_EQ(stats->GetBoolean("boolean"), true); + + ASSERT_TRUE(stats->IsNumber("number")); + EXPECT_EQ(stats->GetNumber("number"), 42.0); + + ASSERT_TRUE(stats->IsString("string")); + EXPECT_EQ(stats->GetString("string"), "text"); + + ASSERT_TRUE(stats->IsSequenceBoolean("sequenceBoolean")); + EXPECT_EQ(stats->GetSequenceBoolean("sequenceBoolean"), + std::vector<bool> { true }); + + ASSERT_TRUE(stats->IsSequenceNumber("sequenceNumber")); + EXPECT_EQ(stats->GetSequenceNumber("sequenceNumber"), + std::vector<double> { 42.0 }); + + ASSERT_TRUE(stats->IsSequenceString("sequenceString")); + EXPECT_EQ(stats->GetSequenceString("sequenceString"), + std::vector<std::string> { "text" }); +} + +TEST_F(TestStatsDictionaryTest, TestStatsDictionaryShouldKeepReportAlive) { + std::unique_ptr<TestStatsDictionary> stats = report_->Get("RTCTestStatsID"); + EXPECT_TRUE(stats); + report_ = nullptr; + EXPECT_EQ(stats->GetString("string"), "text"); +} + +} // namespace + +} // namespace content diff --git a/chromium/chrome/browser/media/webrtc/webrtc_apprtc_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_apprtc_browsertest.cc new file mode 100644 index 00000000000..e46f6188f28 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_apprtc_browsertest.cc @@ -0,0 +1,259 @@ +// 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/command_line.h" +#include "base/path_service.h" +#include "base/process/launch.h" +#include "base/rand_util.h" +#include "base/threading/thread_restrictions.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/infobars/infobar_responder.h" +#include "chrome/browser/infobars/infobar_service.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/browser/permissions/permission_request_manager.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "media/base/media_switches.h" +#include "net/test/python_utils.h" +#include "ui/gl/gl_switches.h" + +const char kTitlePageOfAppEngineAdminPage[] = "Instances"; + +const char kIsApprtcCallUpJavascript[] = + "var remoteVideo = document.querySelector('#remote-video');" + "var remoteVideoActive =" + " remoteVideo != null &&" + " remoteVideo.classList.contains('active');" + "window.domAutomationController.send(remoteVideoActive.toString());"; + +// WebRTC-AppRTC integration test. Requires a real webcam and microphone +// on the running system. This test is not meant to run in the main browser +// test suite since normal tester machines do not have webcams. +// +// This test will bring up a AppRTC instance on localhost and verify that the +// call gets up when connecting to the same room from two tabs in a browser. +class WebRtcApprtcBrowserTest : public WebRtcTestBase { + public: + WebRtcApprtcBrowserTest() {} + + void SetUpCommandLine(base::CommandLine* command_line) override { + EXPECT_FALSE(command_line->HasSwitch(switches::kUseFakeUIForMediaStream)); + + // The video playback will not work without a GPU, so force its use here. + command_line->AppendSwitch(switches::kUseGpuInTests); + // This test fails on some Mac bots if no default devices are specified on + // the command line. + base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( + switches::kUseFakeDeviceForMediaStream, + "audio-input-default-id=default,video-input-default-id=default"); + } + + void TearDown() override { + // Kill any processes we may have brought up. Note: this isn't perfect, + // especially if the test hangs or if we're on Windows. + LOG(INFO) << "Entering TearDown"; + if (dev_appserver_.IsValid()) + dev_appserver_.Terminate(0, false); + if (collider_server_.IsValid()) + collider_server_.Terminate(0, false); + LOG(INFO) << "Exiting TearDown"; + } + + protected: + bool LaunchApprtcInstanceOnLocalhost(const std::string& port) { + base::FilePath appengine_dev_appserver = GetSourceDir().Append( + FILE_PATH_LITERAL("third_party/webrtc/rtc_tools/testing/browsertest/" + "apprtc/temp/google-cloud-sdk/bin/dev_appserver.py")); + if (!base::PathExists(appengine_dev_appserver)) { + LOG(ERROR) << "Missing appengine sdk at " << + appengine_dev_appserver.value() << ".\n" << + test::kAdviseOnGclientSolution; + return false; + } + + base::FilePath apprtc_dir = GetSourceDir().Append( + FILE_PATH_LITERAL("third_party/webrtc/rtc_tools/testing/" + "browsertest/apprtc/out/app_engine")); + if (!base::PathExists(apprtc_dir)) { + LOG(ERROR) << "Missing AppRTC AppEngine app at " << + apprtc_dir.value() << ".\n" << test::kAdviseOnGclientSolution; + return false; + } + if (!base::PathExists(apprtc_dir.Append(FILE_PATH_LITERAL("app.yaml")))) { + LOG(ERROR) << "The AppRTC AppEngine app at " << apprtc_dir.value() + << " appears to have not been built." + << "This should have been done by webrtc.DEPS scripts."; + return false; + } + + base::CommandLine command_line(base::CommandLine::NO_PROGRAM); + EXPECT_TRUE(GetPythonCommand(&command_line)); + + command_line.AppendArgPath(appengine_dev_appserver); + command_line.AppendArgPath(apprtc_dir); + command_line.AppendArg("--port=" + port); + command_line.AppendArg("--admin_port=9998"); + command_line.AppendArg("--skip_sdk_update_check"); + command_line.AppendArg("--clear_datastore=yes"); + + DVLOG(1) << "Running " << command_line.GetCommandLineString(); + dev_appserver_ = base::LaunchProcess(command_line, base::LaunchOptions()); + return dev_appserver_.IsValid(); + } + + bool LaunchColliderOnLocalHost(const std::string& apprtc_url, + const std::string& collider_port) { + // The go workspace should be created, and collidermain built, at the + // runhooks stage when webrtc.DEPS/build_apprtc_collider.py runs. +#if defined(OS_WIN) + base::FilePath collider_server = GetSourceDir().Append( + FILE_PATH_LITERAL("third_party/webrtc/rtc_tools/testing/" + "browsertest/collider/collidermain.exe")); +#else + base::FilePath collider_server = GetSourceDir().Append( + FILE_PATH_LITERAL("third_party/webrtc/rtc_tools/testing/" + "browsertest/collider/collidermain")); +#endif + if (!base::PathExists(collider_server)) { + LOG(ERROR) << "Missing Collider server binary at " << + collider_server.value() << ".\n" << test::kAdviseOnGclientSolution; + return false; + } + + base::CommandLine command_line(collider_server); + + command_line.AppendArg("-tls=false"); + command_line.AppendArg("-port=" + collider_port); + command_line.AppendArg("-room-server=" + apprtc_url); + + DVLOG(1) << "Running " << command_line.GetCommandLineString(); + collider_server_ = base::LaunchProcess(command_line, base::LaunchOptions()); + return collider_server_.IsValid(); + } + + bool LocalApprtcInstanceIsUp() { + // Load the admin page and see if we manage to load it right. + ui_test_utils::NavigateToURL(browser(), GURL("localhost:9998")); + content::WebContents* tab_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + std::string javascript = + "window.domAutomationController.send(document.title)"; + std::string result; + if (!content::ExecuteScriptAndExtractString(tab_contents, javascript, + &result)) + return false; + + return result == kTitlePageOfAppEngineAdminPage; + } + + bool WaitForCallToComeUp(content::WebContents* tab_contents) { + return test::PollingWaitUntil(kIsApprtcCallUpJavascript, "true", + tab_contents); + } + + bool EvalInJavascriptFile(content::WebContents* tab_contents, + const base::FilePath& path) { + std::string javascript; + if (!ReadFileToString(path, &javascript)) { + LOG(ERROR) << "Missing javascript code at " << path.value() << "."; + return false; + } + + if (!content::ExecuteScript(tab_contents, javascript)) { + LOG(ERROR) << "Failed to execute the following javascript: " << + javascript; + return false; + } + return true; + } + + bool DetectLocalVideoPlaying(content::WebContents* tab_contents) { + // The remote video tag is called "local-video" in the AppRTC code. + return DetectVideoPlaying(tab_contents, "local-video"); + } + + bool DetectRemoteVideoPlaying(content::WebContents* tab_contents) { + // The remote video tag is called "remote-video" in the AppRTC code. + return DetectVideoPlaying(tab_contents, "remote-video"); + } + + bool DetectVideoPlaying(content::WebContents* tab_contents, + const std::string& video_tag) { + if (!EvalInJavascriptFile(tab_contents, GetSourceDir().Append( + FILE_PATH_LITERAL("chrome/test/data/webrtc/test_functions.js")))) + return false; + if (!EvalInJavascriptFile(tab_contents, GetSourceDir().Append( + FILE_PATH_LITERAL("chrome/test/data/webrtc/video_detector.js")))) + return false; + + StartDetectingVideo(tab_contents, video_tag); + WaitForVideoToPlay(tab_contents); + return true; + } + + base::FilePath GetSourceDir() { + base::FilePath source_dir; + base::PathService::Get(base::DIR_SOURCE_ROOT, &source_dir); + return source_dir; + } + + private: + base::Process dev_appserver_; + base::Process collider_server_; +}; + +IN_PROC_BROWSER_TEST_F(WebRtcApprtcBrowserTest, MANUAL_WorksOnApprtc) { + base::ScopedAllowBlockingForTesting allow_blocking; + DetectErrorsInJavaScript(); + ASSERT_TRUE(LaunchApprtcInstanceOnLocalhost("9999")); + ASSERT_TRUE(LaunchColliderOnLocalHost("http://localhost:9999", "8089")); + while (!LocalApprtcInstanceIsUp()) + DVLOG(1) << "Waiting for AppRTC to come up..."; + + GURL room_url = GURL("http://localhost:9999/r/some_room" + "?wshpp=localhost:8089&wstls=false"); + + // Set up the left tab. + chrome::AddTabAt(browser(), GURL(), -1, true); + content::WebContents* left_tab = + browser()->tab_strip_model()->GetActiveWebContents(); + PermissionRequestManager::FromWebContents(left_tab) + ->set_auto_response_for_test(PermissionRequestManager::ACCEPT_ALL); + InfoBarResponder left_infobar_responder( + InfoBarService::FromWebContents(left_tab), InfoBarResponder::ACCEPT); + ui_test_utils::NavigateToURL(browser(), room_url); + + // Wait for the local video to start playing. This is needed, because opening + // a new tab too quickly, by sending the current tab to the background, can + // lead to the request for starting the video capture in the current tab to + // not get sent before it comes back to the foreground (which in this test + // case is never). + ASSERT_TRUE(DetectLocalVideoPlaying(left_tab)); + + // Set up the right tab. + chrome::AddTabAt(browser(), GURL(), -1, true); + content::WebContents* right_tab = + browser()->tab_strip_model()->GetActiveWebContents(); + PermissionRequestManager::FromWebContents(right_tab) + ->set_auto_response_for_test(PermissionRequestManager::ACCEPT_ALL); + InfoBarResponder right_infobar_responder( + InfoBarService::FromWebContents(right_tab), InfoBarResponder::ACCEPT); + ui_test_utils::NavigateToURL(browser(), room_url); + + ASSERT_TRUE(WaitForCallToComeUp(left_tab)); + ASSERT_TRUE(WaitForCallToComeUp(right_tab)); + + ASSERT_TRUE(DetectRemoteVideoPlaying(left_tab)); + ASSERT_TRUE(DetectRemoteVideoPlaying(right_tab)); + + chrome::CloseWebContents(browser(), left_tab, false); + chrome::CloseWebContents(browser(), right_tab, false); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_browsertest.cc new file mode 100644 index 00000000000..3afb1d4d9c8 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_browsertest.cc @@ -0,0 +1,344 @@ +// 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/command_line.h" +#include "base/deferred_sequenced_task_runner.h" +#include "base/test/bind_test_util.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/browser/network_service_instance.h" +#include "content/public/common/content_switches.h" +#include "content/public/common/network_service_util.h" +#include "content/public/test/browser_test_utils.h" +#include "media/base/media_switches.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "net/nqe/network_quality_estimator.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "services/network/network_service.h" +#include "services/network/public/cpp/features.h" +#include "services/network/public/mojom/network_service_test.mojom.h" +#include "third_party/blink/public/common/features.h" + +#if defined(OS_MACOSX) +#include "base/mac/mac_util.h" +#endif + +static const char kMainWebrtcTestHtmlPage[] = + "/webrtc/webrtc_jsep01_test.html"; + +static const char kKeygenAlgorithmRsa[] = + "{ name: \"RSASSA-PKCS1-v1_5\", modulusLength: 2048, publicExponent: " + "new Uint8Array([1, 0, 1]), hash: \"SHA-256\" }"; +static const char kKeygenAlgorithmEcdsa[] = + "{ name: \"ECDSA\", namedCurve: \"P-256\" }"; + +// Top-level integration test for WebRTC. It always uses fake devices; see +// WebRtcWebcamBrowserTest for a test that acquires any real webcam on the +// system. +class WebRtcBrowserTest : public WebRtcTestBase { + public: + WebRtcBrowserTest() : left_tab_(nullptr), right_tab_(nullptr) {} + + void SetUpInProcessBrowserTestFixture() override { + DetectErrorsInJavaScript(); // Look for errors in our rather complex js. + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + // Ensure the infobar is enabled, since we expect that in this test. + EXPECT_FALSE(command_line->HasSwitch(switches::kUseFakeUIForMediaStream)); + + // Always use fake devices. + command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); + + // Flag used by TestWebAudioMediaStream to force garbage collection. + command_line->AppendSwitchASCII(switches::kJavaScriptFlags, "--expose-gc"); + } + + void RunsAudioVideoWebRTCCallInTwoTabs( + const std::string& video_codec = WebRtcTestBase::kUseDefaultVideoCodec, + bool prefer_hw_video_codec = false, + const std::string& offer_cert_keygen_alg = + WebRtcTestBase::kUseDefaultCertKeygen, + const std::string& answer_cert_keygen_alg = + WebRtcTestBase::kUseDefaultCertKeygen) { + StartServerAndOpenTabs(); + + SetupPeerconnectionWithLocalStream(left_tab_, offer_cert_keygen_alg); + SetupPeerconnectionWithLocalStream(right_tab_, answer_cert_keygen_alg); + + if (!video_codec.empty()) { + SetDefaultVideoCodec(left_tab_, video_codec, prefer_hw_video_codec); + SetDefaultVideoCodec(right_tab_, video_codec, prefer_hw_video_codec); + } + NegotiateCall(left_tab_, right_tab_); + + DetectVideoAndHangUp(); + } + + void RunsAudioVideoWebRTCCallInTwoTabsWithClonedCertificate( + const std::string& cert_keygen_alg = + WebRtcTestBase::kUseDefaultCertKeygen) { + StartServerAndOpenTabs(); + + // Generate and clone a certificate, resulting in JavaScript variable + // |gCertificateClone| being set to the resulting clone. + DeleteDatabase(left_tab_); + OpenDatabase(left_tab_); + GenerateAndCloneCertificate(left_tab_, cert_keygen_alg); + CloseDatabase(left_tab_); + DeleteDatabase(left_tab_); + + SetupPeerconnectionWithCertificateAndLocalStream( + left_tab_, "gCertificateClone"); + SetupPeerconnectionWithLocalStream(right_tab_, cert_keygen_alg); + + NegotiateCall(left_tab_, right_tab_); + VerifyLocalDescriptionContainsCertificate(left_tab_, "gCertificate"); + + DetectVideoAndHangUp(); + } + + uint32_t GetPeerToPeerConnectionsCountChangeFromNetworkService() { + uint32_t connection_count = 0u; + if (content::IsInProcessNetworkService()) { + base::RunLoop run_loop; + content::GetNetworkTaskRunner()->PostTask( + FROM_HERE, base::BindLambdaForTesting([&connection_count, &run_loop] { + connection_count = + network::NetworkService::GetNetworkServiceForTesting() + ->network_quality_estimator() + ->GetPeerToPeerConnectionsCountChange(); + run_loop.Quit(); + })); + run_loop.Run(); + return connection_count; + } + + mojo::Remote<network::mojom::NetworkServiceTest> network_service_test; + content::GetNetworkService()->BindTestInterface( + network_service_test.BindNewPipeAndPassReceiver()); + // TODO(crbug.com/901026): Make sure the network process is started to avoid + // a deadlock on Android. + network_service_test.FlushForTesting(); + + mojo::ScopedAllowSyncCallForTesting allow_sync_call; + + bool available = network_service_test->GetPeerToPeerConnectionsCountChange( + &connection_count); + EXPECT_TRUE(available); + + return connection_count; + } + + protected: + void StartServerAndOpenTabs() { + ASSERT_TRUE(embedded_test_server()->Start()); + left_tab_ = OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + right_tab_ = OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + } + + void DetectVideoAndHangUp() { + StartDetectingVideo(left_tab_, "remote-view"); + StartDetectingVideo(right_tab_, "remote-view"); +#if !defined(OS_MACOSX) + // Video is choppy on Mac OS X. http://crbug.com/443542. + WaitForVideoToPlay(left_tab_); + WaitForVideoToPlay(right_tab_); +#endif + HangUp(left_tab_); + HangUp(right_tab_); + } + + content::WebContents* left_tab_; + content::WebContents* right_tab_; +}; + +// TODO(898546): many of these tests are failing on ASan builds. +#if defined(ADDRESS_SANITIZER) +#define MAYBE_WebRtcBrowserTest DISABLED_WebRtcBrowserTest +class DISABLED_WebRtcBrowserTest : public WebRtcBrowserTest {}; +#else +#define MAYBE_WebRtcBrowserTest WebRtcBrowserTest +#endif + +IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsVP8) { + RunsAudioVideoWebRTCCallInTwoTabs("VP8"); +} + +IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsVP9) { + RunsAudioVideoWebRTCCallInTwoTabs("VP9"); +} + +#if BUILDFLAG(RTC_USE_H264) + +IN_PROC_BROWSER_TEST_F(WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsH264) { + // Only run test if run-time feature corresponding to |rtc_use_h264| is on. + if (!base::FeatureList::IsEnabled( + blink::features::kWebRtcH264WithOpenH264FFmpeg)) { + LOG(WARNING) << "Run-time feature WebRTC-H264WithOpenH264FFmpeg disabled. " + "Skipping WebRtcBrowserTest.RunsAudioVideoWebRTCCallInTwoTabsH264 " + "(test \"OK\")"; + return; + } + +#if defined(OS_MACOSX) + // TODO(jam): this test only on 10.12. + if (base::mac::IsOS10_12()) + return; +#endif + + RunsAudioVideoWebRTCCallInTwoTabs("H264", true /* prefer_hw_video_codec */); +} + +#endif // BUILDFLAG(RTC_USE_H264) + +IN_PROC_BROWSER_TEST_F(WebRtcBrowserTest, TestWebAudioMediaStream) { + // This tests against crash regressions for the WebAudio-MediaStream + // integration. + ASSERT_TRUE(embedded_test_server()->Start()); + GURL url(embedded_test_server()->GetURL("/webrtc/webaudio_crash.html")); + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* tab = + browser()->tab_strip_model()->GetActiveWebContents(); + + // A sleep is necessary to be able to detect the crash. + test::SleepInJavascript(tab, 1000); + + ASSERT_FALSE(tab->IsCrashed()); +} + +IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsOfferRsaAnswerRsa) { + RunsAudioVideoWebRTCCallInTwoTabs(WebRtcTestBase::kUseDefaultVideoCodec, + false /* prefer_hw_video_codec */, + kKeygenAlgorithmRsa, kKeygenAlgorithmRsa); +} + +IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsOfferEcdsaAnswerEcdsa) { + RunsAudioVideoWebRTCCallInTwoTabs( + WebRtcTestBase::kUseDefaultVideoCodec, false /* prefer_hw_video_codec */, + kKeygenAlgorithmEcdsa, kKeygenAlgorithmEcdsa); +} + +IN_PROC_BROWSER_TEST_F( + MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsWithClonedCertificateRsa) { + RunsAudioVideoWebRTCCallInTwoTabsWithClonedCertificate(kKeygenAlgorithmRsa); +} + +IN_PROC_BROWSER_TEST_F( + MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsWithClonedCertificateEcdsa) { + RunsAudioVideoWebRTCCallInTwoTabsWithClonedCertificate(kKeygenAlgorithmEcdsa); +} + +IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsOfferRsaAnswerEcdsa) { + RunsAudioVideoWebRTCCallInTwoTabs(WebRtcTestBase::kUseDefaultVideoCodec, + false /* prefer_hw_video_codec */, + kKeygenAlgorithmRsa, kKeygenAlgorithmEcdsa); +} + +IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsOfferEcdsaAnswerRsa) { + RunsAudioVideoWebRTCCallInTwoTabs(WebRtcTestBase::kUseDefaultVideoCodec, + false /* prefer_hw_video_codec */, + kKeygenAlgorithmEcdsa, kKeygenAlgorithmRsa); +} + +IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsGetStatsCallback) { + StartServerAndOpenTabs(); + SetupPeerconnectionWithLocalStream(left_tab_); + SetupPeerconnectionWithLocalStream(right_tab_); + NegotiateCall(left_tab_, right_tab_); + + VerifyStatsGeneratedCallback(left_tab_); + + DetectVideoAndHangUp(); +} + +IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcBrowserTest, + GetPeerToPeerConnectionsCountChangeFromNetworkService) { + EXPECT_EQ(0u, GetPeerToPeerConnectionsCountChangeFromNetworkService()); + + StartServerAndOpenTabs(); + SetupPeerconnectionWithLocalStream(left_tab_); + + SetupPeerconnectionWithLocalStream(right_tab_); + NegotiateCall(left_tab_, right_tab_); + + VerifyStatsGeneratedCallback(left_tab_); + EXPECT_EQ(2u, GetPeerToPeerConnectionsCountChangeFromNetworkService()); + + DetectVideoAndHangUp(); + EXPECT_EQ(0u, GetPeerToPeerConnectionsCountChangeFromNetworkService()); +} + +IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsGetStatsPromise) { + StartServerAndOpenTabs(); + SetupPeerconnectionWithLocalStream(left_tab_); + SetupPeerconnectionWithLocalStream(right_tab_); + CreateDataChannel(left_tab_, "data"); + CreateDataChannel(right_tab_, "data"); + NegotiateCall(left_tab_, right_tab_); + + std::set<std::string> missing_expected_stats; + for (const std::string& type : GetMandatoryStatsTypes(left_tab_)) { + missing_expected_stats.insert(type); + } + for (const std::string& type : VerifyStatsGeneratedPromise(left_tab_)) { + missing_expected_stats.erase(type); + } + for (const std::string& type : missing_expected_stats) { + EXPECT_TRUE(false) << "Expected stats dictionary is missing: " << type; + } + + DetectVideoAndHangUp(); +} + +IN_PROC_BROWSER_TEST_F( + MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsEmitsGatheringStateChange) { + StartServerAndOpenTabs(); + SetupPeerconnectionWithLocalStream(left_tab_); + SetupPeerconnectionWithLocalStream(right_tab_); + NegotiateCall(left_tab_, right_tab_); + + std::string ice_gatheringstate = + ExecuteJavascript("getLastGatheringState()", left_tab_); + + EXPECT_EQ("complete", ice_gatheringstate); + DetectVideoAndHangUp(); +} + +IN_PROC_BROWSER_TEST_F( + MAYBE_WebRtcBrowserTest, + RunsAudioVideoWebRTCCallInTwoTabsEmitsGatheringStateChange_ConnectionCount) { + EXPECT_EQ(0u, GetPeerToPeerConnectionsCountChangeFromNetworkService()); + StartServerAndOpenTabs(); + SetupPeerconnectionWithLocalStream(left_tab_); + SetupPeerconnectionWithLocalStream(right_tab_); + NegotiateCall(left_tab_, right_tab_); + EXPECT_EQ(2u, GetPeerToPeerConnectionsCountChangeFromNetworkService()); + + std::string ice_gatheringstate = + ExecuteJavascript("getLastGatheringState()", left_tab_); + + EXPECT_EQ("complete", ice_gatheringstate); + DetectVideoAndHangUp(); + EXPECT_EQ(0u, GetPeerToPeerConnectionsCountChangeFromNetworkService()); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_browsertest_base.cc b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_base.cc new file mode 100644 index 00000000000..3affb26a72b --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_base.cc @@ -0,0 +1,651 @@ +// 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 "chrome/browser/media/webrtc/webrtc_browsertest_base.h" + +#include <stddef.h> + +#include <limits> + +#include "base/json/json_reader.h" +#include "base/lazy_instance.h" +#include "base/logging.h" +#include "base/macros.h" +#include "base/path_service.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/values.h" +#include "build/build_config.h" +#include "chrome/browser/extensions/chrome_test_extension_loader.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/browser/permissions/permission_request_manager.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/test/browser_test_utils.h" +#include "extensions/browser/extension_registry.h" +#include "net/test/embedded_test_server/embedded_test_server.h" + +#if defined(OS_WIN) +// For fine-grained suppression. +#include "base/win/windows_version.h" +#endif + +const char WebRtcTestBase::kAudioVideoCallConstraints[] = + "{audio: true, video: true}"; +const char WebRtcTestBase::kVideoCallConstraintsQVGA[] = + "{video: {mandatory: {minWidth: 320, maxWidth: 320, " + " minHeight: 240, maxHeight: 240}}}"; +const char WebRtcTestBase::kVideoCallConstraints360p[] = + "{video: {mandatory: {minWidth: 640, maxWidth: 640, " + " minHeight: 360, maxHeight: 360}}}"; +const char WebRtcTestBase::kVideoCallConstraintsVGA[] = + "{video: {mandatory: {minWidth: 640, maxWidth: 640, " + " minHeight: 480, maxHeight: 480}}}"; +const char WebRtcTestBase::kVideoCallConstraints720p[] = + "{video: {mandatory: {minWidth: 1280, maxWidth: 1280, " + " minHeight: 720, maxHeight: 720}}}"; +const char WebRtcTestBase::kVideoCallConstraints1080p[] = + "{video: {mandatory: {minWidth: 1920, maxWidth: 1920, " + " minHeight: 1080, maxHeight: 1080}}}"; +const char WebRtcTestBase::kAudioOnlyCallConstraints[] = "{audio: true}"; +const char WebRtcTestBase::kVideoOnlyCallConstraints[] = "{video: true}"; +const char WebRtcTestBase::kOkGotStream[] = "ok-got-stream"; +const char WebRtcTestBase::kFailedWithNotAllowedError[] = + "failed-with-error-NotAllowedError"; +const char WebRtcTestBase::kAudioVideoCallConstraints360p[] = + "{audio: true, video: {mandatory: {minWidth: 640, maxWidth: 640, " + " minHeight: 360, maxHeight: 360}}}"; +const char WebRtcTestBase::kAudioVideoCallConstraints720p[] = + "{audio: true, video: {mandatory: {minWidth: 1280, maxWidth: 1280, " + " minHeight: 720, maxHeight: 720}}}"; +const char WebRtcTestBase::kUseDefaultCertKeygen[] = "null"; +const char WebRtcTestBase::kUseDefaultAudioCodec[] = ""; +const char WebRtcTestBase::kUseDefaultVideoCodec[] = ""; +const char WebRtcTestBase::kVP9Profile0Specifier[] = "profile-id=0"; +const char WebRtcTestBase::kVP9Profile2Specifier[] = "profile-id=2"; +const char WebRtcTestBase::kUndefined[] = "undefined"; + +namespace { + +base::LazyInstance<bool>::DestructorAtExit hit_javascript_errors_ = + LAZY_INSTANCE_INITIALIZER; + +// Intercepts all log messages. We always attach this handler but only look at +// the results if the test requests so. Note that this will only work if the +// WebrtcTestBase-inheriting test cases do not run in parallel (if they did they +// would race to look at the log, which is global to all tests). +bool JavascriptErrorDetectingLogHandler(int severity, + const char* file, + int line, + size_t message_start, + const std::string& str) { + if (file == NULL || std::string("CONSOLE") != file) + return false; + + // TODO(crbug.com/918871): Fix AppRTC and stop ignoring this error. + if (str.find("Synchronous XHR in page dismissal") != std::string::npos) + return false; + + bool contains_uncaught = str.find("\"Uncaught ") != std::string::npos; + if (severity == logging::LOG_ERROR || + (severity == logging::LOG_INFO && contains_uncaught)) { + hit_javascript_errors_.Get() = true; + } + + return false; +} + +// PermissionRequestObserver --------------------------------------------------- + +// Used to observe the creation of permission prompt without responding. +class PermissionRequestObserver : public PermissionRequestManager::Observer { + public: + explicit PermissionRequestObserver(content::WebContents* web_contents) + : request_manager_( + PermissionRequestManager::FromWebContents(web_contents)), + request_shown_(false), + message_loop_runner_(new content::MessageLoopRunner) { + request_manager_->AddObserver(this); + } + ~PermissionRequestObserver() override { + // Safe to remove twice if it happens. + request_manager_->RemoveObserver(this); + } + + void Wait() { message_loop_runner_->Run(); } + + bool request_shown() const { return request_shown_; } + + private: + // PermissionRequestManager::Observer + void OnBubbleAdded() override { + request_shown_ = true; + request_manager_->RemoveObserver(this); + message_loop_runner_->Quit(); + } + + PermissionRequestManager* request_manager_; + bool request_shown_; + scoped_refptr<content::MessageLoopRunner> message_loop_runner_; + + DISALLOW_COPY_AND_ASSIGN(PermissionRequestObserver); +}; + +std::vector<std::string> JsonArrayToVectorOfStrings( + const std::string& json_array) { + std::unique_ptr<base::Value> value = + base::JSONReader::ReadDeprecated(json_array); + EXPECT_TRUE(value); + EXPECT_TRUE(value->is_list()); + std::unique_ptr<base::ListValue> list = + base::ListValue::From(std::move(value)); + std::vector<std::string> vector; + vector.reserve(list->GetSize()); + for (size_t i = 0; i < list->GetSize(); ++i) { + base::Value* item; + EXPECT_TRUE(list->Get(i, &item)); + EXPECT_TRUE(item->is_string()); + std::string item_str; + EXPECT_TRUE(item->GetAsString(&item_str)); + vector.push_back(std::move(item_str)); + } + return vector; +} + +} // namespace + +WebRtcTestBase::WebRtcTestBase(): detect_errors_in_javascript_(false) { + // The handler gets set for each test method, but that's fine since this + // set operation is idempotent. + logging::SetLogMessageHandler(&JavascriptErrorDetectingLogHandler); + hit_javascript_errors_.Get() = false; + + EnablePixelOutput(); +} + +WebRtcTestBase::~WebRtcTestBase() { + if (detect_errors_in_javascript_) { + EXPECT_FALSE(hit_javascript_errors_.Get()) + << "Encountered javascript errors during test execution (Search " + << "for Uncaught or ERROR:CONSOLE in the test output)."; + } +} + +bool WebRtcTestBase::GetUserMediaAndAccept( + content::WebContents* tab_contents) const { + return GetUserMediaWithSpecificConstraintsAndAccept( + tab_contents, kAudioVideoCallConstraints); +} + +bool WebRtcTestBase::GetUserMediaWithSpecificConstraintsAndAccept( + content::WebContents* tab_contents, + const std::string& constraints) const { + std::string result; + PermissionRequestManager::FromWebContents(tab_contents) + ->set_auto_response_for_test(PermissionRequestManager::ACCEPT_ALL); + PermissionRequestObserver permissionRequestObserver(tab_contents); + GetUserMedia(tab_contents, constraints); + EXPECT_TRUE(permissionRequestObserver.request_shown()); + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab_contents->GetMainFrame(), "obtainGetUserMediaResult();", &result)); + return kOkGotStream == result; +} + +bool WebRtcTestBase::GetUserMediaWithSpecificConstraintsAndAcceptIfPrompted( + content::WebContents* tab_contents, + const std::string& constraints) const { + std::string result; + PermissionRequestManager::FromWebContents(tab_contents) + ->set_auto_response_for_test(PermissionRequestManager::ACCEPT_ALL); + GetUserMedia(tab_contents, constraints); + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab_contents->GetMainFrame(), "obtainGetUserMediaResult();", &result)); + return kOkGotStream == result; +} + +void WebRtcTestBase::GetUserMediaAndDeny(content::WebContents* tab_contents) { + return GetUserMediaWithSpecificConstraintsAndDeny(tab_contents, + kAudioVideoCallConstraints); +} + +void WebRtcTestBase::GetUserMediaWithSpecificConstraintsAndDeny( + content::WebContents* tab_contents, + const std::string& constraints) const { + std::string result; + PermissionRequestManager::FromWebContents(tab_contents) + ->set_auto_response_for_test(PermissionRequestManager::DENY_ALL); + PermissionRequestObserver permissionRequestObserver(tab_contents); + GetUserMedia(tab_contents, constraints); + EXPECT_TRUE(permissionRequestObserver.request_shown()); + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab_contents->GetMainFrame(), "obtainGetUserMediaResult();", &result)); + EXPECT_EQ(kFailedWithNotAllowedError, result); +} + +void WebRtcTestBase::GetUserMediaAndDismiss( + content::WebContents* tab_contents) const { + std::string result; + PermissionRequestManager::FromWebContents(tab_contents) + ->set_auto_response_for_test(PermissionRequestManager::DISMISS); + PermissionRequestObserver permissionRequestObserver(tab_contents); + GetUserMedia(tab_contents, kAudioVideoCallConstraints); + EXPECT_TRUE(permissionRequestObserver.request_shown()); + // A dismiss should be treated like a deny. + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab_contents->GetMainFrame(), "obtainGetUserMediaResult();", &result)); + EXPECT_EQ(kFailedWithNotAllowedError, result); +} + +void WebRtcTestBase::GetUserMediaAndExpectAutoAcceptWithoutPrompt( + content::WebContents* tab_contents) const { + std::string result; + // We issue a GetUserMedia() request. We expect that the origin already has a + // sticky "accept" permission (e.g. because the caller previously called + // GetUserMediaAndAccept()), and therefore the GetUserMedia() request + // automatically succeeds without a prompt. + // If the caller made a mistake, a prompt may show up instead. For this case, + // we set an auto-response to avoid leaving the prompt hanging. The choice of + // DENY_ALL makes sure that the response to the prompt doesn't accidentally + // result in a newly granted media stream permission. + PermissionRequestManager::FromWebContents(tab_contents) + ->set_auto_response_for_test(PermissionRequestManager::DENY_ALL); + PermissionRequestObserver permissionRequestObserver(tab_contents); + GetUserMedia(tab_contents, kAudioVideoCallConstraints); + EXPECT_FALSE(permissionRequestObserver.request_shown()); + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab_contents->GetMainFrame(), "obtainGetUserMediaResult();", &result)); + EXPECT_EQ(kOkGotStream, result); +} + +void WebRtcTestBase::GetUserMediaAndExpectAutoDenyWithoutPrompt( + content::WebContents* tab_contents) const { + std::string result; + // We issue a GetUserMedia() request. We expect that the origin already has a + // sticky "deny" permission (e.g. because the caller previously called + // GetUserMediaAndDeny()), and therefore the GetUserMedia() request + // automatically succeeds without a prompt. + // If the caller made a mistake, a prompt may show up instead. For this case, + // we set an auto-response to avoid leaving the prompt hanging. The choice of + // ACCEPT_ALL makes sure that the response to the prompt doesn't accidentally + // result in a newly granted media stream permission. + PermissionRequestManager::FromWebContents(tab_contents) + ->set_auto_response_for_test(PermissionRequestManager::ACCEPT_ALL); + PermissionRequestObserver permissionRequestObserver(tab_contents); + GetUserMedia(tab_contents, kAudioVideoCallConstraints); + EXPECT_FALSE(permissionRequestObserver.request_shown()); + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab_contents->GetMainFrame(), "obtainGetUserMediaResult();", &result)); + EXPECT_EQ(kFailedWithNotAllowedError, result); +} + +void WebRtcTestBase::GetUserMedia(content::WebContents* tab_contents, + const std::string& constraints) const { + // Request user media: this will launch the media stream info bar or bubble. + std::string result; + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab_contents, "doGetUserMedia(" + constraints + ");", &result)); + EXPECT_TRUE(result == "request-callback-denied" || + result == "request-callback-granted"); +} + +content::WebContents* WebRtcTestBase::OpenPageAndGetUserMediaInNewTab( + const GURL& url) const { + return OpenPageAndGetUserMediaInNewTabWithConstraints( + url, kAudioVideoCallConstraints); +} + +content::WebContents* +WebRtcTestBase::OpenPageAndGetUserMediaInNewTabWithConstraints( + const GURL& url, + const std::string& constraints) const { + chrome::AddTabAt(browser(), GURL(), -1, true); + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* new_tab = + browser()->tab_strip_model()->GetActiveWebContents(); + // Accept if necessary, but don't expect a prompt (because auto-accept is also + // okay). + PermissionRequestManager::FromWebContents(new_tab) + ->set_auto_response_for_test(PermissionRequestManager::ACCEPT_ALL); + GetUserMedia(new_tab, constraints); + std::string result; + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + new_tab->GetMainFrame(), "obtainGetUserMediaResult();", &result)); + EXPECT_EQ(kOkGotStream, result); + return new_tab; +} + +content::WebContents* WebRtcTestBase::OpenTestPageAndGetUserMediaInNewTab( + const std::string& test_page) const { + return OpenPageAndGetUserMediaInNewTab( + embedded_test_server()->GetURL(test_page)); +} + +content::WebContents* WebRtcTestBase::OpenTestPageInNewTab( + const std::string& test_page) const { + chrome::AddTabAt(browser(), GURL(), -1, true); + GURL url = embedded_test_server()->GetURL(test_page); + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* new_tab = + browser()->tab_strip_model()->GetActiveWebContents(); + // Accept if necessary, but don't expect a prompt (because auto-accept is also + // okay). + PermissionRequestManager::FromWebContents(new_tab) + ->set_auto_response_for_test(PermissionRequestManager::ACCEPT_ALL); + return new_tab; +} + +void WebRtcTestBase::CloseLastLocalStream( + content::WebContents* tab_contents) const { + EXPECT_EQ("ok-stopped", + ExecuteJavascript("stopLocalStream();", tab_contents)); +} + +// Convenience method which executes the provided javascript in the context +// of the provided web contents and returns what it evaluated to. +std::string WebRtcTestBase::ExecuteJavascript( + const std::string& javascript, + content::WebContents* tab_contents) const { + std::string result; + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab_contents, javascript, &result)); + return result; +} + +void WebRtcTestBase::ChangeToLegacyGetStats(content::WebContents* tab) const { + content::ExecuteScriptAsync(tab, "changeToLegacyGetStats()"); +} + +void WebRtcTestBase::SetupPeerconnectionWithLocalStream( + content::WebContents* tab, + const std::string& certificate_keygen_algorithm) const { + SetupPeerconnectionWithoutLocalStream(tab, certificate_keygen_algorithm); + EXPECT_EQ("ok-added", ExecuteJavascript("addLocalStream()", tab)); +} + +void WebRtcTestBase::SetupPeerconnectionWithoutLocalStream( + content::WebContents* tab, + const std::string& certificate_keygen_algorithm) const { + std::string javascript = base::StringPrintf( + "preparePeerConnection(%s)", certificate_keygen_algorithm.c_str()); + EXPECT_EQ("ok-peerconnection-created", ExecuteJavascript(javascript, tab)); +} + +void WebRtcTestBase::SetupPeerconnectionWithCertificateAndLocalStream( + content::WebContents* tab, + const std::string& certificate) const { + SetupPeerconnectionWithCertificateWithoutLocalStream(tab, certificate); + EXPECT_EQ("ok-added", ExecuteJavascript("addLocalStream()", tab)); +} + +void WebRtcTestBase::SetupPeerconnectionWithCertificateWithoutLocalStream( + content::WebContents* tab, + const std::string& certificate) const { + std::string javascript = base::StringPrintf( + "preparePeerConnectionWithCertificate(%s)", certificate.c_str()); + EXPECT_EQ("ok-peerconnection-created", ExecuteJavascript(javascript, tab)); +} + +void WebRtcTestBase::SetupPeerconnectionWithConstraintsAndLocalStream( + content::WebContents* tab, + const std::string& constraints, + const std::string& certificate_keygen_algorithm) const { + std::string javascript = base::StringPrintf( + "preparePeerConnection(%s, %s)", certificate_keygen_algorithm.c_str(), + constraints.c_str()); + EXPECT_EQ("ok-peerconnection-created", ExecuteJavascript(javascript, tab)); + EXPECT_EQ("ok-added", ExecuteJavascript("addLocalStream()", tab)); +} + +std::string WebRtcTestBase::CreateLocalOffer( + content::WebContents* from_tab) const { + std::string response = ExecuteJavascript("createLocalOffer({})", from_tab); + EXPECT_EQ("ok-", response.substr(0, 3)) << "Failed to create local offer: " + << response; + + std::string local_offer = response.substr(3); + return local_offer; +} + +std::string WebRtcTestBase::CreateAnswer(std::string local_offer, + content::WebContents* to_tab) const { + std::string javascript = + base::StringPrintf("receiveOfferFromPeer('%s', {})", local_offer.c_str()); + std::string response = ExecuteJavascript(javascript, to_tab); + EXPECT_EQ("ok-", response.substr(0, 3)) + << "Receiving peer failed to receive offer and create answer: " + << response; + + std::string answer = response.substr(3); + response = ExecuteJavascript( + base::StringPrintf("verifyDefaultCodecs('%s')", answer.c_str()), + to_tab); + EXPECT_EQ("ok-", response.substr(0, 3)) + << "Receiving peer failed to verify default codec: " << response; + return answer; +} + +void WebRtcTestBase::ReceiveAnswer(const std::string& answer, + content::WebContents* from_tab) const { + ASSERT_EQ( + "ok-accepted-answer", + ExecuteJavascript( + base::StringPrintf("receiveAnswerFromPeer('%s')", answer.c_str()), + from_tab)); +} + +void WebRtcTestBase::GatherAndSendIceCandidates( + content::WebContents* from_tab, + content::WebContents* to_tab) const { + std::string ice_candidates = + ExecuteJavascript("getAllIceCandidates()", from_tab); + + EXPECT_EQ("ok-received-candidates", ExecuteJavascript( + base::StringPrintf("receiveIceCandidates('%s')", ice_candidates.c_str()), + to_tab)); +} + +void WebRtcTestBase::CreateDataChannel(content::WebContents* tab, + const std::string& label) { + EXPECT_EQ("ok-created", + ExecuteJavascript("createDataChannel('" + label + "')", tab)); +} + +void WebRtcTestBase::NegotiateCall(content::WebContents* from_tab, + content::WebContents* to_tab) const { + std::string local_offer = CreateLocalOffer(from_tab); + std::string answer = CreateAnswer(local_offer, to_tab); + ReceiveAnswer(answer, from_tab); + + // Send all ICE candidates (wait for gathering to finish if necessary). + GatherAndSendIceCandidates(to_tab, from_tab); + GatherAndSendIceCandidates(from_tab, to_tab); +} + +void WebRtcTestBase::VerifyLocalDescriptionContainsCertificate( + content::WebContents* tab, + const std::string& certificate) const { + std::string javascript = base::StringPrintf( + "verifyLocalDescriptionContainsCertificate(%s)", certificate.c_str()); + EXPECT_EQ("ok-verified", ExecuteJavascript(javascript, tab)); +} + +void WebRtcTestBase::HangUp(content::WebContents* from_tab) const { + EXPECT_EQ("ok-call-hung-up", ExecuteJavascript("hangUp()", from_tab)); +} + +void WebRtcTestBase::DetectErrorsInJavaScript() { + detect_errors_in_javascript_ = true; +} + +void WebRtcTestBase::StartDetectingVideo( + content::WebContents* tab_contents, + const std::string& video_element) const { + std::string javascript = base::StringPrintf( + "startDetection('%s', 320, 240)", video_element.c_str()); + EXPECT_EQ("ok-started", ExecuteJavascript(javascript, tab_contents)); +} + +bool WebRtcTestBase::WaitForVideoToPlay( + content::WebContents* tab_contents) const { + bool is_video_playing = test::PollingWaitUntil( + "isVideoPlaying()", "video-playing", tab_contents); + EXPECT_TRUE(is_video_playing); + return is_video_playing; +} + +std::string WebRtcTestBase::GetStreamSize( + content::WebContents* tab_contents, + const std::string& video_element) const { + std::string javascript = + base::StringPrintf("getStreamSize('%s')", video_element.c_str()); + std::string result = ExecuteJavascript(javascript, tab_contents); + EXPECT_TRUE(base::StartsWith(result, "ok-", base::CompareCase::SENSITIVE)); + return result.substr(3); +} + +bool WebRtcTestBase::OnWin8OrHigher() const { +#if defined(OS_WIN) + return base::win::GetVersion() >= base::win::Version::WIN8; +#else + return false; +#endif +} + +void WebRtcTestBase::OpenDatabase(content::WebContents* tab) const { + EXPECT_EQ("ok-database-opened", ExecuteJavascript("openDatabase()", tab)); +} + +void WebRtcTestBase::CloseDatabase(content::WebContents* tab) const { + EXPECT_EQ("ok-database-closed", ExecuteJavascript("closeDatabase()", tab)); +} + +void WebRtcTestBase::DeleteDatabase(content::WebContents* tab) const { + EXPECT_EQ("ok-database-deleted", ExecuteJavascript("deleteDatabase()", tab)); +} + +void WebRtcTestBase::GenerateAndCloneCertificate( + content::WebContents* tab, const std::string& keygen_algorithm) const { + std::string javascript = base::StringPrintf( + "generateAndCloneCertificate(%s)", keygen_algorithm.c_str()); + EXPECT_EQ("ok-generated-and-cloned", ExecuteJavascript(javascript, tab)); +} + +void WebRtcTestBase::VerifyStatsGeneratedCallback( + content::WebContents* tab) const { + EXPECT_EQ("ok-got-stats", + ExecuteJavascript("verifyLegacyStatsGenerated()", tab)); +} + +std::vector<std::string> WebRtcTestBase::VerifyStatsGeneratedPromise( + content::WebContents* tab) const { + std::string result = ExecuteJavascript("verifyStatsGeneratedPromise()", tab); + EXPECT_TRUE(base::StartsWith(result, "ok-", base::CompareCase::SENSITIVE)); + return JsonArrayToVectorOfStrings(result.substr(3)); +} + +double WebRtcTestBase::MeasureGetStatsCallbackPerformance( + content::WebContents* tab) const { + std::string result = ExecuteJavascript( + "measureGetStatsCallbackPerformance()", tab); + EXPECT_TRUE(base::StartsWith(result, "ok-", base::CompareCase::SENSITIVE)); + double ms; + if (!base::StringToDouble(result.substr(3), &ms)) + return std::numeric_limits<double>::infinity(); + return ms; +} + +scoped_refptr<content::TestStatsReportDictionary> +WebRtcTestBase::GetStatsReportDictionary(content::WebContents* tab) const { + std::string result = ExecuteJavascript("getStatsReportDictionary()", tab); + EXPECT_TRUE(base::StartsWith(result, "ok-", base::CompareCase::SENSITIVE)); + std::unique_ptr<base::Value> parsed_json = + base::JSONReader::ReadDeprecated(result.substr(3)); + base::DictionaryValue* dictionary; + CHECK(parsed_json); + CHECK(parsed_json->GetAsDictionary(&dictionary)); + ignore_result(parsed_json.release()); + return scoped_refptr<content::TestStatsReportDictionary>( + new content::TestStatsReportDictionary( + std::unique_ptr<base::DictionaryValue>(dictionary))); +} + +double WebRtcTestBase::MeasureGetStatsPerformance( + content::WebContents* tab) const { + std::string result = ExecuteJavascript("measureGetStatsPerformance()", tab); + EXPECT_TRUE(base::StartsWith(result, "ok-", base::CompareCase::SENSITIVE)); + double ms; + if (!base::StringToDouble(result.substr(3), &ms)) + return std::numeric_limits<double>::infinity(); + return ms; +} + +std::vector<std::string> WebRtcTestBase::GetMandatoryStatsTypes( + content::WebContents* tab) const { + return JsonArrayToVectorOfStrings( + ExecuteJavascript("getMandatoryStatsTypes()", tab)); +} + +void WebRtcTestBase::SetDefaultAudioCodec( + content::WebContents* tab, + const std::string& audio_codec) const { + EXPECT_EQ("ok", ExecuteJavascript( + "setDefaultAudioCodec('" + audio_codec + "')", tab)); +} + +void WebRtcTestBase::SetDefaultVideoCodec(content::WebContents* tab, + const std::string& video_codec, + bool prefer_hw_codec, + const std::string& profile) const { + std::string codec_profile = profile; + // When no |profile| is given, we default VP9 to Profile 0. + if (video_codec.compare("VP9") == 0 && codec_profile.empty()) + codec_profile = kVP9Profile0Specifier; + + EXPECT_EQ("ok", ExecuteJavascript( + "setDefaultVideoCodec('" + video_codec + "'," + + (prefer_hw_codec ? "true" : "false") + "," + + (codec_profile.empty() ? "null" + : "'" + codec_profile + "'") + + ")", + tab)); +} + +void WebRtcTestBase::EnableOpusDtx(content::WebContents* tab) const { + EXPECT_EQ("ok-forced", ExecuteJavascript("forceOpusDtx()", tab)); +} + +std::string WebRtcTestBase::GetDesktopMediaStream(content::WebContents* tab) { + DCHECK(static_cast<bool>(LoadDesktopCaptureExtension())); + + // Post a task to the extension, opening a desktop media stream. + return ExecuteJavascript("openDesktopMediaStream()", tab); +} + +base::Optional<std::string> WebRtcTestBase::LoadDesktopCaptureExtension() { + base::Optional<std::string> extension_id; + if (!desktop_capture_extension_.get()) { + extensions::ChromeTestExtensionLoader loader(browser()->profile()); + base::FilePath extension_path; + EXPECT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &extension_path)); + extension_path = extension_path.AppendASCII("extensions/desktop_capture"); + desktop_capture_extension_ = loader.LoadExtension(extension_path); + LOG(INFO) << "Loaded desktop capture extension, id = " + << desktop_capture_extension_->id(); + + extensions::ExtensionRegistry* registry = + extensions::ExtensionRegistry::Get(browser()->profile()); + + EXPECT_TRUE(registry->enabled_extensions().GetByID( + desktop_capture_extension_->id())); + } + if (desktop_capture_extension_) + extension_id.emplace(desktop_capture_extension_->id()); + return extension_id; +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_browsertest_base.h b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_base.h new file mode 100644 index 00000000000..e85f0026928 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_base.h @@ -0,0 +1,257 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_BROWSERTEST_BASE_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_BROWSERTEST_BASE_H_ + +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/optional.h" +#include "chrome/browser/media/webrtc/test_stats_dictionary.h" +#include "chrome/test/base/in_process_browser_test.h" + +namespace infobars { +class InfoBar; +} + +namespace content { +class WebContents; +} + +namespace extensions { +class Extension; +} + +// Base class for WebRTC browser tests with useful primitives for interacting +// getUserMedia. We use inheritance here because it makes the test code look +// as clean as it can be. +class WebRtcTestBase : public InProcessBrowserTest { + public: + // Typical constraints. + static const char kAudioVideoCallConstraints[]; + static const char kAudioOnlyCallConstraints[]; + static const char kVideoOnlyCallConstraints[]; + static const char kVideoCallConstraintsQVGA[]; + static const char kVideoCallConstraints360p[]; + static const char kVideoCallConstraintsVGA[]; + static const char kVideoCallConstraints720p[]; + static const char kVideoCallConstraints1080p[]; + static const char kAudioVideoCallConstraints360p[]; + static const char kAudioVideoCallConstraints720p[]; + + static const char kOkGotStream[]; + static const char kFailedWithNotAllowedError[]; + + static const char kUseDefaultCertKeygen[]; + static const char kUseDefaultAudioCodec[]; + static const char kUseDefaultVideoCodec[]; + + static const char kVP9Profile0Specifier[]; + static const char kVP9Profile2Specifier[]; + + static const char kUndefined[]; + + enum class StreamArgumentType { + NO_STREAM, + SHARED_STREAM, + INDIVIDUAL_STREAMS + }; + + protected: + WebRtcTestBase(); + ~WebRtcTestBase() override; + + // These all require that the loaded page fulfills the public interface in + // chrome/test/data/webrtc/getusermedia.js. + // If an error is reported back from the getUserMedia call, these functions + // will return false. + // The ...AndAccept()/...AndDeny()/...AndDismiss() functions expect that a + // prompt will be shown (i.e. the current origin in the tab_contents doesn't + // have a saved permission). + bool GetUserMediaAndAccept(content::WebContents* tab_contents) const; + bool GetUserMediaWithSpecificConstraintsAndAccept( + content::WebContents* tab_contents, + const std::string& constraints) const; + bool GetUserMediaWithSpecificConstraintsAndAcceptIfPrompted( + content::WebContents* tab_contents, + const std::string& constraints) const; + void GetUserMediaAndDeny(content::WebContents* tab_contents); + void GetUserMediaWithSpecificConstraintsAndDeny( + content::WebContents* tab_contents, + const std::string& constraints) const; + void GetUserMediaAndDismiss(content::WebContents* tab_contents) const; + void GetUserMediaAndExpectAutoAcceptWithoutPrompt( + content::WebContents* tab_contents) const; + void GetUserMediaAndExpectAutoDenyWithoutPrompt( + content::WebContents* tab_contents) const; + void GetUserMedia(content::WebContents* tab_contents, + const std::string& constraints) const; + + // Convenience method which opens the page at url, calls GetUserMediaAndAccept + // and returns the new tab. + content::WebContents* OpenPageAndGetUserMediaInNewTab(const GURL& url) const; + + // Convenience method which opens the page at url, calls + // GetUserMediaAndAcceptWithSpecificConstraints and returns the new tab. + content::WebContents* OpenPageAndGetUserMediaInNewTabWithConstraints( + const GURL& url, const std::string& constraints) const; + + // Convenience method which gets the URL for |test_page| and calls + // OpenPageAndGetUserMediaInNewTab(). + content::WebContents* OpenTestPageAndGetUserMediaInNewTab( + const std::string& test_page) const; + + // Convenience method which gets the URL for |test_page|, but without calling + // GetUserMedia. + content::WebContents* OpenTestPageInNewTab( + const std::string& test_page) const; + + // Closes the last local stream acquired by the GetUserMedia* methods. + void CloseLastLocalStream(content::WebContents* tab_contents) const; + + std::string ExecuteJavascript(const std::string& javascript, + content::WebContents* tab_contents) const; + + // TODO(https://crbug.com/1004239): Remove this function as soon as browser + // tests stop relying on the legacy getStats() API. + void ChangeToLegacyGetStats(content::WebContents* tab) const; + + // Sets up a peer connection in the tab and adds the current local stream + // (which you can prepare by calling one of the GetUserMedia* methods above). + // Optionally, |certificate_keygen_algorithm| is JavaScript for an + // |AlgorithmIdentifier| to be used as parameter to + // |RTCPeerConnection.generateCertificate|. The resulting certificate will be + // used by the peer connection. Or use |kUseDefaultCertKeygen| to use a + // certificate. + void SetupPeerconnectionWithLocalStream( + content::WebContents* tab, + const std::string& certificate_keygen_algorithm = + kUseDefaultCertKeygen) const; + // Same as above but does not add the local stream. + void SetupPeerconnectionWithoutLocalStream( + content::WebContents* tab, + const std::string& certificate_keygen_algorithm = + kUseDefaultCertKeygen) const; + // Same as |SetupPeerconnectionWithLocalStream| except a certificate is + // specified, which is a reference to an |RTCCertificate| object. + void SetupPeerconnectionWithCertificateAndLocalStream( + content::WebContents* tab, + const std::string& certificate) const; + // Same as above but does not add the local stream. + void SetupPeerconnectionWithCertificateWithoutLocalStream( + content::WebContents* tab, + const std::string& certificate) const; + // Same as |SetupPeerconnectionWithLocalStream| except RTCPeerConnection + // constraints are specified. + void SetupPeerconnectionWithConstraintsAndLocalStream( + content::WebContents* tab, + const std::string& constraints, + const std::string& certificate_keygen_algorithm = + kUseDefaultCertKeygen) const; + + void CreateDataChannel(content::WebContents* tab, const std::string& label); + + // Exchanges offers and answers between the peer connections in the + // respective tabs. Before calling this, you must have prepared peer + // connections in both tabs and configured them as you like (for instance by + // calling SetupPeerconnectionWithLocalStream). + // If |video_codec| is not |kUseDefaultVideoCodec|, the SDP offer is modified + // (and SDP answer verified) so that the specified video codec (case-sensitive + // name) is used during the call instead of the default one. + void NegotiateCall(content::WebContents* from_tab, + content::WebContents* to_tab) const; + + void VerifyLocalDescriptionContainsCertificate( + content::WebContents* tab, + const std::string& certificate) const; + + // Hangs up a negotiated call. + void HangUp(content::WebContents* from_tab) const; + + // Call this to enable monitoring of javascript errors for this test method. + // This will only work if the tests are run sequentially by the test runner + // (i.e. with --test-launcher-developer-mode or --test-launcher-jobs=1). + void DetectErrorsInJavaScript(); + + // Methods for detecting if video is playing (the loaded page must have + // chrome/test/data/webrtc/video_detector.js and its dependencies loaded to + // make that work). Looks at a 320x240 area of the target video tag. + void StartDetectingVideo(content::WebContents* tab_contents, + const std::string& video_element) const; + bool WaitForVideoToPlay(content::WebContents* tab_contents) const; + + // Returns the stream size as a string on the format <width>x<height>. + std::string GetStreamSize(content::WebContents* tab_contents, + const std::string& video_element) const; + + // Returns true if we're on Windows 8 or higher. + bool OnWin8OrHigher() const; + + void OpenDatabase(content::WebContents* tab) const; + void CloseDatabase(content::WebContents* tab) const; + void DeleteDatabase(content::WebContents* tab) const; + + void GenerateAndCloneCertificate(content::WebContents* tab, + const std::string& keygen_algorithm) const; + + void VerifyStatsGeneratedCallback(content::WebContents* tab) const; + double MeasureGetStatsCallbackPerformance(content::WebContents* tab) const; + std::vector<std::string> VerifyStatsGeneratedPromise( + content::WebContents* tab) const; + scoped_refptr<content::TestStatsReportDictionary> GetStatsReportDictionary( + content::WebContents* tab) const; + double MeasureGetStatsPerformance(content::WebContents* tab) const; + std::vector<std::string> GetMandatoryStatsTypes( + content::WebContents* tab) const; + + // Change the default audio/video codec in the offer SDP. + void SetDefaultAudioCodec(content::WebContents* tab, + const std::string& audio_codec) const; + // |prefer_hw_codec| controls what codec with name |video_codec| (and with + // profile |profile| if given)should be selected. This parameter only matters + // if there are multiple codecs with the same name, which can be the case for + // H264. + void SetDefaultVideoCodec(content::WebContents* tab, + const std::string& video_codec, + bool prefer_hw_codec = false, + const std::string& profile = std::string()) const; + + // Add 'usedtx=1' to the offer SDP. + void EnableOpusDtx(content::WebContents* tab) const; + + // Try to open a dekstop media stream, and return the stream id. + // On failure, will return empty string. + std::string GetDesktopMediaStream(content::WebContents* tab); + base::Optional<std::string> LoadDesktopCaptureExtension(); + + private: + void CloseInfoBarInTab(content::WebContents* tab_contents, + infobars::InfoBar* infobar) const; + + std::string CreateLocalOffer(content::WebContents* from_tab) const; + std::string CreateAnswer(std::string local_offer, + content::WebContents* to_tab) const; + void ReceiveAnswer(const std::string& answer, + content::WebContents* from_tab) const; + void GatherAndSendIceCandidates(content::WebContents* from_tab, + content::WebContents* to_tab) const; + bool HasStreamWithTrack(content::WebContents* tab, + const char* function_name, + std::string stream_id, + std::string track_id) const; + + infobars::InfoBar* GetUserMediaAndWaitForInfoBar( + content::WebContents* tab_contents, + const std::string& constraints) const; + + bool detect_errors_in_javascript_; + scoped_refptr<const extensions::Extension> desktop_capture_extension_; + + DISALLOW_COPY_AND_ASSIGN(WebRtcTestBase); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_BROWSERTEST_BASE_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_browsertest_common.cc b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_common.cc new file mode 100644 index 00000000000..3bb61917d8c --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_common.cc @@ -0,0 +1,180 @@ +// 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 "chrome/browser/media/webrtc/webrtc_browsertest_common.h" + +#include "base/files/file_util.h" +#include "base/path_service.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/test/test_timeouts.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/common/chrome_paths.h" +#include "content/public/test/browser_test_utils.h" + +namespace test { + +// Relative to the chrome/test/data directory. +const base::FilePath::CharType kReferenceFilesDirName[] = + FILE_PATH_LITERAL("webrtc/resources"); +const base::FilePath::CharType kReferenceFileName360p[] = + FILE_PATH_LITERAL("reference_video_640x360_30fps"); +const base::FilePath::CharType kReferenceFileName720p[] = + FILE_PATH_LITERAL("reference_video_1280x720_30fps"); +const base::FilePath::CharType kYuvFileExtension[] = FILE_PATH_LITERAL("yuv"); +const base::FilePath::CharType kY4mFileExtension[] = FILE_PATH_LITERAL("y4m"); + +// This message describes how to modify your .gclient to get the reference +// video files downloaded for you. +const char kAdviseOnGclientSolution[] = + "To run this test, you must run download_from_google_storage --config\n" + "and follow the instructions (use 'browser' for project id)\n" + "You also need to add this solution to your .gclient:\n" + "{\n" + " \"name\" : \"webrtc.DEPS\",\n" + " \"url\" : \"https://chromium.googlesource.com/chromium/deps/" + "webrtc/webrtc.DEPS\",\n" + "}\n" + "and run gclient sync. This will download the required ref files."; + +#if defined(THREAD_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(ADDRESS_SANITIZER) +#if defined(OS_CHROMEOS) +const int kDefaultPollIntervalMsec = 2000; +#else +const int kDefaultPollIntervalMsec = 1000; +#endif // OS_CHROMEOS +#else +#if defined(OS_CHROMEOS) +const int kDefaultPollIntervalMsec = 500; +#else +const int kDefaultPollIntervalMsec = 250; +#endif // OS_CHROMEOS +#endif + +bool IsErrorResult(const std::string& result) { + return base::StartsWith(result, "failed-", + base::CompareCase::INSENSITIVE_ASCII); +} + +base::FilePath GetReferenceFilesDir() { + base::FilePath test_data_dir; + base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir); + + return test_data_dir.Append(kReferenceFilesDirName); +} + +base::FilePath GetToolForPlatform(const std::string& tool_name) { + base::FilePath tools_dir = + GetReferenceFilesDir().Append(FILE_PATH_LITERAL("tools")); +#if defined(OS_WIN) + return tools_dir + .Append(FILE_PATH_LITERAL("win")) + .AppendASCII(tool_name) + .AddExtension(FILE_PATH_LITERAL("exe")); +#elif defined(OS_MACOSX) + return tools_dir.Append(FILE_PATH_LITERAL("mac")).AppendASCII(tool_name); +#elif defined(OS_LINUX) + return tools_dir.Append(FILE_PATH_LITERAL("linux")).AppendASCII(tool_name); +#else + CHECK(false) << "Can't retrieve tool " << tool_name << " on this platform."; + return base::FilePath(); +#endif +} + +bool HasReferenceFilesInCheckout() { + if (!base::PathExists(GetReferenceFilesDir())) { + LOG(ERROR) + << "Cannot find the working directory for the reference video " + << "files, expected at " << GetReferenceFilesDir().value() << ". " << + kAdviseOnGclientSolution; + return false; + } + return HasYuvAndY4mFile(test::kReferenceFileName360p) && + HasYuvAndY4mFile(test::kReferenceFileName720p); +} + +bool HasYuvAndY4mFile(const base::FilePath::CharType* reference_file) { + base::FilePath webrtc_reference_video_yuv = GetReferenceFilesDir() + .Append(reference_file).AddExtension(kYuvFileExtension); + if (!base::PathExists(webrtc_reference_video_yuv)) { + LOG(ERROR) + << "Missing YUV reference video to be used for quality" + << " comparison, expected at " << webrtc_reference_video_yuv.value() + << ". " << kAdviseOnGclientSolution; + return false; + } + + base::FilePath webrtc_reference_video_y4m = GetReferenceFilesDir() + .Append(reference_file).AddExtension(kY4mFileExtension); + if (!base::PathExists(webrtc_reference_video_y4m)) { + LOG(ERROR) + << "Missing Y4M reference video to be used for quality" + << " comparison, expected at "<< webrtc_reference_video_y4m.value() + << ". " << kAdviseOnGclientSolution; + return false; + } + return true; +} + +bool SleepInJavascript(content::WebContents* tab_contents, int timeout_msec) { + const std::string javascript = base::StringPrintf( + "setTimeout(function() {" + " window.domAutomationController.send('sleep-ok');" + "}, %d)", timeout_msec); + + std::string result; + bool ok = content::ExecuteScriptAndExtractString( + tab_contents, javascript, &result); + return ok && result == "sleep-ok"; +} + +bool PollingWaitUntil(const std::string& javascript, + const std::string& evaluates_to, + content::WebContents* tab_contents) { + return PollingWaitUntil(javascript, evaluates_to, tab_contents, + kDefaultPollIntervalMsec); +} + +bool PollingWaitUntil(const std::string& javascript, + const std::string& evaluates_to, + content::WebContents* tab_contents, + int poll_interval_msec) { + base::Time start_time = base::Time::Now(); + base::TimeDelta timeout = TestTimeouts::action_max_timeout(); + std::string result; + + while (base::Time::Now() - start_time < timeout) { + std::string result; + if (!content::ExecuteScriptAndExtractString(tab_contents, javascript, + &result)) { + LOG(ERROR) << "Failed to execute javascript " << javascript; + return false; + } + + if (evaluates_to == result) { + return true; + } else if (IsErrorResult(result)) { + LOG(ERROR) << "|" << javascript << "| returned an error: " << result; + return false; + } + + // Sleep a bit here to keep this loop from spinlocking too badly. + if (!SleepInJavascript(tab_contents, poll_interval_msec)) { + // TODO(phoglund): Figure out why this fails every now and then. + // It's not a huge deal if it does though. + LOG(ERROR) << "Failed to sleep."; + } + } + LOG(ERROR) + << "Timed out while waiting for " << javascript + << " to evaluate to " << evaluates_to << "; last result was '" << result + << "'"; + return false; +} + +} // namespace test diff --git a/chromium/chrome/browser/media/webrtc/webrtc_browsertest_common.h b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_common.h new file mode 100644 index 00000000000..cd3ecef1f4c --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_common.h @@ -0,0 +1,67 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_BROWSERTEST_COMMON_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_BROWSERTEST_COMMON_H_ + +#include <string> + +#include "base/files/file_path.h" +#include "base/process/process_handle.h" + +namespace content { +class WebContents; +} + +namespace test { + +// Reference file locations. + +// Checks if the user has the reference files directory, returns true if so. +// If the user's checkout don't have these dirs, they need to configure their +// .gclient as described in chrome/test/data/webrtc/resources/README. The reason +// for this is that we don't want to burden regular chrome devs with downloading +// these sizable reference files by default. +bool HasReferenceFilesInCheckout(); + +// Verifies both the YUV and Y4M version of the reference file exists. +bool HasYuvAndY4mFile(const base::FilePath::CharType* reference_file); + +// Retrieves the reference files dir, to which file names can be appended. +base::FilePath GetReferenceFilesDir(); + +// Retrieves a tool binary path from chrome/test/data/webrtc/resources/tools, +// according to platform. If we're running on Linux, requesting pesq will yield +// chrome/test/data/webrtc/resources/tools/linux/pesq, whereas the same call on +// Windows will yield chrome/test/data/webrtc/resources/tools/win/pesq.exe. +// This function does not check the binary actually exists. +base::FilePath GetToolForPlatform(const std::string& tool_name); + +extern const base::FilePath::CharType kReferenceFileName360p[]; +extern const base::FilePath::CharType kReferenceFileName720p[]; +extern const base::FilePath::CharType kYuvFileExtension[]; +extern const base::FilePath::CharType kY4mFileExtension[]; +extern const char kAdviseOnGclientSolution[]; + +// Executes javascript code which will sleep for |timeout_msec| milliseconds. +// Returns true on success. +bool SleepInJavascript(content::WebContents* tab_contents, int timeout_msec); + +// This function will execute the provided |javascript| until it causes a call +// to window.domAutomationController.send() with |evaluates_to| as the message. +// That is, we are NOT checking what the javascript evaluates to. Returns false +// if we exceed the TestTimeouts::action_max_timeout(). +// TODO(phoglund): Consider a better interaction method with the javascript +// than polling javascript methods. +bool PollingWaitUntil(const std::string& javascript, + const std::string& evaluates_to, + content::WebContents* tab_contents); +bool PollingWaitUntil(const std::string& javascript, + const std::string& evaluates_to, + content::WebContents* tab_contents, + int poll_interval_msec); + +} // namespace test + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_BROWSERTEST_COMMON_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_browsertest_perf.cc b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_perf.cc new file mode 100644 index 00000000000..9a61637d902 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_perf.cc @@ -0,0 +1,338 @@ +// 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 "chrome/browser/media/webrtc/webrtc_browsertest_perf.h" + +#include <stddef.h> + +#include "base/strings/stringprintf.h" +#include "base/values.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "testing/perf/perf_result_reporter.h" + +namespace { + +constexpr char kMetricPrefixAudioReceive[] = "WebRtcAudioReceive."; +constexpr char kMetricPrefixAudioSend[] = "WebRtcAudioSend."; +constexpr char kMetricPrefixVideoSend[] = "WebRtcVideoSend."; +constexpr char kMetricPrefixVideoRecieve[] = "WebRtcVideoReceive."; +constexpr char kMetricPrefixBwe[] = "WebRtcBwe."; +constexpr char kMetricPacketsLostFrames[] = "packets_lost"; +constexpr char kMetricGoogJitterRecvMs[] = "goog_jitter_recv"; +constexpr char kMetricGoogExpandRatePercent[] = "goog_expand_rate"; +constexpr char kMetricGoogSpeechExpandRatePercent[] = "goog_speech_expand_rate"; +constexpr char kMetricGoogSecondaryDecodeRatePercent[] = + "goog_secondary_decode_rate"; +constexpr char kMetricGoogRttMs[] = "goog_rtt"; +constexpr char kMetricPacketsPerSecondPackets[] = "packets_per_second"; +constexpr char kMetricGoogFpsSentFps[] = "goog_frame_rate_sent"; +constexpr char kMetricGoogFpsInputFps[] = "goog_frame_rate_input"; +constexpr char kMetricGoogFirsReceivedUnitless[] = "goog_firs_recv"; +constexpr char kMetricGoogNacksReceivedUnitless[] = "goog_nacks_recv"; +constexpr char kMetricGoogFrameWidthCount[] = "goog_frame_width"; +constexpr char kMetricGoogFrameHeightCount[] = "goog_frame_height"; +constexpr char kMetricGoogAvgEncodeMs[] = "goog_avg_encode"; +constexpr char kMetricGoogEncodeCpuUsagePercent[] = "goog_encode_cpu_usage"; +constexpr char kMetricGoogFpsRecvFps[] = "goog_frame_rate_recv"; +constexpr char kMetricGoogFpsOutputFps[] = "goog_frame_rate_output"; +constexpr char kMetricGoogActualDelayMs[] = "goog_actual_delay"; +constexpr char kMetricGoogTargetDelayMs[] = "goog_target_delay"; +constexpr char kMetricGoogDecodeTimeMs[] = "goog_decode_time"; +constexpr char kMetricGoogMaxDecodeTimeMs[] = "goog_max_decode_time"; +constexpr char kMetricGoogJitterBufferMs[] = "goog_jitter_buffer"; +constexpr char kMetricGoogRenderDelayMs[] = "goog_render_delay"; +constexpr char kMetricAvailableSendBandwidthBitsPerS[] = "available_send_bw"; +constexpr char kMetricAvailableRecvBandwidthBitsPerS[] = "available_recv_bw"; +constexpr char kMetricTargetEncodeBitrateBitsPerS[] = "target_encode_bitrate"; +constexpr char kMetricActualEncodeBitrateBitsPerS[] = "actual_encode_bitrate"; +constexpr char kMetricTransmitBitrateBitsPerS[] = "transmit_bitrate"; + +perf_test::PerfResultReporter SetUpAudioReceiveReporter( + const std::string& story) { + perf_test::PerfResultReporter reporter(kMetricPrefixAudioReceive, story); + reporter.RegisterFyiMetric(kMetricPacketsLostFrames, "frames"); + reporter.RegisterFyiMetric(kMetricGoogJitterRecvMs, "ms"); + reporter.RegisterFyiMetric(kMetricGoogExpandRatePercent, "%"); + reporter.RegisterFyiMetric(kMetricGoogSpeechExpandRatePercent, "%"); + reporter.RegisterFyiMetric(kMetricGoogSecondaryDecodeRatePercent, "%"); + return reporter; +} + +perf_test::PerfResultReporter SetUpAudioSendReporter(const std::string& story) { + perf_test::PerfResultReporter reporter(kMetricPrefixAudioSend, story); + reporter.RegisterFyiMetric(kMetricGoogJitterRecvMs, "ms"); + reporter.RegisterFyiMetric(kMetricGoogRttMs, "ms"); + reporter.RegisterFyiMetric(kMetricPacketsPerSecondPackets, "packets"); + return reporter; +} + +perf_test::PerfResultReporter SetUpVideoSendReporter(const std::string& story) { + perf_test::PerfResultReporter reporter(kMetricPrefixVideoSend, story); + reporter.RegisterFyiMetric(kMetricGoogFpsSentFps, "fps"); + reporter.RegisterFyiMetric(kMetricGoogFpsInputFps, "fps"); + reporter.RegisterFyiMetric(kMetricGoogFirsReceivedUnitless, "unitless"); + reporter.RegisterFyiMetric(kMetricGoogNacksReceivedUnitless, "unitless"); + reporter.RegisterFyiMetric(kMetricGoogFrameWidthCount, "count"); + reporter.RegisterFyiMetric(kMetricGoogFrameHeightCount, "count"); + reporter.RegisterFyiMetric(kMetricGoogAvgEncodeMs, "ms"); + reporter.RegisterFyiMetric(kMetricGoogRttMs, "ms"); + reporter.RegisterFyiMetric(kMetricGoogEncodeCpuUsagePercent, "%"); + return reporter; +} + +perf_test::PerfResultReporter SetUpVideoReceiveReporter( + const std::string& story) { + perf_test::PerfResultReporter reporter(kMetricPrefixVideoRecieve, story); + reporter.RegisterFyiMetric(kMetricGoogFpsRecvFps, "fps"); + reporter.RegisterFyiMetric(kMetricGoogFpsOutputFps, "fps"); + reporter.RegisterFyiMetric(kMetricPacketsLostFrames, "frames"); + reporter.RegisterFyiMetric(kMetricGoogFrameWidthCount, "count"); + reporter.RegisterFyiMetric(kMetricGoogFrameHeightCount, "count"); + reporter.RegisterFyiMetric(kMetricGoogActualDelayMs, "ms"); + reporter.RegisterFyiMetric(kMetricGoogTargetDelayMs, "ms"); + reporter.RegisterFyiMetric(kMetricGoogDecodeTimeMs, "ms"); + reporter.RegisterFyiMetric(kMetricGoogMaxDecodeTimeMs, "ms"); + reporter.RegisterFyiMetric(kMetricGoogJitterBufferMs, "ms"); + reporter.RegisterFyiMetric(kMetricGoogRenderDelayMs, "ms"); + return reporter; +} + +perf_test::PerfResultReporter SetUpBweReporter(const std::string& story) { + perf_test::PerfResultReporter reporter(kMetricPrefixBwe, story); + reporter.RegisterFyiMetric(kMetricAvailableSendBandwidthBitsPerS, "bits/s"); + reporter.RegisterFyiMetric(kMetricAvailableRecvBandwidthBitsPerS, "bits/s"); + reporter.RegisterFyiMetric(kMetricTargetEncodeBitrateBitsPerS, "bits/s"); + reporter.RegisterFyiMetric(kMetricActualEncodeBitrateBitsPerS, "bits/s"); + reporter.RegisterFyiMetric(kMetricTransmitBitrateBitsPerS, "bits/s"); + return reporter; +} + +} // namespace + +static std::string Statistic(const std::string& statistic, + const std::string& bucket) { + // A ssrc stats key will be on the form stats.<bucket>-<key>.values. + // This will give a json "path" which will dig into the time series for the + // specified statistic. Buckets can be for instance ssrc_1212344, bweforvideo, + // and will each contain a bunch of statistics relevant to their nature. + // Each peer connection has a number of such buckets. + return base::StringPrintf("stats.%s-%s.values", bucket.c_str(), + statistic.c_str()); +} + +static void MaybePrintResultsForAudioReceive( + const std::string& ssrc, + const base::DictionaryValue& pc_dict, + const std::string& story) { + std::string value; + if (!pc_dict.GetString(Statistic("audioOutputLevel", ssrc), &value)) { + // Not an audio receive stream. + return; + } + + auto reporter = SetUpAudioReceiveReporter(story); + EXPECT_TRUE(pc_dict.GetString(Statistic("packetsLost", ssrc), &value)); + reporter.AddResult(kMetricPacketsLostFrames, value); + EXPECT_TRUE(pc_dict.GetString(Statistic("googJitterReceived", ssrc), &value)); + reporter.AddResult(kMetricGoogJitterRecvMs, value); + + EXPECT_TRUE(pc_dict.GetString(Statistic("googExpandRate", ssrc), &value)); + reporter.AddResult(kMetricGoogExpandRatePercent, value); + EXPECT_TRUE( + pc_dict.GetString(Statistic("googSpeechExpandRate", ssrc), &value)); + reporter.AddResult(kMetricGoogSpeechExpandRatePercent, value); + EXPECT_TRUE( + pc_dict.GetString(Statistic("googSecondaryDecodedRate", ssrc), &value)); + reporter.AddResult(kMetricGoogSecondaryDecodeRatePercent, value); +} + +static void MaybePrintResultsForAudioSend(const std::string& ssrc, + const base::DictionaryValue& pc_dict, + const std::string& story) { + std::string value; + if (!pc_dict.GetString(Statistic("audioInputLevel", ssrc), &value)) { + // Not an audio send stream. + return; + } + + auto reporter = SetUpAudioSendReporter(story); + EXPECT_TRUE(pc_dict.GetString(Statistic("googJitterReceived", ssrc), &value)); + reporter.AddResult(kMetricGoogJitterRecvMs, value); + EXPECT_TRUE(pc_dict.GetString(Statistic("googRtt", ssrc), &value)); + reporter.AddResult(kMetricGoogRttMs, value); + EXPECT_TRUE( + pc_dict.GetString(Statistic("packetsSentPerSecond", ssrc), &value)); + reporter.AddResult(kMetricPacketsPerSecondPackets, value); +} + +static void MaybePrintResultsForVideoSend(const std::string& ssrc, + const base::DictionaryValue& pc_dict, + const std::string& story) { + std::string value; + if (!pc_dict.GetString(Statistic("googFrameRateSent", ssrc), &value)) { + // Not a video send stream. + return; + } + + // Graph these by unit: the dashboard expects all stats in one graph to have + // the same unit (e.g. ms, fps, etc). Most graphs, like video_fps, will also + // be populated by the counterparts on the video receiving side. + auto reporter = SetUpVideoSendReporter(story); + reporter.AddResult(kMetricGoogFpsSentFps, value); + EXPECT_TRUE(pc_dict.GetString(Statistic("googFrameRateInput", ssrc), &value)); + reporter.AddResult(kMetricGoogFpsInputFps, value); + + EXPECT_TRUE(pc_dict.GetString(Statistic("googFirsReceived", ssrc), &value)); + reporter.AddResult(kMetricGoogFirsReceivedUnitless, value); + EXPECT_TRUE(pc_dict.GetString(Statistic("googNacksReceived", ssrc), &value)); + reporter.AddResult(kMetricGoogNacksReceivedUnitless, value); + + EXPECT_TRUE(pc_dict.GetString(Statistic("googFrameWidthSent", ssrc), &value)); + reporter.AddResult(kMetricGoogFrameWidthCount, value); + EXPECT_TRUE( + pc_dict.GetString(Statistic("googFrameHeightSent", ssrc), &value)); + reporter.AddResult(kMetricGoogFrameHeightCount, value); + + EXPECT_TRUE(pc_dict.GetString(Statistic("googAvgEncodeMs", ssrc), &value)); + reporter.AddResult(kMetricGoogAvgEncodeMs, value); + EXPECT_TRUE(pc_dict.GetString(Statistic("googRtt", ssrc), &value)); + reporter.AddResult(kMetricGoogRttMs, value); + + EXPECT_TRUE(pc_dict.GetString( + Statistic("googEncodeUsagePercent", ssrc), &value)); + reporter.AddResult(kMetricGoogEncodeCpuUsagePercent, value); +} + +static void MaybePrintResultsForVideoReceive( + const std::string& ssrc, + const base::DictionaryValue& pc_dict, + const std::string& story) { + std::string value; + if (!pc_dict.GetString(Statistic("googFrameRateReceived", ssrc), &value)) { + // Not a video receive stream. + return; + } + + auto reporter = SetUpVideoReceiveReporter(story); + reporter.AddResult(kMetricGoogFpsRecvFps, value); + EXPECT_TRUE( + pc_dict.GetString(Statistic("googFrameRateOutput", ssrc), &value)); + reporter.AddResult(kMetricGoogFpsOutputFps, value); + + EXPECT_TRUE(pc_dict.GetString(Statistic("packetsLost", ssrc), &value)); + reporter.AddResult(kMetricPacketsLostFrames, value); + + EXPECT_TRUE( + pc_dict.GetString(Statistic("googFrameWidthReceived", ssrc), &value)); + reporter.AddResult(kMetricGoogFrameWidthCount, value); + EXPECT_TRUE( + pc_dict.GetString(Statistic("googFrameHeightReceived", ssrc), &value)); + reporter.AddResult(kMetricGoogFrameHeightCount, value); + + EXPECT_TRUE(pc_dict.GetString(Statistic("googCurrentDelayMs", ssrc), &value)); + reporter.AddResult(kMetricGoogActualDelayMs, value); + EXPECT_TRUE(pc_dict.GetString(Statistic("googTargetDelayMs", ssrc), &value)); + reporter.AddResult(kMetricGoogTargetDelayMs, value); + EXPECT_TRUE(pc_dict.GetString(Statistic("googDecodeMs", ssrc), &value)); + reporter.AddResult(kMetricGoogDecodeTimeMs, value); + EXPECT_TRUE(pc_dict.GetString(Statistic("googMaxDecodeMs", ssrc), &value)); + reporter.AddResult(kMetricGoogMaxDecodeTimeMs, value); + EXPECT_TRUE(pc_dict.GetString(Statistic("googJitterBufferMs", ssrc), &value)); + reporter.AddResult(kMetricGoogJitterBufferMs, value); + EXPECT_TRUE(pc_dict.GetString(Statistic("googRenderDelayMs", ssrc), &value)); + reporter.AddResult(kMetricGoogRenderDelayMs, value); +} + +static std::string ExtractSsrcIdentifier(const std::string& key) { + // Example key: ssrc_1234-someStatName. Grab the part before the dash. + size_t key_start_pos = 0; + size_t key_end_pos = key.find("-"); + CHECK(key_end_pos != std::string::npos) << "Could not parse key " << key; + return key.substr(key_start_pos, key_end_pos - key_start_pos); +} + +// Returns the set of unique ssrc identifiers in the call (e.g. ssrc_1234, +// ssrc_12356, etc). |stats_dict| is the .stats dict from one peer connection. +static std::set<std::string> FindAllSsrcIdentifiers( + const base::DictionaryValue& stats_dict) { + std::set<std::string> result; + base::DictionaryValue::Iterator stats_iterator(stats_dict); + + while (!stats_iterator.IsAtEnd()) { + if (stats_iterator.key().find("ssrc_") != std::string::npos) + result.insert(ExtractSsrcIdentifier(stats_iterator.key())); + stats_iterator.Advance(); + } + return result; +} + +namespace test { + +void PrintBweForVideoMetrics(const base::DictionaryValue& pc_dict, + const std::string& modifier, + const std::string& video_codec) { + std::string story = video_codec.empty() ? "baseline_story" : video_codec; + story += modifier; + const std::string kBweStatsKey = "bweforvideo"; + std::string value; + auto reporter = SetUpBweReporter(story); + ASSERT_TRUE(pc_dict.GetString( + Statistic("googAvailableSendBandwidth", kBweStatsKey), &value)); + reporter.AddResult(kMetricAvailableSendBandwidthBitsPerS, value); + ASSERT_TRUE(pc_dict.GetString( + Statistic("googAvailableReceiveBandwidth", kBweStatsKey), &value)); + reporter.AddResult(kMetricAvailableRecvBandwidthBitsPerS, value); + ASSERT_TRUE(pc_dict.GetString( + Statistic("googTargetEncBitrate", kBweStatsKey), &value)); + reporter.AddResult(kMetricTargetEncodeBitrateBitsPerS, value); + ASSERT_TRUE(pc_dict.GetString( + Statistic("googActualEncBitrate", kBweStatsKey), &value)); + reporter.AddResult(kMetricActualEncodeBitrateBitsPerS, value); + ASSERT_TRUE(pc_dict.GetString( + Statistic("googTransmitBitrate", kBweStatsKey), &value)); + reporter.AddResult(kMetricTransmitBitrateBitsPerS, value); +} + +void PrintMetricsForAllStreams(const base::DictionaryValue& pc_dict, + const std::string& modifier, + const std::string& video_codec) { + PrintMetricsForSendStreams(pc_dict, modifier, video_codec); + PrintMetricsForRecvStreams(pc_dict, modifier, video_codec); +} + +void PrintMetricsForSendStreams(const base::DictionaryValue& pc_dict, + const std::string& modifier, + const std::string& video_codec) { + std::string story = video_codec.empty() ? "baseline_story" : video_codec; + story += modifier; + const base::DictionaryValue* stats_dict; + ASSERT_TRUE(pc_dict.GetDictionary("stats", &stats_dict)); + std::set<std::string> ssrc_identifiers = FindAllSsrcIdentifiers(*stats_dict); + + auto ssrc_iterator = ssrc_identifiers.begin(); + for (; ssrc_iterator != ssrc_identifiers.end(); ++ssrc_iterator) { + const std::string& ssrc = *ssrc_iterator; + MaybePrintResultsForAudioSend(ssrc, pc_dict, story); + MaybePrintResultsForVideoSend(ssrc, pc_dict, story); + } +} + +void PrintMetricsForRecvStreams(const base::DictionaryValue& pc_dict, + const std::string& modifier, + const std::string& video_codec) { + std::string story = video_codec.empty() ? "baseline_story" : video_codec; + story += modifier; + const base::DictionaryValue* stats_dict; + ASSERT_TRUE(pc_dict.GetDictionary("stats", &stats_dict)); + std::set<std::string> ssrc_identifiers = FindAllSsrcIdentifiers(*stats_dict); + + auto ssrc_iterator = ssrc_identifiers.begin(); + for (; ssrc_iterator != ssrc_identifiers.end(); ++ssrc_iterator) { + const std::string& ssrc = *ssrc_iterator; + MaybePrintResultsForAudioReceive(ssrc, pc_dict, story); + MaybePrintResultsForVideoReceive(ssrc, pc_dict, story); + } +} + +} // namespace test diff --git a/chromium/chrome/browser/media/webrtc/webrtc_browsertest_perf.h b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_perf.h new file mode 100644 index 00000000000..9394f235b64 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_browsertest_perf.h @@ -0,0 +1,44 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_BROWSERTEST_PERF_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_BROWSERTEST_PERF_H_ + +#include <string> + +namespace base { +class DictionaryValue; +} + +namespace test { + +// These functions takes parsed data (on one peer connection) from the +// peerConnectionDataStore object that is backing webrtc-internals and prints +// metrics they consider interesting using testing/perf/perf_test.h primitives. +// The idea is to put as many webrtc-related metrics as possible into the +// dashboard and thereby track it for regressions. +// +// These functions expect to run under googletest and will use EXPECT_ and +// ASSERT_ macros to signal failure. They take as argument one peer connection's +// stats data and a |modifier| to append to each result bucket. For instance, if +// the modifier is '_oneway', the rtt stat will be logged as goog_rtt in +// the video_tx_oneway bucket. +// If |video_codec| is a non-empty string, the codec name is appended last for +// video metrics, e.g. 'video_tx_oneway_VP9'. +void PrintBweForVideoMetrics(const base::DictionaryValue& pc_dict, + const std::string& modifier, + const std::string& video_codec); +void PrintMetricsForAllStreams(const base::DictionaryValue& pc_dict, + const std::string& modifier, + const std::string& video_codec); +void PrintMetricsForSendStreams(const base::DictionaryValue& pc_dict, + const std::string& modifier, + const std::string& video_codec); +void PrintMetricsForRecvStreams(const base::DictionaryValue& pc_dict, + const std::string& modifier, + const std::string& video_codec); + +} // namespace test + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_BROWSERTEST_PERF_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_desktop_capture_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_desktop_capture_browsertest.cc new file mode 100644 index 00000000000..1b757d9f528 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_desktop_capture_browsertest.cc @@ -0,0 +1,96 @@ +// Copyright 2017 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 "build/build_config.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "media/base/media_switches.h" +#include "net/test/embedded_test_server/embedded_test_server.h" + +namespace { +static const char kMainWebrtcTestHtmlPage[] = "/webrtc/webrtc_jsep01_test.html"; +} // namespace + +// Top-level integration test for WebRTC. Uses an actual desktop capture +// extension to capture whole screen. +class WebRtcDesktopCaptureBrowserTest : public WebRtcTestBase { + public: + WebRtcDesktopCaptureBrowserTest() : left_tab_(nullptr), right_tab_(nullptr) {} + + void SetUpInProcessBrowserTestFixture() override { + DetectErrorsInJavaScript(); // Look for errors in our rather complex js. + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + // Ensure the infobar is enabled, since we expect that in this test. + EXPECT_FALSE(command_line->HasSwitch(switches::kUseFakeUIForMediaStream)); + + // Always use fake devices. + command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); + + // Flags use to automatically select the right dekstop source and get + // around security restrictions. + command_line->AppendSwitchASCII(switches::kAutoSelectDesktopCaptureSource, + "Entire screen"); + command_line->AppendSwitch(switches::kEnableUserMediaScreenCapturing); + } + + protected: + void DetectVideoAndHangUp() { + StartDetectingVideo(left_tab_, "remote-view"); + StartDetectingVideo(right_tab_, "remote-view"); +#if !defined(OS_MACOSX) + // Video is choppy on Mac OS X. http://crbug.com/443542. + WaitForVideoToPlay(left_tab_); + WaitForVideoToPlay(right_tab_); +#endif + HangUp(left_tab_); + HangUp(right_tab_); + } + + content::WebContents* left_tab_; + content::WebContents* right_tab_; +}; + +// TODO(crbug.com/796889): Enable on Mac when thread check crash is fixed. +// TODO(sprang): Figure out why test times out on Win 10 and ChromeOS. +#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +#define MAYBE_RunsScreenshareFromOneTabToAnother \ + RunsScreenshareFromOneTabToAnother +#else +#define MAYBE_RunsScreenshareFromOneTabToAnother \ + DISABLED_RunsScreenshareFromOneTabToAnother +#endif +IN_PROC_BROWSER_TEST_F(WebRtcDesktopCaptureBrowserTest, + MAYBE_RunsScreenshareFromOneTabToAnother) { + ASSERT_TRUE(embedded_test_server()->Start()); + LoadDesktopCaptureExtension(); + left_tab_ = OpenTestPageInNewTab(kMainWebrtcTestHtmlPage); + std::string stream_id = GetDesktopMediaStream(left_tab_); + EXPECT_NE(stream_id, ""); + + LOG(INFO) << "Opened desktop media stream, got id " << stream_id; + + const std::string constraints = + "{audio: false, video: {mandatory: {chromeMediaSource: 'desktop'," + "chromeMediaSourceId: '" + + stream_id + "'}}}"; + EXPECT_TRUE(GetUserMediaWithSpecificConstraintsAndAcceptIfPrompted( + left_tab_, constraints)); + right_tab_ = OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + SetupPeerconnectionWithLocalStream(left_tab_); + SetupPeerconnectionWithLocalStream(right_tab_); + NegotiateCall(left_tab_, right_tab_); + VerifyStatsGeneratedCallback(right_tab_); + DetectVideoAndHangUp(); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_disable_encryption_flag_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_disable_encryption_flag_browsertest.cc new file mode 100644 index 00000000000..c8897585016 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_disable_encryption_flag_browsertest.cc @@ -0,0 +1,99 @@ +// Copyright 2014 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/macros.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/common/channel_info.h" +#include "components/version_info/version_info.h" +#include "content/public/common/content_switches.h" +#include "media/base/media_switches.h" +#include "net/test/embedded_test_server/embedded_test_server.h" + +static const char kMainWebrtcTestHtmlPage[] = + "/webrtc/webrtc_jsep01_test.html"; + +// This tests the --disable-webrtc-encryption command line flag. Disabling +// encryption should only be possible on certain channels. + +// NOTE: The test case for each channel will only be exercised when the browser +// is actually built for that channel. This is not ideal. One can test manually +// by e.g. faking the channel returned in chrome::GetChannel(). It's likely good +// to have the test anyway, even though a failure might not be detected until a +// branch has been promoted to another channel. The unit test for +// ChromeContentBrowserClient::MaybeCopyDisableWebRtcEncryptionSwitch tests for +// all channels however. +// TODO(grunell): Test the different channel cases for any build. +class WebRtcDisableEncryptionFlagBrowserTest : public WebRtcTestBase { + public: + WebRtcDisableEncryptionFlagBrowserTest() {} + ~WebRtcDisableEncryptionFlagBrowserTest() override {} + + void SetUpInProcessBrowserTestFixture() override { + DetectErrorsInJavaScript(); // Look for errors in our rather complex js. + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + // This test should run with fake devices. + command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); + + // Disable encryption with the command line flag. + command_line->AppendSwitch(switches::kDisableWebRtcEncryption); + } + + private: + DISALLOW_COPY_AND_ASSIGN(WebRtcDisableEncryptionFlagBrowserTest); +}; + +// Makes a call and checks that there's encryption or not in the SDP offer. +// TODO(crbug.com/910216): De-flake this for ChromeOs. +// TODO(crbug.com/984879): De-flake this for MSAN Linux. +#if defined(OS_CHROMEOS) || (defined(OS_LINUX) && defined(MEMORY_SANITIZER)) +#define MAYBE_VerifyEncryption DISABLED_VerifyEncryption +#else +#define MAYBE_VerifyEncryption VerifyEncryption +#endif +IN_PROC_BROWSER_TEST_F(WebRtcDisableEncryptionFlagBrowserTest, + MAYBE_VerifyEncryption) { + ASSERT_TRUE(embedded_test_server()->Start()); + + content::WebContents* left_tab = + OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + content::WebContents* right_tab = + OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + + SetupPeerconnectionWithLocalStream(left_tab); + SetupPeerconnectionWithLocalStream(right_tab); + + NegotiateCall(left_tab, right_tab); + + StartDetectingVideo(left_tab, "remote-view"); + StartDetectingVideo(right_tab, "remote-view"); + + WaitForVideoToPlay(left_tab); + WaitForVideoToPlay(right_tab); + + bool should_detect_encryption = true; + version_info::Channel channel = chrome::GetChannel(); + if (channel == version_info::Channel::UNKNOWN || + channel == version_info::Channel::CANARY || + channel == version_info::Channel::DEV) { + should_detect_encryption = false; + } +#if defined(OS_ANDROID) + if (channel == version_info::Channel::BETA) + should_detect_encryption = false; +#endif + + std::string expected_string = should_detect_encryption ? + "crypto-seen" : "no-crypto-seen"; + + ASSERT_EQ(expected_string, + ExecuteJavascript("hasSeenCryptoInSdp()", left_tab)); + + HangUp(left_tab); + HangUp(right_tab); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_history.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_history.cc new file mode 100644 index 00000000000..59f14e44920 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_history.cc @@ -0,0 +1,423 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/webrtc_event_log_history.h" + +#include <limits> +#include <utility> +#include <vector> + +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/memory/ptr_util.h" +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_common.h" + +namespace webrtc_event_logging { + +const size_t kWebRtcEventLogMaxUploadIdBytes = 100; + +namespace { +// Compactness is not important for these few and small files; we therefore +// go with a human-readable format. +const char kCaptureTimeLinePrefix[] = + "Capture time (seconds since UNIX epoch): "; +const char kUploadTimeLinePrefix[] = "Upload time (seconds since UNIX epoch): "; +const char kUploadIdLinePrefix[] = "Upload ID: "; + +// No need to use \r\n for Windows; better have a consistent file format +// between platforms. +const char kEOL[] = "\n"; +static_assert(base::size(kEOL) == 1 + 1 /* +1 for the implicit \0. */, + "SplitString relies on this being a single character."); + +// |time| must *not* be earlier than UNIX epoch start. If it is, the empty +// string is returned. +std::string DeltaFromEpochSeconds(base::Time time) { + if (time.is_null() || time.is_min() || time.is_max()) { + LOG(ERROR) << "Not a valid time (" << time << ")."; + return std::string(); + } + + const base::Time epoch = base::Time::UnixEpoch(); + if (time < epoch) { + LOG(WARNING) << "Time to go back to the future."; + return std::string(); + } + + return base::NumberToString((time - epoch).InSeconds()); +} + +// Helper for ParseTime; see its documentation for details. +base::Time StringToTime(const std::string& time) { + int64_t seconds_from_epoch; + if (!base::StringToInt64(time, &seconds_from_epoch) || + seconds_from_epoch < 0) { + LOG(WARNING) << "Error encountered while reading time."; + return base::Time(); + } + + return base::Time::UnixEpoch() + + base::TimeDelta::FromSeconds(seconds_from_epoch); +} + +// Convert a history file's timestamp, which is the number of seconds since +// UNIX epoch, into a base::Time object. +// This function errors on timestamps from UNIX epoch or before it. +bool ParseTime(const std::string& line, + const std::string& prefix, + base::Time* out) { + DCHECK(line.find(prefix) == 0); + DCHECK(out); + + if (!out->is_null()) { + LOG(WARNING) << "Repeated line."; + return false; + } + + const base::Time time = StringToTime(line.substr(prefix.length())); + if (time.is_null()) { + LOG(WARNING) << "Null time."; + return false; + } + + *out = time; + + return true; +} + +bool ParseString(const std::string& line, + const std::string& prefix, + std::string* out) { + DCHECK(line.find(prefix) == 0); + DCHECK(out); + + if (!out->empty()) { + LOG(WARNING) << "Repeated line."; + return false; + } + + *out = line.substr(prefix.length()); + + if (out->empty()) { + LOG(WARNING) << "Empty string."; + return false; + } + + return true; +} +} // namespace + +std::unique_ptr<WebRtcEventLogHistoryFileWriter> +WebRtcEventLogHistoryFileWriter::Create(const base::FilePath& path) { + auto history_file_writer = + base::WrapUnique(new WebRtcEventLogHistoryFileWriter(path)); + if (!history_file_writer->Init()) { + LOG(WARNING) << "Initialization of history file writer failed."; + return nullptr; + } + return history_file_writer; +} + +WebRtcEventLogHistoryFileWriter::WebRtcEventLogHistoryFileWriter( + const base::FilePath& path) + : path_(path), valid_(false) {} + +bool WebRtcEventLogHistoryFileWriter::Init() { + DCHECK(!valid_); + + if (base::PathExists(path_)) { + if (!base::DeleteFile(path_, /*recursive=*/false)) { + LOG(ERROR) << "History file already exists, and could not be deleted."; + return false; + } else { + LOG(WARNING) << "History file already existed; deleted."; + } + } + + // Attempt to create the file. + constexpr int file_flags = base::File::FLAG_CREATE | base::File::FLAG_WRITE | + base::File::FLAG_EXCLUSIVE_WRITE; + file_.Initialize(path_, file_flags); + if (!file_.IsValid() || !file_.created()) { + LOG(WARNING) << "Couldn't create history file."; + if (!base::DeleteFile(path_, /*recursive=*/false)) { + LOG(ERROR) << "Failed to delete " << path_ << "."; + } + return false; + } + + valid_ = true; + return true; +} + +bool WebRtcEventLogHistoryFileWriter::WriteCaptureTime( + base::Time capture_time) { + DCHECK(valid_); + + if (capture_time.is_null()) { + valid_ = false; + return false; + } + + const std::string delta_seconds = DeltaFromEpochSeconds(capture_time); + if (delta_seconds.empty()) { + valid_ = false; + return false; + } + + const bool written = Write(kCaptureTimeLinePrefix + delta_seconds + kEOL); + if (!written) { + // Error logged by Write(). + valid_ = false; + return false; + } + + return true; +} + +bool WebRtcEventLogHistoryFileWriter::WriteUploadTime(base::Time upload_time) { + DCHECK(valid_); + + if (upload_time.is_null()) { + valid_ = false; + return false; + } + + const std::string delta_seconds = DeltaFromEpochSeconds(upload_time); + if (delta_seconds.empty()) { + valid_ = false; + return false; + } + + const bool written = Write(kUploadTimeLinePrefix + delta_seconds + kEOL); + if (!written) { + valid_ = false; + return false; + } + + return true; +} + +bool WebRtcEventLogHistoryFileWriter::WriteUploadId( + const std::string& upload_id) { + DCHECK(valid_); + DCHECK(!upload_id.empty()); + DCHECK_LE(upload_id.length(), kWebRtcEventLogMaxUploadIdBytes); + + const bool written = Write(kUploadIdLinePrefix + upload_id + kEOL); + if (!written) { + valid_ = false; + return false; + } + + return true; +} + +void WebRtcEventLogHistoryFileWriter::Delete() { + if (!base::DeleteFile(path_, /*recursive=*/false)) { + LOG(ERROR) << "History file could not be deleted."; + } + + valid_ = false; // Like was already false. +} + +base::FilePath WebRtcEventLogHistoryFileWriter::path() const { + DCHECK(valid_); // Can be performed on invalid objects, but likely shouldn't. + return path_; +} + +bool WebRtcEventLogHistoryFileWriter::Write(const std::string& str) { + DCHECK(valid_); + DCHECK(!str.empty()); + DCHECK_LE(str.length(), static_cast<size_t>(std::numeric_limits<int>::max())); + + const int written = file_.WriteAtCurrentPos(str.c_str(), str.length()); + if (written != static_cast<int>(str.length())) { + LOG(WARNING) << "Writing to history file failed."; + valid_ = false; + return false; + } + + // Writes to the history file are infrequent, and happen on a |task_runner_| + // dedicated to event logs. We can therefore afford to Flush() after every + // write, giving us greater confidence that information would not get lost if, + // e.g., Chrome crashes. + file_.Flush(); + + return true; +} + +std::unique_ptr<WebRtcEventLogHistoryFileReader> +WebRtcEventLogHistoryFileReader::Create(const base::FilePath& path) { + auto history_file_reader = + base::WrapUnique(new WebRtcEventLogHistoryFileReader(path)); + if (!history_file_reader->Init()) { + LOG(WARNING) << "Initialization of history file reader failed."; + return nullptr; + } + return history_file_reader; +} + +WebRtcEventLogHistoryFileReader::WebRtcEventLogHistoryFileReader( + const base::FilePath& path) + : path_(path), + local_id_(ExtractRemoteBoundWebRtcEventLogLocalIdFromPath(path_)), + valid_(false) {} + +WebRtcEventLogHistoryFileReader::WebRtcEventLogHistoryFileReader( + WebRtcEventLogHistoryFileReader&& other) + : path_(other.path_), + local_id_(other.local_id_), + capture_time_(other.capture_time_), + upload_time_(other.upload_time_), + upload_id_(other.upload_id_), + valid_(other.valid_) { + other.valid_ = false; +} + +bool WebRtcEventLogHistoryFileReader::Init() { + DCHECK(!valid_); + + if (local_id_.empty()) { + LOG(WARNING) << "Unknown local ID."; + return false; + } + + if (local_id_.length() > kWebRtcEventLogMaxUploadIdBytes) { + LOG(WARNING) << "Excessively long local ID."; + return false; + } + + if (!base::PathExists(path_)) { + LOG(WARNING) << "File does not exist."; + return false; + } + + constexpr int file_flags = base::File::FLAG_OPEN | base::File::FLAG_READ; + base::File file(path_, file_flags); + if (!file.IsValid()) { + LOG(WARNING) << "Couldn't read history file."; + if (!base::DeleteFile(path_, /*recursive=*/false)) { + LOG(ERROR) << "Failed to delete " << path_ << "."; + } + return false; + } + + constexpr size_t kMaxHistoryFileSizeBytes = 1024; + static_assert(kWebRtcEventLogMaxUploadIdBytes < kMaxHistoryFileSizeBytes, ""); + + std::string file_contents; + file_contents.resize(kMaxHistoryFileSizeBytes); + const int read_bytes = file.Read(0, &file_contents[0], file_contents.size()); + if (read_bytes < 0) { + LOG(WARNING) << "Couldn't read contents of history file."; + return false; + } + DCHECK_LE(static_cast<size_t>(read_bytes), file_contents.size()); + file_contents.resize(static_cast<size_t>(read_bytes)); + // Note: In excessively long files, the rest of the file will be ignored; the + // beginning of the file will encounter a parse error. + + if (!Parse(file_contents)) { + LOG(WARNING) << "Parsing of history file failed."; + return false; + } + + valid_ = true; + return true; +} + +std::string WebRtcEventLogHistoryFileReader::LocalId() const { + DCHECK(valid_); + DCHECK(!local_id_.empty()); + return local_id_; +} + +base::Time WebRtcEventLogHistoryFileReader::CaptureTime() const { + DCHECK(valid_); + DCHECK(!capture_time_.is_null()); + return capture_time_; +} + +base::Time WebRtcEventLogHistoryFileReader::UploadTime() const { + DCHECK(valid_); + return upload_time_; // May be null (which indicates "unset"). +} + +std::string WebRtcEventLogHistoryFileReader::UploadId() const { + DCHECK(valid_); + return upload_id_; +} + +base::FilePath WebRtcEventLogHistoryFileReader::path() const { + DCHECK(valid_); + return path_; +} + +bool WebRtcEventLogHistoryFileReader::operator<( + const WebRtcEventLogHistoryFileReader& other) const { + DCHECK(valid_); + DCHECK(!capture_time_.is_null()); + DCHECK(other.valid_); + DCHECK(!other.capture_time_.is_null()); + if (capture_time_ == other.capture_time_) { + // Resolve ties arbitrarily, but consistently (Local IDs are unique). + return LocalId() < other.LocalId(); + } + return (capture_time_ < other.capture_time_); +} + +bool WebRtcEventLogHistoryFileReader::Parse(const std::string& file_contents) { + DCHECK(!valid_); + DCHECK(capture_time_.is_null()); + DCHECK(upload_time_.is_null()); + DCHECK(upload_id_.empty()); + + const std::vector<std::string> lines = + base::SplitString(file_contents, kEOL, base::TRIM_WHITESPACE, + base::SplitResult::SPLIT_WANT_NONEMPTY); + + for (const std::string& line : lines) { + if (line.find(kCaptureTimeLinePrefix) == 0) { + if (!ParseTime(line, kCaptureTimeLinePrefix, &capture_time_)) { + return false; + } + } else if (line.find(kUploadTimeLinePrefix) == 0) { + if (!ParseTime(line, kUploadTimeLinePrefix, &upload_time_)) { + return false; + } + } else if (line.find(kUploadIdLinePrefix) == 0) { + if (!ParseString(line, kUploadIdLinePrefix, &upload_id_)) { + return false; + } + } else { + LOG(WARNING) << "Unrecognized line in history file."; + return false; + } + } + + if (capture_time_.is_null()) { + LOG(WARNING) << "Incomplete history file; capture time unknown."; + return false; + } + + if (!upload_id_.empty() && upload_time_.is_null()) { + LOG(WARNING) << "Incomplete history file; upload time known, " + << "but ID unknown."; + return false; + } + + if (!upload_time_.is_null() && upload_time_ < capture_time_) { + LOG(WARNING) << "Defective history file; claims to have been uploaded " + << "before being captured."; + return false; + } + + return true; +} + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_history.h b/chromium/chrome/browser/media/webrtc/webrtc_event_log_history.h new file mode 100644 index 00000000000..96180ec1ede --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_history.h @@ -0,0 +1,121 @@ +// Copyright 2018 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_HISTORY_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_HISTORY_H_ + +#include <memory> +#include <string> + +#include "base/files/file.h" +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/time/time.h" + +namespace webrtc_event_logging { + +// Writes a small history file to disk, which allows us to remember what logs +// were captured and uploaded, after they are uploaded (whether successfully or +// not), or after they ware pruned (if they expire before an upload opportunity +// presents itself). +class WebRtcEventLogHistoryFileWriter final { + public: + // Creates and initializes a WebRtcEventLogHistoryFileWriter object. + // Overwrites existing files on disk, if any. + // If initialization fails (e.g. couldn't create the file), an empty + // unique_ptr is returned. + static std::unique_ptr<WebRtcEventLogHistoryFileWriter> Create( + const base::FilePath& path); + + // The capture time must be later than UNIX epoch start. + bool WriteCaptureTime(base::Time capture_time); + + // The upload time must be later than UNIX epoch start. + // Writing an upload time earlier than the capture time is not prevented, + // but an invalid history file will be produced. + bool WriteUploadTime(base::Time upload_time); + + // If |upload_id| is empty, it means the upload was not successful. In that + // case, the |upload_time| still denotes the time when the upload started. + // |upload_id|'s length must not exceed kWebRtcEventLogMaxUploadIdBytes. + bool WriteUploadId(const std::string& upload_id); + + // Deletes the file being written to, and invalidates this object. + void Delete(); + + // May only be called on a valid object. + base::FilePath path() const; + + private: + explicit WebRtcEventLogHistoryFileWriter(const base::FilePath& path); + + // Returns true if initialization was successful; false otherwise. + // Overwrites existing files on disk, if any. + bool Init(); + + // Returns true if and only if the entire string was written to the file. + bool Write(const std::string& str); + + const base::FilePath path_; + base::File file_; + bool valid_; + + DISALLOW_COPY_AND_ASSIGN(WebRtcEventLogHistoryFileWriter); +}; + +// Reads from disk a small history file and recovers the data from it. +class WebRtcEventLogHistoryFileReader final { + public: + // Creates and initializes a WebRtcEventLogHistoryFileReader object. + // If initialization fails (e.g. couldn't parse the file), an empty + // unique_ptr is returned. + static std::unique_ptr<WebRtcEventLogHistoryFileReader> Create( + const base::FilePath& path); + + WebRtcEventLogHistoryFileReader(WebRtcEventLogHistoryFileReader&& other); + + // Mandatory fields. + std::string LocalId() const; // Must return a non-empty ID. + base::Time CaptureTime() const; // Must return a non-null base::Time. + + // Optional fields. + base::Time UploadTime() const; // Non-null only if upload was attempted. + std::string UploadId() const; // Non-null only if upload was successful. + + // May only be performed on a valid object. + base::FilePath path() const; + + // Compares by capture time. + bool operator<(const WebRtcEventLogHistoryFileReader& other) const; + + private: + explicit WebRtcEventLogHistoryFileReader(const base::FilePath& path); + + // Returns true if initialization was successful; false otherwise. + // If true is returned, |this| is now valid, and will remain so until + // the object is destroyed or std::move()-ed away from. + bool Init(); + + // Returns true if parsing succeeded; false otherwise. + bool Parse(const std::string& file_contents); + + const base::FilePath path_; + + const std::string local_id_; + + // Mandatory field; must be non-null (and therefore also non-zero). + base::Time capture_time_; + + // Optional fields; may appear 0 or 1 times in the file. + base::Time upload_time_; // Nullness/zero-ness indicates "unset". + std::string upload_id_; // Empty string indicates "unset". + + bool valid_; + + DISALLOW_COPY_AND_ASSIGN(WebRtcEventLogHistoryFileReader); +}; + +} // namespace webrtc_event_logging + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_HISTORY_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager.cc new file mode 100644 index 00000000000..577b56b007a --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager.cc @@ -0,0 +1,1086 @@ +// Copyright 2017 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 "chrome/browser/media/webrtc/webrtc_event_log_manager.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/task/post_task.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/policy/profile_policy_connector.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/chrome_features.h" +#include "chrome/common/pref_names.h" +#include "components/policy/core/common/policy_service.h" +#include "components/prefs/pref_service.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/network_service_instance.h" +#include "content/public/browser/render_process_host.h" + +#if defined OS_CHROMEOS +#include "chrome/browser/chromeos/profiles/profile_helper.h" +#endif + +#if !defined(OS_ANDROID) && !defined(OS_CHROMEOS) +#include "chrome/browser/policy/chrome_browser_policy_connector.h" +#endif + +namespace webrtc_event_logging { + +namespace { + +using BrowserContext = content::BrowserContext; +using BrowserThread = content::BrowserThread; +using RenderProcessHost = content::RenderProcessHost; + +using BrowserContextId = WebRtcEventLogManager::BrowserContextId; + +class PeerConnectionTrackerProxyImpl + : public WebRtcEventLogManager::PeerConnectionTrackerProxy { + public: + ~PeerConnectionTrackerProxyImpl() override = default; + + void EnableWebRtcEventLogging(const WebRtcEventLogPeerConnectionKey& key, + int output_period_ms) override { + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce( + &PeerConnectionTrackerProxyImpl::EnableWebRtcEventLoggingInternal, + key, output_period_ms)); + } + + void DisableWebRtcEventLogging( + const WebRtcEventLogPeerConnectionKey& key) override { + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce( + &PeerConnectionTrackerProxyImpl::DisableWebRtcEventLoggingInternal, + key)); + } + + private: + static void EnableWebRtcEventLoggingInternal( + WebRtcEventLogPeerConnectionKey key, + int output_period_ms) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + RenderProcessHost* host = RenderProcessHost::FromID(key.render_process_id); + if (!host) { + return; // The host has been asynchronously removed; not a problem. + } + host->EnableWebRtcEventLogOutput(key.lid, output_period_ms); + } + + static void DisableWebRtcEventLoggingInternal( + WebRtcEventLogPeerConnectionKey key) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + RenderProcessHost* host = RenderProcessHost::FromID(key.render_process_id); + if (!host) { + return; // The host has been asynchronously removed; not a problem. + } + host->DisableWebRtcEventLogOutput(key.lid); + } +}; + +// Check whether remote-bound logging is generally allowed, although not +// necessarily for any given user profile. +// 1. Certain platforms (mobile) are blocked from remote-bound logging. +// 2. There is a Finch-controlled kill-switch for the feature. +bool IsRemoteLoggingFeatureEnabled() { +#if defined(OS_ANDROID) + bool enabled = false; +#else + bool enabled = base::FeatureList::IsEnabled(features::kWebRtcRemoteEventLog); +#endif + + VLOG(1) << "WebRTC remote-bound event logging " + << (enabled ? "enabled" : "disabled") << "."; + + return enabled; +} + +// Checks whether the Profile is considered managed. Used to +// determine the default value for the policy controlling event logging. +bool IsBrowserManagedForProfile(const Profile* profile) { +// For Chrome OS, exclude the signin profile and ephemeral profiles. +#if defined(OS_CHROMEOS) + if (chromeos::ProfileHelper::IsSigninProfile(profile) || + chromeos::ProfileHelper::IsEphemeralUserProfile(profile)) { + return false; + } +#endif + + // Child accounts should not have a logging default of true so + // we do not consider them as being managed here. + if (profile->IsChild()) { + return false; + } + + if (profile->GetProfilePolicyConnector() + ->policy_service() + ->IsInitializationComplete(policy::POLICY_DOMAIN_CHROME) && + profile->GetProfilePolicyConnector()->IsManaged()) { + return true; + } + + // For desktop, machine level policies (Windows, Linux, Mac OS) can affect + // user profiles, so we consider these profiles managed. +#if !defined(OS_ANDROID) && !defined(OS_CHROMEOS) + return g_browser_process->browser_policy_connector() + ->HasMachineLevelPolicies(); +#else + return false; +#endif +} + +BrowserContext* GetBrowserContext(int render_process_id) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + RenderProcessHost* const host = RenderProcessHost::FromID(render_process_id); + return host ? host->GetBrowserContext() : nullptr; +} + +// Post reply back if non-empty. +template <typename... Args> +inline void MaybeReply(const base::Location& location, + base::OnceCallback<void(Args...)> reply, + Args... args) { + if (reply) { + base::PostTask(location, {BrowserThread::UI}, + base::BindOnce(std::move(reply), args...)); + } +} + +} // namespace + +WebRtcEventLogManager* WebRtcEventLogManager::g_webrtc_event_log_manager = + nullptr; + +std::unique_ptr<WebRtcEventLogManager> +WebRtcEventLogManager::CreateSingletonInstance() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!g_webrtc_event_log_manager); + g_webrtc_event_log_manager = new WebRtcEventLogManager; + return base::WrapUnique<WebRtcEventLogManager>(g_webrtc_event_log_manager); +} + +WebRtcEventLogManager* WebRtcEventLogManager::GetInstance() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + return g_webrtc_event_log_manager; +} + +base::FilePath WebRtcEventLogManager::GetRemoteBoundWebRtcEventLogsDir( + content::BrowserContext* browser_context) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(browser_context); + // Incognito BrowserContext will return their parent profile's directory. + return webrtc_event_logging::GetRemoteBoundWebRtcEventLogsDir( + browser_context->GetPath()); +} + +WebRtcEventLogManager::WebRtcEventLogManager() + : task_runner_(base::CreateSequencedTaskRunner( + {base::ThreadPool(), base::MayBlock(), + base::TaskPriority::BEST_EFFORT, + base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})), + remote_logging_feature_enabled_(IsRemoteLoggingFeatureEnabled()), + local_logs_observer_(nullptr), + remote_logs_observer_(nullptr), + local_logs_manager_(this), + remote_logs_manager_(this, task_runner_), + pc_tracker_proxy_(new PeerConnectionTrackerProxyImpl), + first_browser_context_initializations_done_(false) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!g_webrtc_event_log_manager); + g_webrtc_event_log_manager = this; +} + +WebRtcEventLogManager::~WebRtcEventLogManager() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + for (RenderProcessHost* host : observed_render_process_hosts_) { + host->RemoveObserver(this); + } + + DCHECK(g_webrtc_event_log_manager); + g_webrtc_event_log_manager = nullptr; +} + +void WebRtcEventLogManager::EnableForBrowserContext( + BrowserContext* browser_context, + base::OnceClosure reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(browser_context); + CHECK(!browser_context->IsOffTheRecord()); + + if (!first_browser_context_initializations_done_) { + OnFirstBrowserContextLoaded(); + first_browser_context_initializations_done_ = true; + } + + StartListeningForPrefChangeForBrowserContext(browser_context); + + if (!IsRemoteLoggingAllowedForBrowserContext(browser_context)) { + // If remote-bound logging was enabled during a previous Chrome session, + // it might have produced some pending log files, which we will now + // wish to remove. + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce( + &WebRtcEventLogManager:: + RemovePendingRemoteBoundLogsForNotEnabledBrowserContext, + base::Unretained(this), GetBrowserContextId(browser_context), + browser_context->GetPath(), std::move(reply))); + return; + } + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce( + &WebRtcEventLogManager::EnableRemoteBoundLoggingForBrowserContext, + base::Unretained(this), GetBrowserContextId(browser_context), + browser_context->GetPath(), std::move(reply))); +} + +void WebRtcEventLogManager::DisableForBrowserContext( + content::BrowserContext* browser_context, + base::OnceClosure reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(browser_context); + + StopListeningForPrefChangeForBrowserContext(browser_context); + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce( + &WebRtcEventLogManager::DisableRemoteBoundLoggingForBrowserContext, + base::Unretained(this), GetBrowserContextId(browser_context), + std::move(reply))); +} + +void WebRtcEventLogManager::PeerConnectionAdded( + int render_process_id, + int lid, + base::OnceCallback<void(bool)> reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + RenderProcessHost* rph = RenderProcessHost::FromID(render_process_id); + if (!rph) { + // RPH died before processing of this notification. + MaybeReply(FROM_HERE, std::move(reply), false); + return; + } + + auto it = observed_render_process_hosts_.find(rph); + if (it == observed_render_process_hosts_.end()) { + // This is the first PeerConnection which we see that's associated + // with this RPH. + rph->AddObserver(this); + observed_render_process_hosts_.insert(rph); + } + + const auto browser_context_id = GetBrowserContextId(rph->GetBrowserContext()); + DCHECK_NE(browser_context_id, kNullBrowserContextId); + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce( + &WebRtcEventLogManager::PeerConnectionAddedInternal, + base::Unretained(this), + PeerConnectionKey(render_process_id, lid, browser_context_id), + std::move(reply))); +} + +void WebRtcEventLogManager::PeerConnectionRemoved( + int render_process_id, + int lid, + base::OnceCallback<void(bool)> reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + const auto browser_context_id = GetBrowserContextId(render_process_id); + if (browser_context_id == kNullBrowserContextId) { + // RPH died before processing of this notification. This is handled by + // RenderProcessExited() / RenderProcessHostDestroyed. + MaybeReply(FROM_HERE, std::move(reply), false); + return; + } + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce( + &WebRtcEventLogManager::PeerConnectionRemovedInternal, + base::Unretained(this), + PeerConnectionKey(render_process_id, lid, browser_context_id), + std::move(reply))); +} + +void WebRtcEventLogManager::PeerConnectionStopped( + int render_process_id, + int lid, + base::OnceCallback<void(bool)> reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + return PeerConnectionRemoved(render_process_id, lid, std::move(reply)); +} + +void WebRtcEventLogManager::PeerConnectionSessionIdSet( + int render_process_id, + int lid, + const std::string& session_id, + base::OnceCallback<void(bool)> reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + const auto browser_context_id = GetBrowserContextId(render_process_id); + if (browser_context_id == kNullBrowserContextId) { + // RPH died before processing of this notification. This is handled by + // RenderProcessExited() / RenderProcessHostDestroyed. + MaybeReply(FROM_HERE, std::move(reply), false); + return; + } + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce( + &WebRtcEventLogManager::PeerConnectionSessionIdSetInternal, + base::Unretained(this), + PeerConnectionKey(render_process_id, lid, browser_context_id), + session_id, std::move(reply))); +} + +void WebRtcEventLogManager::EnableLocalLogging( + const base::FilePath& base_path, + base::OnceCallback<void(bool)> reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + EnableLocalLogging(base_path, kDefaultMaxLocalLogFileSizeBytes, + std::move(reply)); +} + +void WebRtcEventLogManager::EnableLocalLogging( + const base::FilePath& base_path, + size_t max_file_size_bytes, + base::OnceCallback<void(bool)> reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!base_path.empty()); + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&WebRtcEventLogManager::EnableLocalLoggingInternal, + base::Unretained(this), base_path, max_file_size_bytes, + std::move(reply))); +} + +void WebRtcEventLogManager::DisableLocalLogging( + base::OnceCallback<void(bool)> reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&WebRtcEventLogManager::DisableLocalLoggingInternal, + base::Unretained(this), std::move(reply))); +} + +void WebRtcEventLogManager::OnWebRtcEventLogWrite( + int render_process_id, + int lid, + const std::string& message, + base::OnceCallback<void(std::pair<bool, bool>)> reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + const BrowserContext* browser_context = GetBrowserContext(render_process_id); + if (!browser_context) { + // RPH died before processing of this notification. + MaybeReply(FROM_HERE, std::move(reply), std::make_pair(false, false)); + return; + } + + const auto browser_context_id = GetBrowserContextId(browser_context); + DCHECK_NE(browser_context_id, kNullBrowserContextId); + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce( + &WebRtcEventLogManager::OnWebRtcEventLogWriteInternal, + base::Unretained(this), + PeerConnectionKey(render_process_id, lid, browser_context_id), + message, std::move(reply))); +} + +void WebRtcEventLogManager::StartRemoteLogging( + int render_process_id, + const std::string& session_id, + size_t max_file_size_bytes, + int output_period_ms, + size_t web_app_id, + base::OnceCallback<void(bool, const std::string&, const std::string&)> + reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(reply); + + BrowserContext* browser_context = GetBrowserContext(render_process_id); + const char* error = nullptr; + + if (!browser_context) { + // RPH died before processing of this notification. + UmaRecordWebRtcEventLoggingApi(WebRtcEventLoggingApiUma::kDeadRph); + error = kStartRemoteLoggingFailureDeadRenderProcessHost; + } else if (!IsRemoteLoggingAllowedForBrowserContext(browser_context)) { + UmaRecordWebRtcEventLoggingApi(WebRtcEventLoggingApiUma::kFeatureDisabled); + error = kStartRemoteLoggingFailureFeatureDisabled; + } else if (browser_context->IsOffTheRecord()) { + // Feature disable in incognito. Since the feature can be disabled for + // non-incognito sessions, this should not expose incognito mode. + UmaRecordWebRtcEventLoggingApi(WebRtcEventLoggingApiUma::kIncognito); + error = kStartRemoteLoggingFailureFeatureDisabled; + } + + if (error) { + base::PostTask(FROM_HERE, {BrowserThread::UI}, + base::BindOnce(std::move(reply), false, std::string(), + std::string(error))); + return; + } + + const auto browser_context_id = GetBrowserContextId(browser_context); + DCHECK_NE(browser_context_id, kNullBrowserContextId); + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&WebRtcEventLogManager::StartRemoteLoggingInternal, + base::Unretained(this), render_process_id, + browser_context_id, session_id, browser_context->GetPath(), + max_file_size_bytes, output_period_ms, web_app_id, + std::move(reply))); +} + +void WebRtcEventLogManager::ClearCacheForBrowserContext( + const BrowserContext* browser_context, + const base::Time& delete_begin, + const base::Time& delete_end, + base::OnceClosure reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + const auto browser_context_id = GetBrowserContextId(browser_context); + DCHECK_NE(browser_context_id, kNullBrowserContextId); + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTaskAndReply( + FROM_HERE, + base::BindOnce( + &WebRtcEventLogManager::ClearCacheForBrowserContextInternal, + base::Unretained(this), browser_context_id, delete_begin, delete_end), + std::move(reply)); +} + +void WebRtcEventLogManager::GetHistory( + BrowserContextId browser_context_id, + base::OnceCallback<void(const std::vector<UploadList::UploadInfo>&)> + reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(reply); + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, base::BindOnce(&WebRtcEventLogManager::GetHistoryInternal, + base::Unretained(this), browser_context_id, + std::move(reply))); +} + +void WebRtcEventLogManager::SetLocalLogsObserver( + WebRtcLocalEventLogsObserver* observer, + base::OnceClosure reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&WebRtcEventLogManager::SetLocalLogsObserverInternal, + base::Unretained(this), observer, std::move(reply))); +} + +void WebRtcEventLogManager::SetRemoteLogsObserver( + WebRtcRemoteEventLogsObserver* observer, + base::OnceClosure reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&WebRtcEventLogManager::SetRemoteLogsObserverInternal, + base::Unretained(this), observer, std::move(reply))); +} + +bool WebRtcEventLogManager::IsRemoteLoggingAllowedForBrowserContext( + BrowserContext* browser_context) const { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(browser_context); + + if (!remote_logging_feature_enabled_) { + return false; + } + + const Profile* profile = Profile::FromBrowserContext(browser_context); + DCHECK(profile); + + const PrefService::Preference* webrtc_event_log_collection_allowed_pref = + profile->GetPrefs()->FindPreference( + prefs::kWebRtcEventLogCollectionAllowed); + DCHECK(webrtc_event_log_collection_allowed_pref); + + if (webrtc_event_log_collection_allowed_pref->IsDefaultValue()) { + // The pref has not been set. GetBoolean would only return the default + // value. However, there is no single default value, + // because it depends on whether Chrome is managed, + // so we check whether Chrome is managed. + // TODO(https://crbug.com/980132): use generalized policy default + // mechanism when it is available. + const bool managed = IsBrowserManagedForProfile(profile); + constexpr bool kCollectionAllowedDefaultManaged = true; + constexpr bool kCollectionAllowedDefaultUnManaged = false; + return managed ? kCollectionAllowedDefaultManaged + : kCollectionAllowedDefaultUnManaged; + } + + // There is a non-default value set, so this value is authoritative. + return profile->GetPrefs()->GetBoolean( + prefs::kWebRtcEventLogCollectionAllowed); +} + +std::unique_ptr<LogFileWriter::Factory> +WebRtcEventLogManager::CreateRemoteLogFileWriterFactory() { + if (remote_log_file_writer_factory_for_testing_) { + return std::move(remote_log_file_writer_factory_for_testing_); +#if !defined(OS_ANDROID) + } else if (base::FeatureList::IsEnabled( + features::kWebRtcRemoteEventLogGzipped)) { + return std::make_unique<GzippedLogFileWriterFactory>( + std::make_unique<GzipLogCompressorFactory>( + std::make_unique<DefaultGzippedSizeEstimator::Factory>())); +#endif + } else { + return std::make_unique<BaseLogFileWriterFactory>(); + } +} + +void WebRtcEventLogManager::RenderProcessExited( + RenderProcessHost* host, + const content::ChildProcessTerminationInfo& info) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + RenderProcessHostExitedDestroyed(host); +} + +void WebRtcEventLogManager::RenderProcessHostDestroyed( + RenderProcessHost* host) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + RenderProcessHostExitedDestroyed(host); +} + +void WebRtcEventLogManager::RenderProcessHostExitedDestroyed( + RenderProcessHost* host) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(host); + + auto it = observed_render_process_hosts_.find(host); + if (it == observed_render_process_hosts_.end()) { + return; // We've never seen PeerConnections associated with this RPH. + } + host->RemoveObserver(this); + observed_render_process_hosts_.erase(host); + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&WebRtcEventLogManager::RenderProcessExitedInternal, + base::Unretained(this), host->GetID())); +} + +void WebRtcEventLogManager::OnLocalLogStarted(PeerConnectionKey peer_connection, + const base::FilePath& file_path) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + constexpr int kLogOutputPeriodMsForLocalLogging = 0; // No batching. + OnLoggingTargetStarted(LoggingTarget::kLocalLogging, peer_connection, + kLogOutputPeriodMsForLocalLogging); + + if (local_logs_observer_) { + local_logs_observer_->OnLocalLogStarted(peer_connection, file_path); + } +} + +void WebRtcEventLogManager::OnLocalLogStopped( + PeerConnectionKey peer_connection) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + OnLoggingTargetStopped(LoggingTarget::kLocalLogging, peer_connection); + + if (local_logs_observer_) { + local_logs_observer_->OnLocalLogStopped(peer_connection); + } +} + +void WebRtcEventLogManager::OnRemoteLogStarted(PeerConnectionKey key, + const base::FilePath& file_path, + int output_period_ms) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + OnLoggingTargetStarted(LoggingTarget::kRemoteLogging, key, output_period_ms); + if (remote_logs_observer_) { + remote_logs_observer_->OnRemoteLogStarted(key, file_path, output_period_ms); + } +} + +void WebRtcEventLogManager::OnRemoteLogStopped( + WebRtcEventLogPeerConnectionKey key) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + OnLoggingTargetStopped(LoggingTarget::kRemoteLogging, key); + if (remote_logs_observer_) { + remote_logs_observer_->OnRemoteLogStopped(key); + } +} + +void WebRtcEventLogManager::OnLoggingTargetStarted(LoggingTarget target, + PeerConnectionKey key, + int output_period_ms) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + auto it = peer_connections_with_event_logging_enabled_in_webrtc_.find(key); + if (it != peer_connections_with_event_logging_enabled_in_webrtc_.end()) { + DCHECK_EQ((it->second & target), 0u); + it->second |= target; + } else { + // This is the first client for WebRTC event logging - let WebRTC know + // that it should start informing us of events. + peer_connections_with_event_logging_enabled_in_webrtc_.emplace(key, target); + pc_tracker_proxy_->EnableWebRtcEventLogging(key, output_period_ms); + } +} + +void WebRtcEventLogManager::OnLoggingTargetStopped(LoggingTarget target, + PeerConnectionKey key) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + // Record that we're no longer performing this type of logging for this PC. + auto it = peer_connections_with_event_logging_enabled_in_webrtc_.find(key); + CHECK(it != peer_connections_with_event_logging_enabled_in_webrtc_.end()); + DCHECK_NE(it->second, 0u); + it->second &= ~target; + + // If we're not doing any other type of logging for this peer connection, + // it's time to stop receiving notifications for it from WebRTC. + if (it->second == 0u) { + peer_connections_with_event_logging_enabled_in_webrtc_.erase(it); + pc_tracker_proxy_->DisableWebRtcEventLogging(key); + } +} + +void WebRtcEventLogManager::StartListeningForPrefChangeForBrowserContext( + BrowserContext* browser_context) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(first_browser_context_initializations_done_); + CHECK(!browser_context->IsOffTheRecord()); + + const auto browser_context_id = GetBrowserContextId(browser_context); + auto it = pref_change_registrars_.emplace(std::piecewise_construct, + std::make_tuple(browser_context_id), + std::make_tuple()); + DCHECK(it.second) << "Already listening."; + PrefChangeRegistrar& registrar = it.first->second; + + Profile* profile = Profile::FromBrowserContext(browser_context); + DCHECK(profile); + registrar.Init(profile->GetPrefs()); + + // * |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + // * base::Unretained(browser_context) is safe, because |browser_context| + // stays alive until Chrome shut-down, at which point we'll stop listening + // as part of its (BrowserContext's) tear-down process. + registrar.Add(prefs::kWebRtcEventLogCollectionAllowed, + base::BindRepeating(&WebRtcEventLogManager::OnPrefChange, + base::Unretained(this), + base::Unretained(browser_context))); +} + +void WebRtcEventLogManager::StopListeningForPrefChangeForBrowserContext( + BrowserContext* browser_context) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + const auto browser_context_id = GetBrowserContextId(browser_context); + + size_t erased_count = pref_change_registrars_.erase(browser_context_id); + DCHECK_EQ(erased_count, 1u); +} + +void WebRtcEventLogManager::OnPrefChange(BrowserContext* browser_context) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(first_browser_context_initializations_done_); + + const Profile* profile = Profile::FromBrowserContext(browser_context); + DCHECK(profile); + + const bool enabled = IsRemoteLoggingAllowedForBrowserContext(browser_context); + + if (!enabled) { + // Dynamic refresh of the policy to DISABLED; stop ongoing logs, remove + // pending log files and stop any active uploads. + ClearCacheForBrowserContext(browser_context, base::Time::Min(), + base::Time::Max(), base::DoNothing()); + } + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + base::OnceClosure task; + if (enabled) { + task = base::BindOnce( + &WebRtcEventLogManager::EnableRemoteBoundLoggingForBrowserContext, + base::Unretained(this), GetBrowserContextId(browser_context), + browser_context->GetPath(), base::OnceClosure()); + } else { + task = base::BindOnce( + &WebRtcEventLogManager::DisableRemoteBoundLoggingForBrowserContext, + base::Unretained(this), GetBrowserContextId(browser_context), + base::OnceClosure()); + } + + task_runner_->PostTask(FROM_HERE, std::move(task)); +} + +void WebRtcEventLogManager::OnFirstBrowserContextLoaded() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + network::NetworkConnectionTracker* network_connection_tracker = + content::GetNetworkConnectionTracker(); + DCHECK(network_connection_tracker); + + auto log_file_writer_factory = CreateRemoteLogFileWriterFactory(); + DCHECK(log_file_writer_factory); + + // |network_connection_tracker| is owned by BrowserProcessImpl, which owns + // the IOThread. The internal task runner on which |this| uses + // |network_connection_tracker|, stops before IOThread dies, so we can trust + // that |network_connection_tracker| will not be used after destruction. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce( + &WebRtcEventLogManager::OnFirstBrowserContextLoadedInternal, + base::Unretained(this), base::Unretained(network_connection_tracker), + std::move(log_file_writer_factory))); +} + +void WebRtcEventLogManager::OnFirstBrowserContextLoadedInternal( + network::NetworkConnectionTracker* network_connection_tracker, + std::unique_ptr<LogFileWriter::Factory> log_file_writer_factory) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(network_connection_tracker); + DCHECK(log_file_writer_factory); + remote_logs_manager_.SetNetworkConnectionTracker(network_connection_tracker); + remote_logs_manager_.SetLogFileWriterFactory( + std::move(log_file_writer_factory)); +} + +void WebRtcEventLogManager::EnableRemoteBoundLoggingForBrowserContext( + BrowserContextId browser_context_id, + const base::FilePath& browser_context_dir, + base::OnceClosure reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_NE(browser_context_id, kNullBrowserContextId); + + remote_logs_manager_.EnableForBrowserContext(browser_context_id, + browser_context_dir); + + MaybeReply(FROM_HERE, std::move(reply)); +} + +void WebRtcEventLogManager::DisableRemoteBoundLoggingForBrowserContext( + BrowserContextId browser_context_id, + base::OnceClosure reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + // Note that the BrowserContext might never have been enabled in the + // remote-bound manager; that's not a problem. + remote_logs_manager_.DisableForBrowserContext(browser_context_id); + + MaybeReply(FROM_HERE, std::move(reply)); +} + +void WebRtcEventLogManager:: + RemovePendingRemoteBoundLogsForNotEnabledBrowserContext( + BrowserContextId browser_context_id, + const base::FilePath& browser_context_dir, + base::OnceClosure reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + remote_logs_manager_.RemovePendingLogsForNotEnabledBrowserContext( + browser_context_id, browser_context_dir); + + MaybeReply(FROM_HERE, std::move(reply)); +} + +void WebRtcEventLogManager::PeerConnectionAddedInternal( + PeerConnectionKey key, + base::OnceCallback<void(bool)> reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + const bool local_result = local_logs_manager_.PeerConnectionAdded(key); + const bool remote_result = remote_logs_manager_.PeerConnectionAdded(key); + DCHECK_EQ(local_result, remote_result); + + MaybeReply(FROM_HERE, std::move(reply), local_result); +} + +void WebRtcEventLogManager::PeerConnectionRemovedInternal( + PeerConnectionKey key, + base::OnceCallback<void(bool)> reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + const bool local_result = local_logs_manager_.PeerConnectionRemoved(key); + const bool remote_result = remote_logs_manager_.PeerConnectionRemoved(key); + DCHECK_EQ(local_result, remote_result); + + MaybeReply(FROM_HERE, std::move(reply), local_result); +} + +void WebRtcEventLogManager::PeerConnectionSessionIdSetInternal( + PeerConnectionKey key, + const std::string& session_id, + base::OnceCallback<void(bool)> reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + const bool result = + remote_logs_manager_.PeerConnectionSessionIdSet(key, session_id); + MaybeReply(FROM_HERE, std::move(reply), result); +} + +void WebRtcEventLogManager::EnableLocalLoggingInternal( + const base::FilePath& base_path, + size_t max_file_size_bytes, + base::OnceCallback<void(bool)> reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + const bool result = + local_logs_manager_.EnableLogging(base_path, max_file_size_bytes); + + MaybeReply(FROM_HERE, std::move(reply), result); +} + +void WebRtcEventLogManager::DisableLocalLoggingInternal( + base::OnceCallback<void(bool)> reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + const bool result = local_logs_manager_.DisableLogging(); + + MaybeReply(FROM_HERE, std::move(reply), result); +} + +void WebRtcEventLogManager::OnWebRtcEventLogWriteInternal( + PeerConnectionKey key, + const std::string& message, + base::OnceCallback<void(std::pair<bool, bool>)> reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + const bool local_result = local_logs_manager_.EventLogWrite(key, message); + const bool remote_result = remote_logs_manager_.EventLogWrite(key, message); + + MaybeReply(FROM_HERE, std::move(reply), + std::make_pair(local_result, remote_result)); +} + +void WebRtcEventLogManager::StartRemoteLoggingInternal( + int render_process_id, + BrowserContextId browser_context_id, + const std::string& session_id, + const base::FilePath& browser_context_dir, + size_t max_file_size_bytes, + int output_period_ms, + size_t web_app_id, + base::OnceCallback<void(bool, const std::string&, const std::string&)> + reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + std::string log_id; + std::string error_message; + const bool result = remote_logs_manager_.StartRemoteLogging( + render_process_id, browser_context_id, session_id, browser_context_dir, + max_file_size_bytes, output_period_ms, web_app_id, &log_id, + &error_message); + + // |log_id| set only if successful; |error_message| set only if unsuccessful. + DCHECK_EQ(result, !log_id.empty()); + DCHECK_EQ(!result, !error_message.empty()); + + base::PostTask( + FROM_HERE, {BrowserThread::UI}, + base::BindOnce(std::move(reply), result, log_id, error_message)); +} + +void WebRtcEventLogManager::ClearCacheForBrowserContextInternal( + BrowserContextId browser_context_id, + const base::Time& delete_begin, + const base::Time& delete_end) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + remote_logs_manager_.ClearCacheForBrowserContext(browser_context_id, + delete_begin, delete_end); +} + +void WebRtcEventLogManager::GetHistoryInternal( + BrowserContextId browser_context_id, + base::OnceCallback<void(const std::vector<UploadList::UploadInfo>&)> + reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(reply); + remote_logs_manager_.GetHistory(browser_context_id, std::move(reply)); +} + +void WebRtcEventLogManager::RenderProcessExitedInternal(int render_process_id) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + local_logs_manager_.RenderProcessHostExitedDestroyed(render_process_id); + remote_logs_manager_.RenderProcessHostExitedDestroyed(render_process_id); +} + +void WebRtcEventLogManager::SetLocalLogsObserverInternal( + WebRtcLocalEventLogsObserver* observer, + base::OnceClosure reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + local_logs_observer_ = observer; + + if (reply) { + base::PostTask(FROM_HERE, {BrowserThread::UI}, std::move(reply)); + } +} + +void WebRtcEventLogManager::SetRemoteLogsObserverInternal( + WebRtcRemoteEventLogsObserver* observer, + base::OnceClosure reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + remote_logs_observer_ = observer; + + if (reply) { + base::PostTask(FROM_HERE, {BrowserThread::UI}, std::move(reply)); + } +} + +void WebRtcEventLogManager::SetClockForTesting(base::Clock* clock, + base::OnceClosure reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(reply); + + auto task = [](WebRtcEventLogManager* manager, base::Clock* clock, + base::OnceClosure reply) { + manager->local_logs_manager_.SetClockForTesting(clock); + + base::PostTask(FROM_HERE, {BrowserThread::UI}, std::move(reply)); + }; + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask(FROM_HERE, base::BindOnce(task, base::Unretained(this), + clock, std::move(reply))); +} + +void WebRtcEventLogManager::SetPeerConnectionTrackerProxyForTesting( + std::unique_ptr<PeerConnectionTrackerProxy> pc_tracker_proxy, + base::OnceClosure reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(reply); + + auto task = [](WebRtcEventLogManager* manager, + std::unique_ptr<PeerConnectionTrackerProxy> pc_tracker_proxy, + base::OnceClosure reply) { + manager->pc_tracker_proxy_ = std::move(pc_tracker_proxy); + + base::PostTask(FROM_HERE, {BrowserThread::UI}, std::move(reply)); + }; + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, base::BindOnce(task, base::Unretained(this), + std::move(pc_tracker_proxy), std::move(reply))); +} + +void WebRtcEventLogManager::SetWebRtcEventLogUploaderFactoryForTesting( + std::unique_ptr<WebRtcEventLogUploader::Factory> uploader_factory, + base::OnceClosure reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(reply); + + auto task = + [](WebRtcEventLogManager* manager, + std::unique_ptr<WebRtcEventLogUploader::Factory> uploader_factory, + base::OnceClosure reply) { + auto& remote_logs_manager = manager->remote_logs_manager_; + remote_logs_manager.SetWebRtcEventLogUploaderFactoryForTesting( + std::move(uploader_factory)); + + base::PostTask(FROM_HERE, {BrowserThread::UI}, std::move(reply)); + }; + + // |this| is destroyed by ~BrowserProcessImpl(), so base::Unretained(this) + // will not be dereferenced after destruction. + task_runner_->PostTask( + FROM_HERE, base::BindOnce(task, base::Unretained(this), + std::move(uploader_factory), std::move(reply))); +} + +void WebRtcEventLogManager::SetRemoteLogFileWriterFactoryForTesting( + std::unique_ptr<LogFileWriter::Factory> factory) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!first_browser_context_initializations_done_) << "Too late."; + DCHECK(!remote_log_file_writer_factory_for_testing_) << "Already called."; + remote_log_file_writer_factory_for_testing_ = std::move(factory); +} + +void WebRtcEventLogManager::UploadConditionsHoldForTesting( + base::OnceCallback<void(bool)> callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + // Unit tests block until |callback| is sent back, so the use + // of base::Unretained(&remote_logs_manager_) is safe. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce( + &WebRtcRemoteEventLogManager::UploadConditionsHoldForTesting, + base::Unretained(&remote_logs_manager_), std::move(callback))); +} + +scoped_refptr<base::SequencedTaskRunner>& +WebRtcEventLogManager::GetTaskRunnerForTesting() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + return task_runner_; +} + +void WebRtcEventLogManager::PostNullTaskForTesting(base::OnceClosure reply) { + task_runner_->PostTask(FROM_HERE, std::move(reply)); +} + +void WebRtcEventLogManager::ShutDownForTesting(base::OnceClosure reply) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + // Unit tests block until |callback| is sent back, so the use + // of base::Unretained(&remote_logs_manager_) is safe. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&WebRtcRemoteEventLogManager::ShutDownForTesting, + base::Unretained(&remote_logs_manager_), + std::move(reply))); +} + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager.h b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager.h new file mode 100644 index 00000000000..6b4b815e6af --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager.h @@ -0,0 +1,429 @@ +// Copyright 2017 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_H_ + +#include <map> +#include <memory> +#include <utility> +#include <vector> + +#include "base/callback.h" +#include "base/containers/flat_set.h" +#include "base/files/file_path.h" +#include "base/memory/scoped_refptr.h" +#include "base/sequenced_task_runner.h" +#include "base/time/clock.h" +#include "base/time/time.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_common.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_local.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_remote.h" +#include "components/prefs/pref_change_registrar.h" +#include "components/upload_list/upload_list.h" +#include "content/public/browser/render_process_host_observer.h" +#include "content/public/browser/webrtc_event_logger.h" + +class WebRTCInternalsIntegrationBrowserTest; + +namespace content { +class BrowserContext; +class NetworkConnectionTracker; +} // namespace content + +namespace webrtc_event_logging { + +// This is a singleton class running in the browser UI thread (ownership of +// the only instance lies in BrowserContext). It is in charge of writing WebRTC +// event logs to temporary files, then uploading those files to remote servers, +// as well as of writing the logs to files which were manually indicated by the +// user from the WebRTCIntenals. (A log may simulatenously be written to both, +// either, or none.) +// The only instance of this class is owned by BrowserProcessImpl. It is +// destroyed from ~BrowserProcessImpl(), at which point any tasks posted to the +// internal SequencedTaskRunner, or coming from another thread, would no longer +// execute. +class WebRtcEventLogManager final : public content::RenderProcessHostObserver, + public content::WebRtcEventLogger, + public WebRtcLocalEventLogsObserver, + public WebRtcRemoteEventLogsObserver { + public: + using BrowserContextId = WebRtcEventLogPeerConnectionKey::BrowserContextId; + + // To turn WebRTC on and off, we go through PeerConnectionTrackerProxy. In + // order to make this toggling easily testable, PeerConnectionTrackerProxyImpl + // will send real messages to PeerConnectionTracker, whereas + // PeerConnectionTrackerProxyForTesting will be a mock that just makes sure + // the correct messages were attempted to be sent. + class PeerConnectionTrackerProxy { + public: + virtual ~PeerConnectionTrackerProxy() = default; + + virtual void EnableWebRtcEventLogging( + const WebRtcEventLogPeerConnectionKey& key, + int output_period_ms) = 0; + + virtual void DisableWebRtcEventLogging( + const WebRtcEventLogPeerConnectionKey& key) = 0; + }; + + // Ensures that no previous instantiation of the class was performed, then + // instantiates the class and returns the object (ownership is transfered to + // the caller). Subsequent calls to GetInstance() will return this object, + // until it is destructed, at which pointer nullptr will be returned by + // subsequent calls. + static std::unique_ptr<WebRtcEventLogManager> CreateSingletonInstance(); + + // Returns the object previously constructed using CreateSingletonInstance(), + // if it was constructed and was not yet destroyed; nullptr otherwise. + static WebRtcEventLogManager* GetInstance(); + + // Given a BrowserContext, return the path to the directory where its + // remote-bound event logs are kept. + // Since incognito sessions don't have such a directory, an empty + // base::FilePath will be returned for them. + static base::FilePath GetRemoteBoundWebRtcEventLogsDir( + content::BrowserContext* browser_context); + + ~WebRtcEventLogManager() override; + + void EnableForBrowserContext(content::BrowserContext* browser_context, + base::OnceClosure reply); + + void DisableForBrowserContext(content::BrowserContext* browser_context, + base::OnceClosure reply); + + void PeerConnectionAdded(int render_process_id, + int lid, // Renderer-local PeerConnection ID. + base::OnceCallback<void(bool)> reply) override; + + void PeerConnectionRemoved(int render_process_id, + int lid, // Renderer-local PeerConnection ID. + base::OnceCallback<void(bool)> reply) override; + + // From the logger's perspective, we treat stopping a peer connection the + // same as we do its removal. Should a stopped peer connection be later + // removed, the removal callback will assume the value |false|. + void PeerConnectionStopped(int render_process_id, + int lid, // Renderer-local PeerConnection ID. + base::OnceCallback<void(bool)> reply) override; + + void PeerConnectionSessionIdSet( + int render_process_id, + int lid, + const std::string& session_id, + base::OnceCallback<void(bool)> reply) override; + + // The file's actual path is derived from |base_path| by adding a timestamp, + // the render process ID and the PeerConnection's local ID. + void EnableLocalLogging(const base::FilePath& base_path, + base::OnceCallback<void(bool)> reply) override; + void EnableLocalLogging(const base::FilePath& base_path, + size_t max_file_size_bytes, + base::OnceCallback<void(bool)> reply); + + void DisableLocalLogging(base::OnceCallback<void(bool)> reply) override; + + void OnWebRtcEventLogWrite( + int render_process_id, + int lid, // Renderer-local PeerConnection ID. + const std::string& message, + base::OnceCallback<void(std::pair<bool, bool>)> reply) override; + + // Start logging a peer connection's WebRTC events to a file, which will + // later be uploaded to a remote server. If a reply is provided, it will be + // posted back to BrowserThread::UI with the log-identifier (if successful) + // of the created log or (if unsuccessful) the error message. + // See the comment in WebRtcRemoteEventLogManager::StartRemoteLogging for + // more details. + void StartRemoteLogging( + int render_process_id, + const std::string& session_id, + size_t max_file_size_bytes, + int output_period_ms, + size_t web_app_id, + base::OnceCallback<void(bool, const std::string&, const std::string&)> + reply); + + // Clear WebRTC event logs associated with a given browser context, in a given + // time range (|delete_begin| inclusive, |delete_end| exclusive), then + // post |reply| back to the thread from which the method was originally + // invoked (which can be any thread). + void ClearCacheForBrowserContext( + const content::BrowserContext* browser_context, + const base::Time& delete_begin, + const base::Time& delete_end, + base::OnceClosure reply); + + // Get the logging history (relevant only to remote-bound logs). This includes + // information such as when logs were captured, when they were uploaded, + // and what their ID in the remote server was. + // Must be called on the UI thread. + // The results to the query are posted using |reply| back to the UI thread. + // If |browser_context_id| is not the ID a profile for which remote-bound + // logging is enabled, an empty list is returned. + // The returned vector is sorted by capture time in ascending order. + void GetHistory( + BrowserContextId browser_context_id, + base::OnceCallback<void(const std::vector<UploadList::UploadInfo>&)> + reply); + + // Set (or unset) an observer that will be informed whenever a local log file + // is started/stopped. The observer needs to be able to either run from + // anywhere. If you need the code to run on specific runners or queues, have + // the observer post them there. + // If a reply callback is given, it will be posted back to BrowserThread::UI + // after the observer has been set. + void SetLocalLogsObserver(WebRtcLocalEventLogsObserver* observer, + base::OnceClosure reply); + + // Set (or unset) an observer that will be informed whenever a remote log file + // is started/stopped. Note that this refers to writing these files to disk, + // not for uploading them to the server. + // The observer needs to be able to either run from anywhere. If you need the + // code to run on specific runners or queues, have the observer post + // them there. + // If a reply callback is given, it will be posted back to BrowserThread::UI + // after the observer has been set. + void SetRemoteLogsObserver(WebRtcRemoteEventLogsObserver* observer, + base::OnceClosure reply); + + private: + friend class WebRtcEventLogManagerTestBase; + friend class ::WebRTCInternalsIntegrationBrowserTest; + + using PeerConnectionKey = WebRtcEventLogPeerConnectionKey; + + // This bitmap allows us to track for which clients (local/remote logging) + // we have turned WebRTC event logging on for a given peer connection, so that + // we may turn it off only when the last client no longer needs it. + enum LoggingTarget : unsigned int { + kLocalLogging = 1 << 0, + kRemoteLogging = 1 << 1 + }; + using LoggingTargetBitmap = std::underlying_type<LoggingTarget>::type; + + WebRtcEventLogManager(); + + bool IsRemoteLoggingAllowedForBrowserContext( + content::BrowserContext* browser_context) const; + + // Determines the exact subclass of LogFileWriter::Factory to be used for + // producing remote-bound logs. + std::unique_ptr<LogFileWriter::Factory> CreateRemoteLogFileWriterFactory(); + + // RenderProcessHostObserver implementation. + void RenderProcessExited( + content::RenderProcessHost* host, + const content::ChildProcessTerminationInfo& info) override; + void RenderProcessHostDestroyed(content::RenderProcessHost* host) override; + + // RenderProcessExited() and RenderProcessHostDestroyed() treated similarly + // by this function. + void RenderProcessHostExitedDestroyed(content::RenderProcessHost* host); + + // WebRtcLocalEventLogsObserver implementation: + void OnLocalLogStarted(PeerConnectionKey peer_connection, + const base::FilePath& file_path) override; + void OnLocalLogStopped(PeerConnectionKey peer_connection) override; + + // WebRtcRemoteEventLogsObserver implementation: + void OnRemoteLogStarted(PeerConnectionKey key, + const base::FilePath& file_path, + int output_period_ms) override; + void OnRemoteLogStopped(PeerConnectionKey key) override; + + void OnLoggingTargetStarted(LoggingTarget target, + PeerConnectionKey key, + int output_period_ms); + void OnLoggingTargetStopped(LoggingTarget target, PeerConnectionKey key); + + void StartListeningForPrefChangeForBrowserContext( + content::BrowserContext* browser_context); + void StopListeningForPrefChangeForBrowserContext( + content::BrowserContext* browser_context); + + void OnPrefChange(content::BrowserContext* browser_context); + + // network_connection_tracker() is not available during instantiation; + // we get it when the first profile is loaded, which is also the earliest + // time when it could be needed. + // The LogFileWriter::Factory is similarly deferred, but for a different + // reason - it makes it easier to allow unit tests to inject their own. + // OnFirstBrowserContextLoaded() is on the UI thread. + // OnFirstBrowserContextLoadedInternal() is the task sent to |task_runner_|. + void OnFirstBrowserContextLoaded(); + void OnFirstBrowserContextLoadedInternal( + network::NetworkConnectionTracker* network_connection_tracker, + std::unique_ptr<LogFileWriter::Factory> log_file_writer_factory); + + void EnableRemoteBoundLoggingForBrowserContext( + BrowserContextId browser_context_id, + const base::FilePath& browser_context_dir, + base::OnceClosure reply); + + void DisableRemoteBoundLoggingForBrowserContext( + BrowserContextId browser_context_id, + base::OnceClosure reply); + + void RemovePendingRemoteBoundLogsForNotEnabledBrowserContext( + BrowserContextId browser_context_id, + const base::FilePath& browser_context_dir, + base::OnceClosure reply); + + void PeerConnectionAddedInternal(PeerConnectionKey key, + base::OnceCallback<void(bool)> reply); + void PeerConnectionRemovedInternal(PeerConnectionKey key, + base::OnceCallback<void(bool)> reply); + + void PeerConnectionSessionIdSetInternal(PeerConnectionKey key, + const std::string& session_id, + base::OnceCallback<void(bool)> reply); + + void EnableLocalLoggingInternal(const base::FilePath& base_path, + size_t max_file_size_bytes, + base::OnceCallback<void(bool)> reply); + void DisableLocalLoggingInternal(base::OnceCallback<void(bool)> reply); + + void OnWebRtcEventLogWriteInternal( + PeerConnectionKey key, + const std::string& message, + base::OnceCallback<void(std::pair<bool, bool>)> reply); + + void StartRemoteLoggingInternal( + int render_process_id, + BrowserContextId browser_context_id, + const std::string& session_id, + const base::FilePath& browser_context_dir, + size_t max_file_size_bytes, + int output_period_ms, + size_t web_app_id, + base::OnceCallback<void(bool, const std::string&, const std::string&)> + reply); + + void ClearCacheForBrowserContextInternal(BrowserContextId browser_context_id, + const base::Time& delete_begin, + const base::Time& delete_end); + + void GetHistoryInternal( + BrowserContextId browser_context_id, + base::OnceCallback<void(const std::vector<UploadList::UploadInfo>&)> + reply); + + void RenderProcessExitedInternal(int render_process_id); + + void SetLocalLogsObserverInternal(WebRtcLocalEventLogsObserver* observer, + base::OnceClosure reply); + + void SetRemoteLogsObserverInternal(WebRtcRemoteEventLogsObserver* observer, + base::OnceClosure reply); + + // Injects a fake clock, to be used by tests. For example, this could be + // used to inject a frozen clock, thereby allowing unit tests to know what a + // local log's filename would end up being. + void SetClockForTesting(base::Clock* clock, base::OnceClosure reply); + + // Injects a PeerConnectionTrackerProxy for testing. The normal tracker proxy + // is used to communicate back to WebRTC whether event logging is desired for + // a given peer connection. Using this function, those indications can be + // intercepted by a unit test. + void SetPeerConnectionTrackerProxyForTesting( + std::unique_ptr<PeerConnectionTrackerProxy> pc_tracker_proxy, + base::OnceClosure reply); + + // Injects a fake uploader, to be used by unit tests. + void SetWebRtcEventLogUploaderFactoryForTesting( + std::unique_ptr<WebRtcEventLogUploader::Factory> uploader_factory, + base::OnceClosure reply); + + // Sets a LogFileWriter factory for remote-bound files. + // Only usable in tests. + // Must be called before the first browser context is enabled. + // Effective immediately. + void SetRemoteLogFileWriterFactoryForTesting( + std::unique_ptr<LogFileWriter::Factory> factory); + + // It is not always feasible to check in unit tests that uploads do not occur + // at a certain time, because that's (sometimes) racy with the event that + // suppresses the upload. We therefore allow unit tests to glimpse into the + // black box and verify that the box is aware that it should not upload. + void UploadConditionsHoldForTesting(base::OnceCallback<void(bool)> callback); + + // This allows unit tests that do not wish to change the task runner to still + // check when certain operations are finished. + // TODO(crbug.com/775415): Remove this and use PostNullTaskForTesting instead. + scoped_refptr<base::SequencedTaskRunner>& GetTaskRunnerForTesting(); + + void PostNullTaskForTesting(base::OnceClosure reply); + + // Documented in WebRtcRemoteEventLogManager. + void ShutDownForTesting(base::OnceClosure reply); + + static WebRtcEventLogManager* g_webrtc_event_log_manager; + + // The main logic will run sequentially on this runner, on which blocking + // tasks are allowed. + scoped_refptr<base::SequencedTaskRunner> task_runner_; + + // Indicates whether remote-bound logging is generally allowed, although + // possibly not for all profiles. This makes it possible for remote-bound to + // be disabled through Finch. + // TODO(crbug.com/775415): Remove this kill-switch. + const bool remote_logging_feature_enabled_; + + // Observer which will be informed whenever a local log file is started or + // stopped. Its callbacks are called synchronously from |task_runner_|, + // so the observer needs to be able to either run from any (sequenced) runner. + WebRtcLocalEventLogsObserver* local_logs_observer_; + + // Observer which will be informed whenever a remote log file is started or + // stopped. Its callbacks are called synchronously from |task_runner_|, + // so the observer needs to be able to either run from any (sequenced) runner. + WebRtcRemoteEventLogsObserver* remote_logs_observer_; + + // Manages local-bound logs - logs stored on the local filesystem when + // logging has been explicitly enabled by the user. + WebRtcLocalEventLogManager local_logs_manager_; + + // Manages remote-bound logs - logs which will be sent to a remote server. + // This is only possible when the appropriate Chrome policy is configured. + WebRtcRemoteEventLogManager remote_logs_manager_; + + // Each loaded BrowserContext is mapped to a PrefChangeRegistrar, which keeps + // us informed about preference changes, thereby allowing as to support + // dynamic refresh. + std::map<BrowserContextId, PrefChangeRegistrar> pref_change_registrars_; + + // This keeps track of which peer connections have event logging turned on + // in WebRTC, and for which client(s). + std::map<PeerConnectionKey, LoggingTargetBitmap> + peer_connections_with_event_logging_enabled_in_webrtc_; + + // The set of RenderProcessHosts with which the manager is registered for + // observation. Allows us to register for each RPH only once, and get notified + // when it exits (cleanly or due to a crash). + // This object is only to be accessed on the UI thread. + base::flat_set<content::RenderProcessHost*> observed_render_process_hosts_; + + // In production, this holds a small object that just tells WebRTC (via + // PeerConnectionTracker) to start/stop producing event logs for a specific + // peer connection. In (relevant) unit tests, a mock will be injected. + std::unique_ptr<PeerConnectionTrackerProxy> pc_tracker_proxy_; + + // The globals network_connection_tracker() and system_request_context() are + // sent down to |remote_logs_manager_| with the first enabled browser context. + // This member must only be accessed on the UI thread. + bool first_browser_context_initializations_done_; + + // May only be set for tests, in which case, it will be passed to + // |remote_logs_manager_| when (and if) produced. + std::unique_ptr<LogFileWriter::Factory> + remote_log_file_writer_factory_for_testing_; + + DISALLOW_COPY_AND_ASSIGN(WebRtcEventLogManager); +}; + +} // namespace webrtc_event_logging + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common.cc new file mode 100644 index 00000000000..59ad7a2b45e --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common.cc @@ -0,0 +1,1013 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/webrtc_event_log_manager_common.h" + +#include <cctype> +#include <limits> + +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/memory/scoped_refptr.h" +#include "base/metrics/histogram_functions.h" +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/strings/stringprintf.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/unguessable_token.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_process_host.h" +#include "third_party/zlib/zlib.h" + +namespace webrtc_event_logging { + +using BrowserContextId = WebRtcEventLogPeerConnectionKey::BrowserContextId; + +const size_t kWebRtcEventLogManagerUnlimitedFileSize = 0; + +const size_t kWebRtcEventLogIdLength = 32; + +// Be careful not to change these without updating the number of characters +// reserved in the filename. See kWebAppIdLength. +const size_t kMinWebRtcEventLogWebAppId = 1; +const size_t kMaxWebRtcEventLogWebAppId = 99; + +// Sentinel value for an invalid web-app ID. +const size_t kInvalidWebRtcEventLogWebAppId = 0; +static_assert(kInvalidWebRtcEventLogWebAppId < kMinWebRtcEventLogWebAppId || + kInvalidWebRtcEventLogWebAppId > kMaxWebRtcEventLogWebAppId, + "Sentinel value must be distinct from legal values."); + +const char kRemoteBoundWebRtcEventLogFileNamePrefix[] = "webrtc_event_log"; + +// Important! These values may be relied on by web-apps. Do not change. +const char kStartRemoteLoggingFailureAlreadyLogging[] = "Already logging."; +const char kStartRemoteLoggingFailureDeadRenderProcessHost[] = + "RPH already dead."; +const char kStartRemoteLoggingFailureFeatureDisabled[] = "Feature disabled."; +const char kStartRemoteLoggingFailureFileCreationError[] = + "Could not create file."; +const char kStartRemoteLoggingFailureFilePathUsedHistory[] = + "Used history file path."; +const char kStartRemoteLoggingFailureFilePathUsedLog[] = "Used log file path."; +const char kStartRemoteLoggingFailureIllegalWebAppId[] = "Illegal web-app ID."; +const char kStartRemoteLoggingFailureLoggingDisabledBrowserContext[] = + "Disabled for browser context."; +const char kStartRemoteLoggingFailureMaxSizeTooLarge[] = + "Excessively large max log size."; +const char kStartRemoteLoggingFailureMaxSizeTooSmall[] = "Max size too small."; +const char kStartRemoteLoggingFailureNoAdditionalActiveLogsAllowed[] = + "No additional active logs allowed."; +const char kStartRemoteLoggingFailureOutputPeriodMsTooLarge[] = + "Excessively large output period (ms)."; +const char kStartRemoteLoggingFailureUnknownOrInactivePeerConnection[] = + "Unknown or inactive peer connection."; +const char kStartRemoteLoggingFailureUnlimitedSizeDisallowed[] = + "Unlimited size disallowed."; + +const BrowserContextId kNullBrowserContextId = + reinterpret_cast<BrowserContextId>(nullptr); + +void UmaRecordWebRtcEventLoggingApi(WebRtcEventLoggingApiUma result) { + base::UmaHistogramEnumeration("WebRtcEventLogging.Api", result); +} + +void UmaRecordWebRtcEventLoggingUpload(WebRtcEventLoggingUploadUma result) { + base::UmaHistogramEnumeration("WebRtcEventLogging.Upload", result); +} + +void UmaRecordWebRtcEventLoggingNetErrorType(int net_error) { + base::UmaHistogramSparse("WebRtcEventLogging.NetError", net_error); +} + +namespace { + +constexpr int kDefaultMemLevel = 8; + +constexpr size_t kGzipHeaderBytes = 15; +constexpr size_t kGzipFooterBytes = 10; + +constexpr size_t kWebAppIdLength = 2; + +// Tracks budget over a resource (such as bytes allowed in a file, etc.). +// Allows an unlimited budget. +class Budget { + public: + // If !max.has_value(), the budget is unlimited. + explicit Budget(base::Optional<size_t> max) : max_(max), current_(0) {} + + // Check whether the budget allows consuming an additional |consumed| of + // the resource. + bool ConsumeAllowed(size_t consumed) const { + if (!max_.has_value()) { + return true; + } + + DCHECK_LE(current_, max_.value()); + + const size_t after_consumption = current_ + consumed; + + if (after_consumption < current_) { + return false; // Wrap-around. + } else if (after_consumption > max_.value()) { + return false; // Budget exceeded. + } else { + return true; + } + } + + // Checks whether the budget has been completely used up. + bool Exhausted() const { return !ConsumeAllowed(0); } + + // Consume an additional |consumed| of the resource. + void Consume(size_t consumed) { + DCHECK(ConsumeAllowed(consumed)); + current_ += consumed; + } + + private: + const base::Optional<size_t> max_; + size_t current_; +}; + +// Writes a log to a file while observing a maximum size. +class BaseLogFileWriter : public LogFileWriter { + public: + // If !max_file_size_bytes.has_value(), an unlimited writer is created. + // If it has a value, it must be at least MinFileSizeBytes(). + BaseLogFileWriter(const base::FilePath& path, + base::Optional<size_t> max_file_size_bytes); + + ~BaseLogFileWriter() override; + + bool Init() override; + + const base::FilePath& path() const override; + + bool MaxSizeReached() const override; + + bool Write(const std::string& input) override; + + bool Close() override; + + void Delete() override; + + protected: + // * Logs are created PRE_INIT. + // * If Init() is successful (potentially writing some header to the log), + // the log becomes ACTIVE. + // * Any error puts the log into an unrecoverable ERRORED state. When an + // errored file is Close()-ed, it is deleted. + // * If Write() is ever denied because of budget constraintss, the file + // becomes FULL. Only metadata is then allowed (subject to its own budget). + // * Closing an ACTIVE or FULL file puts it into CLOSED, at which point the + // file may be used. (Note that closing itself might also yield an error, + // which would put the file into ERRORED, then deleted.) + // * Closed files may be DELETED. + enum class State { PRE_INIT, ACTIVE, FULL, CLOSED, ERRORED, DELETED }; + + // Setter/getter for |state_|. + void SetState(State state); + State state() const { return state_; } + + // Checks whether the budget allows writing an additional |bytes|. + bool WithinBudget(size_t bytes) const; + + // Writes |input| to the file. + // May only be called on ACTIVE or FULL files (for FULL files, only metadata + // such as compression footers, etc., may be written; the budget must still + // be respected). + // It's up to the caller to respect the budget; this will DCHECK on it. + // Returns |true| if writing was successful. |false| indicates an + // unrecoverable error; the file must be discarded. + bool WriteInternal(const std::string& input, bool metadata); + + // Finalizes the file (writes metadata such as compression footer, if any). + // Reports whether the file was successfully finalized. Those which weren't + // should be discarded. + virtual bool Finalize(); + + private: + scoped_refptr<base::SequencedTaskRunner> task_runner_; + const base::FilePath path_; + base::File file_; // Populated by Init(). + State state_; + Budget budget_; +}; + +BaseLogFileWriter::BaseLogFileWriter(const base::FilePath& path, + base::Optional<size_t> max_file_size_bytes) + : task_runner_(base::SequencedTaskRunnerHandle::Get()), + path_(path), + state_(State::PRE_INIT), + budget_(max_file_size_bytes) {} + +BaseLogFileWriter::~BaseLogFileWriter() { + if (!task_runner_->RunsTasksInCurrentSequence()) { + // Chrome shut-down. The original task_runner_ is no longer running, so + // no risk of concurrent access or races. + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + task_runner_ = base::SequencedTaskRunnerHandle::Get(); + } + + if (state() != State::CLOSED && state() != State::DELETED) { + Close(); + } +} + +bool BaseLogFileWriter::Init() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_EQ(state(), State::PRE_INIT); + + // TODO(crbug.com/775415): Use a temporary filename which will indicate + // incompletion, and rename to something that is eligible for upload only + // on an orderly and successful Close(). + + // Attempt to create the file. + constexpr int file_flags = base::File::FLAG_CREATE | base::File::FLAG_WRITE | + base::File::FLAG_EXCLUSIVE_WRITE; + file_.Initialize(path_, file_flags); + if (!file_.IsValid() || !file_.created()) { + LOG(WARNING) << "Couldn't create remote-bound WebRTC event log file."; + if (!base::DeleteFile(path_, /*recursive=*/false)) { + LOG(ERROR) << "Failed to delete " << path_ << "."; + } + SetState(State::ERRORED); + return false; + } + + SetState(State::ACTIVE); + + return true; +} + +const base::FilePath& BaseLogFileWriter::path() const { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + return path_; +} + +bool BaseLogFileWriter::MaxSizeReached() const { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_EQ(state(), State::ACTIVE); + return !WithinBudget(1); +} + +bool BaseLogFileWriter::Write(const std::string& input) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_EQ(state(), State::ACTIVE); + DCHECK(!MaxSizeReached()); + + if (input.empty()) { + return true; + } + + if (!WithinBudget(input.length())) { + SetState(State::FULL); + return false; + } + + const bool did_write = WriteInternal(input, /*metadata=*/false); + if (!did_write) { + SetState(State::ERRORED); + } + return did_write; +} + +bool BaseLogFileWriter::Close() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_NE(state(), State::CLOSED); + DCHECK_NE(state(), State::DELETED); + + const bool result = ((state() != State::ERRORED) && Finalize()); + + if (result) { + file_.Flush(); + file_.Close(); + SetState(State::CLOSED); + } else { + Delete(); // Changes the state to DELETED. + } + + return result; +} + +void BaseLogFileWriter::Delete() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_NE(state(), State::DELETED); + + // The file should be closed before deletion. However, we do not want to go + // through Finalize() and any potential production of a compression footer, + // etc., since we'll be discarding the file anyway. + if (state() != State::CLOSED) { + file_.Close(); + } + + if (!base::DeleteFile(path_, /*recursive=*/false)) { + LOG(ERROR) << "Failed to delete " << path_ << "."; + } + + SetState(State::DELETED); +} + +void BaseLogFileWriter::SetState(State state) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + state_ = state; +} + +bool BaseLogFileWriter::WithinBudget(size_t bytes) const { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + return budget_.ConsumeAllowed(bytes); +} + +bool BaseLogFileWriter::WriteInternal(const std::string& input, bool metadata) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(state() == State::ACTIVE || (state() == State::FULL && metadata)); + DCHECK(WithinBudget(input.length())); + + // base::File's interface does not allow writing more than + // numeric_limits<int>::max() bytes at a time. + DCHECK_LE(input.length(), + static_cast<size_t>(std::numeric_limits<int>::max())); + const int input_len = static_cast<int>(input.length()); + + int written = file_.WriteAtCurrentPos(input.c_str(), input_len); + if (written != input_len) { + LOG(WARNING) << "WebRTC event log couldn't be written to the " + "locally stored file in its entirety."; + return false; + } + + budget_.Consume(static_cast<size_t>(written)); + + return true; +} + +bool BaseLogFileWriter::Finalize() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_NE(state(), State::CLOSED); + DCHECK_NE(state(), State::DELETED); + DCHECK_NE(state(), State::ERRORED); + return true; +} + +// Writes a GZIP-compressed log to a file while observing a maximum size. +class GzippedLogFileWriter : public BaseLogFileWriter { + public: + GzippedLogFileWriter(const base::FilePath& path, + base::Optional<size_t> max_file_size_bytes, + std::unique_ptr<LogCompressor> compressor); + + ~GzippedLogFileWriter() override = default; + + bool Init() override; + + bool MaxSizeReached() const override; + + bool Write(const std::string& input) override; + + protected: + bool Finalize() override; + + private: + std::unique_ptr<LogCompressor> compressor_; +}; + +GzippedLogFileWriter::GzippedLogFileWriter( + const base::FilePath& path, + base::Optional<size_t> max_file_size_bytes, + std::unique_ptr<LogCompressor> compressor) + : BaseLogFileWriter(path, max_file_size_bytes), + compressor_(std::move(compressor)) { + // Factory validates size before instantiation. + DCHECK(!max_file_size_bytes.has_value() || + max_file_size_bytes.value() >= kGzipOverheadBytes); +} + +bool GzippedLogFileWriter::Init() { + if (!BaseLogFileWriter::Init()) { + // Super-class should SetState on its own. + return false; + } + + std::string header; + compressor_->CreateHeader(&header); + + const bool result = WriteInternal(header, /*metadata=*/true); + if (!result) { + SetState(State::ERRORED); + } + + return result; +} + +bool GzippedLogFileWriter::MaxSizeReached() const { + DCHECK_EQ(state(), State::ACTIVE); + + // Note that the overhead used (footer only) assumes state() is State::ACTIVE, + // as DCHECKed above. + return !WithinBudget(1 + kGzipFooterBytes); +} + +bool GzippedLogFileWriter::Write(const std::string& input) { + DCHECK_EQ(state(), State::ACTIVE); + DCHECK(!MaxSizeReached()); + + if (input.empty()) { + return true; + } + + std::string compressed_input; + const auto result = compressor_->Compress(input, &compressed_input); + + switch (result) { + case LogCompressor::Result::OK: { + // |compressor_| guarantees |compressed_input| is within-budget. + bool did_write = WriteInternal(compressed_input, /*metadata=*/false); + if (!did_write) { + SetState(State::ERRORED); + } + return did_write; + } + case LogCompressor::Result::DISALLOWED: { + SetState(State::FULL); + return false; + } + case LogCompressor::Result::ERROR_ENCOUNTERED: { + SetState(State::ERRORED); + return false; + } + } + + NOTREACHED(); + return false; // Appease compiler. +} + +bool GzippedLogFileWriter::Finalize() { + DCHECK_NE(state(), State::CLOSED); + DCHECK_NE(state(), State::DELETED); + DCHECK_NE(state(), State::ERRORED); + + std::string footer; + if (!compressor_->CreateFooter(&footer)) { + LOG(WARNING) << "Compression footer could not be produced."; + SetState(State::ERRORED); + return false; + } + + // |compressor_| guarantees |footer| is within-budget. + if (!WriteInternal(footer, /*metadata=*/true)) { + LOG(WARNING) << "Footer could not be written."; + SetState(State::ERRORED); + return false; + } + + return true; +} + +// Concrete implementation of LogCompressor using GZIP. +class GzipLogCompressor : public LogCompressor { + public: + GzipLogCompressor( + base::Optional<size_t> max_size_bytes, + std::unique_ptr<CompressedSizeEstimator> compressed_size_estimator); + + ~GzipLogCompressor() override; + + void CreateHeader(std::string* output) override; + + Result Compress(const std::string& input, std::string* output) override; + + bool CreateFooter(std::string* output) override; + + private: + // * A compressed log starts out empty (PRE_HEADER). + // * Once the header is produced, the stream is ACTIVE. + // * If it is ever detected that compressing the next input would exceed the + // budget, that input is NOT compressed, and the state becomes FULL, from + // which only writing the footer or discarding the file are allowed. + // * Writing the footer is allowed on an ACTIVE or FULL stream. Then, the + // stream is effectively closed. + // * Any error puts the stream into ERRORED. An errored stream can only + // be discarded. + enum class State { PRE_HEADER, ACTIVE, FULL, POST_FOOTER, ERRORED }; + + // Returns the budget left after reserving the GZIP overhead. + // Optionals without a value, both in the parameters as well as in the + // return value of the function, signal an unlimited amount. + static base::Optional<size_t> SizeAfterOverheadReservation( + base::Optional<size_t> max_size_bytes); + + // Compresses |input| into |output|, while observing the budget (unless + // !budgeted). If |last|, also closes the stream. + Result CompressInternal(const std::string& input, + std::string* output, + bool budgeted, + bool last); + + // Compresses the input data already in |stream_| into |output|. + bool Deflate(int flush, std::string* output); + + State state_; + Budget budget_; + std::unique_ptr<CompressedSizeEstimator> compressed_size_estimator_; + z_stream stream_; +}; + +GzipLogCompressor::GzipLogCompressor( + base::Optional<size_t> max_size_bytes, + std::unique_ptr<CompressedSizeEstimator> compressed_size_estimator) + : state_(State::PRE_HEADER), + budget_(SizeAfterOverheadReservation(max_size_bytes)), + compressed_size_estimator_(std::move(compressed_size_estimator)) { + memset(&stream_, 0, sizeof(z_stream)); + // Using (MAX_WBITS + 16) triggers the creation of a GZIP header. + const int result = + deflateInit2(&stream_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS + 16, + kDefaultMemLevel, Z_DEFAULT_STRATEGY); + DCHECK_EQ(result, Z_OK); +} + +GzipLogCompressor::~GzipLogCompressor() { + const int result = deflateEnd(&stream_); + // Z_DATA_ERROR reports that the stream was not properly terminated, + // but nevertheless correctly released. That happens when we don't + // write the footer. + DCHECK(result == Z_OK || + (result == Z_DATA_ERROR && state_ != State::POST_FOOTER)); +} + +void GzipLogCompressor::CreateHeader(std::string* output) { + DCHECK(output); + DCHECK(output->empty()); + DCHECK_EQ(state_, State::PRE_HEADER); + + const Result result = CompressInternal(std::string(), output, + /*budgeted=*/false, /*last=*/false); + DCHECK_EQ(result, Result::OK); + DCHECK_EQ(output->size(), kGzipHeaderBytes); + + state_ = State::ACTIVE; +} + +LogCompressor::Result GzipLogCompressor::Compress(const std::string& input, + std::string* output) { + DCHECK_EQ(state_, State::ACTIVE); + + if (input.empty()) { + return Result::OK; + } + + const auto result = + CompressInternal(input, output, /*budgeted=*/true, /*last=*/false); + + switch (result) { + case Result::OK: + return result; + case Result::DISALLOWED: + state_ = State::FULL; + return result; + case Result::ERROR_ENCOUNTERED: + state_ = State::ERRORED; + return result; + } + + NOTREACHED(); + return Result::ERROR_ENCOUNTERED; // Appease compiler. +} + +bool GzipLogCompressor::CreateFooter(std::string* output) { + DCHECK(output); + DCHECK(output->empty()); + DCHECK(state_ == State::ACTIVE || state_ == State::FULL); + + const Result result = CompressInternal(std::string(), output, + /*budgeted=*/false, /*last=*/true); + if (result != Result::OK) { // !budgeted -> Result::DISALLOWED impossible. + DCHECK_EQ(result, Result::ERROR_ENCOUNTERED); + // An error message was logged by CompressInternal(). + state_ = State::ERRORED; + return false; + } + + if (output->length() != kGzipFooterBytes) { + LOG(ERROR) << "Incorrect footer size (" << output->length() << ")."; + state_ = State::ERRORED; + return false; + } + + state_ = State::POST_FOOTER; + + return true; +} + +base::Optional<size_t> GzipLogCompressor::SizeAfterOverheadReservation( + base::Optional<size_t> max_size_bytes) { + if (!max_size_bytes.has_value()) { + return base::Optional<size_t>(); + } else { + DCHECK_GE(max_size_bytes.value(), kGzipHeaderBytes + kGzipFooterBytes); + return max_size_bytes.value() - (kGzipHeaderBytes + kGzipFooterBytes); + } +} + +LogCompressor::Result GzipLogCompressor::CompressInternal( + const std::string& input, + std::string* output, + bool budgeted, + bool last) { + DCHECK(output); + DCHECK(output->empty()); + DCHECK(state_ == State::PRE_HEADER || state_ == State::ACTIVE || + (!budgeted && state_ == State::FULL)); + + // Avoid writing to |output| unless the return value is OK. + std::string temp_output; + + if (budgeted) { + const size_t estimated_compressed_size = + compressed_size_estimator_->EstimateCompressedSize(input); + if (!budget_.ConsumeAllowed(estimated_compressed_size)) { + return Result::DISALLOWED; + } + } + + if (last) { + DCHECK(input.empty()); + stream_.next_in = nullptr; + } else { + stream_.next_in = reinterpret_cast<z_const Bytef*>(input.c_str()); + } + + DCHECK_LE(input.length(), + static_cast<size_t>(std::numeric_limits<uInt>::max())); + stream_.avail_in = static_cast<uInt>(input.length()); + + const bool result = Deflate(last ? Z_FINISH : Z_SYNC_FLUSH, &temp_output); + + stream_.next_in = nullptr; // Avoid dangling pointers. + + if (!result) { + // An error message was logged by Deflate(). + return Result::ERROR_ENCOUNTERED; + } + + if (budgeted) { + if (!budget_.ConsumeAllowed(temp_output.length())) { + LOG(WARNING) << "Compressed size was above estimate and unexpectedly " + "exceeded the budget."; + return Result::ERROR_ENCOUNTERED; + } + budget_.Consume(temp_output.length()); + } + + std::swap(*output, temp_output); + return Result::OK; +} + +bool GzipLogCompressor::Deflate(int flush, std::string* output) { + DCHECK((flush != Z_FINISH && stream_.next_in != nullptr) || + (flush == Z_FINISH && stream_.next_in == nullptr)); + DCHECK(output->empty()); + + bool success = true; // Result of this method. + int z_result; // Result of the zlib function. + + size_t total_compressed_size = 0; + + do { + // Allocate some additional buffer. + constexpr uInt kCompressionBuffer = 4 * 1024; + output->resize(total_compressed_size + kCompressionBuffer); + + // This iteration should write directly beyond previous iterations' last + // written byte. + stream_.next_out = + reinterpret_cast<uint8_t*>(&((*output)[total_compressed_size])); + stream_.avail_out = kCompressionBuffer; + + z_result = deflate(&stream_, flush); + + DCHECK_GE(kCompressionBuffer, stream_.avail_out); + const size_t compressed_size = kCompressionBuffer - stream_.avail_out; + + if (flush != Z_FINISH) { + if (z_result != Z_OK) { + LOG(ERROR) << "Compression failed (" << z_result << ")."; + success = false; + break; + } + } else { // flush == Z_FINISH + // End of the stream; we expect the footer to be exactly the size which + // we've set aside for it. + if (z_result != Z_STREAM_END || compressed_size != kGzipFooterBytes) { + LOG(ERROR) << "Compression failed (" << z_result << ", " + << compressed_size << ")."; + success = false; + break; + } + } + + total_compressed_size += compressed_size; + } while (stream_.avail_out == 0 && z_result != Z_STREAM_END); + + stream_.next_out = nullptr; // Avoid dangling pointers. + + if (success) { + output->resize(total_compressed_size); + } else { + output->clear(); + } + + return success; +} + +// Given a string with a textual representation of a web-app ID, return the +// ID in integer form. If the textual representation does not name a valid +// web-app ID, return kInvalidWebRtcEventLogWebAppId. +size_t ExtractWebAppId(base::StringPiece str) { + DCHECK_EQ(str.length(), kWebAppIdLength); + + // Avoid leading '+', etc. + for (size_t i = 0; i < str.length(); i++) { + if (!std::isdigit(str[i])) { + return kInvalidWebRtcEventLogWebAppId; + } + } + + size_t result; + if (!base::StringToSizeT(str, &result) || + result < kMinWebRtcEventLogWebAppId || + result > kMaxWebRtcEventLogWebAppId) { + return kInvalidWebRtcEventLogWebAppId; + } + return result; +} + +} // namespace + +const size_t kGzipOverheadBytes = kGzipHeaderBytes + kGzipFooterBytes; + +const base::FilePath::CharType kWebRtcEventLogUncompressedExtension[] = + FILE_PATH_LITERAL("log"); +const base::FilePath::CharType kWebRtcEventLogGzippedExtension[] = + FILE_PATH_LITERAL("log.gz"); +const base::FilePath::CharType kWebRtcEventLogHistoryExtension[] = + FILE_PATH_LITERAL("hist"); + +size_t BaseLogFileWriterFactory::MinFileSizeBytes() const { + // No overhead incurred; data written straight to the file without metadata. + return 0; +} + +base::FilePath::StringPieceType BaseLogFileWriterFactory::Extension() const { + return kWebRtcEventLogUncompressedExtension; +} + +std::unique_ptr<LogFileWriter> BaseLogFileWriterFactory::Create( + const base::FilePath& path, + base::Optional<size_t> max_file_size_bytes) const { + if (max_file_size_bytes.has_value() && + max_file_size_bytes.value() < MinFileSizeBytes()) { + LOG(WARNING) << "Max size (" << max_file_size_bytes.value() + << ") below minimum size (" << MinFileSizeBytes() << ")."; + return nullptr; + } + + auto result = std::make_unique<BaseLogFileWriter>(path, max_file_size_bytes); + + if (!result->Init()) { + // Error logged by Init. + result.reset(); // Destructor deletes errored files. + } + + return result; +} + +std::unique_ptr<CompressedSizeEstimator> +DefaultGzippedSizeEstimator::Factory::Create() const { + return std::make_unique<DefaultGzippedSizeEstimator>(); +} + +size_t DefaultGzippedSizeEstimator::EstimateCompressedSize( + const std::string& input) const { + // This estimation is not tight. Since we expect to produce logs of + // several MBs, overshooting the estimation by one KB should be + // very safe and still relatively efficient. + constexpr size_t kOverheadOverUncompressedSizeBytes = 1000; + return input.length() + kOverheadOverUncompressedSizeBytes; +} + +GzipLogCompressorFactory::GzipLogCompressorFactory( + std::unique_ptr<CompressedSizeEstimator::Factory> estimator_factory) + : estimator_factory_(std::move(estimator_factory)) {} + +GzipLogCompressorFactory::~GzipLogCompressorFactory() = default; + +size_t GzipLogCompressorFactory::MinSizeBytes() const { + return kGzipOverheadBytes; +} + +std::unique_ptr<LogCompressor> GzipLogCompressorFactory::Create( + base::Optional<size_t> max_size_bytes) const { + if (max_size_bytes.has_value() && max_size_bytes.value() < MinSizeBytes()) { + LOG(WARNING) << "Max size (" << max_size_bytes.value() + << ") below minimum size (" << MinSizeBytes() << ")."; + return nullptr; + } + return std::make_unique<GzipLogCompressor>(max_size_bytes, + estimator_factory_->Create()); +} + +GzippedLogFileWriterFactory::GzippedLogFileWriterFactory( + std::unique_ptr<GzipLogCompressorFactory> gzip_compressor_factory) + : gzip_compressor_factory_(std::move(gzip_compressor_factory)) {} + +GzippedLogFileWriterFactory::~GzippedLogFileWriterFactory() = default; + +size_t GzippedLogFileWriterFactory::MinFileSizeBytes() const { + // Only the compression's own overhead is incurred. + return gzip_compressor_factory_->MinSizeBytes(); +} + +base::FilePath::StringPieceType GzippedLogFileWriterFactory::Extension() const { + return kWebRtcEventLogGzippedExtension; +} + +std::unique_ptr<LogFileWriter> GzippedLogFileWriterFactory::Create( + const base::FilePath& path, + base::Optional<size_t> max_file_size_bytes) const { + if (max_file_size_bytes.has_value() && + max_file_size_bytes.value() < MinFileSizeBytes()) { + LOG(WARNING) << "Size below allowed minimum."; + return nullptr; + } + + auto gzip_compressor = gzip_compressor_factory_->Create(max_file_size_bytes); + if (!gzip_compressor) { + // The factory itself will have logged an error. + return nullptr; + } + + auto result = std::make_unique<GzippedLogFileWriter>( + path, max_file_size_bytes, std::move(gzip_compressor)); + + if (!result->Init()) { + // Error logged by Init. + result.reset(); // Destructor deletes errored files. + } + + return result; +} + +// Create a random identifier of 32 hexadecimal (uppercase) characters. +std::string CreateWebRtcEventLogId() { + // UnguessableToken's interface makes no promisses over case. We therefore + // convert, even if the current implementation does not require it. + std::string log_id = + base::ToUpperASCII(base::UnguessableToken::Create().ToString()); + DCHECK_EQ(log_id.size(), kWebRtcEventLogIdLength); + DCHECK_EQ(log_id.find_first_not_of("0123456789ABCDEF"), std::string::npos); + return log_id; +} + +BrowserContextId GetBrowserContextId( + const content::BrowserContext* browser_context) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + return reinterpret_cast<BrowserContextId>(browser_context); +} + +BrowserContextId GetBrowserContextId(int render_process_id) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + content::RenderProcessHost* const host = + content::RenderProcessHost::FromID(render_process_id); + + content::BrowserContext* const browser_context = + host ? host->GetBrowserContext() : nullptr; + + return GetBrowserContextId(browser_context); +} + +base::FilePath GetRemoteBoundWebRtcEventLogsDir( + const base::FilePath& browser_context_dir) { + const base::FilePath::CharType kRemoteBoundLogSubDirectory[] = + FILE_PATH_LITERAL("webrtc_event_logs"); + return browser_context_dir.Append(kRemoteBoundLogSubDirectory); +} + +base::FilePath WebRtcEventLogPath( + const base::FilePath& remote_logs_dir, + const std::string& log_id, + size_t web_app_id, + const base::FilePath::StringPieceType& extension) { + DCHECK_GE(web_app_id, kMinWebRtcEventLogWebAppId); + DCHECK_LE(web_app_id, kMaxWebRtcEventLogWebAppId); + + static_assert(kWebAppIdLength == 2u, "Fix the code below."); + const std::string web_app_id_str = base::StringPrintf("%02zu", web_app_id); + DCHECK_EQ(web_app_id_str.length(), kWebAppIdLength); + + const std::string filename = + std::string(kRemoteBoundWebRtcEventLogFileNamePrefix) + "_" + + web_app_id_str + "_" + log_id; + + return remote_logs_dir.AppendASCII(filename).AddExtension(extension); +} + +bool IsValidRemoteBoundLogFilename(const std::string& filename) { + // The -1 is because of the implict \0. + const size_t kPrefixLength = + base::size(kRemoteBoundWebRtcEventLogFileNamePrefix) - 1; + + // [prefix]_[web_app_id]_[log_id] + const size_t expected_length = + kPrefixLength + 1 + kWebAppIdLength + 1 + kWebRtcEventLogIdLength; + if (filename.length() != expected_length) { + return false; + } + + size_t index = 0; + + // Expect prefix. + if (filename.find(kRemoteBoundWebRtcEventLogFileNamePrefix) != index) { + return false; + } + index += kPrefixLength; + + // Expect underscore between prefix and web-app ID. + if (filename[index] != '_') { + return false; + } + index += 1; + + // Expect web-app-ID. + const size_t web_app_id = + ExtractWebAppId(base::StringPiece(&filename[index], kWebAppIdLength)); + if (web_app_id == kInvalidWebRtcEventLogWebAppId) { + return false; + } + index += kWebAppIdLength; + + // Expect underscore between web-app ID and log ID. + if (filename[index] != '_') { + return false; + } + index += 1; + + // Expect log ID. + const std::string log_id = filename.substr(index); + DCHECK_EQ(log_id.length(), kWebRtcEventLogIdLength); + const char* const log_id_chars = "0123456789ABCDEF"; + if (filename.find_first_not_of(log_id_chars, index) != std::string::npos) { + return false; + } + + return true; +} + +bool IsValidRemoteBoundLogFilePath(const base::FilePath& path) { + const std::string filename = path.BaseName().RemoveExtension().MaybeAsASCII(); + return IsValidRemoteBoundLogFilename(filename); +} + +base::FilePath GetWebRtcEventLogHistoryFilePath(const base::FilePath& path) { + // TODO(crbug.com/775415): Check for validity (after fixing unit tests). + return path.RemoveExtension().AddExtension(kWebRtcEventLogHistoryExtension); +} + +std::string ExtractRemoteBoundWebRtcEventLogLocalIdFromPath( + const base::FilePath& path) { + const std::string filename = path.BaseName().RemoveExtension().MaybeAsASCII(); + if (!IsValidRemoteBoundLogFilename(filename)) { + LOG(WARNING) << "Invalid remote-bound WebRTC event log filename."; + return std::string(); + } + + DCHECK_GE(filename.length(), kWebRtcEventLogIdLength); + return filename.substr(filename.length() - kWebRtcEventLogIdLength); +} + +size_t ExtractRemoteBoundWebRtcEventLogWebAppIdFromPath( + const base::FilePath& path) { + const std::string filename = path.BaseName().RemoveExtension().MaybeAsASCII(); + if (!IsValidRemoteBoundLogFilename(filename)) { + LOG(WARNING) << "Invalid remote-bound WebRTC event log filename."; + return kInvalidWebRtcEventLogWebAppId; + } + + // The -1 is because of the implict \0. + const size_t kPrefixLength = + base::size(kRemoteBoundWebRtcEventLogFileNamePrefix) - 1; + + // The +1 is for the underscore between the prefix and the web-app ID. + // Length verified by above call to IsValidRemoteBoundLogFilename(). + DCHECK_GE(filename.length(), kPrefixLength + 1 + kWebAppIdLength); + base::StringPiece id_str(&filename[kPrefixLength + 1], kWebAppIdLength); + + return ExtractWebAppId(id_str); +} + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common.h b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common.h new file mode 100644 index 00000000000..c6479729a94 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common.h @@ -0,0 +1,543 @@ +// Copyright 2017 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_COMMON_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_COMMON_H_ + +#include <memory> +#include <string> + +#include "base/files/file_path.h" +#include "base/optional.h" +#include "base/time/time.h" +#include "build/build_config.h" + +namespace content { +class BrowserContext; +} // namespace content + +namespace webrtc_event_logging { + +// This file is intended for: +// 1. Code shared between WebRtcEventLogManager, WebRtcLocalEventLogManager +// and WebRtcRemoteEventLogManager. +// 2. Code specific to either of the above classes, but which also needs +// to be seen by unit tests (such as constants). + +extern const size_t kWebRtcEventLogManagerUnlimitedFileSize; + +extern const size_t kDefaultMaxLocalLogFileSizeBytes; +extern const size_t kMaxNumberLocalWebRtcEventLogFiles; + +extern const size_t kMaxRemoteLogFileSizeBytes; + +extern const int kMaxOutputPeriodMs; + +// Maximum size for a response from Crash, which is the upload ID. +extern const size_t kWebRtcEventLogMaxUploadIdBytes; + +// The number of digits required to encode a remote-bound log ID. +extern const size_t kWebRtcEventLogIdLength; + +// Min/max legal web-app IDs. +extern const size_t kMinWebRtcEventLogWebAppId; +extern const size_t kMaxWebRtcEventLogWebAppId; + +// Sentinel value, guaranteed not to fall inside the range of min-max valid IDs. +extern const size_t kInvalidWebRtcEventLogWebAppId; + +// Limit over the number of concurrently active (currently being written to +// disk) remote-bound log files. This limits IO operations, and so it is +// applied globally (all browser contexts are limited together). +extern const size_t kMaxActiveRemoteBoundWebRtcEventLogs; + +// Limit over the number of pending logs (logs stored on disk and awaiting to +// be uploaded to a remote server). This limit avoids excessive storage. If a +// user chooses to have multiple profiles (and hence browser contexts) on a +// system, it is assumed that the user has enough storage to accommodate +// the increased storage consumption that comes with it. Therefore, this +// limit is applied per browser context. +extern const size_t kMaxPendingRemoteBoundWebRtcEventLogs; + +// Max number of history files that may be kept; after this number is exceeded, +// the oldest logs should be pruned. +extern const size_t kMaxWebRtcEventLogHistoryFiles; + +// Overhead incurred by GZIP due to its header and footer. +extern const size_t kGzipOverheadBytes; + +// Remote-bound log files' names will be of the format: +// [prefix]_[web_app_id]_[log_id].[ext] +// Where: +// * |prefix| is equal to kRemoteBoundWebRtcEventLogFileNamePrefix. +// * |web_app_id| is a number between kMinWebRtcEventLogWebAppId and +// kMaxWebRtcEventLogWebAppId, with zero padding. +// * |log_id| is composed of 32 random characters from '0'-'9' and 'A'-'F'. +// * |ext| is the extension determined by the used LogCompressor::Factory, +// which will be either kWebRtcEventLogUncompressedExtension or +// kWebRtcEventLogGzippedExtension. +extern const char kRemoteBoundWebRtcEventLogFileNamePrefix[]; +extern const base::FilePath::CharType kWebRtcEventLogUncompressedExtension[]; +extern const base::FilePath::CharType kWebRtcEventLogGzippedExtension[]; + +// Logs themselves are kept on disk for kRemoteBoundWebRtcEventLogsMaxRetention, +// or until uploaded. Smaller history files are kept for a longer time, allowing +// Chrome to display on chrome://webrtc-logs/ that these files were captured +// and later uploaded. +extern const base::FilePath::CharType kWebRtcEventLogHistoryExtension[]; + +// Remote-bound event logs will not be uploaded if the time since their last +// modification (meaning the time when they were completed) exceeds this value. +// Such expired files will be purged from disk when examined. +extern const base::TimeDelta kRemoteBoundWebRtcEventLogsMaxRetention; + +// These are made globally visible so that unit tests may check for them. +extern const char kStartRemoteLoggingFailureAlreadyLogging[]; +extern const char kStartRemoteLoggingFailureDeadRenderProcessHost[]; +extern const char kStartRemoteLoggingFailureFeatureDisabled[]; +extern const char kStartRemoteLoggingFailureFileCreationError[]; +extern const char kStartRemoteLoggingFailureFilePathUsedHistory[]; +extern const char kStartRemoteLoggingFailureFilePathUsedLog[]; +extern const char kStartRemoteLoggingFailureIllegalWebAppId[]; +extern const char kStartRemoteLoggingFailureLoggingDisabledBrowserContext[]; +extern const char kStartRemoteLoggingFailureMaxSizeTooLarge[]; +extern const char kStartRemoteLoggingFailureMaxSizeTooSmall[]; +extern const char kStartRemoteLoggingFailureNoAdditionalActiveLogsAllowed[]; +extern const char kStartRemoteLoggingFailureOutputPeriodMsTooLarge[]; +extern const char kStartRemoteLoggingFailureUnknownOrInactivePeerConnection[]; +extern const char kStartRemoteLoggingFailureUnlimitedSizeDisallowed[]; + +// Values for the histogram for the result of the API call to collect +// a WebRTC event log. +// Must match the numbering of WebRtcEventLoggingApiEnum in enums.xml. +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class WebRtcEventLoggingApiUma { + kSuccess = 0, // Log successfully collected. + kDeadRph = 1, // Log not collected. + kFeatureDisabled = 2, // Log not collected. + kIncognito = 3, // Log not collected. + kInvalidArguments = 4, // Log not collected. + kIllegalSessionId = 5, // Log not collected. + kDisabledBrowserContext = 6, // Log not collected. + kUnknownOrInvalidPeerConnection = 7, // Log not collected. + kAlreadyLogging = 8, // Log not collected. + kNoAdditionalLogsAllowed = 9, // Log not collected. + kLogPathNotAvailable = 10, // Log not collected. + kHistoryPathNotAvailable = 11, // Log not collected. + kFileCreationError = 12, // Log not collected. + kMaxValue = kFileCreationError +}; + +void UmaRecordWebRtcEventLoggingApi(WebRtcEventLoggingApiUma result); + +// Values for the histogram for the result of the upload of a WebRTC event log. +// Must match the numbering of WebRtcEventLoggingUploadEnum in enums.xml. +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class WebRtcEventLoggingUploadUma { + kSuccess = 0, // Uploaded successfully. + kLogFileWriteError = 1, // Will not be uploaded. + kActiveLogCancelledDueToCacheClear = 2, // Will not be uploaded. + kPendingLogDeletedDueToCacheClear = 3, // Will not be uploaded. + kHistoryFileCreationError = 4, // Will not be uploaded. + kHistoryFileWriteError = 5, // Will not be uploaded. + kLogFileReadError = 6, // Will not be uploaded. + kLogFileNameError = 7, // Will not be uploaded. + kUploadCancelled = 8, // Upload started then cancelled. + kUploadFailure = 9, // Upload attempted and failed. + kIncompletePastUpload = 10, // Upload attempted and failed. + kExpiredLogFileAtChromeStart = 11, // Expired before upload opportunity. + kExpiredLogFileDuringSession = 12, // Expired before upload opportunity. + kMaxValue = kExpiredLogFileDuringSession +}; + +void UmaRecordWebRtcEventLoggingUpload(WebRtcEventLoggingUploadUma result); + +// Success is signalled by 0. +// All negative values signal errors. +// Positive values are not used. +void UmaRecordWebRtcEventLoggingNetErrorType(int net_error); + +// For a given Chrome session, this is a unique key for PeerConnections. +// It's not, however, unique between sessions (after Chrome is restarted). +struct WebRtcEventLogPeerConnectionKey { + using BrowserContextId = uintptr_t; + + constexpr WebRtcEventLogPeerConnectionKey() + : WebRtcEventLogPeerConnectionKey( + /* render_process_id = */ 0, + /* lid = */ 0, + reinterpret_cast<BrowserContextId>(nullptr)) {} + + constexpr WebRtcEventLogPeerConnectionKey(int render_process_id, + int lid, + BrowserContextId browser_context_id) + : render_process_id(render_process_id), + lid(lid), + browser_context_id(browser_context_id) {} + + bool operator==(const WebRtcEventLogPeerConnectionKey& other) const { + // Each RPH is associated with exactly one BrowserContext. + DCHECK(render_process_id != other.render_process_id || + browser_context_id == other.browser_context_id); + + const bool equal = std::tie(render_process_id, lid) == + std::tie(other.render_process_id, other.lid); + return equal; + } + + bool operator<(const WebRtcEventLogPeerConnectionKey& other) const { + // Each RPH is associated with exactly one BrowserContext. + DCHECK(render_process_id != other.render_process_id || + browser_context_id == other.browser_context_id); + + return std::tie(render_process_id, lid) < + std::tie(other.render_process_id, other.lid); + } + + // These two fields are the actual key; any peer connection is uniquely + // identifiable by the renderer process in which it lives, and its ID within + // that process. + int render_process_id; + int lid; // Renderer-local PeerConnection ID. + + // The BrowserContext is not actually part of the key, but each PeerConnection + // is associated with a BrowserContext, and that BrowserContext is almost + // always necessary, so it makes sense to remember it along with the key. + BrowserContextId browser_context_id; +}; + +// Sentinel value for an unknown BrowserContext. +extern const WebRtcEventLogPeerConnectionKey::BrowserContextId + kNullBrowserContextId; + +// Holds housekeeping information about log files. +struct WebRtcLogFileInfo { + WebRtcLogFileInfo( + WebRtcEventLogPeerConnectionKey::BrowserContextId browser_context_id, + const base::FilePath& path, + base::Time last_modified) + : browser_context_id(browser_context_id), + path(path), + last_modified(last_modified) {} + + WebRtcLogFileInfo(const WebRtcLogFileInfo& other) + : browser_context_id(other.browser_context_id), + path(other.path), + last_modified(other.last_modified) {} + + bool operator<(const WebRtcLogFileInfo& other) const { + if (last_modified != other.last_modified) { + return last_modified < other.last_modified; + } + return path < other.path; // Break ties arbitrarily, but consistently. + } + + // The BrowserContext which produced this file. + const WebRtcEventLogPeerConnectionKey::BrowserContextId browser_context_id; + + // The path to the log file itself. + const base::FilePath path; + + // |last_modified| recorded at BrowserContext initialization. Chrome will + // not modify it afterwards, and neither should the user. + const base::Time last_modified; +}; + +// An observer for notifications of local log files being started/stopped, and +// the paths which will be used for these logs. +class WebRtcLocalEventLogsObserver { + public: + virtual void OnLocalLogStarted(WebRtcEventLogPeerConnectionKey key, + const base::FilePath& file_path) = 0; + virtual void OnLocalLogStopped(WebRtcEventLogPeerConnectionKey key) = 0; + + protected: + virtual ~WebRtcLocalEventLogsObserver() = default; +}; + +// An observer for notifications of remote-bound log files being +// started/stopped. The start event would likely only interest unit tests +// (because it exposes the randomized filename to them). The stop event is of +// general interest, because it would often mean that WebRTC can stop sending +// us event logs for this peer connection. +// Some cases where OnRemoteLogStopped would be called include: +// 1. The PeerConnection has become inactive. +// 2. The file's maximum size has been reached. +// 3. Any type of error while writing to the file. +class WebRtcRemoteEventLogsObserver { + public: + virtual void OnRemoteLogStarted(WebRtcEventLogPeerConnectionKey key, + const base::FilePath& file_path, + int output_period_ms) = 0; + virtual void OnRemoteLogStopped(WebRtcEventLogPeerConnectionKey key) = 0; + + protected: + virtual ~WebRtcRemoteEventLogsObserver() = default; +}; + +// Writes a log to a file while observing a maximum size. +class LogFileWriter { + public: + class Factory { + public: + virtual ~Factory() = default; + + // The smallest size a log file of this type may assume. + virtual size_t MinFileSizeBytes() const = 0; + + // The extension type associated with this type of log files. + virtual base::FilePath::StringPieceType Extension() const = 0; + + // Instantiate and initialize a LogFileWriter. + // If creation or initialization fail, an empty unique_ptr will be returned, + // and it will be guaranteed that the file itself is not created. (If |path| + // had pointed to an existing file, that file will be deleted.) + // If !max_file_size_bytes.has_value(), the LogFileWriter is unlimited. + virtual std::unique_ptr<LogFileWriter> Create( + const base::FilePath& path, + base::Optional<size_t> max_file_size_bytes) const = 0; + }; + + virtual ~LogFileWriter() = default; + + // Init() must be called on each LogFileWriter exactly once, before it's used. + // If initialization fails, no further actions may be performed on the object + // other than Close() and Delete(). + virtual bool Init() = 0; + + // Getter for the path of the file |this| wraps. + virtual const base::FilePath& path() const = 0; + + // Whether the maximum file size was reached. + virtual bool MaxSizeReached() const = 0; + + // Writes to the log file while respecting the file's size limit. + // True is returned if and only if the message was written to the file in + // it entirety. That is, |false| is returned either if a genuine error + // occurs, or when the budget does not allow the next write. + // If |false| is ever returned, only Close() and Delete() may subsequently + // be called. + // The function does *not* close the file. + // The function may not be called if MaxSizeReached(). + virtual bool Write(const std::string& input) = 0; + + // If the file was successfully closed, true is returned, and the file may + // now be used. Otherwise, the file is deleted, and false is returned. + virtual bool Close() = 0; + + // Delete the file from disk. + virtual void Delete() = 0; +}; + +// Produces LogFileWriter instances that perform no compression. +class BaseLogFileWriterFactory : public LogFileWriter::Factory { + public: + ~BaseLogFileWriterFactory() override = default; + + size_t MinFileSizeBytes() const override; + + base::FilePath::StringPieceType Extension() const override; + + std::unique_ptr<LogFileWriter> Create( + const base::FilePath& path, + base::Optional<size_t> max_file_size_bytes) const override; +}; + +// Interface for a class that provides compression of a stream, while attempting +// to observe a limit on the size. +// +// One should note that: +// * For compressors that use a footer, to guarantee proper decompression, +// the footer must be written to the file. +// * In such a case, usually, nothing can be omitted from the file, or the +// footer's CRC (if used) would be wrong. +// * Determining a string's size pre-compression, without performing the actual +// compression, is heuristic in nature. +// +// Therefore, compression might terminate (FULL) earlier than it +// must, or even in theory (which we attempt to avoid in practice) exceed the +// size allowed it, in which case the file will be discarded (ERROR). +class LogCompressor { + public: + // By subclassing this factory, concrete implementations of LogCompressor can + // be produced by unit tests, while keeping their definition in the .cc file. + // (Only the factory needs to be declared in the header.) + class Factory { + public: + virtual ~Factory() = default; + + // The smallest size a log file of this type may assume. + virtual size_t MinSizeBytes() const = 0; + + // Returns a LogCompressor if the parameters are valid and all + // initializations are successful; en empty unique_ptr otherwise. + // If !max_size_bytes.has_value(), an unlimited compressor is created. + virtual std::unique_ptr<LogCompressor> Create( + base::Optional<size_t> max_size_bytes) const = 0; + }; + + // Result of a call to Compress(). + // * OK and ERROR_ENCOUNTERED are self-explanatory. + // * DISALLOWED means that, due to budget constraints, the input could + // not be compressed. The stream is still in a legal state, but only + // a call to CreateFooter() is now allowed. + enum class Result { OK, DISALLOWED, ERROR_ENCOUNTERED }; + + virtual ~LogCompressor() = default; + + // Produces a compression header and writes it to |output|. + // The size does not count towards the max size limit. + // Guaranteed not to fail (nothing can realistically go wrong). + virtual void CreateHeader(std::string* output) = 0; + + // Compresses |input| into |output|. + // * If compression succeeded, and the budget was observed, OK is returned. + // * If the compressor thinks the string, once compressed, will exceed the + // maximum size (when combined with previously compressed strings), + // compression will not be done, and DISALLOWED will be returned. + // This allows producing a valid footer without exceeding the size limit. + // * Unexpected errors in the underlying compressor (e.g. zlib, etc.), + // or unexpectedly getting a compressed string which exceeds the budget, + // will return ERROR_ENCOUNTERED. + // This function may not be called again if DISALLOWED or ERROR_ENCOUNTERED + // were ever returned before, or after CreateFooter() was called. + virtual Result Compress(const std::string& input, std::string* output) = 0; + + // Produces a compression footer and writes it to |output|. + // The footer does not count towards the max size limit. + // May not be called more than once, or if Compress() returned ERROR. + virtual bool CreateFooter(std::string* output) = 0; +}; + +// Estimates the compressed size, without performing compression (except in +// unit tests, where performance is of lesser importance). +// This interface allows unit tests to simulate specific cases, such as +// over/under-estimation, and show that the code using the LogCompressor +// deals with them correctly. (E.g., if the estimation expects the compression +// to not go over-budget, but then it does.) +// The estimator is expected to be stateful. That is, the order of calls to +// EstimateCompressedSize() should correspond to the order of calls +// to Compress(). +class CompressedSizeEstimator { + public: + class Factory { + public: + virtual ~Factory() = default; + virtual std::unique_ptr<CompressedSizeEstimator> Create() const = 0; + }; + + virtual ~CompressedSizeEstimator() = default; + + virtual size_t EstimateCompressedSize(const std::string& input) const = 0; +}; + +// Provides a conservative estimation of the number of bytes required to +// compress a string using GZIP. This estimation is not expected to ever +// be overly optimistic, but the code using it should nevertheless be prepared +// to deal with that theoretical possibility. +class DefaultGzippedSizeEstimator : public CompressedSizeEstimator { + public: + class Factory : public CompressedSizeEstimator::Factory { + public: + ~Factory() override = default; + + std::unique_ptr<CompressedSizeEstimator> Create() const override; + }; + + ~DefaultGzippedSizeEstimator() override = default; + + size_t EstimateCompressedSize(const std::string& input) const override; +}; + +// Interface for producing LogCompressorGzip objects. +class GzipLogCompressorFactory : public LogCompressor::Factory { + public: + explicit GzipLogCompressorFactory( + std::unique_ptr<CompressedSizeEstimator::Factory> estimator_factory); + ~GzipLogCompressorFactory() override; + + size_t MinSizeBytes() const override; + + std::unique_ptr<LogCompressor> Create( + base::Optional<size_t> max_size_bytes) const override; + + private: + std::unique_ptr<CompressedSizeEstimator::Factory> estimator_factory_; +}; + +// Produces LogFileWriter instances that perform compression using GZIP. +class GzippedLogFileWriterFactory : public LogFileWriter::Factory { + public: + explicit GzippedLogFileWriterFactory( + std::unique_ptr<GzipLogCompressorFactory> gzip_compressor_factory); + + ~GzippedLogFileWriterFactory() override; + + size_t MinFileSizeBytes() const override; + + base::FilePath::StringPieceType Extension() const override; + + std::unique_ptr<LogFileWriter> Create( + const base::FilePath& path, + base::Optional<size_t> max_file_size_bytes) const override; + + private: + std::unique_ptr<GzipLogCompressorFactory> gzip_compressor_factory_; +}; + +// Create a random identifier of 32 hexadecimal (uppercase) characters. +std::string CreateWebRtcEventLogId(); + +// Translate a BrowserContext into an ID. This lets us associate PeerConnections +// with BrowserContexts, while making sure that we never call the +// BrowserContext's methods outside of the UI thread (because we can't call them +// at all without a cast that would alert us to the danger). +WebRtcEventLogPeerConnectionKey::BrowserContextId GetBrowserContextId( + const content::BrowserContext* browser_context); + +// Fetches the BrowserContext associated with the render process ID, then +// returns its BrowserContextId. (If the render process has already died, +// it would have no BrowserContext associated, so the ID associated with a +// null BrowserContext will be returned.) +WebRtcEventLogPeerConnectionKey::BrowserContextId GetBrowserContextId( + int render_process_id); + +// Given a BrowserContext's directory, return the path to the directory where +// we store the pending remote-bound logs associated with this BrowserContext. +// This function may be called on any task queue. +base::FilePath GetRemoteBoundWebRtcEventLogsDir( + const base::FilePath& browser_context_dir); + +// Produce the path to a remote-bound WebRTC event log file with the given +// log ID, web-app ID and extension, in the given directory. +base::FilePath WebRtcEventLogPath( + const base::FilePath& remote_logs_dir, + const std::string& log_id, + size_t web_app_id, + const base::FilePath::StringPieceType& extension); + +// Checks whether the path/filename would be a valid reference to a remote-bound +// even log. These functions do not examine the file's content or its extension. +bool IsValidRemoteBoundLogFilename(const std::string& filename); +bool IsValidRemoteBoundLogFilePath(const base::FilePath& path); + +// Given WebRTC event log's path, return the path to the history file that +// is, or would be, associated with it. +base::FilePath GetWebRtcEventLogHistoryFilePath(const base::FilePath& path); + +// Attempts to extract the local ID from the file's path. Returns the empty +// string in case of an error. +std::string ExtractRemoteBoundWebRtcEventLogLocalIdFromPath( + const base::FilePath& path); + +// Attempts to extract the web-app ID from the file's path. +// Returns kInvalidWebRtcEventLogWebAppId in case of an error. +size_t ExtractRemoteBoundWebRtcEventLogWebAppIdFromPath( + const base::FilePath& path); + +} // namespace webrtc_event_logging + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_COMMON_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common_unittest.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common_unittest.cc new file mode 100644 index 00000000000..400d4391500 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common_unittest.cc @@ -0,0 +1,655 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/webrtc_event_log_manager_common.h" + +#include <memory> +#include <numeric> +#include <vector> + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/optional.h" +#include "base/rand_util.h" +#include "base/test/task_environment.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_unittest_helpers.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/zlib/google/compression_utils.h" + +namespace webrtc_event_logging { + +namespace { +constexpr LogCompressor::Result OK = LogCompressor::Result::OK; +constexpr LogCompressor::Result DISALLOWED = LogCompressor::Result::DISALLOWED; +constexpr LogCompressor::Result ERROR_ENCOUNTERED = + LogCompressor::Result::ERROR_ENCOUNTERED; +} // namespace + +// Tests for GzipLogCompressor. +// Note that these tests may not use GzippedSize(), or they would be assuming +// what they set out to prove. (Subsequent tests may use it, though.) +class GzipLogCompressorTest : public ::testing::Test { + public: + ~GzipLogCompressorTest() override = default; + + void Init( + std::unique_ptr<CompressedSizeEstimator::Factory> estimator_factory) { + DCHECK(!compressor_factory_); + DCHECK(estimator_factory); + compressor_factory_ = std::make_unique<GzipLogCompressorFactory>( + std::move(estimator_factory)); + } + + std::string Decompress(const std::string& input) { + std::string output; + EXPECT_TRUE(compression::GzipUncompress(input, &output)); + return output; + } + + std::unique_ptr<GzipLogCompressorFactory> compressor_factory_; +}; + +TEST_F(GzipLogCompressorTest, + GzipLogCompressorFactoryCreatesCompressorIfMinimalSizeOrAbove) { + Init(std::make_unique<PerfectGzipEstimator::Factory>()); + const size_t min_size = compressor_factory_->MinSizeBytes(); + auto compressor = compressor_factory_->Create(min_size); + EXPECT_TRUE(compressor); +} + +TEST_F(GzipLogCompressorTest, + GzipLogCompressorFactoryDoesNotCreateCompressorIfBelowMinimalSize) { + Init(std::make_unique<PerfectGzipEstimator::Factory>()); + const size_t min_size = compressor_factory_->MinSizeBytes(); + ASSERT_GE(min_size, 1u); + auto compressor = compressor_factory_->Create(min_size - 1); + EXPECT_FALSE(compressor); +} + +TEST_F(GzipLogCompressorTest, EmptyStreamReasonableMaxSize) { + Init(std::make_unique<PerfectGzipEstimator::Factory>()); + + auto compressor = compressor_factory_->Create(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(compressor); + + std::string header; + compressor->CreateHeader(&header); + + std::string footer; + ASSERT_TRUE(compressor->CreateFooter(&footer)); + + const std::string simulated_file = header + footer; + EXPECT_EQ(Decompress(simulated_file), std::string()); +} + +TEST_F(GzipLogCompressorTest, EmptyStreamMinimalSize) { + Init(std::make_unique<PerfectGzipEstimator::Factory>()); + + const size_t min_size = compressor_factory_->MinSizeBytes(); + auto compressor = compressor_factory_->Create(min_size); + ASSERT_TRUE(compressor); + + std::string header; + compressor->CreateHeader(&header); + + std::string footer; + ASSERT_TRUE(compressor->CreateFooter(&footer)); + + const std::string simulated_file = header + footer; + EXPECT_EQ(Decompress(simulated_file), std::string()); +} + +TEST_F(GzipLogCompressorTest, SingleCallToCompress) { + Init(std::make_unique<PerfectGzipEstimator::Factory>()); + + auto compressor = compressor_factory_->Create(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(compressor); + + std::string header; + compressor->CreateHeader(&header); + + const std::string input = "Some random text."; + std::string log; + ASSERT_EQ(compressor->Compress(input, &log), OK); + + std::string footer; + ASSERT_TRUE(compressor->CreateFooter(&footer)); + + const std::string simulated_file = header + log + footer; + EXPECT_EQ(Decompress(simulated_file), input); +} + +TEST_F(GzipLogCompressorTest, MultipleCallsToCompress) { + Init(std::make_unique<PerfectGzipEstimator::Factory>()); + + auto compressor = compressor_factory_->Create(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(compressor); + + std::string header; + compressor->CreateHeader(&header); + + const std::vector<std::string> inputs = { + "Some random text.", + "This text is also random. I give you my word for it. 100% random.", + "nejnnc pqmnx0981 mnl<D@ikjed90~~,z."}; + + std::vector<std::string> logs(inputs.size()); + for (size_t i = 0; i < inputs.size(); i++) { + ASSERT_EQ(compressor->Compress(inputs[i], &logs[i]), OK); + } + + std::string footer; + ASSERT_TRUE(compressor->CreateFooter(&footer)); + + const auto input = std::accumulate(begin(inputs), end(inputs), std::string()); + const auto log = std::accumulate(begin(logs), end(logs), std::string()); + + const std::string simulated_file = header + log + footer; + EXPECT_EQ(Decompress(simulated_file), input); +} + +TEST_F(GzipLogCompressorTest, UnlimitedBudgetSanity) { + Init(std::make_unique<PerfectGzipEstimator::Factory>()); + + auto compressor = compressor_factory_->Create(base::Optional<size_t>()); + ASSERT_TRUE(compressor); + + std::string header; + compressor->CreateHeader(&header); + + const std::string input = "Some random text."; + std::string log; + ASSERT_EQ(compressor->Compress(input, &log), OK); + + std::string footer; + ASSERT_TRUE(compressor->CreateFooter(&footer)); + + const std::string simulated_file = header + log + footer; + EXPECT_EQ(Decompress(simulated_file), input); +} + +// Test once with a big input, to provide coverage over inputs that could +// exceed the size of some local buffers in the UUT. +TEST_F(GzipLogCompressorTest, CompressionBigInput) { + Init(std::make_unique<PerfectGzipEstimator::Factory>()); + + auto compressor = compressor_factory_->Create(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(compressor); + + std::string header; + compressor->CreateHeader(&header); + + constexpr size_t kRealisticSizeBytes = 1000 * 1000; + const std::string input = base::RandBytesAsString(kRealisticSizeBytes); + std::string log; + ASSERT_EQ(compressor->Compress(input, &log), OK); + + std::string footer; + ASSERT_TRUE(compressor->CreateFooter(&footer)); + + const std::string simulated_file = header + log + footer; + EXPECT_EQ(Decompress(simulated_file), input); +} + +TEST_F(GzipLogCompressorTest, BudgetExceededByFirstCompressYieldsEmptyFile) { + Init(std::make_unique<PerfectGzipEstimator::Factory>()); + + const std::string input = "This won't fit."; + + auto compressor = compressor_factory_->Create(GzippedSize(input) - 1); + ASSERT_TRUE(compressor); + + std::string header; + compressor->CreateHeader(&header); + + // Focal point #1 - Compress() returns DISALLOWED. + std::string log; + EXPECT_EQ(compressor->Compress(input, &log), DISALLOWED); + + // Focal point #2 - CreateFooter() still succeeds; + std::string footer; + EXPECT_TRUE(compressor->CreateFooter(&footer)); + + // Focal point #3 - the resulting log is parsable, and contains only those + // logs for which Compress() was successful. + // Note that |log| is not supposed to be written to the file, because + // Compress() has disallowed it. + const std::string simulated_file = header + footer; + EXPECT_EQ(Decompress(simulated_file), std::string()); +} + +TEST_F(GzipLogCompressorTest, + BudgetExceededByNonFirstCompressYieldsPartialFile) { + Init(std::make_unique<PerfectGzipEstimator::Factory>()); + + const std::string short_input = "short"; + const std::string long_input = "A somewhat longer input string. @$%^&*()!!2"; + + // Allocate enough budget that |short_input| would be produced, and not yet + // exhaust the budget, but |long_input| won't fit. + auto compressor = compressor_factory_->Create(GzippedSize(short_input) + 1); + ASSERT_TRUE(compressor); + + std::string header; + compressor->CreateHeader(&header); + + std::string short_log; + ASSERT_EQ(compressor->Compress(short_input, &short_log), OK); + + // Focal point #1 - Compress() returns DISALLOWED. + std::string long_log; + EXPECT_EQ(compressor->Compress(long_input, &long_log), DISALLOWED); + EXPECT_TRUE(long_log.empty()); + + // Focal point #2 - CreateFooter() still succeeds; + std::string footer; + EXPECT_TRUE(compressor->CreateFooter(&footer)); + + // Focal point #3 - the resulting log is parsable, and contains only those + // logs for which Compress() was successful. + // Note that |long_log| is not supposed to be written to the file, because + // Compress() has disallowed it. + const std::string simulated_file = header + short_log + footer; + EXPECT_EQ(Decompress(simulated_file), short_input); +} + +TEST_F(GzipLogCompressorTest, + ExceedingBudgetDueToOverlyOptimisticEstimationYieldsError) { + // Use an estimator that will always be overly optimistic. + Init(std::make_unique<NullEstimator::Factory>()); + + // Set a budget that will easily be exceeded. + auto compressor = compressor_factory_->Create(kGzipOverheadBytes + 5); + ASSERT_TRUE(compressor); + + std::string header; + compressor->CreateHeader(&header); + + // Prepare to compress an input that is guaranteed to exceed the budget. + const std::string input = "A string that would not fit in five bytes."; + + // The estimation allowed the compression, but then the compressed output + // ended up being over-budget. + std::string compressed; + EXPECT_EQ(compressor->Compress(input, &compressed), ERROR_ENCOUNTERED); + EXPECT_TRUE(compressed.empty()); +} + +// Tests relevant to all LogFileWriter subclasses. +class LogFileWriterTest + : public ::testing::Test, + public ::testing::WithParamInterface<WebRtcEventLogCompression> { + public: + LogFileWriterTest() { EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); } + + ~LogFileWriterTest() override {} + + void Init(WebRtcEventLogCompression compression) { + DCHECK(!compression_.has_value()) << "Must only be called once."; + compression_ = compression; + log_file_writer_factory_ = CreateLogFileWriterFactory(compression); + path_ = temp_dir_.GetPath() + .Append(FILE_PATH_LITERAL("arbitrary_filename")) + .AddExtension(log_file_writer_factory_->Extension()); + } + + std::unique_ptr<LogFileWriter> CreateWriter(base::Optional<size_t> max_size) { + return log_file_writer_factory_->Create(path_, max_size); + } + + void ExpectFileContents(const base::FilePath& file_path, + const std::string& expected_contents) { + DCHECK(compression_.has_value()) << "Must call Init()."; + + std::string file_contents; + ASSERT_TRUE(base::ReadFileToString(file_path, &file_contents)); + + switch (compression_.value()) { + case WebRtcEventLogCompression::NONE: { + EXPECT_EQ(file_contents, expected_contents); + break; + } + case WebRtcEventLogCompression::GZIP_PERFECT_ESTIMATION: + case WebRtcEventLogCompression::GZIP_NULL_ESTIMATION: { + std::string uncompressed; + ASSERT_TRUE(compression::GzipUncompress(file_contents, &uncompressed)); + EXPECT_EQ(uncompressed, expected_contents); + break; + } + default: { NOTREACHED(); } + } + } + + base::test::TaskEnvironment task_environment_; + base::Optional<WebRtcEventLogCompression> compression_; // Set in Init(). + base::ScopedTempDir temp_dir_; + base::FilePath path_; + std::unique_ptr<LogFileWriter::Factory> log_file_writer_factory_; +}; + +TEST_P(LogFileWriterTest, FactoryCreatesLogFileWriter) { + Init(GetParam()); + EXPECT_TRUE(CreateWriter(log_file_writer_factory_->MinFileSizeBytes())); +} + +#if defined(OS_POSIX) +TEST_P(LogFileWriterTest, FactoryReturnsEmptyUniquePtrIfCantCreateFile) { + Init(GetParam()); + RemoveWritePermissions(temp_dir_.GetPath()); + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + EXPECT_FALSE(writer); +} +#endif // defined(OS_POSIX) + +TEST_P(LogFileWriterTest, CloseSucceedsWhenNoErrorsOccurred) { + Init(GetParam()); + + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(writer); + + EXPECT_TRUE(writer->Close()); +} + +// Other tests check check the case of compression where the estimation is +// close to the file's capacity, reaches or exceeds it. +TEST_P(LogFileWriterTest, CallToWriteSuccedsWhenCapacityFarOff) { + Init(GetParam()); + + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(writer); + + const std::string log = "log"; + EXPECT_TRUE(writer->Write(log)); + + ASSERT_TRUE(writer->Close()); + ExpectFileContents(path_, log); +} + +TEST_P(LogFileWriterTest, CallToWriteWithEmptyStringSucceeds) { + Init(GetParam()); + + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(writer); + + const std::string log = ""; + EXPECT_TRUE(writer->Write(log)); + + ASSERT_TRUE(writer->Close()); + ExpectFileContents(path_, log); +} + +TEST_P(LogFileWriterTest, UnlimitedBudgetSanity) { + Init(GetParam()); + + auto writer = CreateWriter(base::Optional<size_t>()); + ASSERT_TRUE(writer); + + const std::string log = "log"; + EXPECT_TRUE(writer->Write(log)); + + ASSERT_TRUE(writer->Close()); + ExpectFileContents(path_, log); +} + +TEST_P(LogFileWriterTest, DeleteRemovesUnclosedFile) { + Init(GetParam()); + + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(writer); + + writer->Delete(); + EXPECT_FALSE(base::PathExists(path_)); +} + +TEST_P(LogFileWriterTest, DeleteRemovesClosedFile) { + Init(GetParam()); + + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(writer); + + EXPECT_TRUE(writer->Close()); + + writer->Delete(); + EXPECT_FALSE(base::PathExists(path_)); +} + +#if !defined(OS_WIN) // Deleting the open file does not work on Windows. +TEST_P(LogFileWriterTest, WriteDoesNotCrashIfFileRemovedExternally) { + Init(GetParam()); + + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(writer); + + ASSERT_TRUE(base::DeleteFile(path_, /*recursive=*/false)); + ASSERT_FALSE(base::PathExists(path_)); // Sanity on the test itself. + + // It's up to the OS whether this will succeed or fail, but it must not crash. + writer->Write("log"); +} + +TEST_P(LogFileWriterTest, CloseDoesNotCrashIfFileRemovedExternally) { + Init(GetParam()); + + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(writer); + + ASSERT_TRUE(base::DeleteFile(path_, /*recursive=*/false)); + ASSERT_FALSE(base::PathExists(path_)); // Sanity on the test itself. + + // It's up to the OS whether this will succeed or fail, but it must not crash. + writer->Close(); +} + +TEST_P(LogFileWriterTest, DeleteDoesNotCrashIfFileRemovedExternally) { + Init(GetParam()); + + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(writer); + + ASSERT_TRUE(base::DeleteFile(path_, /*recursive=*/false)); + ASSERT_FALSE(base::PathExists(path_)); // Sanity on the test itself. + + // It's up to the OS whether this will succeed or fail, but it must not crash. + writer->Delete(); +} +#endif // !defined(OS_WIN) + +TEST_P(LogFileWriterTest, PathReturnsTheCorrectPath) { + Init(GetParam()); + + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(writer); + + ASSERT_EQ(writer->path(), path_); +} + +INSTANTIATE_TEST_SUITE_P( + Compression, + LogFileWriterTest, + ::testing::Values(WebRtcEventLogCompression::NONE, + WebRtcEventLogCompression::GZIP_PERFECT_ESTIMATION)); + +// Tests for UncompressedLogFileWriterTest only. +class UncompressedLogFileWriterTest : public LogFileWriterTest { + public: + ~UncompressedLogFileWriterTest() override = default; +}; + +TEST_F(UncompressedLogFileWriterTest, + MaxSizeReachedReturnsFalseWhenMaxNotReached) { + Init(WebRtcEventLogCompression::NONE); + + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(writer); + + const std::string log = "log"; + ASSERT_TRUE(writer->Write(log)); + + EXPECT_FALSE(writer->MaxSizeReached()); +} + +TEST_F(UncompressedLogFileWriterTest, MaxSizeReachedReturnsTrueWhenMaxReached) { + Init(WebRtcEventLogCompression::NONE); + + const std::string log = "log"; + + auto writer = CreateWriter(log.size()); + ASSERT_TRUE(writer); + + ASSERT_TRUE(writer->Write(log)); // (CallToWriteSuccedsWhenCapacityReached) + + EXPECT_TRUE(writer->MaxSizeReached()); +} + +TEST_F(UncompressedLogFileWriterTest, CallToWriteSuccedsWhenCapacityReached) { + Init(WebRtcEventLogCompression::NONE); + + const std::string log = "log"; + + auto writer = CreateWriter(log.size()); + ASSERT_TRUE(writer); + + EXPECT_TRUE(writer->Write(log)); + + ASSERT_TRUE(writer->Close()); + ExpectFileContents(path_, log); +} + +TEST_F(UncompressedLogFileWriterTest, CallToWriteFailsWhenCapacityExceeded) { + Init(WebRtcEventLogCompression::NONE); + + const std::string log = "log"; + + auto writer = CreateWriter(log.size() - 1); + ASSERT_TRUE(writer); + + EXPECT_FALSE(writer->Write(log)); + + ASSERT_TRUE(writer->Close()); + ExpectFileContents(path_, std::string()); +} + +TEST_F(UncompressedLogFileWriterTest, WriteCompleteMessagesOnly) { + Init(WebRtcEventLogCompression::NONE); + + const std::string log1 = "01234"; + const std::string log2 = "56789"; + + auto writer = CreateWriter(log1.size() + log2.size() - 1); + ASSERT_TRUE(writer); + + EXPECT_TRUE(writer->Write(log1)); + + EXPECT_FALSE(writer->Write(log2)); + + ASSERT_TRUE(writer->Close()); + ExpectFileContents(path_, log1); +} + +// Tests for GzippedLogFileWriterTest only. +class GzippedLogFileWriterTest : public LogFileWriterTest { + public: + ~GzippedLogFileWriterTest() override = default; +}; + +TEST_F(GzippedLogFileWriterTest, FactoryDeletesFileIfMaxSizeBelowMin) { + Init(WebRtcEventLogCompression::GZIP_NULL_ESTIMATION); + + const size_t min_size = log_file_writer_factory_->MinFileSizeBytes(); + ASSERT_GE(min_size, 1u); + + auto writer = CreateWriter(min_size - 1); + ASSERT_FALSE(writer); + + EXPECT_FALSE(base::PathExists(path_)); +} + +TEST_F(GzippedLogFileWriterTest, MaxSizeReachedReturnsFalseWhenMaxNotReached) { + Init(WebRtcEventLogCompression::GZIP_NULL_ESTIMATION); + + auto writer = CreateWriter(kMaxRemoteLogFileSizeBytes); + ASSERT_TRUE(writer); + + const std::string log = "log"; + ASSERT_TRUE(writer->Write(log)); + EXPECT_FALSE(writer->MaxSizeReached()); +} + +TEST_F(GzippedLogFileWriterTest, MaxSizeReachedReturnsTrueWhenMaxReached) { + // By using a 0 estimation, we allow the compressor to keep going to + // the point of budget saturation. + Init(WebRtcEventLogCompression::GZIP_NULL_ESTIMATION); + + const std::string log = "log"; + + auto writer = CreateWriter(GzippedSize(log)); + ASSERT_TRUE(writer); + + ASSERT_TRUE(writer->Write(log)); // (CallToWriteSuccedsWhenCapacityReached) + EXPECT_TRUE(writer->MaxSizeReached()); +} + +TEST_F(GzippedLogFileWriterTest, CallToWriteSuccedsWhenCapacityReached) { + Init(WebRtcEventLogCompression::GZIP_PERFECT_ESTIMATION); + + const std::string log = "log"; + + auto writer = CreateWriter(GzippedSize(log)); + ASSERT_TRUE(writer); + + EXPECT_TRUE(writer->Write(log)); + + ASSERT_TRUE(writer->Close()); + ExpectFileContents(path_, log); +} + +// Also tests the scenario WriteCompleteMessagesOnly. +TEST_F(GzippedLogFileWriterTest, + CallToWriteFailsWhenCapacityWouldBeExceededButEstimationPreventedWrite) { + Init(WebRtcEventLogCompression::GZIP_PERFECT_ESTIMATION); + + const std::string log1 = "abcde"; + const std::string log2 = "fghij"; + const std::vector<std::string> logs = {log1, log2}; + + // Find out the size necessary for compressing log1 and log2 in two calls. + const size_t compressed_len = GzippedSize(logs); // Vector version. + + auto writer = CreateWriter(compressed_len - 1); + ASSERT_TRUE(writer); + + ASSERT_TRUE(writer->Write(log1)); + + EXPECT_FALSE(writer->Write(log2)); + + // The second write was succesfully prevented; no error should have occurred, + // and it should be possible to produce a meaningful gzipped log file. + EXPECT_TRUE(writer->Close()); + + ExpectFileContents(path_, log1); // Only the in-budget part was written. +} + +// This tests the case when the estimation fails to warn us of a pending +// over-budget write, which leaves us unable to produce a valid compression +// footer for the truncated file. This forces us to discard the file. +TEST_F(GzippedLogFileWriterTest, + CallToWriteFailsWhenCapacityExceededDespiteEstimationAllowingIt) { + // By using a 0 estimation, we allow the compressor to keep going to + // the point of budget saturation. + Init(WebRtcEventLogCompression::GZIP_NULL_ESTIMATION); + + const std::string log = "log"; + + auto writer = CreateWriter(GzippedSize(log) - 1); + ASSERT_TRUE(writer); + + EXPECT_FALSE(writer->Write(log)); + + EXPECT_FALSE(writer->Close()); + EXPECT_FALSE(base::PathExists(path_)); // Errored files deleted by Close(). +} + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service.cc new file mode 100644 index 00000000000..ec5401e454a --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service.cc @@ -0,0 +1,36 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service.h" + +#include "base/callback_forward.h" +#include "base/logging.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager.h" +#include "content/public/browser/browser_context.h" + +namespace webrtc_event_logging { + +WebRtcEventLogManagerKeyedService::WebRtcEventLogManagerKeyedService( + content::BrowserContext* browser_context) + : browser_context_(browser_context) { + DCHECK(!browser_context_->IsOffTheRecord()); + + WebRtcEventLogManager* manager = WebRtcEventLogManager::GetInstance(); + if (manager) { + manager->EnableForBrowserContext(browser_context_, base::OnceClosure()); + reported_ = true; + } else { + reported_ = false; + } +} + +void WebRtcEventLogManagerKeyedService::Shutdown() { + WebRtcEventLogManager* manager = WebRtcEventLogManager::GetInstance(); + if (manager) { + DCHECK(reported_) << "WebRtcEventLogManager constructed too late."; + manager->DisableForBrowserContext(browser_context_, base::OnceClosure()); + } +} + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service.h b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service.h new file mode 100644 index 00000000000..c12cf66f999 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service.h @@ -0,0 +1,43 @@ +// Copyright 2018 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_KEYED_SERVICE_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_KEYED_SERVICE_H_ + +#include "base/macros.h" +#include "components/keyed_service/core/keyed_service.h" + +namespace content { +class BrowserContext; +} // namespace content + +namespace webrtc_event_logging { + +// KeyedService working on behalf of WebRtcEventLogManager, informing it when +// new BrowserContext-s are loaded. +class WebRtcEventLogManagerKeyedService : public KeyedService { + public: + explicit WebRtcEventLogManagerKeyedService( + content::BrowserContext* browser_context); + + ~WebRtcEventLogManagerKeyedService() override = default; + + void Shutdown() override; + + private: + // The BrowserContext associated with this instance of the service. + content::BrowserContext* const browser_context_; + + // Whether the singleton content::WebRtcEventLogger existed at the time this + // service was instantiated, and therefore got the report that this + // BrowserContext was loaded. + // See usage for rationale. + bool reported_; + + DISALLOW_COPY_AND_ASSIGN(WebRtcEventLogManagerKeyedService); +}; + +} // namespace webrtc_event_logging + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_KEYED_SERVICE_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service_factory.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service_factory.cc new file mode 100644 index 00000000000..5660683eebe --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service_factory.cc @@ -0,0 +1,40 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service_factory.h" + +#include "base/logging.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "content/public/browser/browser_context.h" + +namespace webrtc_event_logging { + +// static +WebRtcEventLogManagerKeyedServiceFactory* +WebRtcEventLogManagerKeyedServiceFactory::GetInstance() { + return base::Singleton<WebRtcEventLogManagerKeyedServiceFactory>::get(); +} + +WebRtcEventLogManagerKeyedServiceFactory:: + WebRtcEventLogManagerKeyedServiceFactory() + : BrowserContextKeyedServiceFactory( + "WebRtcEventLogManagerKeyedService", + BrowserContextDependencyManager::GetInstance()) {} + +WebRtcEventLogManagerKeyedServiceFactory:: + ~WebRtcEventLogManagerKeyedServiceFactory() = default; + +bool WebRtcEventLogManagerKeyedServiceFactory:: + ServiceIsCreatedWithBrowserContext() const { + return true; +} + +KeyedService* WebRtcEventLogManagerKeyedServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + DCHECK(!context->IsOffTheRecord()); + return new WebRtcEventLogManagerKeyedService(context); +} + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service_factory.h b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service_factory.h new file mode 100644 index 00000000000..6e7195758f1 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_keyed_service_factory.h @@ -0,0 +1,44 @@ +// Copyright 2018 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_KEYED_SERVICE_FACTORY_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_KEYED_SERVICE_FACTORY_H_ + +#include "base/macros.h" +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class KeyedService; + +namespace content { +class BrowserContext; +} // namespace content + +namespace webrtc_event_logging { + +// Produces WebRtcEventLogManagerKeyedService-s for non-incognito profiles. +class WebRtcEventLogManagerKeyedServiceFactory + : public BrowserContextKeyedServiceFactory { + public: + static WebRtcEventLogManagerKeyedServiceFactory* GetInstance(); + + protected: + bool ServiceIsCreatedWithBrowserContext() const override; + + private: + friend struct base::DefaultSingletonTraits< + WebRtcEventLogManagerKeyedServiceFactory>; + + WebRtcEventLogManagerKeyedServiceFactory(); + ~WebRtcEventLogManagerKeyedServiceFactory() override; + + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; + + DISALLOW_COPY_AND_ASSIGN(WebRtcEventLogManagerKeyedServiceFactory); +}; + +} // namespace webrtc_event_logging + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_KEYED_SERVICE_FACTORY_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_local.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_local.cc new file mode 100644 index 00000000000..d13b25244aa --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_local.cc @@ -0,0 +1,252 @@ +// Copyright 2017 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 "chrome/browser/media/webrtc/webrtc_event_log_manager_local.h" + +#include "base/files/file_util.h" +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "build/build_config.h" +#include "content/public/browser/browser_thread.h" + +#if defined(OS_WIN) +#define NumberToStringType base::NumberToString16 +#else +#define NumberToStringType base::NumberToString +#endif + +namespace webrtc_event_logging { + +#if defined(OS_ANDROID) +const size_t kDefaultMaxLocalLogFileSizeBytes = 10000000; +const size_t kMaxNumberLocalWebRtcEventLogFiles = 3; +#else +const size_t kDefaultMaxLocalLogFileSizeBytes = 60000000; +const size_t kMaxNumberLocalWebRtcEventLogFiles = 5; +#endif + +WebRtcLocalEventLogManager::WebRtcLocalEventLogManager( + WebRtcLocalEventLogsObserver* observer) + : observer_(observer), + clock_for_testing_(nullptr), + max_log_file_size_bytes_(kDefaultMaxLocalLogFileSizeBytes) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + DETACH_FROM_SEQUENCE(io_task_sequence_checker_); +} + +WebRtcLocalEventLogManager::~WebRtcLocalEventLogManager() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); +} + +bool WebRtcLocalEventLogManager::PeerConnectionAdded( + const PeerConnectionKey& key) { + DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_); + + const auto insertion_result = active_peer_connections_.insert(key); + if (!insertion_result.second) { + return false; // Attempt to re-add the PeerConnection. + } + + if (!base_path_.empty() && + log_files_.size() < kMaxNumberLocalWebRtcEventLogFiles) { + // Note that success/failure of starting the local log file is unrelated + // to the success/failure of PeerConnectionAdded(). + StartLogFile(key); + } + + return true; +} + +bool WebRtcLocalEventLogManager::PeerConnectionRemoved( + const PeerConnectionKey& key) { + DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_); + + auto peer_connection = active_peer_connections_.find(key); + + if (peer_connection == active_peer_connections_.end()) { + DCHECK(log_files_.find(key) == log_files_.end()); + return false; + } + + auto local_log = log_files_.find(key); + if (local_log != log_files_.end()) { + // Note that success/failure of stopping the local log file is unrelated + // to the success/failure of PeerConnectionRemoved(). + CloseLogFile(local_log); + } + + active_peer_connections_.erase(peer_connection); + + return true; +} + +bool WebRtcLocalEventLogManager::EnableLogging(const base::FilePath& base_path, + size_t max_file_size_bytes) { + DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_); + + if (!base_path_.empty()) { + return false; + } + + DCHECK_EQ(log_files_.size(), 0u); + + base_path_ = base_path; + + max_log_file_size_bytes_ = + (max_file_size_bytes == kWebRtcEventLogManagerUnlimitedFileSize) + ? base::Optional<size_t>() + : base::Optional<size_t>(max_file_size_bytes); + + for (const PeerConnectionKey& peer_connection : active_peer_connections_) { + if (log_files_.size() >= kMaxNumberLocalWebRtcEventLogFiles) { + break; + } + StartLogFile(peer_connection); + } + + return true; +} + +bool WebRtcLocalEventLogManager::DisableLogging() { + DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_); + + if (base_path_.empty()) { + return false; + } + + for (auto local_log = log_files_.begin(); local_log != log_files_.end();) { + local_log = CloseLogFile(local_log); + } + + base_path_.clear(); // Marks local-logging as disabled. + max_log_file_size_bytes_ = kDefaultMaxLocalLogFileSizeBytes; + + return true; +} + +bool WebRtcLocalEventLogManager::EventLogWrite(const PeerConnectionKey& key, + const std::string& message) { + DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_); + auto it = log_files_.find(key); + if (it == log_files_.end()) { + return false; + } + + const bool write_successful = it->second->Write(message); + + if (!write_successful || it->second->MaxSizeReached()) { + CloseLogFile(it); + } + + return write_successful; +} + +void WebRtcLocalEventLogManager::RenderProcessHostExitedDestroyed( + int render_process_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_); + + // Remove all of the peer connections associated with this render process. + auto pc_it = active_peer_connections_.begin(); + while (pc_it != active_peer_connections_.end()) { + if (pc_it->render_process_id == render_process_id) { + pc_it = active_peer_connections_.erase(pc_it); + } else { + ++pc_it; + } + } + + // Close all of the files that were associated with peer connections which + // belonged to this render process. + auto log_it = log_files_.begin(); + while (log_it != log_files_.end()) { + if (log_it->first.render_process_id == render_process_id) { + log_it = CloseLogFile(log_it); + } else { + ++log_it; + } + } +} + +void WebRtcLocalEventLogManager::SetClockForTesting(base::Clock* clock) { + DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_); + clock_for_testing_ = clock; +} + +void WebRtcLocalEventLogManager::StartLogFile(const PeerConnectionKey& key) { + DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_); + DCHECK(log_files_.find(key) == log_files_.end()); + + // Add some information to the name given by the caller. + base::FilePath file_path = GetFilePath(base_path_, key); + CHECK(!file_path.empty()) << "Couldn't set path for local WebRTC log file."; + + // In the unlikely case that this filename is already taken, find a unique + // number to append to the filename, if possible. + file_path = base::GetUniquePath(file_path); + if (file_path.empty()) { + return; // No available file path was found. + } + + auto log_file = + log_file_writer_factory_.Create(file_path, max_log_file_size_bytes_); + if (!log_file) { + LOG(WARNING) << "Couldn't create and/or open local WebRTC event log file."; + return; + } + + const auto it = log_files_.emplace(key, std::move(log_file)); + DCHECK(it.second); + + // The observer needs to be able to run on any TaskQueue. + if (observer_) { + LogFilesMap::iterator map_iter = it.first; + // map_iter->second is a std::unique_ptr<LogFileWriter>. + observer_->OnLocalLogStarted(key, map_iter->second->path()); + } +} + +WebRtcLocalEventLogManager::LogFilesMap::iterator +WebRtcLocalEventLogManager::CloseLogFile(LogFilesMap::iterator it) { + DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_); + + const PeerConnectionKey peer_connection = it->first; + + it->second->Close(); + it = log_files_.erase(it); + + if (observer_) { + observer_->OnLocalLogStopped(peer_connection); + } + + return it; +} + +base::FilePath WebRtcLocalEventLogManager::GetFilePath( + const base::FilePath& base_path, + const PeerConnectionKey& key) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_); + + base::Time::Exploded now; + if (clock_for_testing_) { + clock_for_testing_->Now().LocalExplode(&now); + } else { + base::Time::Now().LocalExplode(&now); + } + + // [user_defined]_[date]_[time]_[render_process_id]_[lid].[extension] + char stamp[100]; + int written = + base::snprintf(stamp, base::size(stamp), "%04d%02d%02d_%02d%02d_%d_%d", + now.year, now.month, now.day_of_month, now.hour, + now.minute, key.render_process_id, key.lid); + CHECK_GT(written, 0); + CHECK_LT(static_cast<size_t>(written), base::size(stamp)); + + return base_path.InsertBeforeExtension(FILE_PATH_LITERAL("_")) + .AddExtension(log_file_writer_factory_.Extension()) + .InsertBeforeExtensionASCII(base::StringPiece(stamp)); +} + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_local.h b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_local.h new file mode 100644 index 00000000000..6ebf3e49185 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_local.h @@ -0,0 +1,98 @@ +// Copyright 2017 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_LOCAL_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_LOCAL_H_ + +#include <map> +#include <set> +#include <string> + +#include "base/files/file_path.h" +#include "base/optional.h" +#include "base/sequence_checker.h" +#include "base/time/clock.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_common.h" + +namespace webrtc_event_logging { + +class WebRtcLocalEventLogManager final { + using LogFilesMap = + std::map<WebRtcEventLogPeerConnectionKey, std::unique_ptr<LogFileWriter>>; + using PeerConnectionKey = WebRtcEventLogPeerConnectionKey; + + public: + explicit WebRtcLocalEventLogManager(WebRtcLocalEventLogsObserver* observer); + ~WebRtcLocalEventLogManager(); + + bool PeerConnectionAdded(const PeerConnectionKey& key); + bool PeerConnectionRemoved(const PeerConnectionKey& key); + + bool EnableLogging(const base::FilePath& base_path, + size_t max_file_size_bytes); + bool DisableLogging(); + + bool EventLogWrite(const PeerConnectionKey& key, const std::string& message); + + void RenderProcessHostExitedDestroyed(int render_process_id); + + // This function is public, but this entire class is a protected + // implementation detail of WebRtcEventLogManager, which hides this + // function from everybody except its own unit tests. + void SetClockForTesting(base::Clock* clock); + + private: + // Create a local log file. + void StartLogFile(const PeerConnectionKey& key); + + // Closes an active log file. + // Returns an iterator to the next active log file. + LogFilesMap::iterator CloseLogFile(LogFilesMap::iterator it); + + // Derives the name of a local log file. The format is: + // [user_defined]_[date]_[time]_[render_process_id]_[lid].[extension] + base::FilePath GetFilePath(const base::FilePath& base_path, + const PeerConnectionKey& key) const; + + // This object is expected to be created and destroyed on the UI thread, + // but live on its owner's internal, IO-capable task queue. + SEQUENCE_CHECKER(io_task_sequence_checker_); + + // Produces LogFileWriter instances, for writing the logs to files. + BaseLogFileWriterFactory log_file_writer_factory_; + + // Observer which will be informed whenever a local log file is started or + // stopped. Through this, the owning WebRtcEventLogManager can be informed, + // and decide whether it wants to turn notifications from WebRTC on/off. + WebRtcLocalEventLogsObserver* const observer_; + + // For unit tests only, and specifically for unit tests that verify the + // filename format (derived from the current time as well as the renderer PID + // and PeerConnection local ID), we want to make sure that the time and date + // cannot change between the time the clock is read by the unit under test + // (namely WebRtcEventLogManager) and the time it's read by the test. + base::Clock* clock_for_testing_; + + // Currently active peer connections. PeerConnections which have been closed + // are not considered active, regardless of whether they have been torn down. + std::set<PeerConnectionKey> active_peer_connections_; + + // Local log files, stored at the behest of the user (via WebRTCInternals). + LogFilesMap log_files_; + + // If |base_path_| is empty, local logging is disabled. + // If nonempty, local logging is enabled, and all local logs will be saved + // to this directory. + base::FilePath base_path_; + + // The maximum size for local logs, in bytes. + // If !has_value(), the value is unlimited. + base::Optional<size_t> max_log_file_size_bytes_; + + DISALLOW_COPY_AND_ASSIGN(WebRtcLocalEventLogManager); +}; + +} // namespace webrtc_event_logging + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_LOCAL_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_remote.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_remote.cc new file mode 100644 index 00000000000..a69b8d02b3a --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_remote.cc @@ -0,0 +1,1376 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/webrtc_event_log_manager_remote.h" + +#include <algorithm> +#include <iterator> +#include <utility> + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/files/file.h" +#include "base/files/file_enumerator.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/task/post_task.h" +#include "chrome/common/chrome_switches.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" + +namespace webrtc_event_logging { + +// TODO(crbug.com/775415): Change max back to (1u << 29) after resolving the +// issue where we read the entire file into memory. +const size_t kMaxRemoteLogFileSizeBytes = 50000000u; + +const int kDefaultOutputPeriodMs = 5000; +const int kMaxOutputPeriodMs = 60000; + +namespace { +const base::TimeDelta kDefaultProactivePruningDelta = + base::TimeDelta::FromMinutes(5); + +const base::TimeDelta kDefaultWebRtcRemoteEventLogUploadDelay = + base::TimeDelta::FromSeconds(30); + +// Because history files are rarely used, their existence is not kept in memory. +// That means that pruning them involves inspecting data on disk. This is not +// terribly cheap (up to kMaxWebRtcEventLogHistoryFiles files per profile), and +// should therefore be done somewhat infrequently. +const base::TimeDelta kProactiveHistoryFilesPruneDelta = + base::TimeDelta::FromMinutes(30); + +base::TimeDelta GetProactivePendingLogsPruneDelta() { + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + ::switches::kWebRtcRemoteEventLogProactivePruningDelta)) { + const std::string delta_seconds_str = + base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + ::switches::kWebRtcRemoteEventLogProactivePruningDelta); + int64_t seconds; + if (base::StringToInt64(delta_seconds_str, &seconds) && seconds >= 0) { + return base::TimeDelta::FromSeconds(seconds); + } else { + LOG(WARNING) << "Proactive pruning delta could not be parsed."; + } + } + + return kDefaultProactivePruningDelta; +} + +base::TimeDelta GetUploadDelay() { + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + ::switches::kWebRtcRemoteEventLogUploadDelayMs)) { + const std::string delta_seconds_str = + base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + ::switches::kWebRtcRemoteEventLogUploadDelayMs); + int64_t ms; + if (base::StringToInt64(delta_seconds_str, &ms) && ms >= 0) { + return base::TimeDelta::FromMilliseconds(ms); + } else { + LOG(WARNING) << "Upload delay could not be parsed; using default delay."; + } + } + + return kDefaultWebRtcRemoteEventLogUploadDelay; +} + +bool TimePointInRange(const base::Time& time_point, + const base::Time& range_begin, + const base::Time& range_end) { + DCHECK(!time_point.is_null()); + DCHECK(range_begin.is_null() || range_end.is_null() || + range_begin <= range_end); + return (range_begin.is_null() || range_begin <= time_point) && + (range_end.is_null() || time_point < range_end); +} + +// Do not attempt to upload when there is no active connection. +// Do not attempt to upload if the connection is known to be a mobile one. +// Note #1: A device may have multiple connections, so this is not bullet-proof. +// Note #2: Does not attempt to recognize mobile hotspots. +bool UploadSupportedUsingConnectionType( + network::mojom::ConnectionType connection) { + return connection != network::mojom::ConnectionType::CONNECTION_NONE && + connection != network::mojom::ConnectionType::CONNECTION_2G && + connection != network::mojom::ConnectionType::CONNECTION_3G && + connection != network::mojom::ConnectionType::CONNECTION_4G; +} + +// Produce a history file for a given file. +void CreateHistoryFile(const base::FilePath& log_file_path, + const base::Time& capture_time) { + std::unique_ptr<WebRtcEventLogHistoryFileWriter> writer = + WebRtcEventLogHistoryFileWriter::Create( + GetWebRtcEventLogHistoryFilePath(log_file_path)); + if (!writer) { + LOG(ERROR) << "Could not create history file."; + return; + } + + if (!writer->WriteCaptureTime(capture_time)) { + LOG(ERROR) << "Could not write capture time to history file."; + writer->Delete(); + return; + } +} + +// The following is a list of entry types used to transmit information +// from GetHistory() to the caller (normally - the UI). +// Each entry is of type UploadList::UploadInfo. Depending on the entry +// type, the fields in the UploadInfo have different values: +// 1+2. Currently-being-captured or pending -> State::Pending && !upload_time. +// 3. Currently-being-uploaded -> State::Pending && upload_time. +// 4. Pruned before being uploaded -> State::NotUploaded && !upload_time. +// 5. Unsuccessful upload attempt -> State::NotUploaded && upload_time. +// 6. Successfully uploaded -> State::Uploaded. +// +// As for the meaning of the local_id field, its semantics change according to +// the above entry type. +// * For cases 1-3 above, it is the filename, since the log is still on disk. +// * For cases 5-6 above, it is the local log ID that the now-deleted file used +// * to have. +namespace history { +UploadList::UploadInfo CreateActivelyCapturedLogEntry( + const base::FilePath& path, + const base::Time& capture_time) { + using State = UploadList::UploadInfo::State; + const std::string filename = path.BaseName().MaybeAsASCII(); + DCHECK(!filename.empty()); + return UploadList::UploadInfo(std::string(), base::Time(), filename, + capture_time, State::Pending); +} + +UploadList::UploadInfo CreatePendingLogEntry( + const WebRtcLogFileInfo& log_info) { + using State = UploadList::UploadInfo::State; + const std::string filename = log_info.path.BaseName().MaybeAsASCII(); + DCHECK(!filename.empty()); + return UploadList::UploadInfo(std::string(), base::Time(), filename, + log_info.last_modified, State::Pending); +} + +UploadList::UploadInfo CreateActivelyUploadedLogEntry( + const WebRtcLogFileInfo& log_info, + const base::Time& upload_time) { + using State = UploadList::UploadInfo::State; + const std::string filename = log_info.path.BaseName().MaybeAsASCII(); + DCHECK(!filename.empty()); + return UploadList::UploadInfo(std::string(), upload_time, filename, + log_info.last_modified, State::Pending); +} + +UploadList::UploadInfo CreateEntryFromHistoryFileReader( + const WebRtcEventLogHistoryFileReader& reader) { + using State = UploadList::UploadInfo::State; + const auto state = + reader.UploadId().empty() ? State::NotUploaded : State::Uploaded; + return UploadList::UploadInfo(reader.UploadId(), reader.UploadTime(), + reader.LocalId(), reader.CaptureTime(), state); +} +} // namespace history +} // namespace + +const size_t kMaxActiveRemoteBoundWebRtcEventLogs = 3; +const size_t kMaxPendingRemoteBoundWebRtcEventLogs = 5; +static_assert(kMaxActiveRemoteBoundWebRtcEventLogs <= + kMaxPendingRemoteBoundWebRtcEventLogs, + "This assumption affects unit test coverage."); +const size_t kMaxWebRtcEventLogHistoryFiles = 50; + +// Maximum time to keep remote-bound logs on disk. +const base::TimeDelta kRemoteBoundWebRtcEventLogsMaxRetention = + base::TimeDelta::FromDays(7); + +// Maximum time to keep history files on disk. These serve to display an upload +// on chrome://webrtc-logs/. It is persisted for longer than the log itself. +const base::TimeDelta kHistoryFileRetention = base::TimeDelta::FromDays(30); + +WebRtcRemoteEventLogManager::WebRtcRemoteEventLogManager( + WebRtcRemoteEventLogsObserver* observer, + scoped_refptr<base::SequencedTaskRunner> task_runner) + : upload_suppression_disabled_( + base::CommandLine::ForCurrentProcess()->HasSwitch( + ::switches::kWebRtcRemoteEventLogUploadNoSuppression)), + upload_delay_(GetUploadDelay()), + proactive_pending_logs_prune_delta_(GetProactivePendingLogsPruneDelta()), + proactive_prune_scheduling_started_(false), + observer_(observer), + network_connection_tracker_(nullptr), + uploading_supported_for_connection_type_(false), + scheduled_upload_tasks_(0), + uploader_factory_( + std::make_unique<WebRtcEventLogUploaderImpl::Factory>()), + task_runner_(task_runner), + weak_ptr_factory_( + std::make_unique<base::WeakPtrFactory<WebRtcRemoteEventLogManager>>( + this)) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + // Proactive pruning would not do anything at the moment; it will be started + // with the first enabled browser context. This will all have the benefit + // of doing so on |task_runner_| rather than the UI thread. +} + +WebRtcRemoteEventLogManager::~WebRtcRemoteEventLogManager() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + // TODO(crbug.com/775415): Purge from disk files which were being uploaded + // while destruction took place, thereby avoiding endless attempts to upload + // the same file. + + if (weak_ptr_factory_) { + // Not a unit test; that would have gone through ShutDownForTesting(). + const bool will_delete = + task_runner_->DeleteSoon(FROM_HERE, weak_ptr_factory_.release()); + DCHECK(!will_delete) + << "Task runners must have been stopped by this stage of shutdown."; + } + + if (network_connection_tracker_) { + // * |network_connection_tracker_| might already have posted a task back + // to us, but it will not run, because |task_runner_| has already been + // stopped. + // * RemoveNetworkConnectionObserver() should generally be called on the + // same thread as AddNetworkConnectionObserver(), but in this case it's + // okay to remove on a separate thread, because this only happens during + // Chrome shutdown, when no others tasks are running; there can be no + // concurrently executing notification from the tracker. + network_connection_tracker_->RemoveNetworkConnectionObserver(this); + } +} + +void WebRtcRemoteEventLogManager::SetNetworkConnectionTracker( + network::NetworkConnectionTracker* network_connection_tracker) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(network_connection_tracker); + DCHECK(!network_connection_tracker_); + + // |this| is only destroyed (on the UI thread) after |task_runner_| stops, + // so AddNetworkConnectionObserver() is safe. + + network_connection_tracker_ = network_connection_tracker; + network_connection_tracker_->AddNetworkConnectionObserver(this); + + auto callback = + base::BindOnce(&WebRtcRemoteEventLogManager::OnConnectionChanged, + weak_ptr_factory_->GetWeakPtr()); + network::mojom::ConnectionType connection_type; + const bool sync_answer = network_connection_tracker_->GetConnectionType( + &connection_type, std::move(callback)); + + if (sync_answer) { + OnConnectionChanged(connection_type); + } + + // Because this happens while enabling the first browser context, there is no + // necessity to consider uploading yet. + DCHECK_EQ(enabled_browser_contexts_.size(), 0u); +} + +void WebRtcRemoteEventLogManager::SetLogFileWriterFactory( + std::unique_ptr<LogFileWriter::Factory> log_file_writer_factory) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(log_file_writer_factory); + DCHECK(!log_file_writer_factory_); + log_file_writer_factory_ = std::move(log_file_writer_factory); +} + +void WebRtcRemoteEventLogManager::EnableForBrowserContext( + BrowserContextId browser_context_id, + const base::FilePath& browser_context_dir) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(network_connection_tracker_) + << "SetNetworkConnectionTracker not called."; + DCHECK(log_file_writer_factory_) << "SetLogFileWriterFactory() not called."; + DCHECK(!BrowserContextEnabled(browser_context_id)) << "Already enabled."; + + const base::FilePath remote_bound_logs_dir = + GetRemoteBoundWebRtcEventLogsDir(browser_context_dir); + if (!MaybeCreateLogsDirectory(remote_bound_logs_dir)) { + LOG(WARNING) + << "WebRtcRemoteEventLogManager couldn't create logs directory."; + return; + } + + enabled_browser_contexts_.emplace(browser_context_id, remote_bound_logs_dir); + + LoadLogsDirectory(browser_context_id, remote_bound_logs_dir); + + if (!proactive_prune_scheduling_started_) { + proactive_prune_scheduling_started_ = true; + + if (!proactive_pending_logs_prune_delta_.is_zero()) { + RecurringlyPrunePendingLogs(); + } + + RecurringlyPruneHistoryFiles(); + } +} + +void WebRtcRemoteEventLogManager::DisableForBrowserContext( + BrowserContextId browser_context_id) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (!BrowserContextEnabled(browser_context_id)) { + return; // Enabling may have failed due to lacking permissions. + } + + enabled_browser_contexts_.erase(browser_context_id); + +#if DCHECK_IS_ON() + // DisableForBrowserContext() is called in one of two cases: + // 1. If Chrome is shutting down. In that case, all the RPHs associated with + // this BrowserContext must already have exited, which should have + // implicitly stopped all active logs. + // 2. Remote-bound logging is no longer allowed for this BrowserContext. + // In that case, some peer connections associated with this BrowserContext + // might still be active, or become active at a later time, but all + // logs must have already been stopped. + auto pred = [browser_context_id](decltype(active_logs_)::value_type& log) { + return log.first.browser_context_id == browser_context_id; + }; + DCHECK(std::count_if(active_logs_.begin(), active_logs_.end(), pred) == 0u); +#endif + + // Pending logs for this BrowserContext are no longer eligible for upload. + for (auto it = pending_logs_.begin(); it != pending_logs_.end();) { + if (it->browser_context_id == browser_context_id) { + it = pending_logs_.erase(it); + } else { + ++it; + } + } + + // Active uploads of logs associated with this BrowserContext must be stopped. + MaybeCancelUpload(base::Time::Min(), base::Time::Max(), browser_context_id); + + // Active logs may have been removed, which could remove upload suppression, + // or pending logs which were about to be uploaded may have been removed, + // so uploading may no longer be possible. + ManageUploadSchedule(); +} + +bool WebRtcRemoteEventLogManager::PeerConnectionAdded( + const PeerConnectionKey& key) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + PrunePendingLogs(); // Infrequent event - good opportunity to prune. + + const auto result = active_peer_connections_.emplace(key, std::string()); + + // An upload about to start might need to be suppressed. + ManageUploadSchedule(); + + return result.second; +} + +bool WebRtcRemoteEventLogManager::PeerConnectionRemoved( + const PeerConnectionKey& key) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + PrunePendingLogs(); // Infrequent event - good opportunity to prune. + + const auto peer_connection = active_peer_connections_.find(key); + if (peer_connection == active_peer_connections_.end()) { + return false; + } + + MaybeStopRemoteLogging(key); + + active_peer_connections_.erase(peer_connection); + + ManageUploadSchedule(); // Suppression might have been removed. + + return true; +} + +bool WebRtcRemoteEventLogManager::PeerConnectionSessionIdSet( + const PeerConnectionKey& key, + const std::string& session_id) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + PrunePendingLogs(); // Infrequent event - good opportunity to prune. + + if (session_id.empty()) { + LOG(ERROR) << "Empty session ID."; + return false; + } + + auto peer_connection = active_peer_connections_.find(key); + if (peer_connection == active_peer_connections_.end()) { + return false; // Unknown peer connection; already closed? + } + + if (!peer_connection->second.empty()) { + LOG(ERROR) << "Session ID already set."; + return false; + } + + peer_connection->second = session_id; + + return true; +} + +bool WebRtcRemoteEventLogManager::StartRemoteLogging( + int render_process_id, + BrowserContextId browser_context_id, + const std::string& session_id, + const base::FilePath& browser_context_dir, + size_t max_file_size_bytes, + int output_period_ms, + size_t web_app_id, + std::string* log_id, + std::string* error_message) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(log_id); + DCHECK(log_id->empty()); + DCHECK(error_message); + DCHECK(error_message->empty()); + + if (output_period_ms < 0) { + output_period_ms = kDefaultOutputPeriodMs; + } + + if (!AreLogParametersValid(max_file_size_bytes, output_period_ms, web_app_id, + error_message)) { + // |error_message| will have been set by AreLogParametersValid(). + DCHECK(!error_message->empty()) << "AreLogParametersValid() reported an " + "error without an error message."; + UmaRecordWebRtcEventLoggingApi(WebRtcEventLoggingApiUma::kInvalidArguments); + return false; + } + + if (session_id.empty()) { + *error_message = kStartRemoteLoggingFailureUnknownOrInactivePeerConnection; + UmaRecordWebRtcEventLoggingApi(WebRtcEventLoggingApiUma::kIllegalSessionId); + return false; + } + + if (!BrowserContextEnabled(browser_context_id)) { + // Remote-bound event logging has either not yet been enabled for this + // BrowserContext, or has been recently disabled. This error should not + // really be reached, barring a timing issue. + *error_message = kStartRemoteLoggingFailureLoggingDisabledBrowserContext; + UmaRecordWebRtcEventLoggingApi( + WebRtcEventLoggingApiUma::kDisabledBrowserContext); + return false; + } + + PeerConnectionKey key; + if (!FindPeerConnection(render_process_id, session_id, &key)) { + *error_message = kStartRemoteLoggingFailureUnknownOrInactivePeerConnection; + UmaRecordWebRtcEventLoggingApi( + WebRtcEventLoggingApiUma::kUnknownOrInvalidPeerConnection); + return false; + } + + // May not restart active remote logs. + auto it = active_logs_.find(key); + if (it != active_logs_.end()) { + LOG(ERROR) << "Remote logging already underway for " << session_id << "."; + *error_message = kStartRemoteLoggingFailureAlreadyLogging; + UmaRecordWebRtcEventLoggingApi(WebRtcEventLoggingApiUma::kAlreadyLogging); + return false; + } + + // This is a good opportunity to prune the list of pending logs, potentially + // making room for this file. + PrunePendingLogs(); + + if (!AdditionalActiveLogAllowed(key.browser_context_id)) { + *error_message = kStartRemoteLoggingFailureNoAdditionalActiveLogsAllowed; + UmaRecordWebRtcEventLoggingApi( + WebRtcEventLoggingApiUma::kNoAdditionalLogsAllowed); + return false; + } + + return StartWritingLog(key, browser_context_dir, max_file_size_bytes, + output_period_ms, web_app_id, log_id, error_message); +} + +bool WebRtcRemoteEventLogManager::EventLogWrite(const PeerConnectionKey& key, + const std::string& message) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + auto it = active_logs_.find(key); + if (it == active_logs_.end()) { + return false; + } + + const bool write_successful = it->second->Write(message); + if (!write_successful || it->second->MaxSizeReached()) { + // Note: If the file is invalid, CloseLogFile() will discard it. + CloseLogFile(it, /*make_pending=*/true); + ManageUploadSchedule(); + } + + return write_successful; +} + +void WebRtcRemoteEventLogManager::ClearCacheForBrowserContext( + BrowserContextId browser_context_id, + const base::Time& delete_begin, + const base::Time& delete_end) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + // Rationale for the order: + // 1. Active logs cancelled. This has no side effects, and can be safely + // done before anything else. + // 2. Pending logs removed, before they can be considered as the + // next log to be uploaded. This may cause history files to be created. + // 3. Remove history files, including those that #2 might have created. + // 4. Cancel any active upload precisely at a time when nothing being cleared + // by ClearCacheForBrowserContext() could accidentally replace it. + // 5. Explicitly consider uploading, now that things have changed. + MaybeCancelActiveLogs(delete_begin, delete_end, browser_context_id); + MaybeRemovePendingLogs(delete_begin, delete_end, browser_context_id, + /*is_cache_clear=*/true); + MaybeRemoveHistoryFiles(delete_begin, delete_end, browser_context_id); + MaybeCancelUpload(delete_begin, delete_end, browser_context_id); + ManageUploadSchedule(); +} + +void WebRtcRemoteEventLogManager::GetHistory( + BrowserContextId browser_context_id, + base::OnceCallback<void(const std::vector<UploadList::UploadInfo>&)> + reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + std::vector<UploadList::UploadInfo> history; + + if (!BrowserContextEnabled(browser_context_id)) { + LOG(ERROR) << "Unknown |browser_context_id|."; + base::PostTask(FROM_HERE, {content::BrowserThread::UI}, + base::BindOnce(std::move(reply), history)); + return; + } + + PrunePendingLogs(browser_context_id); + + const base::Time now = base::Time::Now(); + + std::set<WebRtcEventLogHistoryFileReader> history_files = + PruneAndLoadHistoryFilesForBrowserContext( + base::Time::Min(), now - kHistoryFileRetention, browser_context_id); + for (const auto& history_file : history_files) { + history.push_back(history::CreateEntryFromHistoryFileReader(history_file)); + } + + for (const WebRtcLogFileInfo& log_info : pending_logs_) { + if (browser_context_id == log_info.browser_context_id) { + history.push_back(history::CreatePendingLogEntry(log_info)); + } + } + + for (const auto& it : active_logs_) { + if (browser_context_id == it.first.browser_context_id) { + history.push_back( + history::CreateActivelyCapturedLogEntry(it.second->path(), now)); + } + } + + if (uploader_) { + const WebRtcLogFileInfo log_info = uploader_->GetWebRtcLogFileInfo(); + if (browser_context_id == log_info.browser_context_id) { + history.push_back(history::CreateActivelyUploadedLogEntry(log_info, now)); + } + } + + // Sort according to capture time, for consistent orders regardless of + // future operations on the log files. + auto cmp = [](const UploadList::UploadInfo& lhs, + const UploadList::UploadInfo& rhs) { + if (lhs.capture_time == rhs.capture_time) { + // Resolve ties arbitrarily, but consistently. (Local ID expected to be + // distinct for distinct items; if not, anything goes.) + return lhs.local_id < rhs.local_id; + } + return (lhs.capture_time < rhs.capture_time); + }; + std::sort(history.begin(), history.end(), cmp); + + base::PostTask(FROM_HERE, {content::BrowserThread::UI}, + base::BindOnce(std::move(reply), history)); +} + +void WebRtcRemoteEventLogManager::RemovePendingLogsForNotEnabledBrowserContext( + BrowserContextId browser_context_id, + const base::FilePath& browser_context_dir) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(!BrowserContextEnabled(browser_context_id)); + const base::FilePath remote_bound_logs_dir = + GetRemoteBoundWebRtcEventLogsDir(browser_context_dir); + if (!base::DeleteFile(remote_bound_logs_dir, /*recursive=*/true)) { + LOG(ERROR) << "Failed to delete `" << remote_bound_logs_dir << "."; + } +} + +void WebRtcRemoteEventLogManager::RenderProcessHostExitedDestroyed( + int render_process_id) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + // Remove all of the peer connections associated with this render process. + // It's important to do this before closing the actual files, because closing + // files can trigger a new upload if no active peer connections are present. + auto pc_it = active_peer_connections_.begin(); + while (pc_it != active_peer_connections_.end()) { + if (pc_it->first.render_process_id == render_process_id) { + pc_it = active_peer_connections_.erase(pc_it); + } else { + ++pc_it; + } + } + + // Close all of the files that were associated with peer connections which + // belonged to this render process. + auto log_it = active_logs_.begin(); + while (log_it != active_logs_.end()) { + if (log_it->first.render_process_id == render_process_id) { + log_it = CloseLogFile(log_it, /*make_pending=*/true); + } else { + ++log_it; + } + } + + ManageUploadSchedule(); +} + +void WebRtcRemoteEventLogManager::OnConnectionChanged( + network::mojom::ConnectionType type) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + // Even if switching from WiFi to Ethernet, or between to WiFi connections, + // reset the timer (if running) until an upload is permissible due to stable + // upload-supporting conditions. + time_when_upload_conditions_met_ = base::TimeTicks(); + + uploading_supported_for_connection_type_ = + UploadSupportedUsingConnectionType(type); + + ManageUploadSchedule(); + + // TODO(crbug.com/775415): Support pausing uploads when connection goes down, + // or switches to an unsupported connection type. +} + +void WebRtcRemoteEventLogManager::SetWebRtcEventLogUploaderFactoryForTesting( + std::unique_ptr<WebRtcEventLogUploader::Factory> uploader_factory) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(uploader_factory); + uploader_factory_ = std::move(uploader_factory); +} + +void WebRtcRemoteEventLogManager::UploadConditionsHoldForTesting( + base::OnceCallback<void(bool)> callback) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + base::PostTask(FROM_HERE, {content::BrowserThread::UI}, + base::BindOnce(std::move(callback), UploadConditionsHold())); +} + +void WebRtcRemoteEventLogManager::ShutDownForTesting(base::OnceClosure reply) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + weak_ptr_factory_->InvalidateWeakPtrs(); + weak_ptr_factory_.reset(); + base::PostTask(FROM_HERE, {content::BrowserThread::UI}, + base::BindOnce(std::move(reply))); +} + +bool WebRtcRemoteEventLogManager::AreLogParametersValid( + size_t max_file_size_bytes, + int output_period_ms, + size_t web_app_id, + std::string* error_message) const { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (max_file_size_bytes == kWebRtcEventLogManagerUnlimitedFileSize) { + LOG(WARNING) << "Unlimited file sizes not allowed for remote-bound logs."; + *error_message = kStartRemoteLoggingFailureUnlimitedSizeDisallowed; + return false; + } + + if (max_file_size_bytes < log_file_writer_factory_->MinFileSizeBytes()) { + LOG(WARNING) << "File size below minimum allowed."; + *error_message = kStartRemoteLoggingFailureMaxSizeTooSmall; + return false; + } + + if (max_file_size_bytes > kMaxRemoteLogFileSizeBytes) { + LOG(WARNING) << "File size exceeds maximum allowed."; + *error_message = kStartRemoteLoggingFailureMaxSizeTooLarge; + return false; + } + + if (output_period_ms > kMaxOutputPeriodMs) { + LOG(WARNING) << "Output period (ms) exceeds maximum allowed."; + *error_message = kStartRemoteLoggingFailureOutputPeriodMsTooLarge; + return false; + } + + if (web_app_id < kMinWebRtcEventLogWebAppId || + web_app_id > kMaxWebRtcEventLogWebAppId) { + LOG(WARNING) << "Illegal web-app identifier."; + *error_message = kStartRemoteLoggingFailureIllegalWebAppId; + return false; + } + + return true; +} + +bool WebRtcRemoteEventLogManager::BrowserContextEnabled( + BrowserContextId browser_context_id) const { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + const auto it = enabled_browser_contexts_.find(browser_context_id); + return it != enabled_browser_contexts_.cend(); +} + +WebRtcRemoteEventLogManager::LogFilesMap::iterator +WebRtcRemoteEventLogManager::CloseLogFile(LogFilesMap::iterator it, + bool make_pending) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + const PeerConnectionKey peer_connection = it->first; // Copy, not reference. + + const bool valid_file = it->second->Close(); + if (valid_file) { + if (make_pending) { + // The current time is a good enough approximation of the file's last + // modification time. + const base::Time last_modified = base::Time::Now(); + + // The stopped log becomes a pending log. + const auto emplace_result = + pending_logs_.emplace(peer_connection.browser_context_id, + it->second->path(), last_modified); + DCHECK(emplace_result.second); // No pre-existing entry. + } else { + const base::FilePath log_file_path = it->second->path(); + if (!base::DeleteFile(log_file_path, /*recursive=*/false)) { + LOG(ERROR) << "Failed to delete " << log_file_path << "."; + } + } + } else { // !valid_file + // Close() deleted the file. + UmaRecordWebRtcEventLoggingUpload( + WebRtcEventLoggingUploadUma::kLogFileWriteError); + } + + it = active_logs_.erase(it); + + if (observer_) { + observer_->OnRemoteLogStopped(peer_connection); + } + + return it; +} + +bool WebRtcRemoteEventLogManager::MaybeCreateLogsDirectory( + const base::FilePath& remote_bound_logs_dir) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (base::PathExists(remote_bound_logs_dir)) { + if (!base::DirectoryExists(remote_bound_logs_dir)) { + LOG(ERROR) << "Path for remote-bound logs is taken by a non-directory."; + return false; + } + } else if (!base::CreateDirectory(remote_bound_logs_dir)) { + LOG(ERROR) << "Failed to create the local directory for remote-bound logs."; + return false; + } + + // TODO(crbug.com/775415): Test for appropriate permissions. + + return true; +} + +void WebRtcRemoteEventLogManager::LoadLogsDirectory( + BrowserContextId browser_context_id, + const base::FilePath& remote_bound_logs_dir) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + const auto separator = + base::FilePath::StringType(1, base::FilePath::kExtensionSeparator); + const base::Time now = base::Time::Now(); + + std::set<std::pair<base::FilePath, base::Time>> log_files_to_delete; + std::set<base::FilePath> history_files_to_delete; + + // Iterate over all of the files in the directory; find the ones that need + // to be deleted. Skip unknown files; they may belong to the OS. + base::FileEnumerator enumerator(remote_bound_logs_dir, + /*recursive=*/false, + base::FileEnumerator::FILES); + for (auto path = enumerator.Next(); !path.empty(); path = enumerator.Next()) { + const base::FileEnumerator::FileInfo info = enumerator.GetInfo(); + const base::FilePath::StringType extension = info.GetName().Extension(); + if (extension == separator + kWebRtcEventLogUncompressedExtension || + extension == separator + kWebRtcEventLogGzippedExtension) { + const bool loaded = LoadPendingLogInfo( + browser_context_id, path, enumerator.GetInfo().GetLastModifiedTime()); + if (!loaded) { + log_files_to_delete.insert( + std::make_pair(path, info.GetLastModifiedTime())); + } + } else if (extension == separator + kWebRtcEventLogHistoryExtension) { + auto reader = LoadHistoryFile(browser_context_id, path, base::Time::Min(), + now - kHistoryFileRetention); + if (!reader) { + history_files_to_delete.insert(path); + } + } + } + + // Remove expired logs. + for (const auto& file_to_delete : log_files_to_delete) { + // Produce history file, unless we're discarding this log file precisely + // because we see it has a history file associated. + const base::FilePath& log_file_path = file_to_delete.first; + if (!base::PathExists(GetWebRtcEventLogHistoryFilePath(log_file_path))) { + const base::Time capture_time = file_to_delete.second; + CreateHistoryFile(log_file_path, capture_time); + } + + // Remove the log file itself. + if (!base::DeleteFile(log_file_path, /*recursive=*/false)) { + LOG(ERROR) << "Failed to delete " << file_to_delete.first << "."; + } + } + + // Remove expired history files. + for (const base::FilePath& history_file_path : history_files_to_delete) { + if (!base::DeleteFile(history_file_path, /*recursive=*/false)) { + LOG(ERROR) << "Failed to delete " << history_file_path << "."; + } + } + + ManageUploadSchedule(); +} + +bool WebRtcRemoteEventLogManager::LoadPendingLogInfo( + BrowserContextId browser_context_id, + const base::FilePath& path, + base::Time last_modified) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (!IsValidRemoteBoundLogFilePath(path)) { + return false; + } + + const base::FilePath history_path = GetWebRtcEventLogHistoryFilePath(path); + if (base::PathExists(history_path)) { + // Log file has associated history file, indicating an upload was started + // for it. We should delete the original log from disk. + UmaRecordWebRtcEventLoggingUpload( + WebRtcEventLoggingUploadUma::kIncompletePastUpload); + return false; + } + + const base::Time now = base::Time::Now(); + if (last_modified + kRemoteBoundWebRtcEventLogsMaxRetention < now) { + UmaRecordWebRtcEventLoggingUpload( + WebRtcEventLoggingUploadUma::kExpiredLogFileAtChromeStart); + return false; + } + + auto it = pending_logs_.emplace(browser_context_id, path, last_modified); + DCHECK(it.second); // No pre-existing entry. + + return true; +} + +std::unique_ptr<WebRtcEventLogHistoryFileReader> +WebRtcRemoteEventLogManager::LoadHistoryFile( + BrowserContextId browser_context_id, + const base::FilePath& path, + const base::Time& prune_begin, + const base::Time& prune_end) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (!IsValidRemoteBoundLogFilePath(path)) { + return nullptr; + } + + std::unique_ptr<WebRtcEventLogHistoryFileReader> reader = + WebRtcEventLogHistoryFileReader::Create(path); + if (!reader) { + return nullptr; + } + + const base::Time capture_time = reader->CaptureTime(); + if (prune_begin <= capture_time && capture_time <= prune_end) { + return nullptr; + } + + const base::Time upload_time = reader->UploadTime(); + if (!upload_time.is_null()) { + if (prune_begin <= upload_time && upload_time <= prune_end) { + return nullptr; + } + } + + return reader; +} + +std::set<WebRtcEventLogHistoryFileReader> +WebRtcRemoteEventLogManager::PruneAndLoadHistoryFilesForBrowserContext( + const base::Time& prune_begin, + const base::Time& prune_end, + BrowserContextId browser_context_id) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + std::set<WebRtcEventLogHistoryFileReader> history_files; + + auto browser_contexts_it = enabled_browser_contexts_.find(browser_context_id); + if (browser_contexts_it == enabled_browser_contexts_.end()) { + return history_files; + } + + std::set<base::FilePath> files_to_delete; + + base::FileEnumerator enumerator(browser_contexts_it->second, + /*recursive=*/false, + base::FileEnumerator::FILES); + + for (auto path = enumerator.Next(); !path.empty(); path = enumerator.Next()) { + const base::FileEnumerator::FileInfo info = enumerator.GetInfo(); + const base::FilePath::StringType extension = info.GetName().Extension(); + const auto separator = + base::FilePath::StringType(1, base::FilePath::kExtensionSeparator); + if (extension != separator + kWebRtcEventLogHistoryExtension) { + continue; + } + + if (uploader_) { + const base::FilePath log_path = uploader_->GetWebRtcLogFileInfo().path; + const base::FilePath history_path = + GetWebRtcEventLogHistoryFilePath(log_path); + if (path == history_path) { + continue; + } + } + + auto reader = + LoadHistoryFile(browser_context_id, path, prune_begin, prune_end); + if (reader) { + history_files.insert(std::move(*reader)); + reader.reset(); // |reader| in undetermined state after move(). + } else { // Defective or expired. + files_to_delete.insert(path); + } + } + + // |history_files| is sorted by log capture time in ascending order; + // remove the oldest entries until kMaxWebRtcEventLogHistoryFiles is obeyed. + size_t num_history_files = history_files.size(); + for (auto it = history_files.begin(); + num_history_files > kMaxWebRtcEventLogHistoryFiles; + --num_history_files) { + DCHECK(it != history_files.end()); + files_to_delete.insert(it->path()); + it = history_files.erase(it); + } + + for (const base::FilePath& path : files_to_delete) { + if (!base::DeleteFile(path, /*recursive=*/false)) { + LOG(ERROR) << "Failed to delete " << path << "."; + } + } + + return history_files; +} + +bool WebRtcRemoteEventLogManager::StartWritingLog( + const PeerConnectionKey& key, + const base::FilePath& browser_context_dir, + size_t max_file_size_bytes, + int output_period_ms, + size_t web_app_id, + std::string* log_id_out, + std::string* error_message_out) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + // The log is assigned a universally unique ID (with high probability). + const std::string log_id = CreateWebRtcEventLogId(); + + // Use the log ID as part of the filename. In the highly unlikely event that + // this filename is already taken, or that an earlier log with the same name + // existed and left a history file behind, it will be treated the same way as + // any other failure to start the log file. + // TODO(crbug.com/775415): Add a unit test for above comment. + const base::FilePath remote_logs_dir = + GetRemoteBoundWebRtcEventLogsDir(browser_context_dir); + const base::FilePath log_path = + WebRtcEventLogPath(remote_logs_dir, log_id, web_app_id, + log_file_writer_factory_->Extension()); + + if (base::PathExists(log_path)) { + LOG(ERROR) << "Previously used ID selected."; + *error_message_out = kStartRemoteLoggingFailureFilePathUsedLog; + UmaRecordWebRtcEventLoggingApi( + WebRtcEventLoggingApiUma::kLogPathNotAvailable); + return false; + } + + const base::FilePath history_file_path = + GetWebRtcEventLogHistoryFilePath(log_path); + if (base::PathExists(history_file_path)) { + LOG(ERROR) << "Previously used ID selected."; + *error_message_out = kStartRemoteLoggingFailureFilePathUsedHistory; + UmaRecordWebRtcEventLoggingApi( + WebRtcEventLoggingApiUma::kHistoryPathNotAvailable); + return false; + } + + // The log is now ACTIVE. + DCHECK_NE(max_file_size_bytes, kWebRtcEventLogManagerUnlimitedFileSize); + auto log_file = + log_file_writer_factory_->Create(log_path, max_file_size_bytes); + if (!log_file) { + LOG(ERROR) << "Failed to initialize remote-bound WebRTC event log file."; + *error_message_out = kStartRemoteLoggingFailureFileCreationError; + UmaRecordWebRtcEventLoggingApi( + WebRtcEventLoggingApiUma::kFileCreationError); + return false; + } + const auto it = active_logs_.emplace(key, std::move(log_file)); + DCHECK(it.second); + + observer_->OnRemoteLogStarted(key, it.first->second->path(), + output_period_ms); + + UmaRecordWebRtcEventLoggingApi(WebRtcEventLoggingApiUma::kSuccess); + + *log_id_out = log_id; + return true; +} + +void WebRtcRemoteEventLogManager::MaybeStopRemoteLogging( + const PeerConnectionKey& key) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + const auto it = active_logs_.find(key); + if (it == active_logs_.end()) { + return; + } + + CloseLogFile(it, /*make_pending=*/true); + + ManageUploadSchedule(); +} + +void WebRtcRemoteEventLogManager::PrunePendingLogs( + base::Optional<BrowserContextId> browser_context_id) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + MaybeRemovePendingLogs( + base::Time::Min(), + base::Time::Now() - kRemoteBoundWebRtcEventLogsMaxRetention, + browser_context_id, /*is_cache_clear=*/false); +} + +void WebRtcRemoteEventLogManager::RecurringlyPrunePendingLogs() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(!proactive_pending_logs_prune_delta_.is_zero()); + DCHECK(proactive_prune_scheduling_started_); + + PrunePendingLogs(); + + task_runner_->PostDelayedTask( + FROM_HERE, + base::BindOnce(&WebRtcRemoteEventLogManager::RecurringlyPrunePendingLogs, + weak_ptr_factory_->GetWeakPtr()), + proactive_pending_logs_prune_delta_); +} + +void WebRtcRemoteEventLogManager::PruneHistoryFiles() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + for (auto it = enabled_browser_contexts_.begin(); + it != enabled_browser_contexts_.end(); ++it) { + const BrowserContextId browser_context_id = it->first; + MaybeRemoveHistoryFiles(base::Time::Min(), + base::Time::Now() - kHistoryFileRetention, + browser_context_id); + } +} + +void WebRtcRemoteEventLogManager::RecurringlyPruneHistoryFiles() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(proactive_prune_scheduling_started_); + + PruneHistoryFiles(); + + task_runner_->PostDelayedTask( + FROM_HERE, + base::BindOnce(&WebRtcRemoteEventLogManager::RecurringlyPruneHistoryFiles, + weak_ptr_factory_->GetWeakPtr()), + kProactiveHistoryFilesPruneDelta); +} + +void WebRtcRemoteEventLogManager::MaybeCancelActiveLogs( + const base::Time& delete_begin, + const base::Time& delete_end, + BrowserContextId browser_context_id) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + for (auto it = active_logs_.begin(); it != active_logs_.end();) { + // Since the file is active, assume it's still being modified. + if (MatchesFilter(it->first.browser_context_id, base::Time::Now(), + browser_context_id, delete_begin, delete_end)) { + UmaRecordWebRtcEventLoggingUpload( + WebRtcEventLoggingUploadUma::kActiveLogCancelledDueToCacheClear); + it = CloseLogFile(it, /*make_pending=*/false); + } else { + ++it; + } + } +} + +void WebRtcRemoteEventLogManager::MaybeRemovePendingLogs( + const base::Time& delete_begin, + const base::Time& delete_end, + base::Optional<BrowserContextId> browser_context_id, + bool is_cache_clear) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + for (auto it = pending_logs_.begin(); it != pending_logs_.end();) { + if (MatchesFilter(it->browser_context_id, it->last_modified, + browser_context_id, delete_begin, delete_end)) { + UmaRecordWebRtcEventLoggingUpload( + is_cache_clear + ? WebRtcEventLoggingUploadUma::kPendingLogDeletedDueToCacheClear + : WebRtcEventLoggingUploadUma::kExpiredLogFileDuringSession); + + if (!base::DeleteFile(it->path, /*recursive=*/false)) { + LOG(ERROR) << "Failed to delete " << it->path << "."; + } + + // Produce a history file (they have longer retention) to replace the log. + if (is_cache_clear) { // Will be immediately deleted otherwise. + CreateHistoryFile(it->path, it->last_modified); + } + + it = pending_logs_.erase(it); + } else { + ++it; + } + } + + // The last pending log might have been removed. + if (!UploadConditionsHold()) { + time_when_upload_conditions_met_ = base::TimeTicks(); + } +} + +void WebRtcRemoteEventLogManager::MaybeRemoveHistoryFiles( + const base::Time& delete_begin, + const base::Time& delete_end, + BrowserContextId browser_context_id) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + PruneAndLoadHistoryFilesForBrowserContext(delete_begin, delete_end, + browser_context_id); + return; +} + +void WebRtcRemoteEventLogManager::MaybeCancelUpload( + const base::Time& delete_begin, + const base::Time& delete_end, + BrowserContextId browser_context_id) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (!uploader_) { + return; + } + + const WebRtcLogFileInfo& info = uploader_->GetWebRtcLogFileInfo(); + if (!MatchesFilter(info.browser_context_id, info.last_modified, + browser_context_id, delete_begin, delete_end)) { + return; + } + + // Cancel the upload. + // * If the upload has asynchronously completed by now, the uploader would + // have posted a task back to our queue to delete it and move on to the + // next file; cancellation is reported as unsuccessful in that case. In that + // case, we avoid resetting |uploader_| until that callback task executes. + // * If the upload was still underway when we cancelled it, then we can + // safely reset |uploader_| and move on to the next file the next time + // ManageUploadSchedule() is called. + const bool cancelled = uploader_->Cancel(); + if (cancelled) { + uploader_.reset(); + } +} + +bool WebRtcRemoteEventLogManager::MatchesFilter( + BrowserContextId log_browser_context_id, + const base::Time& log_last_modification, + base::Optional<BrowserContextId> filter_browser_context_id, + const base::Time& filter_range_begin, + const base::Time& filter_range_end) const { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + if (filter_browser_context_id && + *filter_browser_context_id != log_browser_context_id) { + return false; + } + return TimePointInRange(log_last_modification, filter_range_begin, + filter_range_end); +} + +bool WebRtcRemoteEventLogManager::AdditionalActiveLogAllowed( + BrowserContextId browser_context_id) const { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + // Limit over concurrently active logs (across BrowserContext-s). + if (active_logs_.size() >= kMaxActiveRemoteBoundWebRtcEventLogs) { + return false; + } + + // Limit over the number of pending logs (per BrowserContext). We count active + // logs too, since they become pending logs once completed. + const size_t active_count = std::count_if( + active_logs_.begin(), active_logs_.end(), + [browser_context_id](const decltype(active_logs_)::value_type& log) { + return log.first.browser_context_id == browser_context_id; + }); + const size_t pending_count = std::count_if( + pending_logs_.begin(), pending_logs_.end(), + [browser_context_id](const decltype(pending_logs_)::value_type& log) { + return log.browser_context_id == browser_context_id; + }); + return active_count + pending_count < kMaxPendingRemoteBoundWebRtcEventLogs; +} + +bool WebRtcRemoteEventLogManager::UploadSuppressed() const { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + return !upload_suppression_disabled_ && !active_peer_connections_.empty(); +} + +bool WebRtcRemoteEventLogManager::UploadConditionsHold() const { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + return !uploader_ && !pending_logs_.empty() && !UploadSuppressed() && + uploading_supported_for_connection_type_; +} + +void WebRtcRemoteEventLogManager::ManageUploadSchedule() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + PrunePendingLogs(); // Avoid uploading freshly expired files. + + if (!UploadConditionsHold()) { + time_when_upload_conditions_met_ = base::TimeTicks(); + return; + } + + if (!time_when_upload_conditions_met_.is_null()) { + // Conditions have been holding for a while; MaybeStartUploading() has + // already been scheduled when |time_when_upload_conditions_met_| was set. + return; + } + + ++scheduled_upload_tasks_; + + time_when_upload_conditions_met_ = base::TimeTicks::Now(); + + task_runner_->PostDelayedTask( + FROM_HERE, + base::BindOnce(&WebRtcRemoteEventLogManager::MaybeStartUploading, + weak_ptr_factory_->GetWeakPtr()), + upload_delay_); +} + +void WebRtcRemoteEventLogManager::MaybeStartUploading() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_GT(scheduled_upload_tasks_, 0u); + + // Since MaybeStartUploading() was scheduled, conditions might have stopped + // holding at some point. They may have even stopped and started several times + // while the currently running task was scheduled, meaning several tasks could + // be pending now, only the last of which should really end up uploading. + + if (time_when_upload_conditions_met_.is_null()) { + // Conditions no longer hold; no way to know how many (now irrelevant) other + // similar tasks are pending, if any. + } else if (base::TimeTicks::Now() - time_when_upload_conditions_met_ < + upload_delay_) { + // Conditions have stopped holding, then started holding again; there has + // to be a more recent task scheduled, that will take over later. + DCHECK_GT(scheduled_upload_tasks_, 1u); + } else { + // It's up to the rest of the code to turn |scheduled_upload_tasks_| off + // if the conditions have at some point stopped holding, or it wouldn't + // know to turn it on when they resume. + DCHECK(UploadConditionsHold()); + + // When the upload we're about to start finishes, there will be another + // delay of length |upload_delay_| before the next one starts. + time_when_upload_conditions_met_ = base::TimeTicks(); + + auto callback = base::BindOnce( + &WebRtcRemoteEventLogManager::OnWebRtcEventLogUploadComplete, + weak_ptr_factory_->GetWeakPtr()); + + // The uploader takes ownership of the file; it's no longer considered to be + // pending. (If the upload fails, the log will be deleted.) + // TODO(crbug.com/775415): Add more refined retry behavior, so that we would + // not delete the log permanently if the network is just down, on the one + // hand, but also would not be uploading unlimited data on endless retries + // on the other hand. + // TODO(crbug.com/775415): Rename the file before uploading, so that we + // would not retry the upload after restarting Chrome, if the upload is + // interrupted. + uploader_ = + uploader_factory_->Create(*pending_logs_.begin(), std::move(callback)); + pending_logs_.erase(pending_logs_.begin()); + } + + --scheduled_upload_tasks_; +} + +void WebRtcRemoteEventLogManager::OnWebRtcEventLogUploadComplete( + const base::FilePath& log_file, + bool upload_successful) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(uploader_); + uploader_.reset(); + ManageUploadSchedule(); +} + +bool WebRtcRemoteEventLogManager::FindPeerConnection( + int render_process_id, + const std::string& session_id, + PeerConnectionKey* key) const { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(!session_id.empty()); + + const auto it = FindNextPeerConnection(active_peer_connections_.cbegin(), + render_process_id, session_id); + if (it == active_peer_connections_.cend()) { + return false; + } + + // Make sure that the session ID is unique for the renderer process, + // though not necessarily between renderer processes. + // (The helper exists solely to allow this DCHECK.) + DCHECK(FindNextPeerConnection(std::next(it), render_process_id, session_id) == + active_peer_connections_.cend()); + + *key = it->first; + return true; +} + +WebRtcRemoteEventLogManager::PeerConnectionMap::const_iterator +WebRtcRemoteEventLogManager::FindNextPeerConnection( + PeerConnectionMap::const_iterator begin, + int render_process_id, + const std::string& session_id) const { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(!session_id.empty()); + const auto end = active_peer_connections_.cend(); + for (auto it = begin; it != end; ++it) { + if (it->first.render_process_id == render_process_id && + it->second == session_id) { + return it; + } + } + return end; +} + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_remote.h b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_remote.h new file mode 100644 index 00000000000..ff1f93a93e6 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_remote.h @@ -0,0 +1,487 @@ +// Copyright 2018 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_REMOTE_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_REMOTE_H_ + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/memory/weak_ptr.h" +#include "base/optional.h" +#include "base/sequenced_task_runner.h" +#include "base/time/time.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_history.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_common.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_uploader.h" +#include "components/upload_list/upload_list.h" +#include "services/network/public/cpp/network_connection_tracker.h" + +namespace webrtc_event_logging { + +class WebRtcRemoteEventLogManager final + : public network::NetworkConnectionTracker::NetworkConnectionObserver { + using BrowserContextId = WebRtcEventLogPeerConnectionKey::BrowserContextId; + using LogFilesMap = + std::map<WebRtcEventLogPeerConnectionKey, std::unique_ptr<LogFileWriter>>; + using PeerConnectionKey = WebRtcEventLogPeerConnectionKey; + + public: + WebRtcRemoteEventLogManager( + WebRtcRemoteEventLogsObserver* observer, + scoped_refptr<base::SequencedTaskRunner> task_runner); + ~WebRtcRemoteEventLogManager() override; + + // Sets a network::NetworkConnectionTracker which will be used to track + // network connectivity. + // Must not be called more than once. + // Must be called before any call to EnableForBrowserContext(). + void SetNetworkConnectionTracker( + network::NetworkConnectionTracker* network_connection_tracker); + + // Sets a LogFileWriter factory. + // Must not be called more than once. + // Must be called before any call to EnableForBrowserContext(). + void SetLogFileWriterFactory( + std::unique_ptr<LogFileWriter::Factory> log_file_writer_factory); + + // Enables remote-bound logging for a given BrowserContext. Logs stored during + // previous sessions become eligible for upload, and recording of new logs for + // peer connections associated with this BrowserContext, in the + // BrowserContext's user-data directory, becomes possible. + // This method would typically be called when a BrowserContext is initialized. + // Enabling for the same BrowserContext twice in a row, without disabling + // in between, is an error. + void EnableForBrowserContext(BrowserContextId browser_context_id, + const base::FilePath& browser_context_dir); + + // Disables remote-bound logging for a given BrowserContext. Pending logs from + // earlier (while it was enabled) may no longer be uploaded, additional + // logs will not be created, and any active uploads associated with the + // BrowserContext will be cancelled. + // Disabling for a BrowserContext which was not enabled is not an error, + // because the caller is not required to know whether a previous call + // to EnableForBrowserContext() was successful. + void DisableForBrowserContext(BrowserContextId browser_context_id); + + // Called to inform |this| of peer connections being added/removed. + // This information is used to: + // 1. Make decisions about when to upload previously finished logs. + // 2. When a peer connection is removed, if it was being logged, its log + // changes from ACTIVE to PENDING. + // The return value of both methods indicates only the consistency of the + // information with previously received information (e.g. can't remove a + // peer connection that was never added, etc.). + bool PeerConnectionAdded(const PeerConnectionKey& key); + bool PeerConnectionRemoved(const PeerConnectionKey& key); + + // Called to inform |this| that a peer connection has been associated + // with |session_id|. After this, it is possible to refer to that peer + // connection using StartRemoteLogging() by providing |session_id|. + bool PeerConnectionSessionIdSet(const PeerConnectionKey& key, + const std::string& session_id); + + // Attempt to start logging the WebRTC events of an active peer connection. + // Logging is subject to several restrictions: + // 1. May not log more than kMaxNumberActiveRemoteWebRtcEventLogFiles logs + // at the same time. + // 2. Each browser context may have only kMaxPendingLogFilesPerBrowserContext + // pending logs. Since active logs later become pending logs, it is also + // forbidden to start a remote-bound log that would, once completed, become + // a pending log that would exceed that limit. + // 3. The maximum file size must be sensible. + // + // If all of the restrictions were observed, and if a file was successfully + // created, true will be returned. + // + // If the call succeeds, the log's identifier will be written to |log_id|. + // The log identifier is exactly 32 uppercase ASCII characters from the + // ranges 0-9 and A-F. + // + // The log's filename will also incorporate |web_app_id|. + // |web_app_id| must be between 1 and 99 (inclusive); error otherwise. + // + // If the call fails, an error message is written to |error_message|. + // The error message will be specific to the failure (as opposed to a generic + // one) is produced only if that error message is useful for the caller: + // * Bad parameters. + // * Function called at a time when the caller could know it would fail, + // such as for a peer connection that was already logged. + // We intentionally avoid giving specific errors in some cases, so as + // to avoid leaking information such as having too many active and/or + // pending logs. + bool StartRemoteLogging(int render_process_id, + BrowserContextId browser_context_id, + const std::string& session_id, + const base::FilePath& browser_context_dir, + size_t max_file_size_bytes, + int output_period_ms, + size_t web_app_id, + std::string* log_id, + std::string* error_message); + + // If an active remote-bound log exists for the given peer connection, this + // will append |message| to that log. + // If writing |message| to the log would exceed the log's maximum allowed + // size, the write is disallowed and the file is closed instead (and changes + // from ACTIVE to PENDING). + // If the log file's capacity is exhausted as a result of this function call, + // or if a write error occurs, the file is closed, and the remote-bound log + // changes from ACTIVE to PENDING. + // True is returned if and only if |message| was written in its entirety to + // an active log. + bool EventLogWrite(const PeerConnectionKey& key, const std::string& message); + + // Clear PENDING WebRTC event logs associated with a given browser context, + // in a given time range, then post |reply| back to the thread from which + // the method was originally invoked (which can be any thread). + // Log files currently being written are *not* interrupted. + // Active uploads *are* interrupted. + void ClearCacheForBrowserContext(BrowserContextId browser_context_id, + const base::Time& delete_begin, + const base::Time& delete_end); + + // See documentation of same method in WebRtcEventLogManager for details. + void GetHistory( + BrowserContextId browser_context_id, + base::OnceCallback<void(const std::vector<UploadList::UploadInfo>&)> + reply); + + // Works on not-enabled BrowserContext-s, which means the logs are never made + // eligible for upload. Useful when a BrowserContext is loaded which in + // the past had remote-logging enabled, but no longer does. + void RemovePendingLogsForNotEnabledBrowserContext( + BrowserContextId browser_context_id, + const base::FilePath& browser_context_dir); + + // An implicit PeerConnectionRemoved() on all of the peer connections that + // were associated with the renderer process. + void RenderProcessHostExitedDestroyed(int render_process_id); + + // network::NetworkConnectionTracker::NetworkConnectionObserver implementation + void OnConnectionChanged(network::mojom::ConnectionType type) override; + + // Unit tests may use this to inject null uploaders, or ones which are + // directly controlled by the unit test (succeed or fail according to the + // test's needs). + // Note that for simplicity's sake, this may be called from outside the + // task queue on which this object lives (WebRtcEventLogManager::task_queue_). + // Therefore, if a test calls this, it should call it before it initializes + // any BrowserContext with pending log files in its directory. + void SetWebRtcEventLogUploaderFactoryForTesting( + std::unique_ptr<WebRtcEventLogUploader::Factory> uploader_factory); + + // Exposes UploadConditionsHold() to unit tests. See WebRtcEventLogManager's + // documentation for the rationale. + void UploadConditionsHoldForTesting(base::OnceCallback<void(bool)> callback); + + // In production code, |task_runner_| stops running tasks as part of Chrome's + // shut-down process, before |this| is torn down. In unit tests, this is + // not the case. + void ShutDownForTesting(base::OnceClosure reply); + + private: + using PeerConnectionMap = std::map<PeerConnectionKey, std::string>; + + // Validates log parameters. + // If valid, returns true. Otherwise, false, and |error_message| gets + // a relevant error. + bool AreLogParametersValid(size_t max_file_size_bytes, + int output_period_ms, + size_t web_app_id, + std::string* error_message) const; + + // Checks whether a browser context has already been enabled via a call to + // EnableForBrowserContext(), and not yet disabled using a call to + // DisableForBrowserContext(). + bool BrowserContextEnabled(BrowserContextId browser_context_id) const; + + // Closes an active log file. + // If |make_pending| is true, closing the file changes its state from ACTIVE + // to PENDING. If |make_pending| is false, or if the file couldn't be closed + // correctly, the file will be deleted. + // Returns an iterator to the next ACTIVE file. + LogFilesMap::iterator CloseLogFile(LogFilesMap::iterator it, + bool make_pending); + + // Attempts to create the directory where we'll write the logs, if it does + // not already exist. Returns true if the directory exists (either it already + // existed, or it was successfully created). + bool MaybeCreateLogsDirectory(const base::FilePath& remote_bound_logs_dir); + + // Scans the user data directory associated with the BrowserContext + // associated with the given BrowserContextId remote-bound logs that were + // created during previous Chrome sessions and for history files, + // then process them (discard expired files, etc.) + void LoadLogsDirectory(BrowserContextId browser_context_id, + const base::FilePath& remote_bound_logs_dir); + + // Loads the pending log file whose path is |path|, into the BrowserContext + // indicated by |browser_context_id|. Note that the contents of the file are + // note read by this method. + // Returns true if the file was loaded correctly, and should be kept on disk; + // false if the file was not loaded (e.g. incomplete or expired), and needs + // to be deleted. + bool LoadPendingLogInfo(BrowserContextId browser_context_id, + const base::FilePath& path, + base::Time last_modified); + + // Loads a history file. Returns a WebRtcEventLogHistoryFileReader if the + // file was loaded correctly, and should be kept on disk; nullptr otherwise, + // signaling that the file should be deleted. + // |prune_begin| and |prune_end| define a time range where, if the log falls + // within the range, it will not be loaded. + std::unique_ptr<WebRtcEventLogHistoryFileReader> LoadHistoryFile( + BrowserContextId browser_context_id, + const base::FilePath& path, + const base::Time& prune_begin, + const base::Time& prune_end); + + // Deletes any history logs associated with |browser_context_id| captured or + // uploaded between |prune_begin| and |prune_end|, inclusive, then returns a + // set of readers for the remaining (meaning not-pruned) history files. + std::set<WebRtcEventLogHistoryFileReader> + PruneAndLoadHistoryFilesForBrowserContext( + const base::Time& prune_begin, + const base::Time& prune_end, + BrowserContextId browser_context_id); + + // Attempts the creation of a locally stored file into which a remote-bound + // log may be written. The log-identifier is returned if successful, the empty + // string otherwise. + bool StartWritingLog(const PeerConnectionKey& key, + const base::FilePath& browser_context_dir, + size_t max_file_size_bytes, + int output_period_ms, + size_t web_app_id, + std::string* log_id_out, + std::string* error_message_out); + + // Checks if the referenced peer connection has an associated active + // remote-bound log. If it does, the log is changed from ACTIVE to PENDING. + void MaybeStopRemoteLogging(const PeerConnectionKey& key); + + // Get rid of pending logs whose age exceeds our retention policy. + // On the one hand, we want to remove expired files as soon as possible, but + // on the other hand, we don't want to waste CPU by checking this too often. + // Therefore, we prune pending files: + // 1. When a new BrowserContext is initalized, thereby also pruning the + // pending logs contributed by that BrowserContext. + // 2. Before initiating a new upload, thereby avoiding uploading a file that + // has just now expired. + // 3. On infrequent events - peer connection addition/removal, but NOT + // on something that could potentially be frequent, such as EventLogWrite. + // Note that the last modification date of a file, which is the value measured + // against for retention, is only read from disk once per file, meaning + // this check is not too expensive. + // If a |browser_context_id| is provided, logs are only pruned for it. + void PrunePendingLogs( + base::Optional<BrowserContextId> browser_context_id = base::nullopt); + + // PrunePendingLogs() and schedule the next proactive pending logs prune. + void RecurringlyPrunePendingLogs(); + + // Removes expired history files. + // Since these are small, and since looking for them is not as cheap as + // looking for pending logs, we do not make an effort to remove them as + // soon as possible. + void PruneHistoryFiles(); + + // PruneHistoryFiles() and schedule the next proactive history files prune. + void RecurringlyPruneHistoryFiles(); + + // Cancels and deletes active logs which match the given filter criteria, as + // described by MatchesFilter's documentation. + // This method not trigger any pending logs to be uploaded, allowing it to + // be safely used in a context that clears browsing data. + void MaybeCancelActiveLogs(const base::Time& delete_begin, + const base::Time& delete_end, + BrowserContextId browser_context_id); + + // Removes pending logs files which match the given filter criteria, as + // described by MatchesFilter's documentation. + // This method not trigger any pending logs to be uploaded, allowing it to + // be safely used in a context that clears browsing data. + void MaybeRemovePendingLogs( + const base::Time& delete_begin, + const base::Time& delete_end, + base::Optional<BrowserContextId> browser_context_id, + bool is_cache_clear); + + // Remove all history files associated with |browser_context_id| which were + // either captured or uploaded between |delete_begin| and |delete_end|. + // This method not trigger any pending logs to be uploaded, allowing it to + // be safely used in a context that clears browsing data. + void MaybeRemoveHistoryFiles(const base::Time& delete_begin, + const base::Time& delete_end, + BrowserContextId browser_context_id); + + // If the currently uploaded file matches the given filter criteria, as + // described by MatchesFilter's documentation, the upload will be + // cancelled, and the log file deleted. If this happens, the next pending log + // file will be considered for upload. + // This method is used to ensure that clearing of browsing data by the user + // does not leave the currently-uploaded file on disk, even for the duration + // of the upload. + // This method not trigger any pending logs to be uploaded, allowing it to + // be safely used in a context that clears browsing data. + void MaybeCancelUpload(const base::Time& delete_begin, + const base::Time& delete_end, + BrowserContextId browser_context_id); + + // Checks whether a log file matches a range and (potentially) BrowserContext: + // * A file matches if its last modification date was at or later than + // |filter_range_begin|, and earlier than |filter_range_end|. + // * If a null time-point is given as either |filter_range_begin| or + // |filter_range_end|, it is treated as "beginning-of-time" or + // "end-of-time", respectively. + // * If |filter_browser_context_id| is set, only log files associated with it + // can match the filter. + bool MatchesFilter(BrowserContextId log_browser_context_id, + const base::Time& log_last_modification, + base::Optional<BrowserContextId> filter_browser_context_id, + const base::Time& filter_range_begin, + const base::Time& filter_range_end) const; + + // Return |true| if and only if we can start another active log (with respect + // to limitations on the numbers active and pending logs). + bool AdditionalActiveLogAllowed(BrowserContextId browser_context_id) const; + + // Uploading suppressed while active peer connections exist (unless + // suppression) is turned off from the command line. + bool UploadSuppressed() const; + + // Check whether all the conditions necessary for uploading log files are + // currently satisfied. + // 1. There may be no active peer connections which might be adversely + // affected by the bandwidth consumption of the upload. + // 2. Chrome has a network connection, and that conneciton is either a wired + // one, or WiFi. (That is, not 3G, etc.) + // 3. Naturally, a file pending upload must exist. + bool UploadConditionsHold() const; + + // When the conditions necessary for uploading first hold, schedule a delayed + // task to upload (MaybeStartUploading). If they ever stop holding, void it. + void ManageUploadSchedule(); + + // Posted as a delayed task by ManageUploadSchedule. If not voided until + // executed, will initiate an upload of the next log file. + void MaybeStartUploading(); + + // Callback for the success/failure of an upload. + // When an upload is complete, it might be time to upload the next file. + // Note: |log_file| and |upload_successful| are ignored in production; they + // are used in unit tests, so we keep them here to make things simpler, so + // that this method would match WebRtcEventLogUploader::UploadResultCallback + // without adaptation. + void OnWebRtcEventLogUploadComplete(const base::FilePath& log_file, + bool upload_successful); + + // Given a renderer process ID and peer connection's session ID, find the + // peer connection to which they refer. + bool FindPeerConnection(int render_process_id, + const std::string& session_id, + PeerConnectionKey* key) const; + + // Find the next peer connection in a map to which the renderer process ID + // and session ID refer. + // This helper allows FindPeerConnection() to DCHECK on uniqueness of the ID + // without descending down a recursive rabbit hole. + PeerConnectionMap::const_iterator FindNextPeerConnection( + PeerConnectionMap::const_iterator begin, + int render_process_id, + const std::string& session_id) const; + + // Normally, uploading is suppressed while there are active peer connections. + // This may be disabled from the command line. + const bool upload_suppression_disabled_; + + // The conditions for upload must hold for this much time, uninterrupted, + // before an upload may be initiated. + const base::TimeDelta upload_delay_; + + // If non-zero, every |proactive_pending_logs_prune_delta_|, pending logs + // will be pruned. This avoids them staying around on disk for longer than + // their expiration if no event occurs which triggers reactive pruning. + const base::TimeDelta proactive_pending_logs_prune_delta_; + + // Proactive pruning, if enabled, starts with the first enabled browser + // context. To avoid unnecessary complexity, if that browser context is + // disabled, proactive pruning is not disabled. + bool proactive_prune_scheduling_started_; + + // This is used to inform WebRtcEventLogManager when remote-bound logging + // of a peer connection starts/stops, which allows WebRtcEventLogManager to + // decide when to ask WebRTC to start/stop sending event logs. + WebRtcRemoteEventLogsObserver* const observer_; + + // The IDs of the BrowserContexts for which logging is enabled, mapped to + // the directory where each BrowserContext's remote-bound logs are stored. + std::map<BrowserContextId, base::FilePath> enabled_browser_contexts_; + + // Currently active peer connections, mapped to their session IDs (once the + // session ID is set). + // PeerConnections which have been closed are not considered active, + // regardless of whether they have been torn down. + PeerConnectionMap active_peer_connections_; + + // Creates LogFileWriter instances (compressed/uncompressed, etc.). + std::unique_ptr<LogFileWriter::Factory> log_file_writer_factory_; + + // Remote-bound logs which we're currently in the process of writing to disk. + LogFilesMap active_logs_; + + // Remote-bound logs which have been written to disk before (either during + // this Chrome session or during an earlier one), and which are no waiting to + // be uploaded. + std::set<WebRtcLogFileInfo> pending_logs_; + + // Null if no ongoing upload, or an uploader which owns a file, and is + // currently busy uploading it to a remote server. + std::unique_ptr<WebRtcEventLogUploader> uploader_; + + // Provides notifications of network changes. + network::NetworkConnectionTracker* network_connection_tracker_; + + // Whether the network we are currently connected to, if any, is one over + // which we may upload. + bool uploading_supported_for_connection_type_; + + // If the conditions for initiating an upload do not hold, this will be + // set to an empty base::TimeTicks. + // If the conditions were found to hold, this will record the time when they + // started holding. (It will be set back to 0 if they ever cease holding.) + base::TimeTicks time_when_upload_conditions_met_; + + // This is a vehicle for DCHECKs to ensure code sanity. It counts the number + // of scheduled tasks of MaybeStartUploading(), and proves that we never + // end up with a scheduled upload that never occurs. + size_t scheduled_upload_tasks_; + + // Producer of uploader objects. (In unit tests, this would create + // null-implementation uploaders, or uploaders whose behavior is controlled + // by the unit test.) + std::unique_ptr<WebRtcEventLogUploader::Factory> uploader_factory_; + + // |this| is created and destroyed on the UI thread, but operates on the + // following IO-capable sequenced task runner. + scoped_refptr<base::SequencedTaskRunner> task_runner_; + + // Weak pointer factory. Only expected to be useful for unit tests, because + // in production, |task_runner_| is stopped during shut-down, so tasks will + // either find the pointer to be valid, or not run because the runner has + // already been stopped. + // Note that the unique_ptr is used just to make it clearer that ownership is + // here. In reality, this is never auto-destroyed; see destructor for details. + std::unique_ptr<base::WeakPtrFactory<WebRtcRemoteEventLogManager>> + weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(WebRtcRemoteEventLogManager); +}; + +} // namespace webrtc_event_logging + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_REMOTE_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_unittest.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_unittest.cc new file mode 100644 index 00000000000..e887b44737b --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_unittest.cc @@ -0,0 +1,4934 @@ +// Copyright 2017 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 "chrome/browser/media/webrtc/webrtc_event_log_manager.h" + +#include <algorithm> +#include <list> +#include <map> +#include <memory> +#include <numeric> +#include <queue> +#include <string> +#include <tuple> +#include <utility> +#include <vector> + +#include "base/big_endian.h" +#include "base/bind.h" +#include "base/files/file.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_refptr.h" +#include "base/optional.h" +#include "base/run_loop.h" +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/synchronization/waitable_event.h" +#include "base/test/gtest_util.h" +#include "base/test/scoped_command_line.h" +#include "base/test/scoped_feature_list.h" +#include "base/test/simple_test_clock.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_common.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_unittest_helpers.h" +#include "chrome/browser/policy/profile_policy_connector.h" +#include "chrome/browser/prefs/browser_prefs.h" +#include "chrome/common/chrome_features.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/testing_pref_store.h" +#include "components/sync_preferences/pref_service_mock_factory.h" +#include "components/sync_preferences/pref_service_syncable.h" +#include "content/public/browser/network_service_instance.h" +#include "content/public/test/browser_task_environment.h" +#include "content/public/test/mock_render_process_host.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_network_connection_tracker.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/zlib/google/compression_utils.h" + +#if !defined(OS_ANDROID) && !defined(OS_CHROMEOS) +#include "chrome/browser/policy/chrome_browser_policy_connector.h" +#include "components/policy/core/common/mock_configuration_policy_provider.h" +#include "components/policy/core/common/policy_map.h" +#include "components/policy/core/common/policy_types.h" +#endif + +namespace webrtc_event_logging { + +#if defined(OS_WIN) +#define NumberToStringType base::NumberToString16 +#else +#define NumberToStringType base::NumberToString +#endif + +using ::testing::_; +using ::testing::Invoke; +using ::testing::NiceMock; + +using BrowserContext = content::BrowserContext; +using BrowserContextId = WebRtcEventLogPeerConnectionKey::BrowserContextId; +using MockRenderProcessHost = content::MockRenderProcessHost; +using PeerConnectionKey = WebRtcEventLogPeerConnectionKey; +using RenderProcessHost = content::RenderProcessHost; + +using Compression = WebRtcEventLogCompression; + +namespace { + +#if !defined(OS_ANDROID) + +auto SaveFilePathTo(base::Optional<base::FilePath>* output) { + return [output](PeerConnectionKey ignored_key, base::FilePath file_path, + int output_period_ms = 0) { *output = file_path; }; +} + +auto SaveKeyAndFilePathTo(base::Optional<PeerConnectionKey>* key_output, + base::Optional<base::FilePath>* file_path_output) { + return [key_output, file_path_output](PeerConnectionKey key, + base::FilePath file_path) { + *key_output = key; + *file_path_output = file_path; + }; +} + +const int kMaxActiveRemoteLogFiles = + static_cast<int>(kMaxActiveRemoteBoundWebRtcEventLogs); +const int kMaxPendingRemoteLogFiles = + static_cast<int>(kMaxPendingRemoteBoundWebRtcEventLogs); +const char kSessionId[] = "session_id"; + +base::Time GetLastModificationTime(const base::FilePath& file_path) { + base::File::Info file_info; + if (!base::GetFileInfo(file_path, &file_info)) { + EXPECT_TRUE(false); + return base::Time(); + } + return file_info.last_modified; +} + +#endif + +// Common default/arbitrary values. +constexpr int kLid = 478; +constexpr size_t kWebAppId = 42; + +PeerConnectionKey GetPeerConnectionKey(RenderProcessHost* rph, int lid) { + const BrowserContext* browser_context = rph->GetBrowserContext(); + const auto browser_context_id = GetBrowserContextId(browser_context); + return PeerConnectionKey(rph->GetID(), lid, browser_context_id); +} + +bool CreateRemoteBoundLogFile(const base::FilePath& dir, + size_t web_app_id, + const base::FilePath::StringPieceType& extension, + base::Time capture_time, + base::FilePath* file_path, + base::File* file) { + *file_path = + dir.AsEndingWithSeparator() + .InsertBeforeExtensionASCII(kRemoteBoundWebRtcEventLogFileNamePrefix) + .InsertBeforeExtensionASCII("_") + .InsertBeforeExtensionASCII(std::to_string(web_app_id)) + .InsertBeforeExtensionASCII("_") + .InsertBeforeExtensionASCII(CreateWebRtcEventLogId()) + .AddExtension(extension); + + constexpr int file_flags = base::File::FLAG_CREATE | base::File::FLAG_WRITE | + base::File::FLAG_EXCLUSIVE_WRITE; + file->Initialize(*file_path, file_flags); + if (!file->IsValid() || !file->created()) { + return false; + } + + if (!base::TouchFile(*file_path, capture_time, capture_time)) { + return false; + } + + return true; +} + +// This implementation does not upload files, nor pretends to have finished an +// upload. Most importantly, it does not get rid of the locally-stored log file +// after finishing a simulated upload; this is useful because it keeps the file +// on disk, where unit tests may inspect it. +// This class enforces an expectation over the upload being cancelled or not. +class NullWebRtcEventLogUploader : public WebRtcEventLogUploader { + public: + NullWebRtcEventLogUploader(const WebRtcLogFileInfo& log_file, + bool cancellation_expected) + : log_file_(log_file), + cancellation_expected_(cancellation_expected), + was_cancelled_(false) {} + + ~NullWebRtcEventLogUploader() override { + EXPECT_EQ(was_cancelled_, cancellation_expected_); + } + + const WebRtcLogFileInfo& GetWebRtcLogFileInfo() const override { + return log_file_; + } + + bool Cancel() override { + EXPECT_TRUE(cancellation_expected_); + if (was_cancelled_) { // Should not be called more than once. + EXPECT_TRUE(false); + return false; + } + was_cancelled_ = true; + return true; + } + + class Factory : public WebRtcEventLogUploader::Factory { + public: + Factory(bool cancellation_expected, + base::Optional<size_t> expected_instance_count = base::nullopt) + : cancellation_expected_(cancellation_expected), + expected_instance_count_(expected_instance_count), + instance_count_(0) {} + + ~Factory() override { + if (expected_instance_count_.has_value()) { + EXPECT_EQ(instance_count_, expected_instance_count_.value()); + } + } + + std::unique_ptr<WebRtcEventLogUploader> Create( + const WebRtcLogFileInfo& log_file, + UploadResultCallback callback) override { + if (expected_instance_count_.has_value()) { + EXPECT_LE(++instance_count_, expected_instance_count_.value()); + } + return std::make_unique<NullWebRtcEventLogUploader>( + log_file, cancellation_expected_); + } + + private: + const bool cancellation_expected_; + const base::Optional<size_t> expected_instance_count_; + size_t instance_count_; + }; + + private: + const WebRtcLogFileInfo log_file_; + const bool cancellation_expected_; + bool was_cancelled_; +}; + +class MockWebRtcLocalEventLogsObserver : public WebRtcLocalEventLogsObserver { + public: + ~MockWebRtcLocalEventLogsObserver() override = default; + MOCK_METHOD2(OnLocalLogStarted, + void(PeerConnectionKey, const base::FilePath&)); + MOCK_METHOD1(OnLocalLogStopped, void(PeerConnectionKey)); +}; + +class MockWebRtcRemoteEventLogsObserver : public WebRtcRemoteEventLogsObserver { + public: + ~MockWebRtcRemoteEventLogsObserver() override = default; + MOCK_METHOD3(OnRemoteLogStarted, + void(PeerConnectionKey, const base::FilePath&, int)); + MOCK_METHOD1(OnRemoteLogStopped, void(PeerConnectionKey)); +}; + +} // namespace + +class WebRtcEventLogManagerTestBase : public ::testing::Test { + public: + WebRtcEventLogManagerTestBase() + : test_shared_url_loader_factory_( + base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( + &test_url_loader_factory_)), + run_loop_(std::make_unique<base::RunLoop>()), + uploader_run_loop_(std::make_unique<base::RunLoop>()), + browser_context_(nullptr), + browser_context_id_(GetBrowserContextId(browser_context_.get())) { + TestingBrowserProcess::GetGlobal()->SetSharedURLLoaderFactory( + test_shared_url_loader_factory_); + + // Avoid proactive pruning; it has the potential to mess up tests, as well + // as keep pendings tasks around with a dangling reference to the unit + // under test. (Zero is a sentinel value for disabling proactive pruning.) + scoped_command_line_.GetProcessCommandLine()->AppendSwitchASCII( + ::switches::kWebRtcRemoteEventLogProactivePruningDelta, "0"); + + EXPECT_TRUE(local_logs_base_dir_.CreateUniqueTempDir()); + local_logs_base_path_ = local_logs_base_dir_.GetPath().Append( + FILE_PATH_LITERAL("local_event_logs")); + + EXPECT_TRUE(profiles_dir_.CreateUniqueTempDir()); + } + + ~WebRtcEventLogManagerTestBase() override { + WaitForPendingTasks(); + + base::RunLoop run_loop; + event_log_manager_->ShutDownForTesting(run_loop.QuitClosure()); + run_loop.Run(); + + // We do not want to satisfy any unsatisfied expectations by destroying + // |rph_|, |browser_context_|, etc., at the end of the test, before we + // destroy |event_log_manager_|. However, we must also make sure that their + // destructors do not attempt to access |event_log_manager_|, which in + // normal code lives forever, but not in the unit tests. + event_log_manager_.reset(); + + // Guard against unexpected state changes. + EXPECT_TRUE(webrtc_state_change_instructions_.empty()); + } + + void SetUp() override { + SetUpNetworkConnection(true, + network::mojom::ConnectionType::CONNECTION_ETHERNET); + SetLocalLogsObserver(&local_observer_); + SetRemoteLogsObserver(&remote_observer_); + LoadMainTestProfile(); +#if !defined(OS_ANDROID) && !defined(OS_CHROMEOS) + policy::BrowserPolicyConnectorBase::SetPolicyProviderForTesting(&provider_); +#endif + } + + void TearDown() override { +#if !defined(OS_ANDROID) && !defined(OS_CHROMEOS) + TestingBrowserProcess::GetGlobal()->ShutdownBrowserPolicyConnector(); +#endif + } + + void SetUpNetworkConnection(bool respond_synchronously, + network::mojom::ConnectionType connection_type) { + auto* tracker = network::TestNetworkConnectionTracker::GetInstance(); + tracker->SetRespondSynchronously(respond_synchronously); + tracker->SetConnectionType(connection_type); + } + + void SetConnectionType(network::mojom::ConnectionType connection_type) { + network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType( + connection_type); + } + + void CreateWebRtcEventLogManager( + base::Optional<Compression> remote = base::nullopt) { + DCHECK(!event_log_manager_); + + event_log_manager_ = WebRtcEventLogManager::CreateSingletonInstance(); + + local_log_extension_ = kWebRtcEventLogUncompressedExtension; + + if (remote.has_value()) { + auto factory = CreateLogFileWriterFactory(remote.value()); + remote_log_extension_ = factory->Extension(); + event_log_manager_->SetRemoteLogFileWriterFactoryForTesting( + std::move(factory)); + } else { + // kWebRtcRemoteEventLogGzipped is turned on by default. + remote_log_extension_ = kWebRtcEventLogGzippedExtension; + } + } + + void LoadMainTestProfile() { + browser_context_ = CreateBrowserContext("browser_context_"); + browser_context_id_ = GetBrowserContextId(browser_context_.get()); + rph_ = std::make_unique<MockRenderProcessHost>(browser_context_.get()); + } + + void UnloadMainTestProfile() { + rph_.reset(); + browser_context_.reset(); + browser_context_id_ = GetBrowserContextId(browser_context_.get()); + } + + void WaitForReply() { + run_loop_->Run(); + run_loop_.reset(new base::RunLoop); // Allow re-blocking. + } + + void Reply() { run_loop_->QuitWhenIdle(); } + + base::OnceClosure ReplyClosure() { + // Intermediary pointer used to help the compiler distinguish between + // the overloaded Reply() functions. + void (WebRtcEventLogManagerTestBase::*function)() = + &WebRtcEventLogManagerTestBase::Reply; + return base::BindOnce(function, base::Unretained(this)); + } + + template <typename T> + void Reply(T* output, T val) { + *output = val; + run_loop_->QuitWhenIdle(); + } + + template <typename T> + base::OnceCallback<void(T)> ReplyClosure(T* output) { + // Intermediary pointer used to help the compiler distinguish between + // the overloaded Reply() functions. + void (WebRtcEventLogManagerTestBase::*function)(T*, T) = + &WebRtcEventLogManagerTestBase::Reply; + return base::BindOnce(function, base::Unretained(this), output); + } + + void Reply(bool* output_bool, + std::string* output_str1, + std::string* output_str2, + bool bool_val, + const std::string& str1_val, + const std::string& str2_val) { + *output_bool = bool_val; + *output_str1 = str1_val; + *output_str2 = str2_val; + run_loop_->QuitWhenIdle(); + } + + base::OnceCallback<void(bool, const std::string&, const std::string&)> + ReplyClosure(bool* output_bool, + std::string* output_str1, + std::string* output_str2) { + // Intermediary pointer used to help the compiler distinguish between + // the overloaded Reply() functions. + void (WebRtcEventLogManagerTestBase::*function)( + bool*, std::string*, std::string*, bool, const std::string&, + const std::string&) = &WebRtcEventLogManagerTestBase::Reply; + return base::BindOnce(function, base::Unretained(this), output_bool, + output_str1, output_str2); + } + + bool PeerConnectionAdded(const PeerConnectionKey& key) { + bool result; + event_log_manager_->PeerConnectionAdded(key.render_process_id, key.lid, + ReplyClosure(&result)); + WaitForReply(); + return result; + } + + bool PeerConnectionRemoved(const PeerConnectionKey& key) { + bool result; + event_log_manager_->PeerConnectionRemoved(key.render_process_id, key.lid, + ReplyClosure(&result)); + WaitForReply(); + return result; + } + + bool PeerConnectionSessionIdSet(const PeerConnectionKey& key, + const std::string& session_id) { + bool result; + event_log_manager_->PeerConnectionSessionIdSet( + key.render_process_id, key.lid, session_id, ReplyClosure(&result)); + WaitForReply(); + return result; + } + + bool PeerConnectionSessionIdSet(const PeerConnectionKey& key) { + return PeerConnectionSessionIdSet(key, GetUniqueId(key)); + } + + bool PeerConnectionStopped(const PeerConnectionKey& key) { + bool result; + event_log_manager_->PeerConnectionStopped(key.render_process_id, key.lid, + ReplyClosure(&result)); + WaitForReply(); + return result; + } + + bool EnableLocalLogging( + size_t max_size_bytes = kWebRtcEventLogManagerUnlimitedFileSize) { + return EnableLocalLogging(local_logs_base_path_, max_size_bytes); + } + + bool EnableLocalLogging( + base::FilePath local_logs_base_path, + size_t max_size_bytes = kWebRtcEventLogManagerUnlimitedFileSize) { + bool result; + event_log_manager_->EnableLocalLogging(local_logs_base_path, max_size_bytes, + ReplyClosure(&result)); + WaitForReply(); + return result; + } + + bool DisableLocalLogging() { + bool result; + event_log_manager_->DisableLocalLogging(ReplyClosure(&result)); + WaitForReply(); + return result; + } + + bool StartRemoteLogging(const PeerConnectionKey& key, + const std::string& session_id, + size_t max_size_bytes, + int output_period_ms, + size_t web_app_id, + std::string* log_id_output = nullptr, + std::string* error_message_output = nullptr) { + bool result; + std::string log_id; + std::string error_message; + + event_log_manager_->StartRemoteLogging( + key.render_process_id, session_id, max_size_bytes, output_period_ms, + web_app_id, ReplyClosure(&result, &log_id, &error_message)); + + WaitForReply(); + + // If successful, only |log_id|. If unsuccessful, only |error_message| set. + DCHECK_EQ(result, !log_id.empty()); + DCHECK_EQ(!result, !error_message.empty()); + + if (log_id_output) { + *log_id_output = log_id; + } + + if (error_message_output) { + *error_message_output = error_message; + } + + return result; + } + + bool StartRemoteLogging(const PeerConnectionKey& key, + const std::string& session_id, + std::string* log_id_output = nullptr, + std::string* error_message_output = nullptr) { + return StartRemoteLogging(key, session_id, kMaxRemoteLogFileSizeBytes, 0, + kWebAppId, log_id_output, error_message_output); + } + + bool StartRemoteLogging(const PeerConnectionKey& key, + std::string* log_id_output = nullptr, + std::string* error_message_output = nullptr) { + return StartRemoteLogging(key, GetUniqueId(key), kMaxRemoteLogFileSizeBytes, + 0, kWebAppId, log_id_output, + error_message_output); + } + + void ClearCacheForBrowserContext( + const content::BrowserContext* browser_context, + const base::Time& delete_begin, + const base::Time& delete_end) { + event_log_manager_->ClearCacheForBrowserContext( + browser_context, delete_begin, delete_end, ReplyClosure()); + WaitForReply(); + } + + std::vector<UploadList::UploadInfo> GetHistory( + BrowserContextId browser_context_id) { + std::vector<UploadList::UploadInfo> result; + + base::RunLoop run_loop; + + auto reply = [](base::RunLoop* run_loop, + std::vector<UploadList::UploadInfo>* output, + const std::vector<UploadList::UploadInfo>& input) { + *output = input; + run_loop->Quit(); + }; + event_log_manager_->GetHistory(browser_context_id, + base::BindOnce(reply, &run_loop, &result)); + run_loop.Run(); + + return result; + } + + void SetLocalLogsObserver(WebRtcLocalEventLogsObserver* observer) { + event_log_manager_->SetLocalLogsObserver(observer, ReplyClosure()); + WaitForReply(); + } + + void SetRemoteLogsObserver(WebRtcRemoteEventLogsObserver* observer) { + event_log_manager_->SetRemoteLogsObserver(observer, ReplyClosure()); + WaitForReply(); + } + + void SetWebRtcEventLogUploaderFactoryForTesting( + std::unique_ptr<WebRtcEventLogUploader::Factory> factory) { + event_log_manager_->SetWebRtcEventLogUploaderFactoryForTesting( + std::move(factory), ReplyClosure()); + WaitForReply(); + } + + std::pair<bool, bool> OnWebRtcEventLogWrite(const PeerConnectionKey& key, + const std::string& message) { + std::pair<bool, bool> result; + event_log_manager_->OnWebRtcEventLogWrite(key.render_process_id, key.lid, + message, ReplyClosure(&result)); + WaitForReply(); + return result; + } + + void FreezeClockAt(const base::Time::Exploded& frozen_time_exploded) { + base::Time frozen_time; + ASSERT_TRUE( + base::Time::FromLocalExploded(frozen_time_exploded, &frozen_time)); + frozen_clock_.SetNow(frozen_time); + event_log_manager_->SetClockForTesting(&frozen_clock_, ReplyClosure()); + WaitForReply(); + } + + void SetWebRtcEventLoggingState(const PeerConnectionKey& key, + bool event_logging_enabled) { + webrtc_state_change_instructions_.emplace(key, event_logging_enabled); + } + + void ExpectWebRtcStateChangeInstruction(const PeerConnectionKey& key, + bool enabled) { + ASSERT_FALSE(webrtc_state_change_instructions_.empty()); + auto& instruction = webrtc_state_change_instructions_.front(); + EXPECT_EQ(instruction.key.render_process_id, key.render_process_id); + EXPECT_EQ(instruction.key.lid, key.lid); + EXPECT_EQ(instruction.enabled, enabled); + webrtc_state_change_instructions_.pop(); + } + + void SetPeerConnectionTrackerProxyForTesting( + std::unique_ptr<WebRtcEventLogManager::PeerConnectionTrackerProxy> + pc_tracker_proxy) { + event_log_manager_->SetPeerConnectionTrackerProxyForTesting( + std::move(pc_tracker_proxy), ReplyClosure()); + WaitForReply(); + } + + // Allows either creating a TestingProfile with a predetermined name + // (useful when trying to "reload" a profile), or one with an arbitrary name. + virtual std::unique_ptr<TestingProfile> CreateBrowserContext() { + return CreateBrowserContext(std::string()); + } + virtual std::unique_ptr<TestingProfile> CreateBrowserContext( + std::string profile_name) { + return CreateBrowserContext(profile_name, true /* is_managed_profile */, + false /* has_device_level_policies */, + true /* policy_allows_remote_logging */); + } + virtual std::unique_ptr<TestingProfile> CreateBrowserContext( + std::string profile_name, + bool is_managed_profile, + bool has_device_level_policies, + base::Optional<bool> policy_allows_remote_logging) { + // If profile name not specified, select a unique name. + if (profile_name.empty()) { + static size_t index = 0; + profile_name = std::to_string(++index); + } + + // Set a directory for the profile, derived from its name, so that + // recreating the profile will get the same directory. + const base::FilePath profile_path = + profiles_dir_.GetPath().AppendASCII(profile_name); + if (base::PathExists(profile_path)) { + EXPECT_TRUE(base::DirectoryExists(profile_path)); + } else { + EXPECT_TRUE(base::CreateDirectory(profile_path)); + } + + // Prepare to specify preferences for the profile. + sync_preferences::PrefServiceMockFactory factory; + factory.set_user_prefs(base::WrapRefCounted(new TestingPrefStore())); + scoped_refptr<user_prefs::PrefRegistrySyncable> registry( + new user_prefs::PrefRegistrySyncable); + sync_preferences::PrefServiceSyncable* regular_prefs = + factory.CreateSyncable(registry.get()).release(); + + // Set the preference associated with the policy for WebRTC remote-bound + // event logging. + RegisterUserProfilePrefs(registry.get()); + if (policy_allows_remote_logging.has_value()) { + regular_prefs->SetBoolean(prefs::kWebRtcEventLogCollectionAllowed, + policy_allows_remote_logging.value()); + } + +#if !defined(OS_ANDROID) && !defined(OS_CHROMEOS) + policy::PolicyMap policy_map; + if (has_device_level_policies) { + policy_map.Set("test-policy", policy::POLICY_LEVEL_MANDATORY, + policy::POLICY_SCOPE_MACHINE, + policy::POLICY_SOURCE_PLATFORM, + std::make_unique<base::Value>("test"), nullptr); + } + provider_.UpdateChromePolicy(policy_map); +#else + if (has_device_level_policies) { + // This should never happen. + // Device level policies cannot be set on Chrome OS and Android. + EXPECT_TRUE(false); + } +#endif + + // Build the profile. + TestingProfile::Builder profile_builder; + profile_builder.SetProfileName(profile_name); + profile_builder.SetPath(profile_path); + profile_builder.SetPrefService(base::WrapUnique(regular_prefs)); + profile_builder.OverridePolicyConnectorIsManagedForTesting( + is_managed_profile); + std::unique_ptr<TestingProfile> profile = profile_builder.Build(); + + // Blocks on the unit under test's task runner, so that we won't proceed + // with the test (e.g. check that files were created) before finished + // processing this even (which is signaled to it from + // BrowserContext::EnableForBrowserContext). + WaitForPendingTasks(); + + return profile; + } + + base::FilePath RemoteBoundLogsDir(BrowserContext* browser_context) const { + return RemoteBoundLogsDir(browser_context->GetPath()); + } + + base::FilePath RemoteBoundLogsDir( + const base::FilePath& browser_context_base_dir) const { + return GetRemoteBoundWebRtcEventLogsDir(browser_context_base_dir); + } + + // Initiate an arbitrary synchronous operation, allowing any tasks pending + // on the manager's internal task queue to be completed. + // If given a RunLoop, we first block on it. The reason to do both is that + // with the RunLoop we wait on some tasks which we know also post additional + // tasks, then, after that chain is completed, we also wait for any potential + // leftovers. For example, the run loop could wait for n-1 files to be + // uploaded, then it is released when the last one's upload is initiated, + // then we wait for the last file's upload to be completed. + void WaitForPendingTasks(base::RunLoop* run_loop = nullptr) { + if (run_loop) { + run_loop->Run(); + } + + base::WaitableEvent event(base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED); + event_log_manager_->GetTaskRunnerForTesting()->PostTask( + FROM_HERE, + base::BindOnce([](base::WaitableEvent* event) { event->Signal(); }, + &event)); + event.Wait(); + } + + void SuppressUploading() { + if (!upload_suppressing_browser_context_) { // First suppression. + upload_suppressing_browser_context_ = CreateBrowserContext(); + } + DCHECK(!upload_suppressing_rph_) << "Uploading already suppressed."; + upload_suppressing_rph_ = std::make_unique<MockRenderProcessHost>( + upload_suppressing_browser_context_.get()); + const auto key = GetPeerConnectionKey(upload_suppressing_rph_.get(), 0); + ASSERT_TRUE(PeerConnectionAdded(key)); + } + + void UnsuppressUploading() { + DCHECK(upload_suppressing_rph_) << "Uploading not suppressed."; + const auto key = GetPeerConnectionKey(upload_suppressing_rph_.get(), 0); + ASSERT_TRUE(PeerConnectionRemoved(key)); + upload_suppressing_rph_.reset(); + } + + void ExpectLocalFileContents(const base::FilePath& file_path, + const std::string& expected_contents) { + std::string file_contents; + ASSERT_TRUE(base::ReadFileToString(file_path, &file_contents)); + EXPECT_EQ(file_contents, expected_contents); + } + + void ExpectRemoteFileContents(const base::FilePath& file_path, + const std::string& expected_event_log) { + std::string file_contents; + ASSERT_TRUE(base::ReadFileToString(file_path, &file_contents)); + + if (remote_log_extension_ == kWebRtcEventLogUncompressedExtension) { + EXPECT_EQ(file_contents, expected_event_log); + } else if (remote_log_extension_ == kWebRtcEventLogGzippedExtension) { + std::string uncompressed_log; + ASSERT_TRUE( + compression::GzipUncompress(file_contents, &uncompressed_log)); + EXPECT_EQ(uncompressed_log, expected_event_log); + } else { + NOTREACHED(); + } + } + + // When the peer connection's ID is not the focus of the test, this allows + // us to conveniently assign unique IDs to peer connections. + std::string GetUniqueId(int render_process_id, int lid) { + return std::to_string(render_process_id) + "_" + std::to_string(lid); + } + std::string GetUniqueId(const PeerConnectionKey& key) { + return GetUniqueId(key.render_process_id, key.lid); + } + + bool UploadConditionsHold() { + base::RunLoop run_loop; + bool result; + + auto callback = [](base::RunLoop* run_loop, bool* result_out, bool result) { + *result_out = result; + run_loop->QuitWhenIdle(); + }; + + event_log_manager_->UploadConditionsHoldForTesting( + base::BindOnce(callback, &run_loop, &result)); + run_loop.Run(); + + return result; + } + + // Testing utilities. + content::BrowserTaskEnvironment task_environment_; + base::test::ScopedFeatureList scoped_feature_list_; + base::test::ScopedCommandLine scoped_command_line_; + base::SimpleTestClock frozen_clock_; + network::TestURLLoaderFactory test_url_loader_factory_; + scoped_refptr<network::SharedURLLoaderFactory> + test_shared_url_loader_factory_; + +#if !defined(OS_ANDROID) && !defined(OS_CHROMEOS) + policy::MockConfigurationPolicyProvider provider_; +#endif + + // The main loop, which allows waiting for the operations invoked on the + // unit-under-test to be completed. Do not use this object directly from the + // tests, since that would be error-prone. (Specifically, one must not produce + // two events that could produce replies, without waiting on the first reply + // in between.) + std::unique_ptr<base::RunLoop> run_loop_; + + // Allows waiting for upload operations. + std::unique_ptr<base::RunLoop> uploader_run_loop_; + + // Unit under test. + std::unique_ptr<WebRtcEventLogManager> event_log_manager_; + + // Extensions associated with local/remote-bound event logs. Depends on + // whether they're compressed. + base::FilePath::StringPieceType local_log_extension_; + base::FilePath::StringPieceType remote_log_extension_; + + // The directory which will contain all profiles. + base::ScopedTempDir profiles_dir_; + + // Default BrowserContext and RenderProcessHost, to be used by tests which + // do not require anything fancy (such as seeding the BrowserContext with + // pre-existing logs files from a previous session, or working with multiple + // BrowserContext objects). + + std::unique_ptr<TestingProfile> browser_context_; + BrowserContextId browser_context_id_; + std::unique_ptr<MockRenderProcessHost> rph_; + + // Used for suppressing the upload of finished files, by creating an active + // remote-bound log associated with an independent BrowserContext which + // does not otherwise interfere with the test. + std::unique_ptr<TestingProfile> upload_suppressing_browser_context_; + std::unique_ptr<MockRenderProcessHost> upload_suppressing_rph_; + + // The directory where we'll save local log files. + base::ScopedTempDir local_logs_base_dir_; + // local_logs_base_dir_ + log files' name prefix. + base::FilePath local_logs_base_path_; + + // WebRtcEventLogManager instructs WebRTC, via PeerConnectionTracker, to + // only send WebRTC messages for certain peer connections. Some tests make + // sure that this is done correctly, by waiting for these notifications, then + // testing them. + // Because a single action - disabling of local logging - could crease a + // series of such instructions, we keep a queue of them. However, were one + // to actually test that scenario, one would have to account for the lack + // of a guarantee over the order in which these instructions are produced. + struct WebRtcStateChangeInstruction { + WebRtcStateChangeInstruction(PeerConnectionKey key, bool enabled) + : key(key), enabled(enabled) {} + PeerConnectionKey key; + bool enabled; + }; + std::queue<WebRtcStateChangeInstruction> webrtc_state_change_instructions_; + + // Observers for local/remote logging being started/stopped. By having them + // here, we achieve two goals: + // 1. Reduce boilerplate in the tests themselves. + // 2. Avoid lifetime issues, where the observer might be deallocated before + // a RenderProcessHost is deallocated (which can potentially trigger a + // callback on the observer). + NiceMock<MockWebRtcLocalEventLogsObserver> local_observer_; + NiceMock<MockWebRtcRemoteEventLogsObserver> remote_observer_; + + DISALLOW_COPY_AND_ASSIGN(WebRtcEventLogManagerTestBase); +}; + +#if !defined(OS_ANDROID) + +class WebRtcEventLogManagerTest : public WebRtcEventLogManagerTestBase, + public ::testing::WithParamInterface<bool> { + public: + WebRtcEventLogManagerTest() { + scoped_feature_list_.InitAndEnableFeature(features::kWebRtcRemoteEventLog); + + // Use a low delay, or the tests would run for quite a long time. + scoped_command_line_.GetProcessCommandLine()->AppendSwitchASCII( + ::switches::kWebRtcRemoteEventLogUploadDelayMs, "100"); + } + + ~WebRtcEventLogManagerTest() override = default; + + void SetUp() override { + CreateWebRtcEventLogManager(Compression::GZIP_PERFECT_ESTIMATION); + + WebRtcEventLogManagerTestBase::SetUp(); + + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<NullWebRtcEventLogUploader::Factory>(false)); + } +}; + +class WebRtcEventLogManagerTestCacheClearing + : public WebRtcEventLogManagerTest { + public: + ~WebRtcEventLogManagerTestCacheClearing() override = default; + + void CreatePendingLogFiles(BrowserContext* browser_context) { + ASSERT_TRUE(pending_logs_.find(browser_context) == pending_logs_.end()); + auto& elements = pending_logs_[browser_context]; + elements = std::make_unique<BrowserContextAssociatedElements>(); + + for (size_t i = 0; i < kMaxActiveRemoteBoundWebRtcEventLogs; ++i) { + elements->rphs.push_back( + std::make_unique<MockRenderProcessHost>(browser_context)); + const auto key = GetPeerConnectionKey(elements->rphs[i].get(), kLid); + elements->file_paths.push_back(CreatePendingRemoteLogFile(key)); + ASSERT_TRUE(elements->file_paths[i]); + ASSERT_TRUE(base::PathExists(*elements->file_paths[i])); + + pending_latest_mod_ = GetLastModificationTime(*elements->file_paths[i]); + if (pending_earliest_mod_.is_null()) { // First file. + pending_earliest_mod_ = pending_latest_mod_; + } + } + } + + void ClearPendingLogFiles() { pending_logs_.clear(); } + + base::Optional<base::FilePath> CreateRemoteLogFile( + const PeerConnectionKey& key, + bool pending) { + base::Optional<base::FilePath> file_path; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + EXPECT_TRUE(PeerConnectionAdded(key)); + EXPECT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_TRUE(StartRemoteLogging(key)); + if (pending) { + // Transition from ACTIVE to PENDING. + EXPECT_TRUE(PeerConnectionRemoved(key)); + } + return file_path; + } + + base::Optional<base::FilePath> CreateActiveRemoteLogFile( + const PeerConnectionKey& key) { + return CreateRemoteLogFile(key, false); + } + + base::Optional<base::FilePath> CreatePendingRemoteLogFile( + const PeerConnectionKey& key) { + return CreateRemoteLogFile(key, true); + } + + protected: + // When closing a file, rather than check its last modification date, which + // is potentially expensive, WebRtcRemoteEventLogManager reads the system + // clock, which should be close enough. For tests, however, the difference + // could be enough to flake the tests, if not for this epsilon. Given the + // focus of the tests that use this, this epsilon can be arbitrarily large. + static const base::TimeDelta kEpsion; + + struct BrowserContextAssociatedElements { + std::vector<std::unique_ptr<MockRenderProcessHost>> rphs; + std::vector<base::Optional<base::FilePath>> file_paths; + }; + + std::map<const BrowserContext*, + std::unique_ptr<BrowserContextAssociatedElements>> + pending_logs_; + + // Latest modification times of earliest and latest pending log files. + base::Time pending_earliest_mod_; + base::Time pending_latest_mod_; +}; + +const base::TimeDelta WebRtcEventLogManagerTestCacheClearing::kEpsion = + base::TimeDelta::FromHours(1); + +class WebRtcEventLogManagerTestWithRemoteLoggingDisabled + : public WebRtcEventLogManagerTestBase, + public ::testing::WithParamInterface<bool> { + public: + WebRtcEventLogManagerTestWithRemoteLoggingDisabled() + : feature_enabled_(GetParam()), policy_enabled_(!feature_enabled_) { + if (feature_enabled_) { + scoped_feature_list_.InitAndEnableFeature( + features::kWebRtcRemoteEventLog); + } else { + scoped_feature_list_.InitAndDisableFeature( + features::kWebRtcRemoteEventLog); + } + CreateWebRtcEventLogManager(); + } + + ~WebRtcEventLogManagerTestWithRemoteLoggingDisabled() override = default; + + // Override CreateBrowserContext() to use policy_enabled_. + std::unique_ptr<TestingProfile> CreateBrowserContext() override { + return CreateBrowserContext(std::string()); + } + std::unique_ptr<TestingProfile> CreateBrowserContext( + std::string profile_name) override { + return CreateBrowserContext(profile_name, policy_enabled_, + false /* has_device_level_policies */, + policy_enabled_); + } + std::unique_ptr<TestingProfile> CreateBrowserContext( + std::string profile_name, + bool is_managed_profile, + bool has_device_level_policies, + base::Optional<bool> policy_allows_remote_logging) override { + DCHECK_EQ(policy_enabled_, policy_allows_remote_logging.value()); + return WebRtcEventLogManagerTestBase::CreateBrowserContext( + profile_name, is_managed_profile, has_device_level_policies, + policy_allows_remote_logging); + } + + private: + const bool feature_enabled_; // Whether the Finch kill-switch is engaged. + const bool policy_enabled_; // Whether the policy is enabled for the profile. +}; + +class WebRtcEventLogManagerTestPolicy : public WebRtcEventLogManagerTestBase { + public: + ~WebRtcEventLogManagerTestPolicy() override = default; + + // Defer to setup from the body. + void SetUp() override {} + + void SetUp(bool feature_enabled) { + if (feature_enabled) { + scoped_feature_list_.InitAndEnableFeature( + features::kWebRtcRemoteEventLog); + } else { + scoped_feature_list_.InitAndDisableFeature( + features::kWebRtcRemoteEventLog); + } + + scoped_command_line_.GetProcessCommandLine()->AppendSwitchASCII( + ::switches::kWebRtcRemoteEventLogUploadDelayMs, "0"); + + CreateWebRtcEventLogManager(Compression::GZIP_PERFECT_ESTIMATION); + + WebRtcEventLogManagerTestBase::SetUp(); + } +}; + +class WebRtcEventLogManagerTestUploadSuppressionDisablingFlag + : public WebRtcEventLogManagerTestBase { + public: + WebRtcEventLogManagerTestUploadSuppressionDisablingFlag() { + scoped_feature_list_.InitAndEnableFeature(features::kWebRtcRemoteEventLog); + + scoped_command_line_.GetProcessCommandLine()->AppendSwitch( + ::switches::kWebRtcRemoteEventLogUploadNoSuppression); + + // Use a low delay, or the tests would run for quite a long time. + scoped_command_line_.GetProcessCommandLine()->AppendSwitchASCII( + ::switches::kWebRtcRemoteEventLogUploadDelayMs, "100"); + + CreateWebRtcEventLogManager(); + } + + ~WebRtcEventLogManagerTestUploadSuppressionDisablingFlag() override = default; +}; + +class WebRtcEventLogManagerTestForNetworkConnectivity + : public WebRtcEventLogManagerTestBase, + public ::testing::WithParamInterface< + std::tuple<bool, + network::mojom::ConnectionType, + network::mojom::ConnectionType>> { + public: + WebRtcEventLogManagerTestForNetworkConnectivity() + : get_conn_type_is_sync_(std::get<0>(GetParam())), + supported_type_(std::get<1>(GetParam())), + unsupported_type_(std::get<2>(GetParam())) { + scoped_feature_list_.InitAndEnableFeature(features::kWebRtcRemoteEventLog); + + // Use a low delay, or the tests would run for quite a long time. + scoped_command_line_.GetProcessCommandLine()->AppendSwitchASCII( + ::switches::kWebRtcRemoteEventLogUploadDelayMs, "100"); + + CreateWebRtcEventLogManager(); + } + + ~WebRtcEventLogManagerTestForNetworkConnectivity() override = default; + + void UnloadProfileAndSeedPendingLog() { + DCHECK(browser_context_path_.empty()) << "Not expected to be called twice."; + + // Unload the profile, but remember where it stores its files (for sanity). + browser_context_path_ = browser_context_->GetPath(); + const base::FilePath remote_logs_dir = + RemoteBoundLogsDir(browser_context_.get()); + UnloadMainTestProfile(); + + // Seed the remote logs' directory with one log file, simulating the + // creation of logs in a previous session. + ASSERT_TRUE(base::CreateDirectory(remote_logs_dir)); + + base::FilePath file_path; + ASSERT_TRUE(CreateRemoteBoundLogFile( + remote_logs_dir, kWebAppId, remote_log_extension_, base::Time::Now(), + &file_path, &file_)); + + expected_files_.emplace_back(browser_context_id_, file_path, + GetLastModificationTime(file_path)); + } + + const bool get_conn_type_is_sync_; + const network::mojom::ConnectionType supported_type_; + const network::mojom::ConnectionType unsupported_type_; + + base::FilePath browser_context_path_; // For sanity over the test itself. + std::list<WebRtcLogFileInfo> expected_files_; + base::File file_; +}; + +class WebRtcEventLogManagerTestUploadDelay + : public WebRtcEventLogManagerTestBase { + public: + ~WebRtcEventLogManagerTestUploadDelay() override = default; + + void SetUp() override { + // Intercept and block the call to SetUp(). The test body will call + // the version that sets an upload delay instead. + } + + void SetUp(size_t upload_delay_ms) { + scoped_feature_list_.InitAndEnableFeature(features::kWebRtcRemoteEventLog); + + scoped_command_line_.GetProcessCommandLine()->AppendSwitchASCII( + ::switches::kWebRtcRemoteEventLogUploadDelayMs, + std::to_string(upload_delay_ms)); + + CreateWebRtcEventLogManager(); + + WebRtcEventLogManagerTestBase::SetUp(); + } + + // There's a trade-off between the test runtime and the likelihood of a + // false-positive (lowered when the time is increased). + // Since false-positives can be caught handled even if only manifesting + // occasionally, this value should be enough. + static const size_t kDefaultUploadDelayMs = 500; + + // For tests where we don't intend to wait, prevent flakiness by setting + // an unrealistically long delay. + static const size_t kIntentionallyExcessiveDelayMs = 1000 * 1000 * 1000; +}; + +// For testing compression issues. +class WebRtcEventLogManagerTestCompression + : public WebRtcEventLogManagerTestBase { + public: + WebRtcEventLogManagerTestCompression() { + scoped_feature_list_.InitAndEnableFeature(features::kWebRtcRemoteEventLog); + + scoped_command_line_.GetProcessCommandLine()->AppendSwitchASCII( + ::switches::kWebRtcRemoteEventLogUploadDelayMs, "0"); + } + + ~WebRtcEventLogManagerTestCompression() override = default; + + void SetUp() override { + // Defer until Init(), which will allow the test body more control. + } + + void Init(base::Optional<WebRtcEventLogCompression> remote_compression = + base::Optional<WebRtcEventLogCompression>()) { + CreateWebRtcEventLogManager(remote_compression); + + WebRtcEventLogManagerTestBase::SetUp(); + } +}; + +class WebRtcEventLogManagerTestIncognito + : public WebRtcEventLogManagerTestBase { + public: + WebRtcEventLogManagerTestIncognito() : incognito_profile_(nullptr) { + scoped_feature_list_.InitAndEnableFeature(features::kWebRtcRemoteEventLog); + CreateWebRtcEventLogManager(); + } + + ~WebRtcEventLogManagerTestIncognito() override { + incognito_rph_.reset(); + if (incognito_profile_) { + DCHECK(browser_context_); + browser_context_->DestroyOffTheRecordProfile(); + } + } + + void SetUp() override { + WebRtcEventLogManagerTestBase::SetUp(); + + incognito_profile_ = browser_context_->GetOffTheRecordProfile(); + incognito_rph_ = + std::make_unique<MockRenderProcessHost>(incognito_profile_); + } + + Profile* incognito_profile_; + std::unique_ptr<MockRenderProcessHost> incognito_rph_; +}; + +class WebRtcEventLogManagerTestHistory : public WebRtcEventLogManagerTestBase { + public: + WebRtcEventLogManagerTestHistory() { + scoped_command_line_.GetProcessCommandLine()->AppendSwitchASCII( + ::switches::kWebRtcRemoteEventLogUploadDelayMs, "0"); + + CreateWebRtcEventLogManager(); + } + + ~WebRtcEventLogManagerTestHistory() override = default; + + // Allows us to test that a time is as expected, down to UNIX time's + // lower resolution than our clock. + static bool IsSameTimeWhenTruncatedToSeconds(base::Time a, base::Time b) { + if (a.is_null() || b.is_null()) { + return false; + } + const base::TimeDelta delta = std::max(a, b) - std::min(a, b); + return delta.InSeconds() == 0; + } + + // Allows us to check that the timestamps are roughly what we expect. + // Doing better than this would require too much effort. + static bool IsSmallTimeDelta(base::Time a, base::Time b) { + if (a.is_null() || b.is_null()) { + return false; + } + + // Way more than is "small", to make sure tests don't become flaky. + // If the timestamp is ever off, it's likely to be off by more than this, + // though, or the problem would not truly be severe enough to worry about. + const base::TimeDelta small_delta = base::TimeDelta::FromMinutes(15); + + return (std::max(a, b) - std::min(a, b) <= small_delta); + } +}; + +namespace { + +class PeerConnectionTrackerProxyForTesting + : public WebRtcEventLogManager::PeerConnectionTrackerProxy { + public: + explicit PeerConnectionTrackerProxyForTesting( + WebRtcEventLogManagerTestBase* test) + : test_(test) {} + + ~PeerConnectionTrackerProxyForTesting() override = default; + + void EnableWebRtcEventLogging(const PeerConnectionKey& key, + int output_period_ms) override { + test_->SetWebRtcEventLoggingState(key, true); + } + void DisableWebRtcEventLogging(const PeerConnectionKey& key) override { + test_->SetWebRtcEventLoggingState(key, false); + } + + private: + WebRtcEventLogManagerTestBase* const test_; +}; + +// The factory for the following fake uploader produces a sequence of +// uploaders which fail the test if given a file other than that which they +// expect. The factory itself likewise fails the test if destroyed before +// producing all expected uploaders, or if it's asked for more uploaders than +// it expects to create. This allows us to test for sequences of uploads. +class FileListExpectingWebRtcEventLogUploader : public WebRtcEventLogUploader { + public: + class Factory : public WebRtcEventLogUploader::Factory { + public: + Factory(std::list<WebRtcLogFileInfo>* expected_files, + bool result, + base::RunLoop* run_loop) + : result_(result), run_loop_(run_loop) { + expected_files_.swap(*expected_files); + if (expected_files_.empty()) { + run_loop_->QuitWhenIdle(); + } + } + + ~Factory() override { EXPECT_TRUE(expected_files_.empty()); } + + std::unique_ptr<WebRtcEventLogUploader> Create( + const WebRtcLogFileInfo& log_file, + UploadResultCallback callback) override { + if (expected_files_.empty()) { + EXPECT_FALSE(true); // More files uploaded than expected. + } else { + EXPECT_EQ(log_file.path, expected_files_.front().path); + // Because LoadMainTestProfile() and UnloadMainTestProfile() mess up the + // BrowserContextId in ways that would not happen in production, + // we cannot verify |log_file.browser_context_id| is correct. + // This is unimportant to the test. + + base::DeleteFile(log_file.path, false); + expected_files_.pop_front(); + } + + if (expected_files_.empty()) { + run_loop_->QuitWhenIdle(); + } + + return std::make_unique<FileListExpectingWebRtcEventLogUploader>( + log_file, result_, std::move(callback)); + } + + private: + std::list<WebRtcLogFileInfo> expected_files_; + const bool result_; + base::RunLoop* const run_loop_; + }; + + // The logic is in the factory; the uploader just reports success so that the + // next file may become eligible for uploading. + FileListExpectingWebRtcEventLogUploader(const WebRtcLogFileInfo& log_file, + bool result, + UploadResultCallback callback) + : log_file_(log_file) { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), log_file_.path, result)); + } + + ~FileListExpectingWebRtcEventLogUploader() override = default; + + const WebRtcLogFileInfo& GetWebRtcLogFileInfo() const override { + return log_file_; + } + + bool Cancel() override { + NOTREACHED() << "Incompatible with this kind of test."; + return true; + } + + private: + const WebRtcLogFileInfo log_file_; +}; + +} // namespace + +TEST_F(WebRtcEventLogManagerTest, PeerConnectionAddedReturnsTrue) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_TRUE(PeerConnectionAdded(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionAddedReturnsFalseIfAlreadyAdded) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + EXPECT_FALSE(PeerConnectionAdded(key)); +} + +TEST_F(WebRtcEventLogManagerTest, PeerConnectionRemovedReturnsTrue) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + EXPECT_TRUE(PeerConnectionRemoved(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionRemovedReturnsFalseIfNeverAdded) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_FALSE(PeerConnectionRemoved(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionRemovedReturnsFalseIfAlreadyRemoved) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionRemoved(key)); + EXPECT_FALSE(PeerConnectionRemoved(key)); +} + +TEST_F(WebRtcEventLogManagerTest, PeerConnectionSessionIdSetReturnsTrue) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + EXPECT_TRUE(PeerConnectionSessionIdSet(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionSessionIdSetReturnsFalseIfEmptyString) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + EXPECT_FALSE(PeerConnectionSessionIdSet(key, "")); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionSessionIdSetReturnsFalseIfPeerConnectionNeverAdded) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_FALSE(PeerConnectionSessionIdSet(key, kSessionId)); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionSessionIdSetReturnsFalseIfAlreadyCalledSameId) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + EXPECT_FALSE(PeerConnectionSessionIdSet(key, kSessionId)); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionSessionIdSetReturnsFalseIfPeerConnectionAlreadyRemoved) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionRemoved(key)); + EXPECT_FALSE(PeerConnectionSessionIdSet(key, kSessionId)); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionSessionIdSetReturnsFalseIfAlreadyCalledDifferentId) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, "id1")); + EXPECT_FALSE(PeerConnectionSessionIdSet(key, "id2")); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionSessionIdSetCalledOnRecreatedPeerConnectionSanity) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + ASSERT_TRUE(PeerConnectionRemoved(key)); + ASSERT_TRUE(PeerConnectionAdded(key)); + EXPECT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); +} + +TEST_F(WebRtcEventLogManagerTest, EnableLocalLoggingReturnsTrue) { + EXPECT_TRUE(EnableLocalLogging()); +} + +TEST_F(WebRtcEventLogManagerTest, + EnableLocalLoggingReturnsFalseIfCalledWhenAlreadyEnabled) { + ASSERT_TRUE(EnableLocalLogging()); + EXPECT_FALSE(EnableLocalLogging()); +} + +TEST_F(WebRtcEventLogManagerTest, DisableLocalLoggingReturnsTrue) { + ASSERT_TRUE(EnableLocalLogging()); + EXPECT_TRUE(DisableLocalLogging()); +} + +TEST_F(WebRtcEventLogManagerTest, DisableLocalLoggingReturnsIfNeverEnabled) { + EXPECT_FALSE(DisableLocalLogging()); +} + +TEST_F(WebRtcEventLogManagerTest, DisableLocalLoggingReturnsIfAlreadyDisabled) { + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(DisableLocalLogging()); + EXPECT_FALSE(DisableLocalLogging()); +} + +TEST_F(WebRtcEventLogManagerTest, + OnWebRtcEventLogWriteReturnsFalseAndFalseWhenAllLoggingDisabled) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + // Note that EnableLocalLogging() and StartRemoteLogging() weren't called. + ASSERT_TRUE(PeerConnectionAdded(key)); + EXPECT_EQ(OnWebRtcEventLogWrite(key, "log"), std::make_pair(false, false)); +} + +TEST_F(WebRtcEventLogManagerTest, + OnWebRtcEventLogWriteReturnsFalseAndFalseForUnknownPeerConnection) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(EnableLocalLogging()); + // Note that PeerConnectionAdded() wasn't called. + EXPECT_EQ(OnWebRtcEventLogWrite(key, "log"), std::make_pair(false, false)); +} + +TEST_F(WebRtcEventLogManagerTest, + OnWebRtcEventLogWriteReturnsLocalTrueWhenPcKnownAndLocalLoggingOn) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionAdded(key)); + EXPECT_EQ(OnWebRtcEventLogWrite(key, "log"), std::make_pair(true, false)); +} + +TEST_F(WebRtcEventLogManagerTest, + OnWebRtcEventLogWriteReturnsRemoteTrueWhenPcKnownAndRemoteLogging) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + EXPECT_EQ(OnWebRtcEventLogWrite(key, "log"), std::make_pair(false, true)); +} + +TEST_F(WebRtcEventLogManagerTest, + OnWebRtcEventLogWriteReturnsTrueAndTrueeWhenAllLoggingEnabled) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(StartRemoteLogging(key)); + EXPECT_EQ(OnWebRtcEventLogWrite(key, "log"), std::make_pair(true, true)); +} + +TEST_F(WebRtcEventLogManagerTest, + OnLocalLogStartedNotCalledIfLocalLoggingEnabledWithoutPeerConnections) { + EXPECT_CALL(local_observer_, OnLocalLogStarted(_, _)).Times(0); + ASSERT_TRUE(EnableLocalLogging()); +} + +TEST_F(WebRtcEventLogManagerTest, + OnLocalLogStoppedNotCalledIfLocalLoggingDisabledWithoutPeerConnections) { + EXPECT_CALL(local_observer_, OnLocalLogStopped(_)).Times(0); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(DisableLocalLogging()); +} + +TEST_F(WebRtcEventLogManagerTest, + OnLocalLogStartedCalledForPeerConnectionAddedAndLocalLoggingEnabled) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_CALL(local_observer_, OnLocalLogStarted(key, _)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(EnableLocalLogging()); +} + +TEST_F(WebRtcEventLogManagerTest, + OnLocalLogStartedCalledForLocalLoggingEnabledAndPeerConnectionAdded) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_CALL(local_observer_, OnLocalLogStarted(key, _)).Times(1); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionAdded(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + OnLocalLogStoppedCalledAfterLocalLoggingDisabled) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_CALL(local_observer_, OnLocalLogStopped(key)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(DisableLocalLogging()); +} + +TEST_F(WebRtcEventLogManagerTest, + OnLocalLogStoppedCalledAfterPeerConnectionRemoved) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_CALL(local_observer_, OnLocalLogStopped(key)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionRemoved(key)); +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogCreatesEmptyFileWhenStarted) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + base::Optional<base::FilePath> file_path; + ON_CALL(local_observer_, OnLocalLogStarted(key, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + // Make sure the file would be closed, so that we could safely read it. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ExpectLocalFileContents(*file_path, std::string()); +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogCreateAndWriteToFile) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> file_path; + ON_CALL(local_observer_, OnLocalLogStarted(key, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + const std::string log = "To strive, to seek, to find, and not to yield."; + ASSERT_EQ(OnWebRtcEventLogWrite(key, log), std::make_pair(true, false)); + + // Make sure the file would be closed, so that we could safely read it. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ExpectLocalFileContents(*file_path, log); +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogMultipleWritesToSameFile) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> file_path; + ON_CALL(local_observer_, OnLocalLogStarted(key, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + const std::string logs[] = {"Old age hath yet his honour and his toil;", + "Death closes all: but something ere the end,", + "Some work of noble note, may yet be done,", + "Not unbecoming men that strove with Gods."}; + for (const std::string& log : logs) { + ASSERT_EQ(OnWebRtcEventLogWrite(key, log), std::make_pair(true, false)); + } + + // Make sure the file would be closed, so that we could safely read it. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ExpectLocalFileContents( + *file_path, + std::accumulate(std::begin(logs), std::end(logs), std::string())); +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogFileSizeLimitNotExceeded) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> file_path; + ON_CALL(local_observer_, OnLocalLogStarted(key, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + const std::string log = "There lies the port; the vessel puffs her sail:"; + const size_t file_size_limit_bytes = log.length() / 2; + + ASSERT_TRUE(EnableLocalLogging(file_size_limit_bytes)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + // Failure is reported, because not everything could be written. The file + // will also be closed. + EXPECT_CALL(local_observer_, OnLocalLogStopped(key)).Times(1); + ASSERT_EQ(OnWebRtcEventLogWrite(key, log), std::make_pair(false, false)); + + // Additional calls to Write() have no effect. + ASSERT_EQ(OnWebRtcEventLogWrite(key, "ignored"), + std::make_pair(false, false)); + + ExpectLocalFileContents(*file_path, std::string()); +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogSanityOverUnlimitedFileSizes) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> file_path; + ON_CALL(local_observer_, OnLocalLogStarted(key, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(EnableLocalLogging(kWebRtcEventLogManagerUnlimitedFileSize)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + const std::string log1 = "Who let the dogs out?"; + const std::string log2 = "Woof, woof, woof, woof, woof!"; + ASSERT_EQ(OnWebRtcEventLogWrite(key, log1), std::make_pair(true, false)); + ASSERT_EQ(OnWebRtcEventLogWrite(key, log2), std::make_pair(true, false)); + + // Make sure the file would be closed, so that we could safely read it. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ExpectLocalFileContents(*file_path, log1 + log2); +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogNoWriteAfterLogStopped) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> file_path; + ON_CALL(local_observer_, OnLocalLogStarted(key, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + const std::string log_before = "log_before_stop"; + ASSERT_EQ(OnWebRtcEventLogWrite(key, log_before), + std::make_pair(true, false)); + EXPECT_CALL(local_observer_, OnLocalLogStopped(key)).Times(1); + ASSERT_TRUE(PeerConnectionRemoved(key)); + + const std::string log_after = "log_after_stop"; + ASSERT_EQ(OnWebRtcEventLogWrite(key, log_after), + std::make_pair(false, false)); + + ExpectLocalFileContents(*file_path, log_before); +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogOnlyWritesTheLogsAfterStarted) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + // Calls to Write() before the log was started are ignored. + EXPECT_CALL(local_observer_, OnLocalLogStarted(_, _)).Times(0); + const std::string log1 = "The lights begin to twinkle from the rocks:"; + ASSERT_EQ(OnWebRtcEventLogWrite(key, log1), std::make_pair(false, false)); + ASSERT_TRUE(base::IsDirectoryEmpty(local_logs_base_dir_.GetPath())); + + base::Optional<base::FilePath> file_path; + EXPECT_CALL(local_observer_, OnLocalLogStarted(key, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + // Calls after the log started have an effect. The calls to Write() from + // before the log started are not remembered. + const std::string log2 = "The long day wanes: the slow moon climbs: the deep"; + ASSERT_EQ(OnWebRtcEventLogWrite(key, log2), std::make_pair(true, false)); + + // Make sure the file would be closed, so that we could safely read it. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ExpectLocalFileContents(*file_path, log2); +} + +// Note: This test also covers the scenario LocalLogExistingFilesNotOverwritten, +// which is therefore not explicitly tested. +TEST_F(WebRtcEventLogManagerTest, LocalLoggingRestartCreatesNewFile) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + const std::vector<std::string> logs = {"<setup>", "<punchline>", "<encore>"}; + std::vector<base::Optional<PeerConnectionKey>> keys(logs.size()); + std::vector<base::Optional<base::FilePath>> file_paths(logs.size()); + + ASSERT_TRUE(PeerConnectionAdded(key)); + + for (size_t i = 0; i < logs.size(); ++i) { + ON_CALL(local_observer_, OnLocalLogStarted(_, _)) + .WillByDefault(Invoke(SaveKeyAndFilePathTo(&keys[i], &file_paths[i]))); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(keys[i]); + ASSERT_EQ(*keys[i], key); + ASSERT_TRUE(file_paths[i]); + ASSERT_FALSE(file_paths[i]->empty()); + ASSERT_EQ(OnWebRtcEventLogWrite(key, logs[i]), std::make_pair(true, false)); + ASSERT_TRUE(DisableLocalLogging()); + } + + for (size_t i = 0; i < logs.size(); ++i) { + ExpectLocalFileContents(*file_paths[i], logs[i]); + } +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogMultipleActiveFiles) { + ASSERT_TRUE(EnableLocalLogging()); + + std::list<MockRenderProcessHost> rphs; + for (size_t i = 0; i < 3; ++i) { + rphs.emplace_back(browser_context_.get()); // (MockRenderProcessHost ctor) + } + + std::vector<PeerConnectionKey> keys; + for (auto& rph : rphs) { + keys.push_back(GetPeerConnectionKey(&rph, kLid)); + } + + std::vector<base::Optional<base::FilePath>> file_paths(keys.size()); + for (size_t i = 0; i < keys.size(); ++i) { + ON_CALL(local_observer_, OnLocalLogStarted(keys[i], _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_paths[i]))); + ASSERT_TRUE(PeerConnectionAdded(keys[i])); + ASSERT_TRUE(file_paths[i]); + ASSERT_FALSE(file_paths[i]->empty()); + } + + std::vector<std::string> logs; + for (size_t i = 0; i < keys.size(); ++i) { + logs.emplace_back(std::to_string(rph_->GetID()) + std::to_string(kLid)); + ASSERT_EQ(OnWebRtcEventLogWrite(keys[i], logs[i]), + std::make_pair(true, false)); + } + + // Make sure the file woulds be closed, so that we could safely read them. + ASSERT_TRUE(DisableLocalLogging()); + + for (size_t i = 0; i < keys.size(); ++i) { + ExpectLocalFileContents(*file_paths[i], logs[i]); + } +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogLimitActiveLocalLogFiles) { + ASSERT_TRUE(EnableLocalLogging()); + + const int kMaxLocalLogFiles = + static_cast<int>(kMaxNumberLocalWebRtcEventLogFiles); + for (int i = 0; i < kMaxLocalLogFiles; ++i) { + const auto key = GetPeerConnectionKey(rph_.get(), i); + EXPECT_CALL(local_observer_, OnLocalLogStarted(key, _)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(key)); + } + + EXPECT_CALL(local_observer_, OnLocalLogStarted(_, _)).Times(0); + const auto last_key = GetPeerConnectionKey(rph_.get(), kMaxLocalLogFiles); + ASSERT_TRUE(PeerConnectionAdded(last_key)); +} + +// When a log reaches its maximum size limit, it is closed, and no longer +// counted towards the limit. +TEST_F(WebRtcEventLogManagerTest, LocalLogFilledLogNotCountedTowardsLogsLimit) { + const std::string log = "very_short_log"; + ASSERT_TRUE(EnableLocalLogging(log.size())); + + const int kMaxLocalLogFiles = + static_cast<int>(kMaxNumberLocalWebRtcEventLogFiles); + for (int i = 0; i < kMaxLocalLogFiles; ++i) { + const auto key = GetPeerConnectionKey(rph_.get(), i); + EXPECT_CALL(local_observer_, OnLocalLogStarted(key, _)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(key)); + } + + // By writing to one of the logs, we fill it and end up closing it, allowing + // an additional log to be written. + const auto removed_key = GetPeerConnectionKey(rph_.get(), 0); + EXPECT_EQ(OnWebRtcEventLogWrite(removed_key, log), + std::make_pair(true, false)); + + // We now have room for one additional log. + const auto last_key = GetPeerConnectionKey(rph_.get(), kMaxLocalLogFiles); + EXPECT_CALL(local_observer_, OnLocalLogStarted(last_key, _)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(last_key)); +} + +TEST_F(WebRtcEventLogManagerTest, + LocalLogForRemovedPeerConnectionNotCountedTowardsLogsLimit) { + ASSERT_TRUE(EnableLocalLogging()); + + const int kMaxLocalLogFiles = + static_cast<int>(kMaxNumberLocalWebRtcEventLogFiles); + for (int i = 0; i < kMaxLocalLogFiles; ++i) { + const auto key = GetPeerConnectionKey(rph_.get(), i); + EXPECT_CALL(local_observer_, OnLocalLogStarted(key, _)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(key)); + } + + // When one peer connection is removed, one log is stopped, thereby allowing + // an additional log to be opened. + const auto removed_key = GetPeerConnectionKey(rph_.get(), 0); + EXPECT_CALL(local_observer_, OnLocalLogStopped(removed_key)).Times(1); + ASSERT_TRUE(PeerConnectionRemoved(removed_key)); + + // We now have room for one additional log. + const auto last_key = GetPeerConnectionKey(rph_.get(), kMaxLocalLogFiles); + EXPECT_CALL(local_observer_, OnLocalLogStarted(last_key, _)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(last_key)); +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogIllegalPath) { + // Since the log file won't be properly opened, these will not be called. + EXPECT_CALL(local_observer_, OnLocalLogStarted(_, _)).Times(0); + EXPECT_CALL(local_observer_, OnLocalLogStopped(_)).Times(0); + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + + // See the documentation of the function for why |true| is expected despite + // the path being illegal. + const base::FilePath illegal_path(FILE_PATH_LITERAL(":!@#$%|`^&*\\/")); + EXPECT_TRUE(EnableLocalLogging(illegal_path)); + + EXPECT_TRUE(base::IsDirectoryEmpty(local_logs_base_dir_.GetPath())); +} + +#if defined(OS_POSIX) +TEST_F(WebRtcEventLogManagerTest, LocalLogLegalPathWithoutPermissionsSanity) { + RemoveWritePermissions(local_logs_base_dir_.GetPath()); + + // Since the log file won't be properly opened, these will not be called. + EXPECT_CALL(local_observer_, OnLocalLogStarted(_, _)).Times(0); + EXPECT_CALL(local_observer_, OnLocalLogStopped(_)).Times(0); + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + + // See the documentation of the function for why |true| is expected despite + // the path being illegal. + EXPECT_TRUE(EnableLocalLogging(local_logs_base_path_)); + + EXPECT_TRUE(base::IsDirectoryEmpty(local_logs_base_dir_.GetPath())); + + // Write() has no effect (but is handled gracefully). + EXPECT_EQ(OnWebRtcEventLogWrite(key, "Why did the chicken cross the road?"), + std::make_pair(false, false)); + EXPECT_TRUE(base::IsDirectoryEmpty(local_logs_base_dir_.GetPath())); + + // Logging was enabled, even if it had no effect because of the lacking + // permissions; therefore, the operation of disabling it makes sense. + EXPECT_TRUE(DisableLocalLogging()); + EXPECT_TRUE(base::IsDirectoryEmpty(local_logs_base_dir_.GetPath())); +} +#endif // defined(OS_POSIX) + +TEST_F(WebRtcEventLogManagerTest, LocalLogEmptyStringHandledGracefully) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + // By writing a log after the empty string, we show that no odd behavior is + // encountered, such as closing the file (an actual bug from WebRTC). + const std::vector<std::string> logs = {"<setup>", "", "<encore>"}; + + base::Optional<base::FilePath> file_path; + + ON_CALL(local_observer_, OnLocalLogStarted(key, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + for (size_t i = 0; i < logs.size(); ++i) { + ASSERT_EQ(OnWebRtcEventLogWrite(key, logs[i]), std::make_pair(true, false)); + } + ASSERT_TRUE(DisableLocalLogging()); + + ExpectLocalFileContents( + *file_path, + std::accumulate(std::begin(logs), std::end(logs), std::string())); +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogFilenameMatchesExpectedFormat) { + using StringType = base::FilePath::StringType; + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> file_path; + ON_CALL(local_observer_, OnLocalLogStarted(key, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + const base::Time::Exploded frozen_time_exploded{ + 2017, // Four digit year "2007" + 9, // 1-based month (values 1 = January, etc.) + 3, // 0-based day of week (0 = Sunday, etc.) + 6, // 1-based day of month (1-31) + 10, // Hour within the current day (0-23) + 43, // Minute within the current hour (0-59) + 29, // Second within the current minute. + 0 // Milliseconds within the current second (0-999) + }; + ASSERT_TRUE(frozen_time_exploded.HasValidValues()); + FreezeClockAt(frozen_time_exploded); + + const StringType user_defined = FILE_PATH_LITERAL("user_defined"); + const base::FilePath local_logs_base_path = + local_logs_base_dir_.GetPath().Append(user_defined); + + ASSERT_TRUE(EnableLocalLogging(local_logs_base_path)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + // [user_defined]_[date]_[time]_[render_process_id]_[lid].[extension] + const StringType date = FILE_PATH_LITERAL("20170906"); + const StringType time = FILE_PATH_LITERAL("1043"); + base::FilePath expected_path = local_logs_base_path; + expected_path = local_logs_base_path.InsertBeforeExtension( + FILE_PATH_LITERAL("_") + date + FILE_PATH_LITERAL("_") + time + + FILE_PATH_LITERAL("_") + NumberToStringType(rph_->GetID()) + + FILE_PATH_LITERAL("_") + NumberToStringType(kLid)); + expected_path = expected_path.AddExtension(local_log_extension_); + + EXPECT_EQ(file_path, expected_path); +} + +TEST_F(WebRtcEventLogManagerTest, + LocalLogFilenameMatchesExpectedFormatRepeatedFilename) { + using StringType = base::FilePath::StringType; + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> file_path_1; + base::Optional<base::FilePath> file_path_2; + EXPECT_CALL(local_observer_, OnLocalLogStarted(key, _)) + .WillOnce(Invoke(SaveFilePathTo(&file_path_1))) + .WillOnce(Invoke(SaveFilePathTo(&file_path_2))); + + const base::Time::Exploded frozen_time_exploded{ + 2017, // Four digit year "2007" + 9, // 1-based month (values 1 = January, etc.) + 3, // 0-based day of week (0 = Sunday, etc.) + 6, // 1-based day of month (1-31) + 10, // Hour within the current day (0-23) + 43, // Minute within the current hour (0-59) + 29, // Second within the current minute. + 0 // Milliseconds within the current second (0-999) + }; + ASSERT_TRUE(frozen_time_exploded.HasValidValues()); + FreezeClockAt(frozen_time_exploded); + + const StringType user_defined_portion = FILE_PATH_LITERAL("user_defined"); + const base::FilePath local_logs_base_path = + local_logs_base_dir_.GetPath().Append(user_defined_portion); + + ASSERT_TRUE(EnableLocalLogging(local_logs_base_path)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(file_path_1); + ASSERT_FALSE(file_path_1->empty()); + + // [user_defined]_[date]_[time]_[render_process_id]_[lid].[extension] + const StringType date = FILE_PATH_LITERAL("20170906"); + const StringType time = FILE_PATH_LITERAL("1043"); + base::FilePath expected_path_1 = local_logs_base_path; + expected_path_1 = local_logs_base_path.InsertBeforeExtension( + FILE_PATH_LITERAL("_") + date + FILE_PATH_LITERAL("_") + time + + FILE_PATH_LITERAL("_") + NumberToStringType(rph_->GetID()) + + FILE_PATH_LITERAL("_") + NumberToStringType(kLid)); + expected_path_1 = expected_path_1.AddExtension(local_log_extension_); + + ASSERT_EQ(file_path_1, expected_path_1); + + ASSERT_TRUE(DisableLocalLogging()); + ASSERT_TRUE(EnableLocalLogging(local_logs_base_path)); + ASSERT_TRUE(file_path_2); + ASSERT_FALSE(file_path_2->empty()); + + const base::FilePath expected_path_2 = + expected_path_1.InsertBeforeExtension(FILE_PATH_LITERAL(" (1)")); + + // Focus of the test - starting the same log again produces a new file, + // with an expected new filename. + ASSERT_EQ(file_path_2, expected_path_2); +} + +TEST_F(WebRtcEventLogManagerTest, + OnRemoteLogStartedNotCalledIfRemoteLoggingNotEnabled) { + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(_, _, _)).Times(0); + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + EXPECT_TRUE(PeerConnectionSessionIdSet(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + OnRemoteLogStoppedNotCalledIfRemoteLoggingNotEnabled) { + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(_)).Times(0); + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + EXPECT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(PeerConnectionRemoved(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + OnRemoteLogStartedCalledIfRemoteLoggingEnabled) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + OnRemoteLogStoppedCalledIfRemoteLoggingEnabledThenPcRemoved) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(key)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(PeerConnectionRemoved(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + BrowserContextInitializationCreatesDirectoryForRemoteLogs) { + auto browser_context = CreateBrowserContext(); + const base::FilePath remote_logs_path = + RemoteBoundLogsDir(browser_context.get()); + EXPECT_TRUE(base::DirectoryExists(remote_logs_path)); + EXPECT_TRUE(base::IsDirectoryEmpty(remote_logs_path)); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingReturnsFalseIfUnknownPeerConnection) { + const auto key = GetPeerConnectionKey(rph_.get(), 0); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, "id", nullptr, &error_message)); + EXPECT_EQ(error_message, + kStartRemoteLoggingFailureUnknownOrInactivePeerConnection); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingReturnsFalseIfUnknownSessionId) { + const auto key = GetPeerConnectionKey(rph_.get(), 0); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, "wrong_id", nullptr, &error_message)); + EXPECT_EQ(error_message, + kStartRemoteLoggingFailureUnknownOrInactivePeerConnection); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingReturnsTrueIfKnownSessionId) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + EXPECT_TRUE(StartRemoteLogging(key, kSessionId)); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingReturnsFalseIfRestartAttempt) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + ASSERT_TRUE(StartRemoteLogging(key, kSessionId)); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, kSessionId, nullptr, &error_message)); + EXPECT_EQ(error_message, kStartRemoteLoggingFailureAlreadyLogging); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingReturnsFalseIfUnlimitedFileSize) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, kSessionId, + kWebRtcEventLogManagerUnlimitedFileSize, 0, + kWebAppId, nullptr, &error_message)); + EXPECT_EQ(error_message, kStartRemoteLoggingFailureUnlimitedSizeDisallowed); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingReturnsTrueIfFileSizeAtOrBelowLimit) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + EXPECT_TRUE(StartRemoteLogging(key, kSessionId, kMaxRemoteLogFileSizeBytes, 0, + kWebAppId)); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingReturnsFalseIfFileSizeToSmall) { + const size_t min_size = + CreateLogFileWriterFactory(Compression::GZIP_NULL_ESTIMATION) + ->MinFileSizeBytes(); + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, kSessionId, min_size - 1, 0, kWebAppId, + nullptr, &error_message)); + EXPECT_EQ(error_message, kStartRemoteLoggingFailureMaxSizeTooSmall); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingReturnsFalseIfExcessivelyLargeFileSize) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, kSessionId, + kMaxRemoteLogFileSizeBytes + 1, 0, kWebAppId, + nullptr, &error_message)); + EXPECT_EQ(error_message, kStartRemoteLoggingFailureMaxSizeTooLarge); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingReturnsFalseIfExcessivelyLargeOutputPeriodMs) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, kSessionId, kMaxRemoteLogFileSizeBytes, + kMaxOutputPeriodMs + 1, kWebAppId, nullptr, + &error_message)); + EXPECT_EQ(error_message, kStartRemoteLoggingFailureOutputPeriodMsTooLarge); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingReturnsFalseIfPeerConnectionAlreadyClosed) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + ASSERT_TRUE(PeerConnectionRemoved(key)); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, kSessionId, nullptr, &error_message)); + EXPECT_EQ(error_message, + kStartRemoteLoggingFailureUnknownOrInactivePeerConnection); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingDoesNotReturnIdWhenUnsuccessful) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + ASSERT_TRUE(PeerConnectionRemoved(key)); + + std::string log_id; + ASSERT_FALSE(StartRemoteLogging(key, kSessionId, &log_id)); + + EXPECT_TRUE(log_id.empty()); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingReturnsLegalIdWhenSuccessful) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + + std::string log_id; + ASSERT_TRUE(StartRemoteLogging(key, kSessionId, &log_id)); + + EXPECT_EQ(log_id.size(), 32u); + EXPECT_EQ(log_id.find_first_not_of("0123456789ABCDEF"), std::string::npos); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingSavesToFileWithCorrectFileNameFormat) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> file_path; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + std::string log_id; + ASSERT_TRUE(StartRemoteLogging(key, &log_id)); + + // Compare filename (without extension). + const std::string filename = + file_path->BaseName().RemoveExtension().MaybeAsASCII(); + ASSERT_FALSE(filename.empty()); + + const std::string expected_filename = + std::string(kRemoteBoundWebRtcEventLogFileNamePrefix) + "_" + + std::to_string(kWebAppId) + "_" + log_id; + EXPECT_EQ(filename, expected_filename); + + // Compare extension. + EXPECT_EQ( + base::FilePath::kExtensionSeparator + remote_log_extension_.as_string(), + file_path->Extension()); +} + +TEST_F(WebRtcEventLogManagerTest, StartRemoteLoggingCreatesEmptyFile) { + base::Optional<base::FilePath> file_path; + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + + // Close file before examining its contents. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ExpectRemoteFileContents(*file_path, std::string()); +} + +TEST_F(WebRtcEventLogManagerTest, RemoteLogFileCreatedInCorrectDirectory) { + // Set up separate browser contexts; each one will get one log. + constexpr size_t kLogsNum = 3; + std::unique_ptr<TestingProfile> browser_contexts[kLogsNum]; + std::vector<std::unique_ptr<MockRenderProcessHost>> rphs; + for (size_t i = 0; i < kLogsNum; ++i) { + browser_contexts[i] = CreateBrowserContext(); + rphs.emplace_back( + std::make_unique<MockRenderProcessHost>(browser_contexts[i].get())); + } + + // Prepare to store the logs' paths in distinct memory locations. + base::Optional<base::FilePath> file_paths[kLogsNum]; + for (size_t i = 0; i < kLogsNum; ++i) { + const auto key = GetPeerConnectionKey(rphs[i].get(), kLid); + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&file_paths[i]))); + } + + // Start one log for each browser context. + for (const auto& rph : rphs) { + const auto key = GetPeerConnectionKey(&*rph, kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + } + + // All log files must be created in their own context's directory. + for (size_t i = 0; i < base::size(browser_contexts); ++i) { + ASSERT_TRUE(file_paths[i]); + EXPECT_TRUE(browser_contexts[i]->GetPath().IsParent(*file_paths[i])); + } +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingSanityIfDuplicateIdsInDifferentRendererProcesses) { + std::unique_ptr<MockRenderProcessHost> rphs[2] = { + std::make_unique<MockRenderProcessHost>(browser_context_.get()), + std::make_unique<MockRenderProcessHost>(browser_context_.get()), + }; + + PeerConnectionKey keys[2] = {GetPeerConnectionKey(rphs[0].get(), 0), + GetPeerConnectionKey(rphs[1].get(), 0)}; + + // The ID is shared, but that's not a problem, because the renderer process + // are different. + const std::string id = "shared_id"; + ASSERT_TRUE(PeerConnectionAdded(keys[0])); + PeerConnectionSessionIdSet(keys[0], id); + ASSERT_TRUE(PeerConnectionAdded(keys[1])); + PeerConnectionSessionIdSet(keys[1], id); + + // Make sure the logs get written to separate files. + base::Optional<base::FilePath> file_paths[2]; + for (size_t i = 0; i < 2; ++i) { + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(keys[i], _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&file_paths[i]))); + } + + EXPECT_TRUE(StartRemoteLogging(keys[0], id)); + EXPECT_TRUE(StartRemoteLogging(keys[1], id)); + + EXPECT_TRUE(file_paths[0]); + EXPECT_TRUE(file_paths[1]); + EXPECT_NE(file_paths[0], file_paths[1]); +} + +TEST_F(WebRtcEventLogManagerTest, + OnWebRtcEventLogWriteWritesToTheRemoteBoundFile) { + base::Optional<base::FilePath> file_path; + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + + const char* const log = "1 + 1 = 3"; + EXPECT_EQ(OnWebRtcEventLogWrite(key, log), std::make_pair(false, true)); + + // Close file before examining its contents. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ExpectRemoteFileContents(*file_path, log); +} + +TEST_F(WebRtcEventLogManagerTest, WriteToBothLocalAndRemoteFiles) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + base::Optional<base::FilePath> local_path; + EXPECT_CALL(local_observer_, OnLocalLogStarted(key, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&local_path))); + + base::Optional<base::FilePath> remote_path; + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&remote_path))); + + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(StartRemoteLogging(key)); + + ASSERT_TRUE(local_path); + ASSERT_FALSE(local_path->empty()); + ASSERT_TRUE(remote_path); + ASSERT_FALSE(remote_path->empty()); + + const char* const log = "logloglog"; + ASSERT_EQ(OnWebRtcEventLogWrite(key, log), std::make_pair(true, true)); + + // Ensure the flushing of the file to disk before attempting to read them. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ExpectLocalFileContents(*local_path, log); + ExpectRemoteFileContents(*remote_path, log); +} + +TEST_F(WebRtcEventLogManagerTest, MultipleWritesToSameRemoteBoundLogfile) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> file_path; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + const std::string logs[] = {"ABC", "DEF", "XYZ"}; + for (const std::string& log : logs) { + ASSERT_EQ(OnWebRtcEventLogWrite(key, log), std::make_pair(false, true)); + } + + // Make sure the file would be closed, so that we could safely read it. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ExpectRemoteFileContents( + *file_path, + std::accumulate(std::begin(logs), std::end(logs), std::string())); +} + +TEST_F(WebRtcEventLogManagerTest, + RemoteLogFileSizeLimitNotExceededSingleWrite) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + base::Optional<base::FilePath> file_path; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + const std::string log = "tpyo"; + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + ASSERT_TRUE( + StartRemoteLogging(key, kSessionId, GzippedSize(log) - 1, 0, kWebAppId)); + + // Failure is reported, because not everything could be written. The file + // will also be closed. + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(key)).Times(1); + ASSERT_EQ(OnWebRtcEventLogWrite(key, log), std::make_pair(false, false)); + + // Make sure the file would be closed, so that we could safely read it. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + // No partial writes occurred. + ExpectRemoteFileContents(*file_path, std::string()); +} + +TEST_F(WebRtcEventLogManagerTest, + RemoteLogFileSizeLimitNotExceededMultipleWrites) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + base::Optional<base::FilePath> file_path; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + const std::string log1 = "abcabc"; + const std::string log2 = "defghijklmnopqrstuvwxyz"; + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key, kSessionId)); + ASSERT_TRUE( + StartRemoteLogging(key, kSessionId, 1 + GzippedSize(log1), 0, kWebAppId)); + + // First write works. + ASSERT_EQ(OnWebRtcEventLogWrite(key, log1), std::make_pair(false, true)); + + // On the second write, failure is reported, because not everything could be + // written. The file will also be closed. + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(key)).Times(1); + ASSERT_EQ(OnWebRtcEventLogWrite(key, log2), std::make_pair(false, false)); + + ExpectRemoteFileContents(*file_path, log1); +} + +TEST_F(WebRtcEventLogManagerTest, + LogMultipleActiveRemoteLogsSameBrowserContext) { + const std::vector<PeerConnectionKey> keys = { + GetPeerConnectionKey(rph_.get(), 0), GetPeerConnectionKey(rph_.get(), 1), + GetPeerConnectionKey(rph_.get(), 2)}; + + std::vector<base::Optional<base::FilePath>> file_paths(keys.size()); + for (size_t i = 0; i < keys.size(); ++i) { + ON_CALL(remote_observer_, OnRemoteLogStarted(keys[i], _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_paths[i]))); + ASSERT_TRUE(PeerConnectionAdded(keys[i])); + ASSERT_TRUE(PeerConnectionSessionIdSet(keys[i])); + ASSERT_TRUE(StartRemoteLogging(keys[i])); + ASSERT_TRUE(file_paths[i]); + ASSERT_FALSE(file_paths[i]->empty()); + } + + std::vector<std::string> logs; + for (size_t i = 0; i < keys.size(); ++i) { + logs.emplace_back(std::to_string(rph_->GetID()) + std::to_string(i)); + ASSERT_EQ(OnWebRtcEventLogWrite(keys[i], logs[i]), + std::make_pair(false, true)); + } + + // Make sure the file woulds be closed, so that we could safely read them. + for (auto& key : keys) { + ASSERT_TRUE(PeerConnectionRemoved(key)); + } + + for (size_t i = 0; i < keys.size(); ++i) { + ExpectRemoteFileContents(*file_paths[i], logs[i]); + } +} + +TEST_F(WebRtcEventLogManagerTest, + LogMultipleActiveRemoteLogsDifferentBrowserContexts) { + constexpr size_t kLogsNum = 3; + std::unique_ptr<TestingProfile> browser_contexts[kLogsNum]; + std::vector<std::unique_ptr<MockRenderProcessHost>> rphs; + for (size_t i = 0; i < kLogsNum; ++i) { + browser_contexts[i] = CreateBrowserContext(); + rphs.emplace_back( + std::make_unique<MockRenderProcessHost>(browser_contexts[i].get())); + } + + std::vector<PeerConnectionKey> keys; + for (auto& rph : rphs) { + keys.push_back(GetPeerConnectionKey(rph.get(), kLid)); + } + + std::vector<base::Optional<base::FilePath>> file_paths(keys.size()); + for (size_t i = 0; i < keys.size(); ++i) { + ON_CALL(remote_observer_, OnRemoteLogStarted(keys[i], _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_paths[i]))); + ASSERT_TRUE(PeerConnectionAdded(keys[i])); + ASSERT_TRUE(PeerConnectionSessionIdSet(keys[i])); + ASSERT_TRUE(StartRemoteLogging(keys[i])); + ASSERT_TRUE(file_paths[i]); + ASSERT_FALSE(file_paths[i]->empty()); + } + + std::vector<std::string> logs; + for (size_t i = 0; i < keys.size(); ++i) { + logs.emplace_back(std::to_string(rph_->GetID()) + std::to_string(i)); + ASSERT_EQ(OnWebRtcEventLogWrite(keys[i], logs[i]), + std::make_pair(false, true)); + } + + // Make sure the file woulds be closed, so that we could safely read them. + for (auto& key : keys) { + ASSERT_TRUE(PeerConnectionRemoved(key)); + } + + for (size_t i = 0; i < keys.size(); ++i) { + ExpectRemoteFileContents(*file_paths[i], logs[i]); + } +} + +TEST_F(WebRtcEventLogManagerTest, DifferentRemoteLogsMayHaveDifferentMaximums) { + const std::string logs[2] = {"abra", "cadabra"}; + std::vector<base::Optional<base::FilePath>> file_paths(base::size(logs)); + std::vector<PeerConnectionKey> keys; + for (size_t i = 0; i < base::size(logs); ++i) { + keys.push_back(GetPeerConnectionKey(rph_.get(), i)); + ON_CALL(remote_observer_, OnRemoteLogStarted(keys[i], _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_paths[i]))); + } + + for (size_t i = 0; i < keys.size(); ++i) { + ASSERT_TRUE(PeerConnectionAdded(keys[i])); + const std::string session_id = GetUniqueId(keys[i]); + ASSERT_TRUE(PeerConnectionSessionIdSet(keys[i], session_id)); + ASSERT_TRUE(StartRemoteLogging(keys[i], session_id, GzippedSize(logs[i]), 0, + kWebAppId)); + } + + for (size_t i = 0; i < keys.size(); ++i) { + // The write is successful, but the file closed, indicating that the + // maximum file size has been reached. + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(keys[i])).Times(1); + ASSERT_EQ(OnWebRtcEventLogWrite(keys[i], logs[i]), + std::make_pair(false, true)); + ASSERT_TRUE(file_paths[i]); + ExpectRemoteFileContents(*file_paths[i], logs[i]); + } +} + +TEST_F(WebRtcEventLogManagerTest, RemoteLogFileClosedWhenCapacityReached) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + base::Optional<base::FilePath> file_path; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + const std::string log = "Let X equal X."; + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key, GetUniqueId(key), GzippedSize(log), 0, + kWebAppId)); + ASSERT_TRUE(file_path); + + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(key)).Times(1); + EXPECT_EQ(OnWebRtcEventLogWrite(key, log), std::make_pair(false, true)); +} + +#if defined(OS_POSIX) +// TODO(crbug.com/775415): Add unit tests for lacking read permissions when +// looking to upload the file. +TEST_F(WebRtcEventLogManagerTest, + FailureToCreateRemoteLogsDirHandledGracefully) { + const base::FilePath browser_context_dir = browser_context_->GetPath(); + const base::FilePath remote_logs_path = + RemoteBoundLogsDir(browser_context_.get()); + + // Unload the profile, delete its remove logs directory, and remove write + // permissions from it, thereby preventing it from being created again. + UnloadMainTestProfile(); + ASSERT_TRUE(base::DeleteFile(remote_logs_path, /*recursive=*/true)); + RemoveWritePermissions(browser_context_dir); + + // Graceful handling by BrowserContext::EnableForBrowserContext, despite + // failing to create the remote logs' directory.. + LoadMainTestProfile(); + EXPECT_FALSE(base::DirectoryExists(remote_logs_path)); + + // Graceful handling of PeerConnectionAdded: True returned because the + // remote-logs' manager can still safely reason about the state of peer + // connections even if one of its browser contexts is defective.) + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + // Graceful handling of StartRemoteLogging: False returned because it's + // impossible to write the log to a file. + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, nullptr, &error_message)); + EXPECT_EQ(error_message, + kStartRemoteLoggingFailureLoggingDisabledBrowserContext); + + // Graceful handling of OnWebRtcEventLogWrite: False returned because the + // log could not be written at all, let alone in its entirety. + const char* const log = "This is not a log."; + EXPECT_EQ(OnWebRtcEventLogWrite(key, log), std::make_pair(false, false)); + + // Graceful handling of PeerConnectionRemoved: True returned because the + // remote-logs' manager can still safely reason about the state of peer + // connections even if one of its browser contexts is defective. + EXPECT_TRUE(PeerConnectionRemoved(key)); +} + +TEST_F(WebRtcEventLogManagerTest, GracefullyHandleFailureToStartRemoteLogFile) { + // WebRTC logging will not be turned on. + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(_, _, _)).Times(0); + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(_)).Times(0); + + // Remove write permissions from the directory. + const base::FilePath remote_logs_path = + RemoteBoundLogsDir(browser_context_.get()); + ASSERT_TRUE(base::DirectoryExists(remote_logs_path)); + RemoveWritePermissions(remote_logs_path); + + // StartRemoteLogging() will now fail. + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, nullptr, &error_message)); + EXPECT_EQ(error_message, kStartRemoteLoggingFailureFileCreationError); + EXPECT_EQ(OnWebRtcEventLogWrite(key, "abc"), std::make_pair(false, false)); + EXPECT_TRUE(base::IsDirectoryEmpty(remote_logs_path)); +} +#endif // defined(OS_POSIX) + +TEST_F(WebRtcEventLogManagerTest, RemoteLogLimitActiveLogFiles) { + for (int i = 0; i < kMaxActiveRemoteLogFiles + 1; ++i) { + const auto key = GetPeerConnectionKey(rph_.get(), i); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + } + + for (int i = 0; i < kMaxActiveRemoteLogFiles; ++i) { + const auto key = GetPeerConnectionKey(rph_.get(), i); + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)).Times(1); + ASSERT_TRUE(StartRemoteLogging(key)); + } + + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(_, _, _)).Times(0); + const auto new_key = + GetPeerConnectionKey(rph_.get(), kMaxActiveRemoteLogFiles); + EXPECT_FALSE(StartRemoteLogging(new_key)); +} + +TEST_F(WebRtcEventLogManagerTest, + RemoteLogFilledLogNotCountedTowardsLogsLimit) { + const std::string log = "very_short_log"; + + for (int i = 0; i < kMaxActiveRemoteLogFiles; ++i) { + const auto key = GetPeerConnectionKey(rph_.get(), i); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)).Times(1); + ASSERT_TRUE(StartRemoteLogging(key, GetUniqueId(key), GzippedSize(log), 0, + kWebAppId)); + } + + // By writing to one of the logs until it reaches capacity, we fill it, + // causing it to close, therefore allowing an additional log. + const auto removed_key = GetPeerConnectionKey(rph_.get(), 0); + EXPECT_EQ(OnWebRtcEventLogWrite(removed_key, log), + std::make_pair(false, true)); + + // We now have room for one additional log. + const auto new_key = + GetPeerConnectionKey(rph_.get(), kMaxActiveRemoteLogFiles); + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(new_key, _, _)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(new_key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(new_key)); + ASSERT_TRUE(StartRemoteLogging(new_key)); +} + +TEST_F(WebRtcEventLogManagerTest, + RemoteLogForRemovedPeerConnectionNotCountedTowardsLogsLimit) { + for (int i = 0; i < kMaxActiveRemoteLogFiles; ++i) { + const auto key = GetPeerConnectionKey(rph_.get(), i); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)).Times(1); + ASSERT_TRUE(StartRemoteLogging(key)); + } + + // By removing a peer connection associated with one of the logs, we allow + // an additional log. + const auto removed_key = GetPeerConnectionKey(rph_.get(), 0); + ASSERT_TRUE(PeerConnectionRemoved(removed_key)); + + // We now have room for one additional log. + const auto last_key = + GetPeerConnectionKey(rph_.get(), kMaxActiveRemoteLogFiles); + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(last_key, _, _)).Times(1); + ASSERT_TRUE(PeerConnectionAdded(last_key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(last_key)); + ASSERT_TRUE(StartRemoteLogging(last_key)); +} + +TEST_F(WebRtcEventLogManagerTest, + ActiveLogsForBrowserContextCountedTowardsItsPendingsLogsLimit) { + SuppressUploading(); + + // Produce kMaxPendingRemoteLogFiles pending logs. + for (int i = 0; i < kMaxPendingRemoteLogFiles; ++i) { + const auto key = GetPeerConnectionKey(rph_.get(), i); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(PeerConnectionRemoved(key)); + } + + // It is now impossible to start another *active* log for that BrowserContext, + // because we have too many pending logs (and active logs become pending + // once completed). + const auto forbidden = + GetPeerConnectionKey(rph_.get(), kMaxPendingRemoteLogFiles); + ASSERT_TRUE(PeerConnectionAdded(forbidden)); + ASSERT_TRUE(PeerConnectionSessionIdSet(forbidden)); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(forbidden, nullptr, &error_message)); + EXPECT_EQ(error_message, + kStartRemoteLoggingFailureNoAdditionalActiveLogsAllowed); +} + +TEST_F(WebRtcEventLogManagerTest, + ObserveLimitOnMaximumPendingLogsPerBrowserContext) { + SuppressUploading(); + + // Create additional BrowserContexts for the test. + std::unique_ptr<TestingProfile> browser_contexts[2] = { + CreateBrowserContext(), CreateBrowserContext()}; + std::unique_ptr<MockRenderProcessHost> rphs[2] = { + std::make_unique<MockRenderProcessHost>(browser_contexts[0].get()), + std::make_unique<MockRenderProcessHost>(browser_contexts[1].get())}; + + // Allowed to start kMaxPendingRemoteLogFiles for each BrowserContext. + // Specifically, we can do it for the first BrowserContext. + for (int i = 0; i < kMaxPendingRemoteLogFiles; ++i) { + const auto key = GetPeerConnectionKey(rphs[0].get(), i); + // The log could be opened: + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + // The log changes state from ACTIVE to PENDING: + EXPECT_TRUE(PeerConnectionRemoved(key)); + } + + // Not allowed to start any more remote-bound logs for the BrowserContext on + // which the limit was reached. + const auto key0 = + GetPeerConnectionKey(rphs[0].get(), kMaxPendingRemoteLogFiles); + ASSERT_TRUE(PeerConnectionAdded(key0)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key0)); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key0, nullptr, &error_message)); + EXPECT_EQ(error_message, + kStartRemoteLoggingFailureNoAdditionalActiveLogsAllowed); + + // Other BrowserContexts aren't limit by the previous one's limit. + const auto key1 = GetPeerConnectionKey(rphs[1].get(), 0); + ASSERT_TRUE(PeerConnectionAdded(key1)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key1)); + EXPECT_TRUE(StartRemoteLogging(key1)); +} + +// This also tests the scenario UploadOrderDependsOnLastModificationTime. +TEST_F(WebRtcEventLogManagerTest, + LogsFromPreviousSessionBecomePendingLogsWhenBrowserContextInitialized) { + // Unload the profile, but remember where it stores its files. + const base::FilePath browser_context_path = browser_context_->GetPath(); + const base::FilePath remote_logs_dir = + RemoteBoundLogsDir(browser_context_.get()); + UnloadMainTestProfile(); + + // Seed the remote logs' directory with log files, simulating the + // creation of logs in a previous session. + std::list<WebRtcLogFileInfo> expected_files; + ASSERT_TRUE(base::CreateDirectory(remote_logs_dir)); + + // Avoid arbitrary ordering due to files being created in the same second. + // This is OK in production, but can confuse the test, which expects a + // specific order. + base::Time time = + base::Time::Now() - + base::TimeDelta::FromSeconds(kMaxPendingRemoteBoundWebRtcEventLogs); + + for (size_t i = 0; i < kMaxPendingRemoteBoundWebRtcEventLogs; ++i) { + time += base::TimeDelta::FromSeconds(1); + + base::FilePath file_path; + base::File file; + ASSERT_TRUE(CreateRemoteBoundLogFile(remote_logs_dir, kWebAppId, + remote_log_extension_, time, + &file_path, &file)); + + expected_files.emplace_back(browser_context_id_, file_path, time); + } + + // This factory enforces the expectation that the files will be uploaded, + // all of them, only them, and in the order expected. + base::RunLoop run_loop; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, true, &run_loop)); + + LoadMainTestProfile(); + ASSERT_EQ(browser_context_->GetPath(), browser_context_path); + + WaitForPendingTasks(&run_loop); +} + +// It is possible for remote-bound logs to be compressed or uncompressed. +// We show that logs from a previous session are captured even if they are +// different, with regards to compression, compared to last time. +TEST_F(WebRtcEventLogManagerTest, + LogsCapturedPreviouslyMadePendingEvenIfDifferentExtensionsUsed) { + // Unload the profile, but remember where it stores its files. + const base::FilePath browser_context_path = browser_context_->GetPath(); + const base::FilePath remote_logs_dir = + RemoteBoundLogsDir(browser_context_.get()); + UnloadMainTestProfile(); + + // Seed the remote logs' directory with log files, simulating the + // creation of logs in a previous session. + std::list<WebRtcLogFileInfo> expected_files; + ASSERT_TRUE(base::CreateDirectory(remote_logs_dir)); + + base::FilePath::StringPieceType extensions[] = { + kWebRtcEventLogUncompressedExtension, kWebRtcEventLogGzippedExtension}; + ASSERT_LE(base::size(extensions), kMaxPendingRemoteBoundWebRtcEventLogs) + << "Lacking test coverage."; + + // Avoid arbitrary ordering due to files being created in the same second. + // This is OK in production, but can confuse the test, which expects a + // specific order. + base::Time time = + base::Time::Now() - + base::TimeDelta::FromSeconds(kMaxPendingRemoteBoundWebRtcEventLogs); + + for (size_t i = 0, ext = 0; i < kMaxPendingRemoteBoundWebRtcEventLogs; ++i) { + time += base::TimeDelta::FromSeconds(1); + + const auto& extension = extensions[ext]; + ext = (ext + 1) % base::size(extensions); + + base::FilePath file_path; + base::File file; + ASSERT_TRUE(CreateRemoteBoundLogFile(remote_logs_dir, kWebAppId, extension, + time, &file_path, &file)); + + expected_files.emplace_back(browser_context_id_, file_path, time); + } + + // This factory enforces the expectation that the files will be uploaded, + // all of them, only them, and in the order expected. + base::RunLoop run_loop; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, true, &run_loop)); + + LoadMainTestProfile(); + ASSERT_EQ(browser_context_->GetPath(), browser_context_path); + + WaitForPendingTasks(&run_loop); +} + +TEST_P(WebRtcEventLogManagerTest, + WhenPeerConnectionRemovedFinishedRemoteLogUploadedAndFileDeleted) { + // |upload_result| show that the files are deleted independent of the + // upload's success / failure. + const bool upload_result = GetParam(); + + const auto key = GetPeerConnectionKey(rph_.get(), 1); + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(log_file); + + base::RunLoop run_loop; + std::list<WebRtcLogFileInfo> expected_files = {WebRtcLogFileInfo( + browser_context_id_, *log_file, GetLastModificationTime(*log_file))}; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, upload_result, &run_loop)); + + // Peer connection removal triggers next upload. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + WaitForPendingTasks(&run_loop); + + EXPECT_TRUE( + base::IsDirectoryEmpty(RemoteBoundLogsDir(browser_context_.get()))); +} + +TEST_P(WebRtcEventLogManagerTest, DestroyedRphTriggersLogUpload) { + // |upload_result| show that the files are deleted independent of the + // upload's success / failure. + const bool upload_result = GetParam(); + + const auto key = GetPeerConnectionKey(rph_.get(), 1); + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(log_file); + + base::RunLoop run_loop; + std::list<WebRtcLogFileInfo> expected_files = {WebRtcLogFileInfo( + browser_context_id_, *log_file, GetLastModificationTime(*log_file))}; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, upload_result, &run_loop)); + + // RPH destruction stops all active logs and triggers next upload. + rph_.reset(); + + WaitForPendingTasks(&run_loop); + + EXPECT_TRUE( + base::IsDirectoryEmpty(RemoteBoundLogsDir(browser_context_.get()))); +} + +// Note that SuppressUploading() and UnSuppressUploading() use the behavior +// guaranteed by this test. +TEST_F(WebRtcEventLogManagerTest, UploadOnlyWhenNoActivePeerConnections) { + const auto untracked = GetPeerConnectionKey(rph_.get(), 0); + const auto tracked = GetPeerConnectionKey(rph_.get(), 1); + + // Suppresses the uploading of the "tracked" peer connection's log. + ASSERT_TRUE(PeerConnectionAdded(untracked)); + ASSERT_TRUE(PeerConnectionSessionIdSet(untracked)); + + // The tracked peer connection's log is not uploaded when finished, because + // another peer connection is still active. + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(tracked, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(PeerConnectionAdded(tracked)); + ASSERT_TRUE(PeerConnectionSessionIdSet(tracked)); + ASSERT_TRUE(StartRemoteLogging(tracked)); + ASSERT_TRUE(log_file); + ASSERT_TRUE(PeerConnectionRemoved(tracked)); + + // Perform another action synchronously, so that we may be assured that the + // observer's lack of callbacks was not a timing fluke. + OnWebRtcEventLogWrite(untracked, "Ook!"); + + // Having been convinced that |tracked|'s log was not uploded while + // |untracked| was active, close |untracked| and see that |tracked|'s log + // is now uploaded. + base::RunLoop run_loop; + std::list<WebRtcLogFileInfo> expected_uploads = {WebRtcLogFileInfo( + browser_context_id_, *log_file, GetLastModificationTime(*log_file))}; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_uploads, true, &run_loop)); + ASSERT_TRUE(PeerConnectionRemoved(untracked)); + + WaitForPendingTasks(&run_loop); +} + +TEST_F(WebRtcEventLogManagerTest, ExpiredFilesArePrunedRatherThanUploaded) { + constexpr size_t kExpired = 0; + constexpr size_t kFresh = 1; + DCHECK_GE(kMaxPendingRemoteBoundWebRtcEventLogs, 2u) + << "Please restructure the test to use separate browser contexts."; + + const base::FilePath remote_logs_dir = + RemoteBoundLogsDir(browser_context_.get()); + + UnloadMainTestProfile(); + + base::FilePath file_paths[2]; + for (size_t i = 0; i < 2; ++i) { + base::File file; + ASSERT_TRUE(CreateRemoteBoundLogFile( + remote_logs_dir, kWebAppId, remote_log_extension_, base::Time::Now(), + &file_paths[i], &file)); + } + + // Touch() requires setting the last access time as well. Keep it current, + // showing that only the last modification time matters. + base::File::Info file_info; + ASSERT_TRUE(base::GetFileInfo(file_paths[0], &file_info)); + + // Set the expired file's last modification time to past max retention. + const base::Time expired_mod_time = base::Time::Now() - + kRemoteBoundWebRtcEventLogsMaxRetention - + base::TimeDelta::FromSeconds(1); + ASSERT_TRUE(base::TouchFile(file_paths[kExpired], file_info.last_accessed, + expired_mod_time)); + + // Show that the expired file is not uploaded. + base::RunLoop run_loop; + std::list<WebRtcLogFileInfo> expected_files = { + WebRtcLogFileInfo(browser_context_id_, file_paths[kFresh], + GetLastModificationTime(file_paths[kFresh]))}; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, true, &run_loop)); + + // Recognize the files as pending by initializing their BrowserContext. + LoadMainTestProfile(); + + WaitForPendingTasks(&run_loop); + + // Both the uploaded file as well as the expired file have no been removed + // from local disk. + for (const base::FilePath& file_path : file_paths) { + EXPECT_FALSE(base::PathExists(file_path)); + } +} + +// TODO(crbug.com/775415): Add a test showing that a file expiring while another +// is being uploaded, is not uploaded after the current upload is completed. +// This is significant because Chrome might stay up for a long time. + +TEST_F(WebRtcEventLogManagerTest, RemoteLogEmptyStringHandledGracefully) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + // By writing a log after the empty string, we show that no odd behavior is + // encountered, such as closing the file (an actual bug from WebRTC). + const std::vector<std::string> logs = {"<setup>", "", "<encore>"}; + + base::Optional<base::FilePath> file_path; + + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + for (size_t i = 0; i < logs.size(); ++i) { + ASSERT_EQ(OnWebRtcEventLogWrite(key, logs[i]), std::make_pair(false, true)); + } + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ExpectRemoteFileContents( + *file_path, + std::accumulate(std::begin(logs), std::end(logs), std::string())); +} + +#if defined(OS_POSIX) +TEST_F(WebRtcEventLogManagerTest, + UnopenedRemoteLogFilesNotCountedTowardsActiveLogsLimit) { + std::unique_ptr<TestingProfile> browser_contexts[2]; + std::unique_ptr<MockRenderProcessHost> rphs[2]; + for (size_t i = 0; i < 2; ++i) { + browser_contexts[i] = CreateBrowserContext(); + rphs[i] = + std::make_unique<MockRenderProcessHost>(browser_contexts[i].get()); + } + + constexpr size_t without_permissions = 0; + constexpr size_t with_permissions = 1; + + // Remove write permissions from one directory. + const base::FilePath permissions_lacking_remote_logs_path = + RemoteBoundLogsDir(browser_contexts[without_permissions].get()); + ASSERT_TRUE(base::DirectoryExists(permissions_lacking_remote_logs_path)); + RemoveWritePermissions(permissions_lacking_remote_logs_path); + + // Fail to start a log associated with the permission-lacking directory. + const auto without_permissions_key = + GetPeerConnectionKey(rphs[without_permissions].get(), 0); + ASSERT_TRUE(PeerConnectionAdded(without_permissions_key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(without_permissions_key)); + std::string error; + ASSERT_FALSE(StartRemoteLogging(without_permissions_key, nullptr, &error)); + EXPECT_EQ(error, kStartRemoteLoggingFailureFileCreationError); + + // Show that this was not counted towards the limit of active files. + for (int i = 0; i < kMaxActiveRemoteLogFiles; ++i) { + const auto with_permissions_key = + GetPeerConnectionKey(rphs[with_permissions].get(), i); + ASSERT_TRUE(PeerConnectionAdded(with_permissions_key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(with_permissions_key)); + EXPECT_TRUE(StartRemoteLogging(with_permissions_key)); + } +} +#endif // defined(OS_POSIX) + +TEST_F(WebRtcEventLogManagerTest, + NoStartWebRtcSendingEventLogsWhenLocalEnabledWithoutPeerConnection) { + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(EnableLocalLogging()); + EXPECT_TRUE(webrtc_state_change_instructions_.empty()); +} + +TEST_F(WebRtcEventLogManagerTest, + NoStartWebRtcSendingEventLogsWhenPeerConnectionButNoLoggingEnabled) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_TRUE(webrtc_state_change_instructions_.empty()); +} + +TEST_F(WebRtcEventLogManagerTest, + StartWebRtcSendingEventLogsWhenLocalEnabledThenPeerConnectionAdded) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ExpectWebRtcStateChangeInstruction(key, true); +} + +TEST_F(WebRtcEventLogManagerTest, + StartWebRtcSendingEventLogsWhenPeerConnectionAddedThenLocalEnabled) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(EnableLocalLogging()); + ExpectWebRtcStateChangeInstruction(key, true); +} + +TEST_F(WebRtcEventLogManagerTest, + StartWebRtcSendingEventLogsWhenRemoteLoggingEnabled) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ExpectWebRtcStateChangeInstruction(key, true); +} + +TEST_F(WebRtcEventLogManagerTest, + InstructWebRtcToStopSendingEventLogsWhenLocalLoggingStopped) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + // Setup + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(EnableLocalLogging()); + ExpectWebRtcStateChangeInstruction(key, true); + + // Test + ASSERT_TRUE(DisableLocalLogging()); + ExpectWebRtcStateChangeInstruction(key, false); +} + +// #1 - Local logging was the cause of the logs. +TEST_F(WebRtcEventLogManagerTest, + InstructWebRtcToStopSendingEventLogsWhenPeerConnectionRemoved1) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + // Setup + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(EnableLocalLogging()); + ExpectWebRtcStateChangeInstruction(key, true); + + // Test + ASSERT_TRUE(PeerConnectionRemoved(key)); + ExpectWebRtcStateChangeInstruction(key, false); +} + +// #2 - Remote logging was the cause of the logs. +TEST_F(WebRtcEventLogManagerTest, + InstructWebRtcToStopSendingEventLogsWhenPeerConnectionRemoved2) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + // Setup + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ExpectWebRtcStateChangeInstruction(key, true); + + // Test + ASSERT_TRUE(PeerConnectionRemoved(key)); + ExpectWebRtcStateChangeInstruction(key, false); +} + +// #1 - Local logging added first. +TEST_F(WebRtcEventLogManagerTest, + SecondLoggingTargetDoesNotInitiateWebRtcLogging1) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + // Setup + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(EnableLocalLogging()); + ExpectWebRtcStateChangeInstruction(key, true); + + // Test + ASSERT_TRUE(StartRemoteLogging(key)); + EXPECT_TRUE(webrtc_state_change_instructions_.empty()); +} + +// #2 - Remote logging added first. +TEST_F(WebRtcEventLogManagerTest, + SecondLoggingTargetDoesNotInitiateWebRtcLogging2) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + // Setup + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ExpectWebRtcStateChangeInstruction(key, true); + + // Test + ASSERT_TRUE(EnableLocalLogging()); + EXPECT_TRUE(webrtc_state_change_instructions_.empty()); +} + +TEST_F(WebRtcEventLogManagerTest, + DisablingLocalLoggingWhenRemoteLoggingEnabledDoesNotStopWebRtcLogging) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + // Setup + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(StartRemoteLogging(key)); + ExpectWebRtcStateChangeInstruction(key, true); + + // Test + ASSERT_TRUE(DisableLocalLogging()); + EXPECT_TRUE(webrtc_state_change_instructions_.empty()); + + // Cleanup + ASSERT_TRUE(PeerConnectionRemoved(key)); + ExpectWebRtcStateChangeInstruction(key, false); +} + +TEST_F(WebRtcEventLogManagerTest, + DisablingLocalLoggingAfterPcRemovalHasNoEffectOnWebRtcLogging) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + // Setup + SetPeerConnectionTrackerProxyForTesting( + std::make_unique<PeerConnectionTrackerProxyForTesting>(this)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(StartRemoteLogging(key)); + ExpectWebRtcStateChangeInstruction(key, true); + + // Test + ASSERT_TRUE(PeerConnectionRemoved(key)); + ExpectWebRtcStateChangeInstruction(key, false); + ASSERT_TRUE(DisableLocalLogging()); + EXPECT_TRUE(webrtc_state_change_instructions_.empty()); +} + +// Once a peer connection with a given key was removed, it may not again be +// added. But, if this impossible case occurs, WebRtcEventLogManager will +// not crash. +TEST_F(WebRtcEventLogManagerTest, SanityOverRecreatingTheSamePeerConnection) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + OnWebRtcEventLogWrite(key, "log1"); + ASSERT_TRUE(PeerConnectionRemoved(key)); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + OnWebRtcEventLogWrite(key, "log2"); +} + +// The logs would typically be binary. However, the other tests only cover ASCII +// characters, for readability. This test shows that this is not a problem. +TEST_F(WebRtcEventLogManagerTest, LogAllPossibleCharacters) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> local_log_file_path; + ON_CALL(local_observer_, OnLocalLogStarted(key, _)) + .WillByDefault(Invoke(SaveFilePathTo(&local_log_file_path))); + + base::Optional<base::FilePath> remote_log_file_path; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&remote_log_file_path))); + + ASSERT_TRUE(EnableLocalLogging()); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(local_log_file_path); + ASSERT_FALSE(local_log_file_path->empty()); + ASSERT_TRUE(remote_log_file_path); + ASSERT_FALSE(remote_log_file_path->empty()); + + std::string all_chars; + for (size_t i = 0; i < 256; ++i) { + all_chars += static_cast<uint8_t>(i); + } + ASSERT_EQ(OnWebRtcEventLogWrite(key, all_chars), std::make_pair(true, true)); + + // Make sure the file would be closed, so that we could safely read it. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ExpectLocalFileContents(*local_log_file_path, all_chars); + ExpectRemoteFileContents(*remote_log_file_path, all_chars); +} + +TEST_F(WebRtcEventLogManagerTest, LocalLogsClosedWhenRenderProcessHostExits) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(EnableLocalLogging()); + + // The expectation for OnLocalLogStopped() will be saturated by this + // destruction of the RenderProcessHost, which triggers an implicit + // removal of all PeerConnections associated with it. + EXPECT_CALL(local_observer_, OnLocalLogStopped(key)).Times(1); + rph_.reset(); +} + +TEST_F(WebRtcEventLogManagerTest, RemoteLogsClosedWhenRenderProcessHostExits) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + + // The expectation for OnRemoteLogStopped() will be saturated by this + // destruction of the RenderProcessHost, which triggers an implicit + // removal of all PeerConnections associated with it. + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(key)).Times(1); + rph_.reset(); +} + +// Once a RenderProcessHost exits/crashes, its PeerConnections are removed, +// which means that they can no longer suppress an upload. +TEST_F(WebRtcEventLogManagerTest, + RenderProcessHostExitCanRemoveUploadSuppression) { + SuppressUploading(); + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + base::Optional<base::FilePath> file_path; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(PeerConnectionRemoved(key)); + ASSERT_TRUE(file_path); + ASSERT_FALSE(file_path->empty()); + + // The above removal is not sufficient to trigger an upload (so the test will + // not be flaky). It's only once we destroy the RPH with which the suppressing + // PeerConnection is associated, that upload will take place. + base::RunLoop run_loop; + std::list<WebRtcLogFileInfo> expected_files = {WebRtcLogFileInfo( + browser_context_id_, *file_path, GetLastModificationTime(*file_path))}; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, true, &run_loop)); + + // We destroy the RPH without explicitly removing its PeerConnection (unlike + // a call to UnsuppressUploading()). + upload_suppressing_rph_.reset(); + + WaitForPendingTasks(&run_loop); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionAddedOverDestroyedRphReturnsFalse) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + rph_.reset(); + EXPECT_FALSE(PeerConnectionAdded(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionRemovedOverDestroyedRphReturnsFalse) { + // Setup - make sure the |false| returned by the function being tested is + // related to the RPH being dead, and not due other restrictions. + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + // Test + rph_.reset(); + EXPECT_FALSE(PeerConnectionRemoved(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + PeerConnectionStoppedOverDestroyedRphReturnsFalse) { + // Setup - make sure the |false| returned by the function being tested is + // related to the RPH being dead, and not due other restrictions. + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + // Test + rph_.reset(); + EXPECT_FALSE(PeerConnectionStopped(key)); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingOverDestroyedRphReturnsFalse) { + // Setup - make sure the |false| returned by the function being tested is + // related to the RPH being dead, and not due other restrictions. + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + // Test + rph_.reset(); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, nullptr, &error_message)); + EXPECT_EQ(error_message, kStartRemoteLoggingFailureDeadRenderProcessHost); +} + +TEST_F(WebRtcEventLogManagerTest, + OnWebRtcEventLogWriteOverDestroyedRphReturnsFalseAndFalse) { + // Setup - make sure the |false| returned by the function being tested is + // related to the RPH being dead, and not due other restrictions. + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(EnableLocalLogging()); + + // Test + rph_.reset(); + EXPECT_EQ(OnWebRtcEventLogWrite(key, "log"), std::make_pair(false, false)); +} + +TEST_F(WebRtcEventLogManagerTest, DifferentProfilesCanHaveDifferentPolicies) { + auto policy_disabled_profile = + CreateBrowserContext("disabled", true /* is_managed_profile */, + false /* has_device_level_policies */, + false /* policy_allows_remote_logging */); + auto policy_disabled_rph = + std::make_unique<MockRenderProcessHost>(policy_disabled_profile.get()); + const auto disabled_key = + GetPeerConnectionKey(policy_disabled_rph.get(), kLid); + + auto policy_enabled_profile = + CreateBrowserContext("enabled", true /* is_managed_profile */, + false /* has_device_level_policies */, + true /* policy_allows_remote_logging */); + auto policy_enabled_rph = + std::make_unique<MockRenderProcessHost>(policy_enabled_profile.get()); + const auto enabled_key = GetPeerConnectionKey(policy_enabled_rph.get(), kLid); + + ASSERT_TRUE(PeerConnectionAdded(disabled_key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(disabled_key)); + + ASSERT_TRUE(PeerConnectionAdded(enabled_key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(enabled_key)); + + EXPECT_FALSE(StartRemoteLogging(disabled_key)); + EXPECT_TRUE(StartRemoteLogging(enabled_key)); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingWithTooLowWebAppIdRejected) { + const size_t web_app_id = kMinWebRtcEventLogWebAppId - 1; + ASSERT_LT(web_app_id, kMinWebRtcEventLogWebAppId); // Avoid wrap-around. + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_FALSE(StartRemoteLogging(key, GetUniqueId(key), + kMaxRemoteLogFileSizeBytes, 0, web_app_id)); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingWithTooHighWebAppIdRejected) { + const size_t web_app_id = kMaxWebRtcEventLogWebAppId + 1; + ASSERT_GT(web_app_id, kMaxWebRtcEventLogWebAppId); // Avoid wrap-around. + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_FALSE(StartRemoteLogging(key, GetUniqueId(key), + kMaxRemoteLogFileSizeBytes, 0, web_app_id)); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingWithInRangeWebAppIdAllowedMin) { + const size_t web_app_id = kMinWebRtcEventLogWebAppId; + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_TRUE(StartRemoteLogging(key, GetUniqueId(key), + kMaxRemoteLogFileSizeBytes, 0, web_app_id)); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingWithInRangeWebAppIdAllowedMax) { + const size_t web_app_id = kMaxWebRtcEventLogWebAppId; + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_TRUE(StartRemoteLogging(key, GetUniqueId(key), + kMaxRemoteLogFileSizeBytes, 0, web_app_id)); +} + +// Only one remote-bound event log allowed per +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingOverMultipleWebAppsDisallowed) { + // Test assumes there are at least two legal web-app IDs. + ASSERT_NE(kMinWebRtcEventLogWebAppId, kMaxWebRtcEventLogWebAppId); + const size_t web_app_ids[2] = {kMinWebRtcEventLogWebAppId, + kMaxWebRtcEventLogWebAppId}; + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_TRUE(StartRemoteLogging( + key, GetUniqueId(key), kMaxRemoteLogFileSizeBytes, 0, web_app_ids[0])); + EXPECT_FALSE(StartRemoteLogging( + key, GetUniqueId(key), kMaxRemoteLogFileSizeBytes, 0, web_app_ids[1])); +} + +TEST_F(WebRtcEventLogManagerTest, + StartRemoteLoggingWebAppIdIncorporatedIntoFileName) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> file_path; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&file_path))); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + const size_t expected_web_app_id = kWebAppId; + ASSERT_TRUE(StartRemoteLogging(key, GetUniqueId(key), + kMaxRemoteLogFileSizeBytes, 0, + expected_web_app_id)); + ASSERT_TRUE(file_path); + + const size_t written_web_app_id = + ExtractRemoteBoundWebRtcEventLogWebAppIdFromPath(*file_path); + EXPECT_EQ(written_web_app_id, expected_web_app_id); +} + +INSTANTIATE_TEST_SUITE_P(UploadCompleteResult, + WebRtcEventLogManagerTest, + ::testing::Bool()); + +TEST_F(WebRtcEventLogManagerTestCacheClearing, + ClearCacheForBrowserContextRemovesPendingFilesInRange) { + SuppressUploading(); + + auto browser_context = CreateBrowserContext("name"); + CreatePendingLogFiles(browser_context.get()); + auto& elements = *(pending_logs_[browser_context.get()]); + + const base::Time earliest_mod = pending_earliest_mod_ - kEpsion; + const base::Time latest_mod = pending_latest_mod_ + kEpsion; + + // Test - ClearCacheForBrowserContext() removed all of the files in the range. + ClearCacheForBrowserContext(browser_context.get(), earliest_mod, latest_mod); + for (size_t i = 0; i < elements.file_paths.size(); ++i) { + EXPECT_FALSE(base::PathExists(*elements.file_paths[i])); + } + + ClearPendingLogFiles(); +} + +TEST_F(WebRtcEventLogManagerTestCacheClearing, + ClearCacheForBrowserContextCancelsActiveLogFilesIfInRange) { + SuppressUploading(); + + // Setup + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + base::Optional<base::FilePath> file_path; + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&file_path))); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(file_path); + ASSERT_TRUE(base::PathExists(*file_path)); + + // Test + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(key)).Times(1); + ClearCacheForBrowserContext( + browser_context_.get(), base::Time::Now() - base::TimeDelta::FromHours(1), + base::Time::Now() + base::TimeDelta::FromHours(1)); + EXPECT_FALSE(base::PathExists(*file_path)); +} + +TEST_F(WebRtcEventLogManagerTestCacheClearing, + ClearCacheForBrowserContextCancelsFileUploadIfInRange) { + // This factory will enforce the expectation that the upload is cancelled. + // WebRtcEventLogUploaderImplTest.CancelOnOngoingUploadDeletesFile is in + // charge of making sure that when the upload is cancelled, the file is + // removed from disk. + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<NullWebRtcEventLogUploader::Factory>(true)); + + // Set up and trigger the uploading of a file. + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + base::Optional<base::FilePath> file_path = CreatePendingRemoteLogFile(key); + + ASSERT_TRUE(file_path); + ASSERT_TRUE(base::PathExists(*file_path)); + const base::Time mod_time = GetLastModificationTime(*file_path); + + // Main part of test - the expectation set up in the the uploader factory + // should now be satisfied. + ClearCacheForBrowserContext(browser_context_.get(), mod_time - kEpsion, + mod_time + kEpsion); +} + +TEST_F(WebRtcEventLogManagerTestCacheClearing, + ClearCacheForBrowserContextDoesNotRemovePendingFilesOutOfRange) { + SuppressUploading(); + + auto browser_context = CreateBrowserContext("name"); + CreatePendingLogFiles(browser_context.get()); + auto& elements = *(pending_logs_[browser_context.get()]); + + // Get a range whose intersection with the files' range is empty. + const base::Time earliest_mod = + pending_earliest_mod_ - base::TimeDelta::FromHours(2); + const base::Time latest_mod = + pending_earliest_mod_ - base::TimeDelta::FromHours(1); + ASSERT_LT(latest_mod, pending_latest_mod_); + + // Test - ClearCacheForBrowserContext() does not remove files not in range. + // (Range chosen to be earlier than the oldest file + ClearCacheForBrowserContext(browser_context.get(), earliest_mod, latest_mod); + for (size_t i = 0; i < elements.file_paths.size(); ++i) { + EXPECT_TRUE(base::PathExists(*elements.file_paths[i])); + } + + ClearPendingLogFiles(); +} + +TEST_F(WebRtcEventLogManagerTestCacheClearing, + ClearCacheForBrowserContextDoesNotCancelActiveLogFilesIfOutOfRange) { + SuppressUploading(); + + // Setup + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + base::Optional<base::FilePath> file_path; + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&file_path))); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(file_path); + ASSERT_TRUE(base::PathExists(*file_path)); + + // Test + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(_)).Times(0); + ClearCacheForBrowserContext( + browser_context_.get(), base::Time::Now() - base::TimeDelta::FromHours(2), + base::Time::Now() - base::TimeDelta::FromHours(1)); + EXPECT_TRUE(base::PathExists(*file_path)); +} + +TEST_F(WebRtcEventLogManagerTestCacheClearing, + ClearCacheForBrowserContextDoesNotCancelFileUploadIfOutOfRange) { + // This factory will enforce the expectation that the upload is not cancelled. + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<NullWebRtcEventLogUploader::Factory>(false)); + + // Set up and trigger the uploading of a file. + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + base::Optional<base::FilePath> file_path = CreatePendingRemoteLogFile(key); + + ASSERT_TRUE(file_path); + ASSERT_TRUE(base::PathExists(*file_path)); + const base::Time mod_time = GetLastModificationTime(*file_path); + + // Main part of test - the expectation set up in the the uploader factory, + // that the upload will not be cancelled, should be shown to hold true. + // should now be satisfied. + ClearCacheForBrowserContext(browser_context_.get(), mod_time + kEpsion, + mod_time + 2 * kEpsion); +} + +TEST_F(WebRtcEventLogManagerTestCacheClearing, + ClearCacheForBrowserContextDoesNotRemovePendingFilesFromOtherProfiles) { + SuppressUploading(); + + auto cleared_browser_context = CreateBrowserContext("cleared"); + CreatePendingLogFiles(cleared_browser_context.get()); + auto& cleared_elements = *(pending_logs_[cleared_browser_context.get()]); + + auto const uncleared_browser_context = CreateBrowserContext("pristine"); + CreatePendingLogFiles(uncleared_browser_context.get()); + auto& uncleared_elements = *(pending_logs_[uncleared_browser_context.get()]); + + ASSERT_EQ(cleared_elements.file_paths.size(), + uncleared_elements.file_paths.size()); + const size_t kFileCount = cleared_elements.file_paths.size(); + + const base::Time earliest_mod = pending_earliest_mod_ - kEpsion; + const base::Time latest_mod = pending_latest_mod_ + kEpsion; + + // Test - ClearCacheForBrowserContext() only removes the files which belong + // to the cleared context. + ClearCacheForBrowserContext(cleared_browser_context.get(), earliest_mod, + latest_mod); + for (size_t i = 0; i < kFileCount; ++i) { + EXPECT_FALSE(base::PathExists(*cleared_elements.file_paths[i])); + EXPECT_TRUE(base::PathExists(*uncleared_elements.file_paths[i])); + } + + ClearPendingLogFiles(); +} + +TEST_F(WebRtcEventLogManagerTestCacheClearing, + ClearCacheForBrowserContextDoesNotCancelActiveLogsFromOtherProfiles) { + SuppressUploading(); + + // Remote-bound active log file that *will* be cleared. + auto cleared_browser_context = CreateBrowserContext("cleared"); + auto cleared_rph = + std::make_unique<MockRenderProcessHost>(cleared_browser_context.get()); + const auto cleared_key = GetPeerConnectionKey(cleared_rph.get(), kLid); + base::Optional<base::FilePath> cleared_file_path = + CreateActiveRemoteLogFile(cleared_key); + + // Remote-bound active log file that will *not* be cleared. + auto uncleared_browser_context = CreateBrowserContext("pristine"); + auto uncleared_rph = + std::make_unique<MockRenderProcessHost>(uncleared_browser_context.get()); + const auto uncleared_key = GetPeerConnectionKey(uncleared_rph.get(), kLid); + base::Optional<base::FilePath> uncleared_file_path = + CreateActiveRemoteLogFile(uncleared_key); + + // Test - ClearCacheForBrowserContext() only removes the files which belong + // to the cleared context. + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(cleared_key)).Times(1); + EXPECT_CALL(remote_observer_, OnRemoteLogStopped(uncleared_key)).Times(0); + ClearCacheForBrowserContext(cleared_browser_context.get(), base::Time::Min(), + base::Time::Max()); + EXPECT_FALSE(base::PathExists(*cleared_file_path)); + EXPECT_TRUE(base::PathExists(*uncleared_file_path)); + + // Cleanup - uncleared_file_path will be closed as part of the shutdown. It + // is time to clear its expectation. + testing::Mock::VerifyAndClearExpectations(&remote_observer_); +} + +TEST_F(WebRtcEventLogManagerTestCacheClearing, + ClearCacheForBrowserContextDoesNotCancelFileUploadFromOtherProfiles) { + // This factory will enforce the expectation that the upload is not cancelled. + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<NullWebRtcEventLogUploader::Factory>(false)); + + // Set up and trigger the uploading of a file. + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + base::Optional<base::FilePath> file_path = CreatePendingRemoteLogFile(key); + + ASSERT_TRUE(file_path); + ASSERT_TRUE(base::PathExists(*file_path)); + const base::Time mod_time = GetLastModificationTime(*file_path); + + // Main part of test - the expectation set up in the the uploader factory, + // that the upload will not be cancelled, should be shown to hold true. + // should now be satisfied. + auto const different_browser_context = CreateBrowserContext(); + ClearCacheForBrowserContext(different_browser_context.get(), + mod_time - kEpsion, mod_time + kEpsion); +} + +// Show that clearing browser cache, while it removes remote-bound logs, does +// not interfere with local-bound logging, even if that happens on the same PC. +TEST_F(WebRtcEventLogManagerTestCacheClearing, + ClearCacheForBrowserContextDoesNotInterfereWithLocalLogs) { + SuppressUploading(); + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + + base::Optional<base::FilePath> local_log; + ON_CALL(local_observer_, OnLocalLogStarted(key, _)) + .WillByDefault(Invoke(SaveFilePathTo(&local_log))); + ASSERT_TRUE(EnableLocalLogging()); + + // This adds a peer connection for |key|, which also triggers + // OnLocalLogStarted() on |local_observer_|. + auto pending_remote_log = CreatePendingRemoteLogFile(key); + + // Test focus - local logging is uninterrupted. + EXPECT_CALL(local_observer_, OnLocalLogStopped(_)).Times(0); + ClearCacheForBrowserContext(browser_context_.get(), base::Time::Min(), + base::Time::Max()); + EXPECT_TRUE(base::PathExists(*local_log)); + + // Sanity on the test itself; the remote log should have been cleared. + ASSERT_FALSE(base::PathExists(*pending_remote_log)); +} + +// When cache clearing cancels the active upload, the next (non-deleted) pending +// file becomes eligible for upload. +TEST_F(WebRtcEventLogManagerTestCacheClearing, + UploadCancellationTriggersUploadOfNextPendingFile) { + // The first created file will start being uploaded, but then cancelled. + // The second file will never be uploaded (deleted while pending). + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<NullWebRtcEventLogUploader::Factory>(true)); + + // Create the files that will be deleted when cache is cleared. + CreatePendingRemoteLogFile(GetPeerConnectionKey(rph_.get(), 0)); + CreatePendingRemoteLogFile(GetPeerConnectionKey(rph_.get(), 1)); + + // Create the not-deleted file under a different profile, to easily make sure + // it does not fit in the ClearCacheForBrowserContext range (less fiddly than + // a time range). + auto other_browser_context = CreateBrowserContext(); + auto other_rph = + std::make_unique<MockRenderProcessHost>(other_browser_context.get()); + const auto key = GetPeerConnectionKey(other_rph.get(), kLid); + base::Optional<base::FilePath> other_file = CreatePendingRemoteLogFile(key); + ASSERT_TRUE(other_file); + + // Switch the uploader factory to one that will allow us to ensure that the + // new file, which is not deleted, is uploaded. + base::RunLoop run_loop; + std::list<WebRtcLogFileInfo> expected_files = { + WebRtcLogFileInfo(GetBrowserContextId(other_browser_context.get()), + *other_file, GetLastModificationTime(*other_file))}; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, true, &run_loop)); + + // Clearing the cache for the first profile, should now trigger the upload + // of the last remaining unclear pending log file - |other_file|. + ClearCacheForBrowserContext(browser_context_.get(), base::Time::Min(), + base::Time::Max()); + WaitForPendingTasks(&run_loop); +} + +TEST_P(WebRtcEventLogManagerTestWithRemoteLoggingDisabled, + SanityPeerConnectionAdded) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + EXPECT_TRUE(PeerConnectionAdded(key)); +} + +TEST_P(WebRtcEventLogManagerTestWithRemoteLoggingDisabled, + SanityPeerConnectionRemoved) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_TRUE(PeerConnectionRemoved(key)); +} + +TEST_P(WebRtcEventLogManagerTestWithRemoteLoggingDisabled, + SanityPeerConnectionStopped) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + PeerConnectionStopped(key); // No crash. +} + +TEST_P(WebRtcEventLogManagerTestWithRemoteLoggingDisabled, + SanityEnableLocalLogging) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(EnableLocalLogging()); +} + +TEST_P(WebRtcEventLogManagerTestWithRemoteLoggingDisabled, + SanityDisableLocalLogging) { + ASSERT_TRUE(EnableLocalLogging()); + EXPECT_TRUE(DisableLocalLogging()); +} + +TEST_P(WebRtcEventLogManagerTestWithRemoteLoggingDisabled, + SanityStartRemoteLogging) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + std::string error_message; + EXPECT_FALSE(StartRemoteLogging(key, nullptr, &error_message)); + EXPECT_EQ(error_message, kStartRemoteLoggingFailureFeatureDisabled); +} + +TEST_P(WebRtcEventLogManagerTestWithRemoteLoggingDisabled, + SanityOnWebRtcEventLogWrite) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_FALSE(StartRemoteLogging(key)); + EXPECT_EQ(OnWebRtcEventLogWrite(key, "log"), std::make_pair(false, false)); +} + +INSTANTIATE_TEST_SUITE_P(, + WebRtcEventLogManagerTestWithRemoteLoggingDisabled, + ::testing::Bool()); + +// This test is redundant; it is provided for completeness; see following tests. +TEST_F(WebRtcEventLogManagerTestPolicy, StartsEnabledAllowsRemoteLogging) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + const bool allow_remote_logging = true; + auto browser_context = CreateBrowserContext( + "name", true /* is_managed_profile */, + false /* has_device_level_policies */, allow_remote_logging); + + auto rph = std::make_unique<MockRenderProcessHost>(browser_context.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_EQ(StartRemoteLogging(key), allow_remote_logging); +} + +// This test is redundant; it is provided for completeness; see following tests. +TEST_F(WebRtcEventLogManagerTestPolicy, StartsDisabledRejectsRemoteLogging) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + const bool allow_remote_logging = false; + auto browser_context = CreateBrowserContext( + "name", true /* is_managed_profile */, + false /* has_device_level_policies */, allow_remote_logging); + + auto rph = std::make_unique<MockRenderProcessHost>(browser_context.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_EQ(StartRemoteLogging(key), allow_remote_logging); +} + +TEST_F(WebRtcEventLogManagerTestPolicy, NotManagedRejectsRemoteLogging) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + const bool allow_remote_logging = false; + auto browser_context = CreateBrowserContext( + "name", false /* is_managed_profile */, + false /* has_device_level_policies */, base::nullopt); + + auto rph = std::make_unique<MockRenderProcessHost>(browser_context.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_EQ(StartRemoteLogging(key), allow_remote_logging); +} + +TEST_F(WebRtcEventLogManagerTestPolicy, + ManagedProfileAllowsRemoteLoggingByDefault) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + const bool allow_remote_logging = true; + auto browser_context = CreateBrowserContext( + "name", true /* is_managed_profile */, + false /* has_device_level_policies */, base::nullopt); + + auto rph = std::make_unique<MockRenderProcessHost>(browser_context.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_EQ(StartRemoteLogging(key), allow_remote_logging); +} + +#if !defined(OS_ANDROID) && !defined(OS_CHROMEOS) +TEST_F(WebRtcEventLogManagerTestPolicy, + ManagedByPlatformPoliciesAllowsRemoteLoggingByDefault) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + const bool allow_remote_logging = true; + auto browser_context = + CreateBrowserContext("name", false /* is_managed_profile */, + true /* has_device_level_policies */, base::nullopt); + + auto rph = std::make_unique<MockRenderProcessHost>(browser_context.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_EQ(StartRemoteLogging(key), allow_remote_logging); +} +#endif + +// #1 and #2 differ in the order of AddPeerConnection and the changing of +// the pref value. +TEST_F(WebRtcEventLogManagerTestPolicy, + StartsEnabledThenDisabledRejectsRemoteLogging1) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + bool allow_remote_logging = true; + auto profile = CreateBrowserContext("name", true /* is_managed_profile */, + false /* has_device_level_policies */, + allow_remote_logging); + + auto rph = std::make_unique<MockRenderProcessHost>(profile.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + allow_remote_logging = !allow_remote_logging; + profile->GetPrefs()->SetBoolean(prefs::kWebRtcEventLogCollectionAllowed, + allow_remote_logging); + + EXPECT_EQ(StartRemoteLogging(key), allow_remote_logging); +} + +// #1 and #2 differ in the order of AddPeerConnection and the changing of +// the pref value. +TEST_F(WebRtcEventLogManagerTestPolicy, + StartsEnabledThenDisabledRejectsRemoteLogging2) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + bool allow_remote_logging = true; + auto profile = CreateBrowserContext("name", true /* is_managed_profile */, + false /* has_device_level_policies */, + allow_remote_logging); + + auto rph = std::make_unique<MockRenderProcessHost>(profile.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + allow_remote_logging = !allow_remote_logging; + profile->GetPrefs()->SetBoolean(prefs::kWebRtcEventLogCollectionAllowed, + allow_remote_logging); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + EXPECT_EQ(StartRemoteLogging(key), allow_remote_logging); +} + +// #1 and #2 differ in the order of AddPeerConnection and the changing of +// the pref value. +TEST_F(WebRtcEventLogManagerTestPolicy, + StartsDisabledThenEnabledAllowsRemoteLogging1) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + bool allow_remote_logging = false; + auto profile = CreateBrowserContext("name", true /* is_managed_profile */, + false /* has_device_level_policies */, + allow_remote_logging); + + auto rph = std::make_unique<MockRenderProcessHost>(profile.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + allow_remote_logging = !allow_remote_logging; + profile->GetPrefs()->SetBoolean(prefs::kWebRtcEventLogCollectionAllowed, + allow_remote_logging); + + EXPECT_EQ(StartRemoteLogging(key), allow_remote_logging); +} + +// #1 and #2 differ in the order of AddPeerConnection and the changing of +// the pref value. +TEST_F(WebRtcEventLogManagerTestPolicy, + StartsDisabledThenEnabledAllowsRemoteLogging2) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + bool allow_remote_logging = false; + auto profile = CreateBrowserContext("name", true /* is_managed_profile */, + false /* has_device_level_policies */, + allow_remote_logging); + + auto rph = std::make_unique<MockRenderProcessHost>(profile.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + allow_remote_logging = !allow_remote_logging; + profile->GetPrefs()->SetBoolean(prefs::kWebRtcEventLogCollectionAllowed, + allow_remote_logging); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + EXPECT_EQ(StartRemoteLogging(key), allow_remote_logging); +} + +TEST_F(WebRtcEventLogManagerTestPolicy, + StartsDisabledThenEnabledUploadsPendingLogFiles) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + bool allow_remote_logging = false; + auto profile = CreateBrowserContext("name", true /* is_managed_profile */, + false /* has_device_level_policies */, + allow_remote_logging); + + auto rph = std::make_unique<MockRenderProcessHost>(profile.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + allow_remote_logging = !allow_remote_logging; + profile->GetPrefs()->SetBoolean(prefs::kWebRtcEventLogCollectionAllowed, + allow_remote_logging); + + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(allow_remote_logging) + << "Must turn on before StartRemoteLogging, to test the right thing."; + ASSERT_EQ(StartRemoteLogging(key), allow_remote_logging); + ASSERT_TRUE(log_file); + + base::RunLoop run_loop; + std::list<WebRtcLogFileInfo> expected_files = {WebRtcLogFileInfo( + browser_context_id_, *log_file, GetLastModificationTime(*log_file))}; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, true, &run_loop)); + + ASSERT_TRUE(PeerConnectionRemoved(key)); + + WaitForPendingTasks(&run_loop); +} + +TEST_F(WebRtcEventLogManagerTestPolicy, + StartsEnabledThenDisabledDoesNotUploadPendingLogFiles) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + SuppressUploading(); + + std::list<WebRtcLogFileInfo> empty_list; + base::RunLoop run_loop; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &empty_list, true, &run_loop)); + + bool allow_remote_logging = true; + auto profile = CreateBrowserContext("name", true /* is_managed_profile */, + false /* has_device_level_policies */, + allow_remote_logging); + + auto rph = std::make_unique<MockRenderProcessHost>(profile.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(allow_remote_logging) + << "Must turn off after StartRemoteLogging, to test the right thing."; + ASSERT_EQ(StartRemoteLogging(key), allow_remote_logging); + ASSERT_TRUE(PeerConnectionRemoved(key)); + + allow_remote_logging = !allow_remote_logging; + profile->GetPrefs()->SetBoolean(prefs::kWebRtcEventLogCollectionAllowed, + allow_remote_logging); + + UnsuppressUploading(); + + WaitForPendingTasks(&run_loop); +} + +TEST_F(WebRtcEventLogManagerTestPolicy, + StartsEnabledThenDisabledDeletesPendingLogFiles) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + SuppressUploading(); + + std::list<WebRtcLogFileInfo> empty_list; + base::RunLoop run_loop; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &empty_list, true, &run_loop)); + + bool allow_remote_logging = true; + auto profile = CreateBrowserContext("name", true /* is_managed_profile */, + false /* has_device_level_policies */, + allow_remote_logging); + + auto rph = std::make_unique<MockRenderProcessHost>(profile.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(allow_remote_logging) + << "Must turn off after StartRemoteLogging, to test the right thing."; + ASSERT_EQ(StartRemoteLogging(key), allow_remote_logging); + ASSERT_TRUE(log_file); + + // Make the file PENDING. + ASSERT_TRUE(PeerConnectionRemoved(key)); + ASSERT_TRUE(base::PathExists(*log_file)); // Test sanity; exists before. + + allow_remote_logging = !allow_remote_logging; + profile->GetPrefs()->SetBoolean(prefs::kWebRtcEventLogCollectionAllowed, + allow_remote_logging); + + WaitForPendingTasks(&run_loop); + + // Test focus - file deleted without being uploaded. + EXPECT_FALSE(base::PathExists(*log_file)); + + // Still not uploaded. + UnsuppressUploading(); + WaitForPendingTasks(); +} + +TEST_F(WebRtcEventLogManagerTestPolicy, + StartsEnabledThenDisabledCancelsAndDeletesCurrentlyUploadedLogFile) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + // This factory expects exactly one log to be uploaded, then cancelled. + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<NullWebRtcEventLogUploader::Factory>(true, 1)); + + bool allow_remote_logging = true; + auto profile = CreateBrowserContext("name", true /* is_managed_profile */, + false /* has_device_level_policies */, + allow_remote_logging); + + auto rph = std::make_unique<MockRenderProcessHost>(profile.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(allow_remote_logging) + << "Must turn off after StartRemoteLogging, to test the right thing."; + ASSERT_EQ(StartRemoteLogging(key), allow_remote_logging); + ASSERT_TRUE(log_file); + + // Log file's upload commences. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + ASSERT_TRUE(base::PathExists(*log_file)); // Test sanity; exists before. + + allow_remote_logging = !allow_remote_logging; + profile->GetPrefs()->SetBoolean(prefs::kWebRtcEventLogCollectionAllowed, + allow_remote_logging); + + WaitForPendingTasks(); + + // Test focus - file deleted without being uploaded. + // When the test terminates, the NullWebRtcEventLogUploader::Factory's + // expectation that one log file was uploaded, and that the upload was + // cancelled, is enforced. + // Deletion of the file not performed by NullWebRtcEventLogUploader; instead, + // WebRtcEventLogUploaderImplTest.CancelOnOngoingUploadDeletesFile tests that. +} + +// This test makes sure that if the policy was enabled in the past, but was +// disabled while Chrome was not running, pending logs created during the +// earlier session will be deleted from disk. +TEST_F(WebRtcEventLogManagerTestPolicy, + PendingLogsFromPreviousSessionRemovedIfPolicyDisabledAtNewSessionStart) { + SetUp(true); // Feature generally enabled (kill-switch not engaged). + + SuppressUploading(); + + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<NullWebRtcEventLogUploader::Factory>(true, 0)); + + bool allow_remote_logging = true; + auto browser_context = CreateBrowserContext( + "name", true /* is_managed_profile */, + false /* has_device_level_policies */, allow_remote_logging); + + const base::FilePath browser_context_dir = + RemoteBoundLogsDir(browser_context.get()); + ASSERT_TRUE(base::DirectoryExists(browser_context_dir)); + + auto rph = std::make_unique<MockRenderProcessHost>(browser_context.get()); + const auto key = GetPeerConnectionKey(rph.get(), kLid); + + // Produce an empty log file in the BrowserContext. It's not uploaded + // because uploading is suppressed. + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(allow_remote_logging) + << "Must turn off after StartRemoteLogging, to test the right thing."; + ASSERT_EQ(StartRemoteLogging(key), allow_remote_logging); + ASSERT_TRUE(PeerConnectionRemoved(key)); + + // Reload the BrowserContext, but this time with the policy disabling + // the feature. + rph.reset(); + browser_context.reset(); + ASSERT_TRUE(base::DirectoryExists(browser_context_dir)); // Test sanity + allow_remote_logging = false; + browser_context = CreateBrowserContext("name", true /* is_managed_profile */, + false /* has_device_level_policies */, + allow_remote_logging); + + // Test focus - pending log files removed, as well as any potential metadata + // associated with remote-bound logging for |browser_context|. + ASSERT_FALSE(base::DirectoryExists(browser_context_dir)); + + // When NullWebRtcEventLogUploader::Factory is destroyed, it will show that + // the deleted log file was never uploaded. + UnsuppressUploading(); + WaitForPendingTasks(); +} + +TEST_F(WebRtcEventLogManagerTestPolicy, + PendingLogsFromPreviousSessionRemovedIfRemoteLoggingKillSwitchEngaged) { + SetUp(false); // Feature generally disabled (kill-switch engaged). + + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<NullWebRtcEventLogUploader::Factory>(true, 0)); + + const std::string name = "name"; + const base::FilePath browser_context_dir = + profiles_dir_.GetPath().AppendASCII(name); + const base::FilePath remote_bound_dir = + RemoteBoundLogsDir(browser_context_dir); + ASSERT_FALSE(base::PathExists(remote_bound_dir)); + + base::FilePath file_path; + base::File file; + ASSERT_TRUE(base::CreateDirectory(remote_bound_dir)); + ASSERT_TRUE(CreateRemoteBoundLogFile(remote_bound_dir, kWebAppId, + remote_log_extension_, base::Time::Now(), + &file_path, &file)); + file.Close(); + + const bool allow_remote_logging = true; + auto browser_context = CreateBrowserContext( + "name", true /* is_managed_profile */, + false /* has_device_level_policies */, allow_remote_logging); + ASSERT_EQ(browser_context->GetPath(), browser_context_dir); // Test sanity + + WaitForPendingTasks(); + + EXPECT_FALSE(base::PathExists(remote_bound_dir)); +} + +TEST_F(WebRtcEventLogManagerTestUploadSuppressionDisablingFlag, + UploadingNotSuppressedByActivePeerConnections) { + SuppressUploading(); + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(log_file); + + base::RunLoop run_loop; + std::list<WebRtcLogFileInfo> expected_files = {WebRtcLogFileInfo( + browser_context_id_, *log_file, GetLastModificationTime(*log_file))}; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, true, &run_loop)); + + ASSERT_TRUE(PeerConnectionRemoved(key)); + WaitForPendingTasks(&run_loop); +} + +TEST_P(WebRtcEventLogManagerTestForNetworkConnectivity, + DoNotUploadPendingLogsIfConnectedToUnsupportedNetworkType) { + SetUpNetworkConnection(get_conn_type_is_sync_, unsupported_type_); + + const auto key = GetPeerConnectionKey(rph_.get(), 1); + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(log_file); + + std::list<WebRtcLogFileInfo> empty_expected_files_list; + base::RunLoop run_loop; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &empty_expected_files_list, true, &run_loop)); + + // Peer connection removal MAY trigger upload, depending on network. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + WaitForPendingTasks(&run_loop); +} + +TEST_P(WebRtcEventLogManagerTestForNetworkConnectivity, + UploadPendingLogsIfConnectedToSupportedNetworkType) { + SetUpNetworkConnection(get_conn_type_is_sync_, supported_type_); + + const auto key = GetPeerConnectionKey(rph_.get(), 1); + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(log_file); + + base::RunLoop run_loop; + std::list<WebRtcLogFileInfo> expected_files = {WebRtcLogFileInfo( + browser_context_id_, *log_file, GetLastModificationTime(*log_file))}; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, true, &run_loop)); + + // Peer connection removal MAY trigger upload, depending on network. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + WaitForPendingTasks(&run_loop); +} + +TEST_P(WebRtcEventLogManagerTestForNetworkConnectivity, + UploadPendingLogsIfConnectionTypeChangesFromUnsupportedToSupported) { + SetUpNetworkConnection(get_conn_type_is_sync_, unsupported_type_); + + const auto key = GetPeerConnectionKey(rph_.get(), 1); + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(log_file); + + // That a peer connection upload is not initiated by this point, is verified + // by previous tests. + ASSERT_TRUE(PeerConnectionRemoved(key)); + WaitForPendingTasks(); + + // Test focus - an upload will be initiated after changing the network type. + base::RunLoop run_loop; + std::list<WebRtcLogFileInfo> expected_files = {WebRtcLogFileInfo( + browser_context_id_, *log_file, GetLastModificationTime(*log_file))}; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, true, &run_loop)); + SetConnectionType(supported_type_); + + WaitForPendingTasks(&run_loop); +} + +TEST_P(WebRtcEventLogManagerTestForNetworkConnectivity, + DoNotUploadPendingLogsAtStartupIfConnectedToUnsupportedNetworkType) { + SetUpNetworkConnection(get_conn_type_is_sync_, unsupported_type_); + + UnloadProfileAndSeedPendingLog(); + + // This factory enforces the expectation that the files will be uploaded, + // all of them, only them, and in the order expected. + std::list<WebRtcLogFileInfo> empty_expected_files_list; + base::RunLoop run_loop; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &empty_expected_files_list, true, &run_loop)); + + LoadMainTestProfile(); + ASSERT_EQ(browser_context_->GetPath(), browser_context_path_); + + WaitForPendingTasks(&run_loop); +} + +TEST_P(WebRtcEventLogManagerTestForNetworkConnectivity, + UploadPendingLogsAtStartupIfConnectedToSupportedNetworkType) { + SetUpNetworkConnection(get_conn_type_is_sync_, supported_type_); + + UnloadProfileAndSeedPendingLog(); + + // This factory enforces the expectation that the files will be uploaded, + // all of them, only them, and in the order expected. + base::RunLoop run_loop; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files_, true, &run_loop)); + + LoadMainTestProfile(); + ASSERT_EQ(browser_context_->GetPath(), browser_context_path_); + + WaitForPendingTasks(&run_loop); +} + +INSTANTIATE_TEST_SUITE_P( + UploadSupportingConnectionTypes, + WebRtcEventLogManagerTestForNetworkConnectivity, + ::testing::Combine( + // Wehther GetConnectionType() responds synchronously. + ::testing::Bool(), + // The upload-supporting network type to be used. + ::testing::Values(network::mojom::ConnectionType::CONNECTION_ETHERNET, + network::mojom::ConnectionType::CONNECTION_WIFI, + network::mojom::ConnectionType::CONNECTION_UNKNOWN), + // The upload-unsupporting network type to be used. + ::testing::Values(network::mojom::ConnectionType::CONNECTION_NONE, + network::mojom::ConnectionType::CONNECTION_4G))); + +TEST_F(WebRtcEventLogManagerTestUploadDelay, DoNotInitiateUploadBeforeDelay) { + SetUp(kIntentionallyExcessiveDelayMs); + + const auto key = GetPeerConnectionKey(rph_.get(), 1); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + + std::list<WebRtcLogFileInfo> empty_list; + base::RunLoop run_loop; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &empty_list, true, &run_loop)); + + // Change log file from ACTIVE to PENDING. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + // Wait a bit and see that the upload was not initiated. (Due to technical + // constraints, we cannot wait forever.) + base::WaitableEvent event(base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED); + event.TimedWait(base::TimeDelta::FromMilliseconds(500)); + + WaitForPendingTasks(&run_loop); +} + +// WhenPeerConnectionRemovedFinishedRemoteLogUploadedAndFileDeleted has some +// overlap with this, but we still include this test for explicitness and +// clarity. +TEST_F(WebRtcEventLogManagerTestUploadDelay, InitiateUploadAfterDelay) { + SetUp(kDefaultUploadDelayMs); + + const auto key = GetPeerConnectionKey(rph_.get(), 1); + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(log_file); + + base::RunLoop run_loop; + std::list<WebRtcLogFileInfo> expected_files = {WebRtcLogFileInfo( + browser_context_id_, *log_file, GetLastModificationTime(*log_file))}; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &expected_files, true, &run_loop)); + + // Change log file from ACTIVE to PENDING. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + WaitForPendingTasks(&run_loop); +} + +TEST_F(WebRtcEventLogManagerTestUploadDelay, + PeerConnectionAddedDuringDelaySuppressesUpload) { + SetUp(kIntentionallyExcessiveDelayMs); + + const auto key1 = GetPeerConnectionKey(rph_.get(), 1); + const auto key2 = GetPeerConnectionKey(rph_.get(), 2); + + ASSERT_TRUE(PeerConnectionAdded(key1)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key1)); + ASSERT_TRUE(StartRemoteLogging(key1)); + + std::list<WebRtcLogFileInfo> empty_list; + base::RunLoop run_loop; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &empty_list, true, &run_loop)); + + // Change log file from ACTIVE to PENDING. + ASSERT_TRUE(PeerConnectionRemoved(key1)); + + // Test focus - after adding a peer connection, the conditions for the upload + // are no longer considered to hold. + // (Test implemented with a glimpse into the black box due to technical + // limitations and the desire to avoid flakiness.) + ASSERT_TRUE(PeerConnectionAdded(key2)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key2)); + EXPECT_FALSE(UploadConditionsHold()); + + WaitForPendingTasks(&run_loop); +} + +TEST_F(WebRtcEventLogManagerTestUploadDelay, + ClearCacheForBrowserContextDuringDelayCancelsItsUpload) { + SetUp(kIntentionallyExcessiveDelayMs); + + const auto key = GetPeerConnectionKey(rph_.get(), 1); + + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key)); + + std::list<WebRtcLogFileInfo> empty_list; + base::RunLoop run_loop; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &empty_list, true, &run_loop)); + + // Change log file from ACTIVE to PENDING. + ASSERT_TRUE(PeerConnectionRemoved(key)); + + // Test focus - after clearing browser cache, the conditions for the upload + // are no longer considered to hold, because the file about to be uploaded + // was deleted. + // (Test implemented with a glimpse into the black box due to technical + // limitations and the desire to avoid flakiness.) + ClearCacheForBrowserContext(browser_context_.get(), base::Time::Min(), + base::Time::Max()); + EXPECT_FALSE(UploadConditionsHold()); + + WaitForPendingTasks(&run_loop); +} + +TEST_F(WebRtcEventLogManagerTestCompression, + ErroredFilesDueToBadEstimationDeletedRatherThanUploaded) { + Init(Compression::GZIP_NULL_ESTIMATION); + + const std::string log = "It's better than bad; it's good."; + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + base::Optional<base::FilePath> log_file; + ON_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .WillByDefault(Invoke(SaveFilePathTo(&log_file))); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_TRUE(StartRemoteLogging(key, GetUniqueId(key), GzippedSize(log) - 1, 0, + kWebAppId)); + ASSERT_TRUE(log_file); + + std::list<WebRtcLogFileInfo> empty_list; + base::RunLoop run_loop; + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<FileListExpectingWebRtcEventLogUploader::Factory>( + &empty_list, true, &run_loop)); + + // Writing fails because the budget is exceeded. + EXPECT_EQ(OnWebRtcEventLogWrite(key, log), std::make_pair(false, false)); + + // The file was deleted due to the error we've instigated (by using an + // intentionally over-optimistic estimation). + EXPECT_FALSE(base::PathExists(*log_file)); + + // If the file is incorrectly still eligible for an upload, this will trigger + // the upload (which will be a test failure). + ASSERT_TRUE(PeerConnectionRemoved(key)); + + WaitForPendingTasks(&run_loop); +} + +TEST_F(WebRtcEventLogManagerTestIncognito, + NoRemoteBoundLogsDirectoryCreatedWhenProfileLoaded) { + const base::FilePath remote_logs_path = + RemoteBoundLogsDir(incognito_profile_); + EXPECT_FALSE(base::DirectoryExists(remote_logs_path)); +} + +TEST_F(WebRtcEventLogManagerTestIncognito, StartRemoteLoggingFails) { + const auto key = GetPeerConnectionKey(incognito_rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_FALSE(StartRemoteLogging(key)); +} + +TEST_F(WebRtcEventLogManagerTestIncognito, + StartRemoteLoggingDoesNotCreateDirectoryOrFiles) { + const auto key = GetPeerConnectionKey(incognito_rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_FALSE(StartRemoteLogging(key)); + + const base::FilePath remote_logs_path = + RemoteBoundLogsDir(incognito_profile_); + EXPECT_FALSE(base::DirectoryExists(remote_logs_path)); +} + +TEST_F(WebRtcEventLogManagerTestIncognito, + OnWebRtcEventLogWriteReturnsFalseForRemotePart) { + const auto key = GetPeerConnectionKey(incognito_rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + ASSERT_FALSE(StartRemoteLogging(key)); + EXPECT_EQ(OnWebRtcEventLogWrite(key, "log"), std::make_pair(false, false)); +} + +TEST_F(WebRtcEventLogManagerTestHistory, + CorrectHistoryReturnedForActivelyWrittenLog) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + base::Optional<base::FilePath> path; + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&path))); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(path); + ASSERT_FALSE(path->BaseName().MaybeAsASCII().empty()); + + const auto history = GetHistory(browser_context_id_); + ASSERT_EQ(history.size(), 1u); + const auto history_entry = history[0]; + + EXPECT_EQ(history_entry.state, UploadList::UploadInfo::State::Pending); + EXPECT_TRUE(IsSmallTimeDelta(history_entry.capture_time, base::Time::Now())); + EXPECT_EQ(history_entry.local_id, path->BaseName().MaybeAsASCII()); + EXPECT_TRUE(history_entry.upload_id.empty()); + EXPECT_TRUE(history_entry.upload_time.is_null()); +} + +TEST_F(WebRtcEventLogManagerTestHistory, CorrectHistoryReturnedForPendingLog) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + base::Optional<base::FilePath> path; + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&path))); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(path); + ASSERT_FALSE(path->BaseName().MaybeAsASCII().empty()); + + SuppressUploading(); + ASSERT_TRUE(PeerConnectionRemoved(key)); + + const auto history = GetHistory(browser_context_id_); + ASSERT_EQ(history.size(), 1u); + const auto history_entry = history[0]; + + EXPECT_EQ(history_entry.state, UploadList::UploadInfo::State::Pending); + EXPECT_TRUE(IsSmallTimeDelta(history_entry.capture_time, base::Time::Now())); + EXPECT_EQ(history_entry.local_id, path->BaseName().MaybeAsASCII()); + EXPECT_TRUE(history_entry.upload_id.empty()); + EXPECT_TRUE(history_entry.upload_time.is_null()); +} + +TEST_F(WebRtcEventLogManagerTestHistory, + CorrectHistoryReturnedForActivelyUploadedLog) { + // This factory expects exactly one log to be uploaded; cancellation is + // expected during tear-down. + SetWebRtcEventLogUploaderFactoryForTesting( + std::make_unique<NullWebRtcEventLogUploader::Factory>(true, 1)); + + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + base::Optional<base::FilePath> path; + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&path))); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(path); + ASSERT_FALSE(path->BaseName().MaybeAsASCII().empty()); + + ASSERT_TRUE(PeerConnectionRemoved(key)); + + const auto history = GetHistory(browser_context_id_); + ASSERT_EQ(history.size(), 1u); + const auto history_entry = history[0]; + + EXPECT_EQ(history_entry.state, UploadList::UploadInfo::State::Pending); + EXPECT_TRUE(IsSmallTimeDelta(history_entry.capture_time, base::Time::Now())); + EXPECT_EQ(history_entry.local_id, path->BaseName().MaybeAsASCII()); + EXPECT_TRUE(history_entry.upload_id.empty()); + EXPECT_TRUE(IsSmallTimeDelta(history_entry.upload_time, base::Time::Now())); + EXPECT_LE(history_entry.capture_time, history_entry.upload_time); + + // Test tear down - trigger uploader cancellation. + ClearCacheForBrowserContext(browser_context_.get(), base::Time::Min(), + base::Time::Max()); +} + +// See ExpiredLogFilesAreReplacedByHistoryFiles for verification of the +// creation of history files of this type. +TEST_F(WebRtcEventLogManagerTestHistory, + ExpiredLogFilesReplacedByHistoryFilesAndGetHistoryReportsAccordingly) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + base::Optional<base::FilePath> log_path; + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&log_path))); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(log_path); + ASSERT_FALSE(log_path->BaseName().MaybeAsASCII().empty()); + + SuppressUploading(); + ASSERT_TRUE(PeerConnectionRemoved(key)); + + UnloadMainTestProfile(); + + // Test sanity. + ASSERT_TRUE(base::PathExists(*log_path)); + + // Pretend more time than kRemoteBoundWebRtcEventLogsMaxRetention has passed. + const base::TimeDelta elapsed_time = + kRemoteBoundWebRtcEventLogsMaxRetention + base::TimeDelta::FromHours(1); + base::File::Info file_info; + ASSERT_TRUE(base::GetFileInfo(*log_path, &file_info)); + + const auto modified_capture_time = file_info.last_modified - elapsed_time; + ASSERT_TRUE(base::TouchFile(*log_path, file_info.last_accessed - elapsed_time, + modified_capture_time)); + + LoadMainTestProfile(); + + ASSERT_FALSE(base::PathExists(*log_path)); + + const auto history = GetHistory(browser_context_id_); + ASSERT_EQ(history.size(), 1u); + const auto history_entry = history[0]; + + EXPECT_EQ(history_entry.state, UploadList::UploadInfo::State::NotUploaded); + EXPECT_TRUE(IsSameTimeWhenTruncatedToSeconds(history_entry.capture_time, + modified_capture_time)); + EXPECT_EQ(history_entry.local_id, + ExtractRemoteBoundWebRtcEventLogLocalIdFromPath(*log_path)); + EXPECT_TRUE(history_entry.upload_id.empty()); + EXPECT_TRUE(history_entry.upload_time.is_null()); +} + +// Since the uploader mocks do not write the history files, it is not easy +// to check that the correct result is returned for GetHistory() for either +// a successful or an unsuccessful upload from the WebRtcEventLogManager level. +// Instead, this is checked by WebRtcEventLogUploaderImplTest. +// TODO(crbug.com/775415): Add the tests mention in the comment above. + +TEST_F(WebRtcEventLogManagerTestHistory, ClearingCacheRemovesHistoryFiles) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + base::Optional<base::FilePath> log_path; + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&log_path))); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(log_path); + ASSERT_FALSE(log_path->BaseName().MaybeAsASCII().empty()); + + SuppressUploading(); + ASSERT_TRUE(PeerConnectionRemoved(key)); + + UnloadMainTestProfile(); + + // Test sanity. + ASSERT_TRUE(base::PathExists(*log_path)); + + // Pretend more time than kRemoteBoundWebRtcEventLogsMaxRetention has passed. + const base::TimeDelta elapsed_time = + kRemoteBoundWebRtcEventLogsMaxRetention + base::TimeDelta::FromHours(1); + base::File::Info file_info; + ASSERT_TRUE(base::GetFileInfo(*log_path, &file_info)); + + const auto modified_capture_time = file_info.last_modified - elapsed_time; + ASSERT_TRUE(base::TouchFile(*log_path, file_info.last_accessed - elapsed_time, + modified_capture_time)); + + LoadMainTestProfile(); + + ASSERT_FALSE(base::PathExists(*log_path)); + + // Setup complete; we now have a history file on disk. Time to see that it is + // removed when cache is cleared. + + // Sanity. + const auto history_path = GetWebRtcEventLogHistoryFilePath(*log_path); + ASSERT_TRUE(base::PathExists(history_path)); + ASSERT_EQ(GetHistory(browser_context_id_).size(), 1u); + + // Test. + ClearCacheForBrowserContext(browser_context_.get(), base::Time::Min(), + base::Time::Max()); + ASSERT_FALSE(base::PathExists(history_path)); + ASSERT_EQ(GetHistory(browser_context_id_).size(), 0u); +} + +TEST_F(WebRtcEventLogManagerTestHistory, + ClearingCacheDoesNotLeaveBehindHistoryForRemovedLogs) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + + base::Optional<base::FilePath> log_path; + EXPECT_CALL(remote_observer_, OnRemoteLogStarted(key, _, _)) + .Times(1) + .WillOnce(Invoke(SaveFilePathTo(&log_path))); + ASSERT_TRUE(StartRemoteLogging(key)); + ASSERT_TRUE(log_path); + ASSERT_FALSE(log_path->BaseName().MaybeAsASCII().empty()); + + ASSERT_TRUE(base::PathExists(*log_path)); + ClearCacheForBrowserContext(browser_context_.get(), base::Time::Min(), + base::Time::Max()); + ASSERT_FALSE(base::PathExists(*log_path)); + + const auto history = GetHistory(browser_context_id_); + EXPECT_EQ(history.size(), 0u); +} + +// TODO(crbug.com/775415): Add a test for the limit on the number of history +// files allowed to remain on disk. + +#else // defined(OS_ANDROID) + +class WebRtcEventLogManagerTestOnMobileDevices + : public WebRtcEventLogManagerTestBase { + public: + WebRtcEventLogManagerTestOnMobileDevices() { + // features::kWebRtcRemoteEventLog not defined on mobile, and can therefore + // not be forced on. This test is here to make sure that when the feature + // is changed to be on by default, it will still be off for mobile devices. + CreateWebRtcEventLogManager(); + } +}; + +TEST_F(WebRtcEventLogManagerTestOnMobileDevices, RemoteBoundLoggingDisabled) { + const auto key = GetPeerConnectionKey(rph_.get(), kLid); + ASSERT_TRUE(PeerConnectionAdded(key)); + ASSERT_TRUE(PeerConnectionSessionIdSet(key)); + EXPECT_FALSE(StartRemoteLogging(key)); +} + +#endif + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_unittest_helpers.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_unittest_helpers.cc new file mode 100644 index 00000000000..30fb9113e19 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_unittest_helpers.cc @@ -0,0 +1,98 @@ +// Copyright (c) 2018 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 "chrome/browser/media/webrtc/webrtc_event_log_manager_unittest_helpers.h" + +#include "base/files/file_util.h" +#include "base/logging.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace webrtc_event_logging { + +// Produce a LogFileWriter::Factory object. +std::unique_ptr<LogFileWriter::Factory> CreateLogFileWriterFactory( + WebRtcEventLogCompression compression) { + switch (compression) { + case WebRtcEventLogCompression::NONE: + return std::make_unique<BaseLogFileWriterFactory>(); + case WebRtcEventLogCompression::GZIP_NULL_ESTIMATION: + return std::make_unique<GzippedLogFileWriterFactory>( + std::make_unique<GzipLogCompressorFactory>( + std::make_unique<NullEstimator::Factory>())); + case WebRtcEventLogCompression::GZIP_PERFECT_ESTIMATION: + return std::make_unique<GzippedLogFileWriterFactory>( + std::make_unique<GzipLogCompressorFactory>( + std::make_unique<PerfectGzipEstimator::Factory>())); + } + NOTREACHED(); + return nullptr; // Appease compiler. +} + +#if defined(OS_POSIX) +void RemoveWritePermissions(const base::FilePath& path) { + int permissions; + ASSERT_TRUE(base::GetPosixFilePermissions(path, &permissions)); + constexpr int write_permissions = base::FILE_PERMISSION_WRITE_BY_USER | + base::FILE_PERMISSION_WRITE_BY_GROUP | + base::FILE_PERMISSION_WRITE_BY_OTHERS; + permissions &= ~write_permissions; + ASSERT_TRUE(base::SetPosixFilePermissions(path, permissions)); +} +#endif // defined(OS_POSIX) + +std::unique_ptr<CompressedSizeEstimator> NullEstimator::Factory::Create() + const { + return std::make_unique<NullEstimator>(); +} + +size_t NullEstimator::EstimateCompressedSize(const std::string& input) const { + return 0; +} + +std::unique_ptr<CompressedSizeEstimator> PerfectGzipEstimator::Factory::Create() + const { + return std::make_unique<PerfectGzipEstimator>(); +} + +PerfectGzipEstimator::PerfectGzipEstimator() { + // This factory will produce an optimistic compressor that will always + // think it can compress additional inputs, which will therefore allow + // us to find out what the real compressed size it, since compression + // will never be suppressed. + GzipLogCompressorFactory factory(std::make_unique<NullEstimator::Factory>()); + + compressor_ = factory.Create(base::Optional<size_t>()); + DCHECK(compressor_); + + std::string ignored; + compressor_->CreateHeader(&ignored); +} + +PerfectGzipEstimator::~PerfectGzipEstimator() = default; + +size_t PerfectGzipEstimator::EstimateCompressedSize( + const std::string& input) const { + std::string output; + EXPECT_EQ(compressor_->Compress(input, &output), LogCompressor::Result::OK); + return output.length(); +} + +size_t GzippedSize(const std::string& uncompressed) { + PerfectGzipEstimator perfect_estimator; + return kGzipOverheadBytes + + perfect_estimator.EstimateCompressedSize(uncompressed); +} + +size_t GzippedSize(const std::vector<std::string>& uncompressed) { + PerfectGzipEstimator perfect_estimator; + + size_t result = kGzipOverheadBytes; + for (const std::string& str : uncompressed) { + result += perfect_estimator.EstimateCompressedSize(str); + } + + return result; +} + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_unittest_helpers.h b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_unittest_helpers.h new file mode 100644 index 00000000000..0fe270ef634 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_unittest_helpers.h @@ -0,0 +1,82 @@ +// Copyright (c) 2018 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_UNITTEST_HELPERS_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_UNITTEST_HELPERS_H_ + +#include <memory> +#include <string> + +#include "base/files/file_path.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_common.h" + +namespace webrtc_event_logging { + +// Which type of compression, if any, LogFileWriterTest should use. +enum class WebRtcEventLogCompression { + NONE, + GZIP_NULL_ESTIMATION, + GZIP_PERFECT_ESTIMATION +}; + +// Produce a LogFileWriter::Factory object. +std::unique_ptr<LogFileWriter::Factory> CreateLogFileWriterFactory( + WebRtcEventLogCompression compression); + +#if defined(OS_POSIX) +void RemoveWritePermissions(const base::FilePath& path); +#endif // defined(OS_POSIX) + +// Always estimates strings to be compressed to zero bytes. +class NullEstimator : public CompressedSizeEstimator { + public: + class Factory : public CompressedSizeEstimator::Factory { + public: + ~Factory() override = default; + + std::unique_ptr<CompressedSizeEstimator> Create() const override; + }; + + ~NullEstimator() override = default; + + size_t EstimateCompressedSize(const std::string& input) const override; +}; + +// Provides a perfect estimation of the compressed size by cheating - performing +// actual compression, then reporting the resulting size. +// This class is stateful; the number, nature and order of calls to +// EstimateCompressedSize() is important. +class PerfectGzipEstimator : public CompressedSizeEstimator { + public: + class Factory : public CompressedSizeEstimator::Factory { + public: + ~Factory() override = default; + + std::unique_ptr<CompressedSizeEstimator> Create() const override; + }; + + PerfectGzipEstimator(); + + ~PerfectGzipEstimator() override; + + size_t EstimateCompressedSize(const std::string& input) const override; + + private: + // This compressor allows EstimateCompressedSize to return an exact estimate. + // EstimateCompressedSize is normally const, but here we fake it, so we set + // it as mutable. + mutable std::unique_ptr<LogCompressor> compressor_; +}; + +// Check the gzipped size of |uncompressed|, including header and footer, +// assuming it were gzipped on its own. +size_t GzippedSize(const std::string& uncompressed); + +// Same as other version, but with elements compressed in sequence. +size_t GzippedSize(const std::vector<std::string>& uncompressed); + +} // namespace webrtc_event_logging + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_MANAGER_UNITTEST_HELPERS_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_uploader.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_uploader.cc new file mode 100644 index 00000000000..17f542efa75 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_uploader.cc @@ -0,0 +1,383 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/webrtc_event_log_uploader.h" + +#include "base/bind.h" +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/strings/stringprintf.h" +#include "base/task/post_task.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "components/version_info/version_info.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "net/base/load_flags.h" +#include "net/base/mime_util.h" +#include "net/http/http_status_code.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/cpp/simple_url_loader.h" +#include "ui/base/text/bytes_formatting.h" + +namespace webrtc_event_logging { + +namespace { +// TODO(crbug.com/817495): Eliminate the duplication with other uploaders. +const char kUploadContentType[] = "multipart/form-data"; +const char kBoundary[] = "----**--yradnuoBgoLtrapitluMklaTelgooG--**----"; + +constexpr size_t kExpectedMimeOverheadBytes = 1000; // Intentional overshot. + +// TODO(crbug.com/817495): Eliminate the duplication with other uploaders. +#if defined(OS_WIN) +const char kProduct[] = "Chrome"; +#elif defined(OS_MACOSX) +const char kProduct[] = "Chrome_Mac"; +#elif defined(OS_LINUX) +const char kProduct[] = "Chrome_Linux"; +#elif defined(OS_ANDROID) +const char kProduct[] = "Chrome_Android"; +#elif defined(OS_CHROMEOS) +const char kProduct[] = "Chrome_ChromeOS"; +#else +#error Platform not supported. +#endif + +constexpr net::NetworkTrafficAnnotationTag + kWebrtcEventLogUploaderTrafficAnnotation = + net::DefineNetworkTrafficAnnotation("webrtc_event_log_uploader", R"( + semantics { + sender: "WebRTC Event Log uploader module" + description: + "Uploads a WebRTC event log to a server called Crash. These logs " + "will not contain private information. They will be used to " + "improve WebRTC (fix bugs, tune performance, etc.)." + trigger: + "A Google service (e.g. Hangouts/Meet) has requested a peer " + "connection to be logged, and the resulting event log to be uploaded " + "at a time deemed to cause the least interference to the user (i.e., " + "when the user is not busy making other VoIP calls)." + data: + "WebRTC events such as the timing of audio playout (but not the " + "content), timing and size of RTP packets sent/received, etc." + destination: GOOGLE_OWNED_SERVICE + } + policy { + cookies_allowed: NO + setting: "Feature controlled only through Chrome policy; " + "no user-facing control surface." + chrome_policy { + WebRtcEventLogCollectionAllowed { + WebRtcEventLogCollectionAllowed: false + } + } + })"); + +void AddFileContents(const char* filename, + const std::string& file_contents, + const std::string& content_type, + std::string* post_data) { + // net::AddMultipartValueForUpload does almost what we want to do here, except + // that it does not add the "filename" attribute. We hack it to force it to. + std::string mime_value_name = + base::StringPrintf("%s\"; filename=\"%s\"", filename, filename); + net::AddMultipartValueForUpload(mime_value_name, file_contents, kBoundary, + content_type, post_data); +} + +std::string MimeContentType() { + const char kBoundaryKeywordAndMisc[] = "; boundary="; + + std::string content_type; + content_type.reserve(sizeof(content_type) + sizeof(kBoundaryKeywordAndMisc) + + sizeof(kBoundary)); + + content_type.append(kUploadContentType); + content_type.append(kBoundaryKeywordAndMisc); + content_type.append(kBoundary); + + return content_type; +} + +void BindURLLoaderFactoryReceiver( + mojo::PendingReceiver<network::mojom::URLLoaderFactory> + url_loader_factory_receiver) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory = + g_browser_process->shared_url_loader_factory(); + DCHECK(shared_url_loader_factory); + shared_url_loader_factory->Clone(std::move(url_loader_factory_receiver)); +} + +void OnURLLoadUploadProgress(uint64_t current, uint64_t total) { + ui::DataUnits unit = ui::GetByteDisplayUnits(total); + VLOG(1) << "WebRTC event log upload progress: " + << FormatBytesWithUnits(current, unit, false) << " / " + << FormatBytesWithUnits(total, unit, true) << "."; +} +} // namespace + +const char WebRtcEventLogUploaderImpl::kUploadURL[] = + "https://clients2.google.com/cr/report"; + +std::unique_ptr<WebRtcEventLogUploader> +WebRtcEventLogUploaderImpl::Factory::Create(const WebRtcLogFileInfo& log_file, + UploadResultCallback callback) { + return std::make_unique<WebRtcEventLogUploaderImpl>( + log_file, std::move(callback), kMaxRemoteLogFileSizeBytes); +} + +std::unique_ptr<WebRtcEventLogUploader> +WebRtcEventLogUploaderImpl::Factory::CreateWithCustomMaxSizeForTesting( + const WebRtcLogFileInfo& log_file, + UploadResultCallback callback, + size_t max_log_file_size_bytes) { + return std::make_unique<WebRtcEventLogUploaderImpl>( + log_file, std::move(callback), max_log_file_size_bytes); +} + +WebRtcEventLogUploaderImpl::WebRtcEventLogUploaderImpl( + const WebRtcLogFileInfo& log_file, + UploadResultCallback callback, + size_t max_log_file_size_bytes) + : log_file_(log_file), + callback_(std::move(callback)), + max_log_file_size_bytes_(max_log_file_size_bytes), + io_task_runner_(base::SequencedTaskRunnerHandle::Get()) { + history_file_writer_ = WebRtcEventLogHistoryFileWriter::Create( + GetWebRtcEventLogHistoryFilePath(log_file_.path)); + if (!history_file_writer_) { + // File either could not be created, or, if a different error occurred, + // Create() will have tried to remove the file it has created. + UmaRecordWebRtcEventLoggingUpload( + WebRtcEventLoggingUploadUma::kHistoryFileCreationError); + ReportResult(false); + return; + } + + const base::Time now = std::max(base::Time::Now(), log_file.last_modified); + if (!history_file_writer_->WriteCaptureTime(log_file.last_modified) || + !history_file_writer_->WriteUploadTime(now)) { + LOG(ERROR) << "Writing to history file failed."; + UmaRecordWebRtcEventLoggingUpload( + WebRtcEventLoggingUploadUma::kHistoryFileWriteError); + DeleteHistoryFile(); // Avoid partial, potentially-corrupt history files. + ReportResult(false); + return; + } + + std::string upload_data; + if (!PrepareUploadData(&upload_data)) { + // History file will reflect a failed upload attempt. + ReportResult(false); // UMA recorded by PrepareUploadData(). + return; + } + + StartUpload(upload_data); +} + +WebRtcEventLogUploaderImpl::~WebRtcEventLogUploaderImpl() { + // WebRtcEventLogUploaderImpl objects' deletion scenarios: + // 1. Upload started and finished - |url_loader_| should have been reset + // so that we would be able to DCHECK and demonstrate that the determinant + // is maintained. + // 2. Upload started and cancelled - behave similarly to a finished upload. + // 3. The upload was never started, due to an early failure (e.g. file not + // found). In that case, |url_loader_| will not have been set. + // 4. Chrome shutdown. + if (io_task_runner_->RunsTasksInCurrentSequence()) { // Scenarios 1-3. + DCHECK(!url_loader_); + } else { // # Scenario #4 - Chrome shutdown. + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + bool will_delete = + io_task_runner_->DeleteSoon(FROM_HERE, url_loader_.release()); + DCHECK(!will_delete) + << "Task runners must have been stopped by this stage of shutdown."; + } +} + +const WebRtcLogFileInfo& WebRtcEventLogUploaderImpl::GetWebRtcLogFileInfo() + const { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + return log_file_; +} + +bool WebRtcEventLogUploaderImpl::Cancel() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // The upload could already have been completed, or maybe was never properly + // started (due to a file read failure, etc.). + const bool upload_was_active = (url_loader_.get() != nullptr); + + // Note that in this case, it might still be that the last bytes hit the + // wire right as we attempt to cancel the upload. OnURLFetchComplete, however, + // will not be called. + url_loader_.reset(); + + DeleteLogFile(); + DeleteHistoryFile(); + + if (upload_was_active) { + UmaRecordWebRtcEventLoggingUpload( + WebRtcEventLoggingUploadUma::kUploadCancelled); + } + + return upload_was_active; +} + +bool WebRtcEventLogUploaderImpl::PrepareUploadData(std::string* upload_data) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + std::string log_file_contents; + if (!base::ReadFileToStringWithMaxSize(log_file_.path, &log_file_contents, + max_log_file_size_bytes_)) { + LOG(WARNING) << "Couldn't read event log file, or max file size exceeded."; + UmaRecordWebRtcEventLoggingUpload( + WebRtcEventLoggingUploadUma::kLogFileReadError); + return false; + } + + DCHECK(upload_data->empty()); + upload_data->reserve(log_file_contents.size() + kExpectedMimeOverheadBytes); + + const std::string filename_str = log_file_.path.BaseName().MaybeAsASCII(); + if (filename_str.empty()) { + LOG(WARNING) << "Log filename is not according to acceptable format."; + UmaRecordWebRtcEventLoggingUpload( + WebRtcEventLoggingUploadUma::kLogFileNameError); + return false; + } + + const char* filename = filename_str.c_str(); + + net::AddMultipartValueForUpload("prod", kProduct, kBoundary, std::string(), + upload_data); + net::AddMultipartValueForUpload("ver", + version_info::GetVersionNumber() + "-webrtc", + kBoundary, std::string(), upload_data); + net::AddMultipartValueForUpload("guid", "0", kBoundary, std::string(), + upload_data); + net::AddMultipartValueForUpload("type", filename, kBoundary, std::string(), + upload_data); + AddFileContents(filename, log_file_contents, "application/log", upload_data); + net::AddMultipartFinalDelimiterForUpload(kBoundary, upload_data); + + return true; +} + +void WebRtcEventLogUploaderImpl::StartUpload(const std::string& upload_data) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + auto resource_request = std::make_unique<network::ResourceRequest>(); + resource_request->url = GURL(kUploadURL); + resource_request->method = "POST"; + resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit; + + // Create a new mojo pipe. It's safe to pass this around and use + // immediately, even though it needs to finish initialization on the UI + // thread. + network::mojom::URLLoaderFactoryPtr url_loader_factory_ptr; + base::PostTask(FROM_HERE, {content::BrowserThread::UI}, + base::BindOnce(BindURLLoaderFactoryReceiver, + mojo::MakeRequest(&url_loader_factory_ptr))); + + url_loader_ = network::SimpleURLLoader::Create( + std::move(resource_request), kWebrtcEventLogUploaderTrafficAnnotation); + url_loader_->AttachStringForUpload(upload_data, MimeContentType()); + url_loader_->SetOnUploadProgressCallback( + base::BindRepeating(OnURLLoadUploadProgress)); + + // See comment in destructor for an explanation about why using + // base::Unretained(this) is safe here. + url_loader_->DownloadToString( + url_loader_factory_ptr.get(), + base::BindOnce(&WebRtcEventLogUploaderImpl::OnURLLoadComplete, + base::Unretained(this)), + kWebRtcEventLogMaxUploadIdBytes); +} + +void WebRtcEventLogUploaderImpl::OnURLLoadComplete( + std::unique_ptr<std::string> response_body) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(url_loader_); + + if (response_body.get() != nullptr && response_body->empty()) { + LOG(WARNING) << "SimpleURLLoader reported upload successful, " + << "but report ID unknown."; + } + + const bool upload_successful = + (response_body.get() != nullptr && !response_body->empty()); + + // NetError() is 0 when no error occurred. + UmaRecordWebRtcEventLoggingNetErrorType(url_loader_->NetError()); + + DCHECK(history_file_writer_); + if (upload_successful) { + if (!history_file_writer_->WriteUploadId(*response_body)) { + // Discard the incomplete, potentially now corrupt history file, but the + // upload is still considered successful. + LOG(ERROR) << "Failed to write upload ID to history file."; + DeleteHistoryFile(); + } + } else { + LOG(WARNING) << "Upload unsuccessful."; + // By not writing an UploadId to the history file, it is inferrable that + // the upload was initiated, but did not end successfully. + } + + UmaRecordWebRtcEventLoggingUpload( + upload_successful ? WebRtcEventLoggingUploadUma::kSuccess + : WebRtcEventLoggingUploadUma::kUploadFailure); + + url_loader_.reset(); // Explicitly maintain determinant. + + ReportResult(upload_successful); +} + +void WebRtcEventLogUploaderImpl::ReportResult(bool result) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // * If the upload was successful, the file is no longer needed. + // * If the upload failed, we don't want to retry, because we run the risk of + // uploading significant amounts of data once again, only for the upload to + // fail again after (as an example) wasting 50MBs of upload bandwidth. + // * If the file was not found, this will simply have no effect (other than + // to LOG() an error). + // TODO(crbug.com/775415): Provide refined retrial behavior. + DeleteLogFile(); + + // Release hold of history file, allowing it to be read, moved or deleted. + history_file_writer_.reset(); + + io_task_runner_->PostTask( + FROM_HERE, base::BindOnce(std::move(callback_), log_file_.path, result)); +} + +void WebRtcEventLogUploaderImpl::DeleteLogFile() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + const bool deletion_successful = + base::DeleteFile(log_file_.path, /*recursive=*/false); + if (!deletion_successful) { + // This is a somewhat serious (though unlikely) error, because now we'll + // try to upload this file again next time Chrome launches. + LOG(ERROR) << "Could not delete pending WebRTC event log file."; + } +} + +void WebRtcEventLogUploaderImpl::DeleteHistoryFile() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + if (!history_file_writer_) { + LOG(ERROR) << "Deletion of history file attempted after uploader " + << "has relinquished ownership of it."; + return; + } + history_file_writer_->Delete(); + history_file_writer_.reset(); +} + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_uploader.h b/chromium/chrome/browser/media/webrtc/webrtc_event_log_uploader.h new file mode 100644 index 00000000000..f0699503180 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_uploader.h @@ -0,0 +1,148 @@ +// Copyright 2018 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_UPLOADER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_UPLOADER_H_ + +#include <memory> +#include <string> + +#include "base/files/file_path.h" +#include "base/memory/scoped_refptr.h" +#include "base/sequenced_task_runner.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_history.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_common.h" +#include "services/network/public/mojom/url_loader_factory.mojom-forward.h" + +namespace network { +class SimpleURLLoader; +} // namespace network + +namespace webrtc_event_logging { + +// A sublcass of this interface will take ownership of a file, and either +// upload it to a remote server (actual implementation), or pretend to do so +// (in unit tests). Upon completion, success/failure will be reported by posting +// an UploadResultCallback task to the task queue on which this object lives. +class WebRtcEventLogUploader { + public: + using UploadResultCallback = + base::OnceCallback<void(const base::FilePath& log_file, + bool upload_successful)>; + + // Since we'll need more than one instance of the abstract + // WebRtcEventLogUploader, we'll need an abstract factory for it. + class Factory { + public: + virtual ~Factory() = default; + + // Creates uploaders. The observer is passed to each call of Create, + // rather than be memorized by the factory's constructor, because factories + // created by unit tests have no visibility into the real implementation's + // observer (WebRtcRemoteEventLogManager). + // This takes ownership of the file. The caller must not attempt to access + // the file after invoking Create(). + virtual std::unique_ptr<WebRtcEventLogUploader> Create( + const WebRtcLogFileInfo& log_file, + UploadResultCallback callback) = 0; + }; + + virtual ~WebRtcEventLogUploader() = default; + + // Getter for the details of the file this uploader is handling. + // Can be called for ongoing, completed, failed or cancelled uploads. + virtual const WebRtcLogFileInfo& GetWebRtcLogFileInfo() const = 0; + + // Cancels the upload, then deletes the log file and its history file. + // Returns true if the upload was cancelled due to this call, and false if + // the upload was already completed or aborted before this call. + // (Aborted uploads are ones where the file could not be read, etc.) + virtual bool Cancel() = 0; +}; + +// Primary implementation of WebRtcEventLogUploader. Uploads log files to crash. +// Deletes log files whether they were successfully uploaded or not. +class WebRtcEventLogUploaderImpl : public WebRtcEventLogUploader { + public: + class Factory : public WebRtcEventLogUploader::Factory { + public: + ~Factory() override = default; + + std::unique_ptr<WebRtcEventLogUploader> Create( + const WebRtcLogFileInfo& log_file, + UploadResultCallback callback) override; + + protected: + friend class WebRtcEventLogUploaderImplTest; + + std::unique_ptr<WebRtcEventLogUploader> CreateWithCustomMaxSizeForTesting( + const WebRtcLogFileInfo& log_file, + UploadResultCallback callback, + size_t max_remote_log_file_size_bytes); + }; + + WebRtcEventLogUploaderImpl( + const WebRtcLogFileInfo& log_file, + UploadResultCallback callback, + size_t max_remote_log_file_size_bytes); + ~WebRtcEventLogUploaderImpl() override; + + const WebRtcLogFileInfo& GetWebRtcLogFileInfo() const override; + + bool Cancel() override; + + private: + friend class WebRtcEventLogUploaderImplTest; + + // Primes the log file for uploading. Returns true if the file could be read, + // in which case |upload_data| will be populated with the data to be uploaded + // (both the log file's contents as well as history for Crash). + // TODO(crbug.com/775415): Avoid reading the entire file into memory. + bool PrepareUploadData(std::string* upload_data); + + // Initiates the file's upload. + void StartUpload(const std::string& upload_data); + + // Callback invoked when the file upload has finished. + // If the |url_loader_| instance it was bound to is deleted before + // its invocation, the callback will not be called. + void OnURLLoadComplete(std::unique_ptr<std::string> response_body); + + // Cleanup and posting of the result callback. + void ReportResult(bool result); + + // Remove the log file which is owned by |this|. + void DeleteLogFile(); + + // Remove the log file which is owned by |this|. + void DeleteHistoryFile(); + + // The URL used for uploading the logs. + static const char kUploadURL[]; + + // Housekeeping information about the uploaded file (path, time of last + // modification, associated BrowserContext). + const WebRtcLogFileInfo log_file_; + + // Callback posted back to signal success or failure. + UploadResultCallback callback_; + + // Maximum allowed file size. In production code, this is a hard-coded, + // but unit tests may set other values. + const size_t max_log_file_size_bytes_; + + // Owns a history file which allows the state of the uploaded log to be + // remembered after it has been uploaded and/or deleted. + std::unique_ptr<WebRtcEventLogHistoryFileWriter> history_file_writer_; + + // This object is in charge of the actual upload. + std::unique_ptr<network::SimpleURLLoader> url_loader_; + + // The object lives on this IO-capable task runner. + scoped_refptr<base::SequencedTaskRunner> io_task_runner_; +}; + +} // namespace webrtc_event_logging + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_EVENT_LOG_UPLOADER_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_uploader_impl_unittest.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_uploader_impl_unittest.cc new file mode 100644 index 00000000000..853cc085998 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_uploader_impl_unittest.cc @@ -0,0 +1,366 @@ +// Copyright 2018 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 "chrome/browser/media/webrtc/webrtc_event_log_uploader.h" + +#include <memory> +#include <string> + +#include "base/bind.h" +#include "base/callback_forward.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_refptr.h" +#include "base/run_loop.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager_common.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "content/public/test/browser_task_environment.h" +#include "net/http/http_status_code.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_url_loader_factory.h" +#include "services/network/test/test_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace webrtc_event_logging { + +using ::testing::StrictMock; +using BrowserContextId = WebRtcEventLogPeerConnectionKey::BrowserContextId; + +namespace { +class UploadObserver { + public: + explicit UploadObserver(base::OnceClosure on_complete_callback) + : on_complete_callback_(std::move(on_complete_callback)) {} + + // Combines the mock functionality via a helper (CompletionCallback), + // as well as unblocks its owner through |on_complete_callback_|. + void OnWebRtcEventLogUploadComplete(const base::FilePath& log_file, + bool upload_successful) { + CompletionCallback(log_file, upload_successful); + std::move(on_complete_callback_).Run(); + } + + MOCK_METHOD2(CompletionCallback, void(const base::FilePath&, bool)); + + private: + base::OnceClosure on_complete_callback_; +}; + +#if defined(OS_POSIX) +void RemovePermissions(const base::FilePath& path, int removed_permissions) { + int permissions; + ASSERT_TRUE(base::GetPosixFilePermissions(path, &permissions)); + permissions &= ~removed_permissions; + ASSERT_TRUE(base::SetPosixFilePermissions(path, permissions)); +} + +void RemoveReadPermissions(const base::FilePath& path) { + constexpr int read_permissions = base::FILE_PERMISSION_READ_BY_USER | + base::FILE_PERMISSION_READ_BY_GROUP | + base::FILE_PERMISSION_READ_BY_OTHERS; + RemovePermissions(path, read_permissions); +} +#endif // defined(OS_POSIX) +} // namespace + +class WebRtcEventLogUploaderImplTest : public ::testing::Test { + public: + WebRtcEventLogUploaderImplTest() + : test_shared_url_loader_factory_( + base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( + &test_url_loader_factory_)), + observer_run_loop_(), + observer_(observer_run_loop_.QuitWhenIdleClosure()) { + TestingBrowserProcess::GetGlobal()->SetSharedURLLoaderFactory( + test_shared_url_loader_factory_); + + EXPECT_TRUE(base::Time::FromString("30 Dec 1983", &kReasonableTime)); + + uploader_factory_ = std::make_unique<WebRtcEventLogUploaderImpl::Factory>(); + } + + ~WebRtcEventLogUploaderImplTest() override { + task_environment_.RunUntilIdle(); + } + + void SetUp() override { + testing_profile_manager_ = std::make_unique<TestingProfileManager>( + TestingBrowserProcess::GetGlobal()); + EXPECT_TRUE(profiles_dir_.CreateUniqueTempDir()); + EXPECT_TRUE(testing_profile_manager_->SetUp(profiles_dir_.GetPath())); + + testing_profile_ = + testing_profile_manager_->CreateTestingProfile("arbitrary_name"); + + browser_context_id_ = GetBrowserContextId(testing_profile_); + + // Create the sub-dir for the remote-bound logs that would have been set + // up by WebRtcEventLogManager, if WebRtcEventLogManager were instantiated. + // Note that the testing profile's overall directory is a temporary one. + const base::FilePath logs_dir = + GetRemoteBoundWebRtcEventLogsDir(testing_profile_->GetPath()); + ASSERT_TRUE(base::CreateDirectory(logs_dir)); + + // Create a log file and put some arbitrary data in it. + // Note that the testing profile's overall directory is a temporary one. + ASSERT_TRUE(base::CreateTemporaryFileInDir(logs_dir, &log_file_)); + constexpr size_t kLogFileSizeBytes = 100u; + const std::string file_contents(kLogFileSizeBytes, 'A'); + ASSERT_EQ( + base::WriteFile(log_file_, file_contents.c_str(), file_contents.size()), + static_cast<int>(file_contents.size())); + } + + // For tests which imitate a response (or several). + void SetURLLoaderResponse(net::HttpStatusCode http_code, int net_error) { + DCHECK(test_shared_url_loader_factory_); + const std::string kResponseId = "ec1ed029734b8f7e"; // Arbitrary. + test_url_loader_factory_.AddResponse( + GURL(WebRtcEventLogUploaderImpl::kUploadURL), + network::CreateURLResponseHead(http_code), kResponseId, + network::URLLoaderCompletionStatus(net_error)); + } + + void StartAndWaitForUpload( + BrowserContextId browser_context_id = BrowserContextId(), + base::Time last_modified_time = base::Time()) { + DCHECK(test_shared_url_loader_factory_); + + if (last_modified_time.is_null()) { + last_modified_time = kReasonableTime; + } + + const WebRtcLogFileInfo log_file_info(browser_context_id, log_file_, + last_modified_time); + + uploader_ = uploader_factory_->Create(log_file_info, ResultCallback()); + + observer_run_loop_.Run(); // Observer was given quit-closure by ctor. + } + + void StartAndWaitForUploadWithCustomMaxSize( + size_t max_log_size_bytes, + BrowserContextId browser_context_id = BrowserContextId(), + base::Time last_modified_time = base::Time()) { + DCHECK(test_shared_url_loader_factory_); + + if (last_modified_time.is_null()) { + last_modified_time = kReasonableTime; + } + + const WebRtcLogFileInfo log_file_info(browser_context_id, log_file_, + last_modified_time); + + uploader_ = uploader_factory_->CreateWithCustomMaxSizeForTesting( + log_file_info, ResultCallback(), max_log_size_bytes); + + observer_run_loop_.Run(); // Observer was given quit-closure by ctor. + } + + void StartUploadThatWillNotTerminate( + BrowserContextId browser_context_id = BrowserContextId(), + base::Time last_modified_time = base::Time()) { + DCHECK(test_shared_url_loader_factory_); + + if (last_modified_time.is_null()) { + last_modified_time = kReasonableTime; + } + + const WebRtcLogFileInfo log_file_info(browser_context_id, log_file_, + last_modified_time); + + uploader_ = uploader_factory_->Create(log_file_info, ResultCallback()); + } + + WebRtcEventLogUploader::UploadResultCallback ResultCallback() { + return base::BindOnce(&UploadObserver::OnWebRtcEventLogUploadComplete, + base::Unretained(&observer_)); + } + + content::BrowserTaskEnvironment task_environment_; + + base::Time kReasonableTime; + + network::TestURLLoaderFactory test_url_loader_factory_; + scoped_refptr<network::SharedURLLoaderFactory> + test_shared_url_loader_factory_; + + base::RunLoop observer_run_loop_; + + base::ScopedTempDir profiles_dir_; + std::unique_ptr<TestingProfileManager> testing_profile_manager_; + TestingProfile* testing_profile_; // |testing_profile_manager_| owns. + BrowserContextId browser_context_id_; + + base::FilePath log_file_; + + StrictMock<UploadObserver> observer_; + + // These (uploader-factory and uploader) are the units under test. + std::unique_ptr<WebRtcEventLogUploaderImpl::Factory> uploader_factory_; + std::unique_ptr<WebRtcEventLogUploader> uploader_; +}; + +TEST_F(WebRtcEventLogUploaderImplTest, SuccessfulUploadReportedToObserver) { + SetURLLoaderResponse(net::HTTP_OK, net::OK); + EXPECT_CALL(observer_, CompletionCallback(log_file_, true)).Times(1); + StartAndWaitForUpload(); + EXPECT_FALSE(base::PathExists(log_file_)); +} + +// Version #1 - request reported as successful, but got an error (404) as the +// HTTP return code. +// Due to the simplicitly of both tests, this also tests the scenario +// FileDeletedAfterUnsuccessfulUpload, rather than giving each its own test. +TEST_F(WebRtcEventLogUploaderImplTest, UnsuccessfulUploadReportedToObserver1) { + SetURLLoaderResponse(net::HTTP_NOT_FOUND, net::OK); + EXPECT_CALL(observer_, CompletionCallback(log_file_, false)).Times(1); + StartAndWaitForUpload(); + EXPECT_FALSE(base::PathExists(log_file_)); +} + +// Version #2 - request reported as failed; HTTP return code ignored, even +// if it's a purported success. +TEST_F(WebRtcEventLogUploaderImplTest, UnsuccessfulUploadReportedToObserver2) { + SetURLLoaderResponse(net::HTTP_NOT_FOUND, net::ERR_FAILED); + EXPECT_CALL(observer_, CompletionCallback(log_file_, false)).Times(1); + StartAndWaitForUpload(); + EXPECT_FALSE(base::PathExists(log_file_)); +} + +#if defined(OS_POSIX) +TEST_F(WebRtcEventLogUploaderImplTest, FailureToReadFileReportedToObserver) { + // Show the failure was independent of the URLLoaderFactory's primed return + // value. + SetURLLoaderResponse(net::HTTP_OK, net::OK); + + RemoveReadPermissions(log_file_); + EXPECT_CALL(observer_, CompletionCallback(log_file_, false)).Times(1); + StartAndWaitForUpload(); +} + +TEST_F(WebRtcEventLogUploaderImplTest, NonExistentFileReportedToObserver) { + // Show the failure was independent of the URLLoaderFactory's primed return + // value. + SetURLLoaderResponse(net::HTTP_OK, net::OK); + + log_file_ = log_file_.Append(FILE_PATH_LITERAL("garbage")); + EXPECT_CALL(observer_, CompletionCallback(log_file_, false)).Times(1); + StartAndWaitForUpload(); +} +#endif // defined(OS_POSIX) + +TEST_F(WebRtcEventLogUploaderImplTest, FilesUpToMaxSizeUploaded) { + int64_t log_file_size_bytes; + ASSERT_TRUE(base::GetFileSize(log_file_, &log_file_size_bytes)); + + SetURLLoaderResponse(net::HTTP_OK, net::OK); + EXPECT_CALL(observer_, CompletionCallback(log_file_, true)).Times(1); + StartAndWaitForUploadWithCustomMaxSize(log_file_size_bytes); + EXPECT_FALSE(base::PathExists(log_file_)); +} + +TEST_F(WebRtcEventLogUploaderImplTest, ExcessivelyLargeFilesNotUploaded) { + int64_t log_file_size_bytes; + ASSERT_TRUE(base::GetFileSize(log_file_, &log_file_size_bytes)); + + SetURLLoaderResponse(net::HTTP_OK, net::OK); + EXPECT_CALL(observer_, CompletionCallback(log_file_, false)).Times(1); + StartAndWaitForUploadWithCustomMaxSize(log_file_size_bytes - 1); + EXPECT_FALSE(base::PathExists(log_file_)); +} + +TEST_F(WebRtcEventLogUploaderImplTest, + CancelBeforeUploadCompletionReturnsTrue) { + const base::Time last_modified = base::Time::Now(); + StartUploadThatWillNotTerminate(browser_context_id_, last_modified); + + EXPECT_TRUE(uploader_->Cancel()); +} + +TEST_F(WebRtcEventLogUploaderImplTest, CancelOnCancelledUploadReturnsFalse) { + const base::Time last_modified = base::Time::Now(); + StartUploadThatWillNotTerminate(browser_context_id_, last_modified); + + ASSERT_TRUE(uploader_->Cancel()); + EXPECT_FALSE(uploader_->Cancel()); +} + +TEST_F(WebRtcEventLogUploaderImplTest, + CancelAfterUploadCompletionReturnsFalse) { + SetURLLoaderResponse(net::HTTP_OK, net::OK); + EXPECT_CALL(observer_, CompletionCallback(log_file_, true)).Times(1); + StartAndWaitForUpload(); + + EXPECT_FALSE(uploader_->Cancel()); +} + +TEST_F(WebRtcEventLogUploaderImplTest, CancelOnAbortedUploadReturnsFalse) { + // Show the failure was independent of the URLLoaderFactory's primed return + // value. + SetURLLoaderResponse(net::HTTP_OK, net::OK); + + log_file_ = log_file_.Append(FILE_PATH_LITERAL("garbage")); + EXPECT_CALL(observer_, CompletionCallback(log_file_, false)).Times(1); + StartAndWaitForUpload(); + + EXPECT_FALSE(uploader_->Cancel()); +} + +TEST_F(WebRtcEventLogUploaderImplTest, CancelOnOngoingUploadDeletesFile) { + const base::Time last_modified = base::Time::Now(); + StartUploadThatWillNotTerminate(browser_context_id_, last_modified); + ASSERT_TRUE(uploader_->Cancel()); + + EXPECT_FALSE(base::PathExists(log_file_)); +} + +TEST_F(WebRtcEventLogUploaderImplTest, + GetWebRtcLogFileInfoReturnsCorrectInfoBeforeUploadDone) { + const base::Time last_modified = base::Time::Now(); + StartUploadThatWillNotTerminate(browser_context_id_, last_modified); + + const WebRtcLogFileInfo info = uploader_->GetWebRtcLogFileInfo(); + EXPECT_EQ(info.browser_context_id, browser_context_id_); + EXPECT_EQ(info.path, log_file_); + EXPECT_EQ(info.last_modified, last_modified); + + // Test tear-down. + ASSERT_TRUE(uploader_->Cancel()); +} + +TEST_F(WebRtcEventLogUploaderImplTest, + GetWebRtcLogFileInfoReturnsCorrectInfoAfterUploadSucceeded) { + SetURLLoaderResponse(net::HTTP_OK, net::OK); + EXPECT_CALL(observer_, CompletionCallback(log_file_, true)).Times(1); + + const base::Time last_modified = base::Time::Now(); + StartAndWaitForUpload(browser_context_id_, last_modified); + + const WebRtcLogFileInfo info = uploader_->GetWebRtcLogFileInfo(); + EXPECT_EQ(info.browser_context_id, browser_context_id_); + EXPECT_EQ(info.path, log_file_); + EXPECT_EQ(info.last_modified, last_modified); +} + +TEST_F(WebRtcEventLogUploaderImplTest, + GetWebRtcLogFileInfoReturnsCorrectInfoWhenCalledOnCancelledUpload) { + const base::Time last_modified = base::Time::Now(); + StartUploadThatWillNotTerminate(browser_context_id_, last_modified); + ASSERT_TRUE(uploader_->Cancel()); + + const WebRtcLogFileInfo info = uploader_->GetWebRtcLogFileInfo(); + EXPECT_EQ(info.browser_context_id, browser_context_id_); + EXPECT_EQ(info.path, log_file_); + EXPECT_EQ(info.last_modified, last_modified); +} + +// TODO(crbug.com/775415): Add a unit test that shows that files with +// non-ASCII filenames are discard. (Or, alternatively, add support for them.) + +} // namespace webrtc_event_logging diff --git a/chromium/chrome/browser/media/webrtc/webrtc_getdisplaymedia_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_getdisplaymedia_browsertest.cc new file mode 100644 index 00000000000..44027f50260 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_getdisplaymedia_browsertest.cc @@ -0,0 +1,196 @@ +// Copyright 2018 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 <string> + +#include "base/strings/stringprintf.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/common/chrome_switches.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "media/base/media_switches.h" + +#if defined(OS_MACOSX) +#include "base/mac/mac_util.h" +#endif + +namespace { + +static const char kMainHtmlPage[] = "/webrtc/webrtc_getdisplaymedia_test.html"; + +struct TestConfig { + const char* display_surface; + const char* logical_surface; + const char* cursor; +}; + +} // namespace + +// Base class for top level tests for getDisplayMedia(). +class WebRtcGetDisplayMediaBrowserTest : public WebRtcTestBase { + public: + void SetUpInProcessBrowserTestFixture() override { + DetectErrorsInJavaScript(); + } + + void RunGetDisplayMedia(content::WebContents* tab, + const std::string& constraints, + bool is_fake_ui = false) { + std::string result; + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab->GetMainFrame(), + base::StringPrintf("runGetDisplayMedia(%s);", constraints.c_str()), + &result)); +#if defined(OS_MACOSX) + // Starting from macOS 10.15, screen capture requires system permissions + // that are disabled by default. The permission is reported as granted + // if the fake UI is used. + EXPECT_EQ(result, base::mac::IsAtMostOS10_14() || is_fake_ui + ? "getdisplaymedia-success" + : "getdisplaymedia-failure"); +#else + EXPECT_EQ(result, "getdisplaymedia-success"); +#endif + } +}; + +// Top level test for getDisplayMedia(). Pops picker Ui and selects desktop +// capture by default. +class WebRtcGetDisplayMediaBrowserTestWithPicker + : public WebRtcGetDisplayMediaBrowserTest { + public: + void SetUpCommandLine(base::CommandLine* command_line) override { + command_line->AppendSwitch( + switches::kEnableExperimentalWebPlatformFeatures); + command_line->AppendSwitchASCII(switches::kAutoSelectDesktopCaptureSource, + "Entire screen"); + } +}; + +// Real desktop capture is flaky on below platforms. +#if defined(OS_CHROMEOS) || defined(OS_WIN) +#define MAYBE_GetDisplayMediaVideo DISABLED_GetDisplayMediaVideo +#else +#define MAYBE_GetDisplayMediaVideo GetDisplayMediaVideo +#endif // defined(OS_CHROMEOS) || defined(OS_WIN) +IN_PROC_BROWSER_TEST_F(WebRtcGetDisplayMediaBrowserTestWithPicker, + MAYBE_GetDisplayMediaVideo) { + ASSERT_TRUE(embedded_test_server()->Start()); + + content::WebContents* tab = OpenTestPageInNewTab(kMainHtmlPage); + std::string constraints("{video:true}"); + RunGetDisplayMedia(tab, constraints); +} + +// Real desktop capture is flaky on below platforms. +#if defined(OS_CHROMEOS) || defined(OS_WIN) +#define MAYBE_GetDisplayMediaVideoAndAudio DISABLED_GetDisplayMediaVideoAndAudio +// On linux debug bots, it's flaky as well. +#elif (defined(OS_LINUX) && !defined(NDEBUG)) +#define MAYBE_GetDisplayMediaVideoAndAudio DISABLED_GetDisplayMediaVideoAndAudio +// On linux asan bots, it's flaky as well - msan and other rel bot are fine. +#elif (defined(OS_LINUX) && defined(ADDRESS_SANITIZER)) +#define MAYBE_GetDisplayMediaVideoAndAudio DISABLED_GetDisplayMediaVideoAndAudio +#else +#define MAYBE_GetDisplayMediaVideoAndAudio GetDisplayMediaVideoAndAudio +#endif // defined(OS_CHROMEOS) || defined(OS_WIN) +IN_PROC_BROWSER_TEST_F(WebRtcGetDisplayMediaBrowserTestWithPicker, + MAYBE_GetDisplayMediaVideoAndAudio) { + ASSERT_TRUE(embedded_test_server()->Start()); + + content::WebContents* tab = OpenTestPageInNewTab(kMainHtmlPage); + std::string constraints("{video:true, audio:true}"); + RunGetDisplayMedia(tab, constraints); +} + +// Top level test for getDisplayMedia(). Skips picker UI and uses fake device +// with specified type. +class WebRtcGetDisplayMediaBrowserTestWithFakeUI + : public WebRtcGetDisplayMediaBrowserTest, + public testing::WithParamInterface<TestConfig> { + public: + WebRtcGetDisplayMediaBrowserTestWithFakeUI() { + test_config_ = GetParam(); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + command_line->AppendSwitch( + switches::kEnableExperimentalWebPlatformFeatures); + command_line->AppendSwitch(switches::kUseFakeUIForMediaStream); + command_line->AppendSwitchASCII( + switches::kUseFakeDeviceForMediaStream, + base::StringPrintf("display-media-type=%s", + test_config_.display_surface)); + } + + protected: + TestConfig test_config_; +}; + +IN_PROC_BROWSER_TEST_P(WebRtcGetDisplayMediaBrowserTestWithFakeUI, + GetDisplayMediaVideo) { + ASSERT_TRUE(embedded_test_server()->Start()); + + content::WebContents* tab = OpenTestPageInNewTab(kMainHtmlPage); + std::string constraints("{video:true}"); + RunGetDisplayMedia(tab, constraints, /*is_fake_ui=*/true); + + std::string result; + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab->GetMainFrame(), "getDisplaySurfaceSetting();", &result)); + EXPECT_EQ(result, test_config_.display_surface); + + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab->GetMainFrame(), "getLogicalSurfaceSetting();", &result)); + EXPECT_EQ(result, test_config_.logical_surface); + + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab->GetMainFrame(), "getCursorSetting();", &result)); + EXPECT_EQ(result, test_config_.cursor); +} + +IN_PROC_BROWSER_TEST_P(WebRtcGetDisplayMediaBrowserTestWithFakeUI, + GetDisplayMediaVideoAndAudio) { + ASSERT_TRUE(embedded_test_server()->Start()); + + content::WebContents* tab = OpenTestPageInNewTab(kMainHtmlPage); + std::string constraints("{video:true, audio:true}"); + RunGetDisplayMedia(tab, constraints, /*is_fake_ui=*/true); + + std::string result; + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab->GetMainFrame(), "hasAudioTrack();", &result)); + EXPECT_EQ(result, "true"); +} + +IN_PROC_BROWSER_TEST_P(WebRtcGetDisplayMediaBrowserTestWithFakeUI, + GetDisplayMediaWithConstraints) { + ASSERT_TRUE(embedded_test_server()->Start()); + + content::WebContents* tab = OpenTestPageInNewTab(kMainHtmlPage); + const int kMaxWidth = 200; + const int kMaxFrameRate = 6; + const std::string& constraints = + base::StringPrintf("{video: {width: {max: %d}, frameRate: {max: %d}}}", + kMaxWidth, kMaxFrameRate); + RunGetDisplayMedia(tab, constraints, /*is_fake_ui=*/true); + + std::string result; + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab->GetMainFrame(), "getWidthSetting();", &result)); + EXPECT_EQ(result, base::StringPrintf("%d", kMaxWidth)); + + EXPECT_TRUE(content::ExecuteScriptAndExtractString( + tab->GetMainFrame(), "getFrameRateSetting();", &result)); + EXPECT_EQ(result, base::StringPrintf("%d", kMaxFrameRate)); +} + +INSTANTIATE_TEST_SUITE_P(, + WebRtcGetDisplayMediaBrowserTestWithFakeUI, + testing::Values(TestConfig{"monitor", "true", "never"}, + TestConfig{"window", "true", "never"}, + TestConfig{"browser", "true", + "never"})); diff --git a/chromium/chrome/browser/media/webrtc/webrtc_getmediadevices_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_getmediadevices_browsertest.cc new file mode 100644 index 00000000000..b5fea966103 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_getmediadevices_browsertest.cc @@ -0,0 +1,360 @@ +// Copyright 2014 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/string_util.h" +#include "base/test/scoped_feature_list.h" +#include "build/build_config.h" +#include "chrome/browser/content_settings/cookie_settings_factory.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/content_settings/core/browser/cookie_settings.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browsing_data_remover.h" +#include "content/public/common/content_features.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "content/public/test/browsing_data_remover_test_util.h" +#include "media/audio/audio_device_description.h" +#include "media/audio/audio_manager.h" +#include "media/base/media_switches.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "testing/gtest/include/gtest/gtest-param-test.h" + +namespace { + +const char kMainWebrtcTestHtmlPage[] = "/webrtc/webrtc_jsep01_test.html"; + +const char kDeviceKindAudioInput[] = "audioinput"; +const char kDeviceKindVideoInput[] = "videoinput"; +const char kDeviceKindAudioOutput[] = "audiooutput"; + +} // namespace + +// Integration test for WebRTC enumerateDevices. It always uses fake devices. +// It needs to be a browser test (and not content browser test) to be able to +// test that labels are cleared or not depending on if access to devices has +// been granted. +class WebRtcGetMediaDevicesBrowserTest + : public WebRtcTestBase, + public testing::WithParamInterface<bool> { + public: + WebRtcGetMediaDevicesBrowserTest() + : has_audio_output_devices_initialized_(false), + has_audio_output_devices_(false) { + std::vector<base::Feature> audio_service_oop_features = { + features::kAudioServiceAudioStreams, + features::kAudioServiceOutOfProcess}; + if (GetParam()) { + // Force audio service out of process to enabled. + audio_service_features_.InitWithFeatures(audio_service_oop_features, {}); + } else { + // Force audio service out of process to disabled. + audio_service_features_.InitWithFeatures({}, audio_service_oop_features); + } + } + + void SetUpInProcessBrowserTestFixture() override { + DetectErrorsInJavaScript(); // Look for errors in our rather complex js. + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + // Ensure the infobar is enabled, since we expect that in this test. + EXPECT_FALSE(command_line->HasSwitch(switches::kUseFakeUIForMediaStream)); + + // Always use fake devices. + command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); + } + + protected: + // This is used for media devices and sources. + struct MediaDeviceInfo { + std::string device_id; // Domain specific device ID. + std::string kind; + std::string label; + std::string group_id; + }; + + void EnumerateDevices(content::WebContents* tab, + std::vector<MediaDeviceInfo>* devices) { + std::string devices_as_json = ExecuteJavascript("enumerateDevices()", tab); + EXPECT_FALSE(devices_as_json.empty()); + + int error_code; + std::string error_message; + std::unique_ptr<base::Value> value = + base::JSONReader::ReadAndReturnErrorDeprecated( + devices_as_json, base::JSON_ALLOW_TRAILING_COMMAS, &error_code, + &error_message); + + ASSERT_TRUE(value.get() != NULL) << error_message; + EXPECT_EQ(value->type(), base::Value::Type::LIST); + + base::ListValue* values; + ASSERT_TRUE(value->GetAsList(&values)); + ASSERT_FALSE(values->empty()); + bool found_audio_input = false; + bool found_video_input = false; + + for (auto it = values->begin(); it != values->end(); ++it) { + const base::DictionaryValue* dict; + MediaDeviceInfo device; + ASSERT_TRUE(it->GetAsDictionary(&dict)); + ASSERT_TRUE(dict->GetString("deviceId", &device.device_id)); + ASSERT_TRUE(dict->GetString("kind", &device.kind)); + ASSERT_TRUE(dict->GetString("label", &device.label)); + ASSERT_TRUE(dict->GetString("groupId", &device.group_id)); + + // Should be HMAC SHA256. + if (!media::AudioDeviceDescription::IsDefaultDevice(device.device_id) && + !(device.device_id == + media::AudioDeviceDescription::kCommunicationsDeviceId)) { + EXPECT_EQ(64ul, device.device_id.length()); + EXPECT_TRUE( + base::ContainsOnlyChars(device.device_id, "0123456789abcdef")); + } + + EXPECT_TRUE(device.kind == kDeviceKindAudioInput || + device.kind == kDeviceKindVideoInput || + device.kind == kDeviceKindAudioOutput); + if (device.kind == kDeviceKindAudioInput) { + found_audio_input = true; + } else if (device.kind == kDeviceKindVideoInput) { + found_video_input = true; + } + + EXPECT_FALSE(device.group_id.empty()); + devices->push_back(device); + } + + EXPECT_TRUE(found_audio_input); + EXPECT_TRUE(found_video_input); + } + + static void CheckEnumerationsAreDifferent( + const std::vector<MediaDeviceInfo>& devices, + const std::vector<MediaDeviceInfo>& devices2) { + for (auto& device : devices) { + auto it = std::find_if(devices2.begin(), devices2.end(), + [&device](const MediaDeviceInfo& device_info) { + return device.device_id == device_info.device_id; + }); + if (device.device_id == media::AudioDeviceDescription::kDefaultDeviceId || + device.device_id == + media::AudioDeviceDescription::kCommunicationsDeviceId) { + EXPECT_NE(it, devices2.end()); + } else { + EXPECT_EQ(it, devices2.end()); + } + + it = std::find_if(devices2.begin(), devices2.end(), + [&device](const MediaDeviceInfo& device_info) { + return device.group_id == device_info.group_id; + }); + EXPECT_EQ(it, devices2.end()); + } + } + + bool has_audio_output_devices_initialized_; + bool has_audio_output_devices_; + + private: + base::test::ScopedFeatureList audio_service_features_; +}; + +IN_PROC_BROWSER_TEST_P(WebRtcGetMediaDevicesBrowserTest, + EnumerateDevicesWithoutAccess) { + ASSERT_TRUE(embedded_test_server()->Start()); + GURL url(embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage)); + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* tab = + browser()->tab_strip_model()->GetActiveWebContents(); + + std::vector<MediaDeviceInfo> devices; + EnumerateDevices(tab, &devices); + + // Labels should be empty if access has not been allowed. + for (const auto& device_info : devices) { + EXPECT_TRUE(device_info.label.empty()); + } +} + +IN_PROC_BROWSER_TEST_P(WebRtcGetMediaDevicesBrowserTest, + EnumerateDevicesWithAccess) { + ASSERT_TRUE(embedded_test_server()->Start()); + GURL url(embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage)); + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* tab = + browser()->tab_strip_model()->GetActiveWebContents(); + + EXPECT_TRUE(GetUserMediaAndAccept(tab)); + + std::vector<MediaDeviceInfo> devices; + EnumerateDevices(tab, &devices); + + // Labels should be non-empty if access has been allowed. + for (const auto& device_info : devices) { + EXPECT_TRUE(!device_info.label.empty()); + } +} + +IN_PROC_BROWSER_TEST_P(WebRtcGetMediaDevicesBrowserTest, + DeviceIdSameGroupIdDiffersAfterReload) { + ASSERT_TRUE(embedded_test_server()->Start()); + GURL url(embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage)); + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* tab = + browser()->tab_strip_model()->GetActiveWebContents(); + std::vector<MediaDeviceInfo> devices; + EnumerateDevices(tab, &devices); + + ui_test_utils::NavigateToURL(browser(), url); + std::vector<MediaDeviceInfo> devices2; + EnumerateDevices(tab, &devices2); + + EXPECT_EQ(devices.size(), devices2.size()); + for (auto& device : devices) { + auto it = std::find_if(devices2.begin(), devices2.end(), + [&device](const MediaDeviceInfo& device_info) { + return device.device_id == device_info.device_id; + }); + EXPECT_NE(it, devices2.end()); + + it = std::find_if(devices2.begin(), devices2.end(), + [&device](const MediaDeviceInfo& device_info) { + return device.group_id == device_info.group_id; + }); + EXPECT_EQ(it, devices2.end()); + } +} + +IN_PROC_BROWSER_TEST_P(WebRtcGetMediaDevicesBrowserTest, + DeviceIdSameGroupIdDiffersAcrossTabs) { + ASSERT_TRUE(embedded_test_server()->Start()); + GURL url(embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage)); + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* tab1 = + browser()->tab_strip_model()->GetActiveWebContents(); + std::vector<MediaDeviceInfo> devices; + EnumerateDevices(tab1, &devices); + + chrome::AddTabAt(browser(), GURL(), -1, true); + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* tab2 = + browser()->tab_strip_model()->GetActiveWebContents(); + std::vector<MediaDeviceInfo> devices2; + EnumerateDevices(tab2, &devices2); + + EXPECT_NE(tab1, tab2); + EXPECT_EQ(devices.size(), devices2.size()); + for (auto& device : devices) { + auto it = std::find_if(devices2.begin(), devices2.end(), + [&device](const MediaDeviceInfo& device_info) { + return device.device_id == device_info.device_id; + }); + EXPECT_NE(it, devices2.end()); + + it = std::find_if(devices2.begin(), devices2.end(), + [&device](const MediaDeviceInfo& device_info) { + return device.group_id == device_info.group_id; + }); + EXPECT_EQ(it, devices2.end()); + } +} + +IN_PROC_BROWSER_TEST_P(WebRtcGetMediaDevicesBrowserTest, + DeviceIdDiffersAfterClearingCookies) { + ASSERT_TRUE(embedded_test_server()->Start()); + GURL url(embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage)); + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* tab = + browser()->tab_strip_model()->GetActiveWebContents(); + std::vector<MediaDeviceInfo> devices; + EnumerateDevices(tab, &devices); + + auto* remover = + content::BrowserContext::GetBrowsingDataRemover(browser()->profile()); + content::BrowsingDataRemoverCompletionObserver completion_observer(remover); + remover->RemoveAndReply( + base::Time(), base::Time::Max(), + content::BrowsingDataRemover::DATA_TYPE_COOKIES, + content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB, + &completion_observer); + completion_observer.BlockUntilCompletion(); + + std::vector<MediaDeviceInfo> devices2; + EnumerateDevices(tab, &devices2); + + EXPECT_EQ(devices.size(), devices2.size()); + CheckEnumerationsAreDifferent(devices, devices2); +} + +IN_PROC_BROWSER_TEST_P(WebRtcGetMediaDevicesBrowserTest, + DeviceIdDiffersAcrossTabsWithCookiesDisabled) { + ASSERT_TRUE(embedded_test_server()->Start()); + GURL url(embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage)); + ui_test_utils::NavigateToURL(browser(), url); + CookieSettingsFactory::GetForProfile(browser()->profile()) + ->SetDefaultCookieSetting(CONTENT_SETTING_BLOCK); + content::WebContents* tab1 = + browser()->tab_strip_model()->GetActiveWebContents(); + std::vector<MediaDeviceInfo> devices; + EnumerateDevices(tab1, &devices); + + chrome::AddTabAt(browser(), GURL(), -1, true); + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* tab2 = + browser()->tab_strip_model()->GetActiveWebContents(); + std::vector<MediaDeviceInfo> devices2; + EnumerateDevices(tab2, &devices2); + + EXPECT_NE(tab1, tab2); + EXPECT_EQ(devices.size(), devices2.size()); + CheckEnumerationsAreDifferent(devices, devices2); +} + +IN_PROC_BROWSER_TEST_P(WebRtcGetMediaDevicesBrowserTest, + DeviceIdDiffersSameTabAfterReloadWithCookiesDisabled) { + ASSERT_TRUE(embedded_test_server()->Start()); + GURL url(embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage)); + ui_test_utils::NavigateToURL(browser(), url); + CookieSettingsFactory::GetForProfile(browser()->profile()) + ->SetDefaultCookieSetting(CONTENT_SETTING_BLOCK); + content::WebContents* tab = + browser()->tab_strip_model()->GetActiveWebContents(); + std::vector<MediaDeviceInfo> devices; + EnumerateDevices(tab, &devices); + + ui_test_utils::NavigateToURL(browser(), url); + tab = browser()->tab_strip_model()->GetActiveWebContents(); + std::vector<MediaDeviceInfo> devices2; + EnumerateDevices(tab, &devices2); + + EXPECT_EQ(devices.size(), devices2.size()); + CheckEnumerationsAreDifferent(devices, devices2); +} + +// We run these tests with the audio service both in and out of the the browser +// process to have waterfall coverage while the feature rolls out. It should be +// removed after launch. +#if defined(OS_WIN) || defined(OS_MACOSX) || \ + (defined(OS_LINUX) && !defined(OS_CHROMEOS)) +// Platforms where the out of process audio service is supported. +INSTANTIATE_TEST_SUITE_P(, + WebRtcGetMediaDevicesBrowserTest, + ::testing::Values(true)); +#else +// Platforms where the out of process audio service is not supported. +INSTANTIATE_TEST_SUITE_P(, + WebRtcGetMediaDevicesBrowserTest, + ::testing::Values(false)); +#endif diff --git a/chromium/chrome/browser/media/webrtc/webrtc_internals_integration_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_internals_integration_browsertest.cc new file mode 100644 index 00000000000..3c49d176b84 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_internals_integration_browsertest.cc @@ -0,0 +1,78 @@ +// Copyright (c) 2018 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/files/file_path.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "base/threading/thread_restrictions.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_switches.h" +#include "media/base/media_switches.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "testing/gtest/include/gtest/gtest.h" + +using webrtc_event_logging::WebRtcEventLogManager; + +namespace { +const char kMainWebrtcTestHtmlPage[] = "/webrtc/webrtc_jsep01_test.html"; +} + +class WebRTCInternalsIntegrationBrowserTest : public WebRtcTestBase { + public: + ~WebRTCInternalsIntegrationBrowserTest() override = default; + + void SetUpCommandLine(base::CommandLine* command_line) override { + InProcessBrowserTest::SetUpDefaultCommandLine(command_line); + + command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); + + { + base::ScopedAllowBlockingForTesting allow_blocking; + ASSERT_TRUE(local_logs_dir_.CreateUniqueTempDir()); + } + command_line->AppendSwitchASCII(switches::kWebRtcLocalEventLogging, + local_logs_dir_.GetPath().MaybeAsASCII()); + } + + // To avoid flaky tests, we need to synchronize with WebRtcEventLogger's + // internal task runners (if any exist) before we examine anything we + // expect to be produced by WebRtcEventLogger (files, etc.). + void WaitForEventLogProcessing() { + WebRtcEventLogManager* manager = WebRtcEventLogManager::GetInstance(); + ASSERT_TRUE(manager); + + base::RunLoop run_loop; + manager->PostNullTaskForTesting(run_loop.QuitWhenIdleClosure()); + run_loop.Run(); + } + + bool IsDirectoryEmpty(const base::FilePath& path) { + base::ScopedAllowBlockingForTesting allow_blocking; + return base::IsDirectoryEmpty(path); + } + + base::ScopedTempDir local_logs_dir_; +}; + +IN_PROC_BROWSER_TEST_F(WebRTCInternalsIntegrationBrowserTest, + IntegrationWithWebRtcEventLogger) { + ASSERT_TRUE(embedded_test_server()->Start()); + + content::WebContents* tab = + OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + + ASSERT_TRUE(IsDirectoryEmpty(local_logs_dir_.GetPath())); // Sanity on test. + + // Local WebRTC event logging turned on from command line using the + // kWebRtcLocalEventLogging flag. When we set up a peer connection, it + // will be logged to a file under |local_logs_dir_|. + SetupPeerconnectionWithLocalStream(tab); + + WaitForEventLogProcessing(); + + EXPECT_FALSE(IsDirectoryEmpty(local_logs_dir_.GetPath())); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_internals_perf_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_internals_perf_browsertest.cc new file mode 100644 index 00000000000..08e26798891 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_internals_perf_browsertest.cc @@ -0,0 +1,309 @@ +// Copyright 2014 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 <memory> + +#include "base/command_line.h" +#include "base/files/file_util.h" +#include "base/json/json_reader.h" +#include "base/macros.h" +#include "base/strings/string_split.h" +#include "base/test/test_timeouts.h" +#include "base/threading/thread_restrictions.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_perf.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "media/base/media_switches.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "testing/perf/perf_test.h" +#include "third_party/blink/public/common/features.h" + +static const char kMainWebrtcTestHtmlPage[] = + "/webrtc/webrtc_jsep01_test.html"; + +std::string MakePerfTestLabel(std::string base, bool opus_dtx) { + if (opus_dtx) { + return base + "_with_opus_dtx"; + } + return base; +} + +// Performance browsertest for WebRTC. This test is manual since it takes long +// to execute and requires the reference files provided by the webrtc.DEPS +// solution (which is only available on WebRTC internal bots). +// Gets its metrics from "chrome://webrtc-internals". +class WebRtcInternalsPerfBrowserTest : public WebRtcTestBase { + public: + void SetUpInProcessBrowserTestFixture() override { + DetectErrorsInJavaScript(); // Look for errors in our rather complex js. + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + // Ensure the infobar is enabled, since we expect that in this test. + EXPECT_FALSE(command_line->HasSwitch(switches::kUseFakeUIForMediaStream)); + + // Play a suitable, somewhat realistic video file. + base::FilePath input_video = test::GetReferenceFilesDir() + .Append(test::kReferenceFileName360p) + .AddExtension(test::kY4mFileExtension); + command_line->AppendSwitchPath(switches::kUseFileForFakeVideoCapture, + input_video); + command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); + } + + // Tries to extract data from peerConnectionDataStore in the webrtc-internals + // tab. The caller owns the parsed data. Returns NULL on failure. + base::DictionaryValue* GetWebrtcInternalsData( + content::WebContents* webrtc_internals_tab) { + std::string all_stats_json = ExecuteJavascript( + "window.domAutomationController.send(" + " JSON.stringify(peerConnectionDataStore));", + webrtc_internals_tab); + + std::unique_ptr<base::Value> parsed_json = + base::JSONReader::ReadDeprecated(all_stats_json); + base::DictionaryValue* result; + if (parsed_json.get() && parsed_json->GetAsDictionary(&result)) { + ignore_result(parsed_json.release()); + return result; + } + + return NULL; + } + + const base::DictionaryValue* GetDataOnPeerConnection( + const base::DictionaryValue* all_data, + int peer_connection_index) { + base::DictionaryValue::Iterator iterator(*all_data); + + for (int i = 0; i < peer_connection_index && !iterator.IsAtEnd(); + --peer_connection_index) { + iterator.Advance(); + } + + const base::DictionaryValue* result; + if (!iterator.IsAtEnd() && iterator.value().GetAsDictionary(&result)) + return result; + + return NULL; + } + + std::unique_ptr<base::DictionaryValue> MeasureWebRtcInternalsData( + int duration_msec) { + chrome::AddTabAt(browser(), GURL(), -1, true); + ui_test_utils::NavigateToURL(browser(), GURL("chrome://webrtc-internals")); + content::WebContents* webrtc_internals_tab = + browser()->tab_strip_model()->GetActiveWebContents(); + + // TODO(https://crbug.com/1004239): Stop relying on the legacy getStats() + // API. + ChangeToLegacyGetStats(webrtc_internals_tab); + test::SleepInJavascript(webrtc_internals_tab, duration_msec); + + return std::unique_ptr<base::DictionaryValue>( + GetWebrtcInternalsData(webrtc_internals_tab)); + } + + void RunsAudioVideoCall60SecsAndLogsInternalMetrics( + const std::string& video_codec, + bool prefer_hw_video_codec = false, + const std::string& video_codec_profile = std::string(), + const std::string& video_codec_print_modifier = std::string()) { + ASSERT_TRUE(test::HasReferenceFilesInCheckout()); + ASSERT_TRUE(embedded_test_server()->Start()); + + ASSERT_GE(TestTimeouts::test_launcher_timeout().InSeconds(), 100) + << "This is a long-running test; you must specify " + "--test-launcher-timeout to have a value of at least 100000."; + ASSERT_GE(TestTimeouts::action_max_timeout().InSeconds(), 100) + << "This is a long-running test; you must specify " + "--ui-test-action-max-timeout to have a value of at least 100000."; + ASSERT_LT(TestTimeouts::action_max_timeout(), + TestTimeouts::test_launcher_timeout()) + << "action_max_timeout needs to be strictly-less-than " + "test_launcher_timeout"; + + content::WebContents* left_tab = + OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + content::WebContents* right_tab = + OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + + SetupPeerconnectionWithLocalStream(left_tab); + SetupPeerconnectionWithLocalStream(right_tab); + + if (!video_codec.empty()) { + SetDefaultVideoCodec(left_tab, video_codec, prefer_hw_video_codec, + video_codec_profile); + SetDefaultVideoCodec(right_tab, video_codec, prefer_hw_video_codec, + video_codec_profile); + } + NegotiateCall(left_tab, right_tab); + + StartDetectingVideo(left_tab, "remote-view"); + StartDetectingVideo(right_tab, "remote-view"); + + WaitForVideoToPlay(left_tab); + WaitForVideoToPlay(right_tab); + + // Let values stabilize, bandwidth ramp up, etc. + test::SleepInJavascript(left_tab, 60000); + + // Start measurements. + std::unique_ptr<base::DictionaryValue> all_data = + MeasureWebRtcInternalsData(10000); + ASSERT_TRUE(all_data.get() != NULL); + + const base::DictionaryValue* first_pc_dict = + GetDataOnPeerConnection(all_data.get(), 0); + ASSERT_TRUE(first_pc_dict != NULL); + const std::string print_modifier = video_codec_print_modifier.empty() + ? video_codec + : video_codec_print_modifier; + test::PrintBweForVideoMetrics(*first_pc_dict, "", print_modifier); + test::PrintMetricsForAllStreams(*first_pc_dict, "", print_modifier); + + HangUp(left_tab); + HangUp(right_tab); + } + + void RunsOneWayCall60SecsAndLogsInternalMetrics( + const std::string& video_codec, + bool opus_dtx) { + ASSERT_TRUE(test::HasReferenceFilesInCheckout()); + ASSERT_TRUE(embedded_test_server()->Start()); + + ASSERT_GE(TestTimeouts::test_launcher_timeout().InSeconds(), 100) + << "This is a long-running test; you must specify " + "--test-launcher-timeout to have a value of at least 100000."; + ASSERT_GE(TestTimeouts::action_max_timeout().InSeconds(), 100) + << "This is a long-running test; you must specify " + "--ui-test-action-max-timeout to have a value of at least 100000."; + ASSERT_LT(TestTimeouts::action_max_timeout(), + TestTimeouts::test_launcher_timeout()) + << "action_max_timeout needs to be strictly-less-than " + "test_launcher_timeout"; + + content::WebContents* left_tab = + OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + content::WebContents* right_tab = + OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + + SetupPeerconnectionWithLocalStream(left_tab); + SetupPeerconnectionWithoutLocalStream(right_tab); + + if (!video_codec.empty()) { + SetDefaultVideoCodec(left_tab, video_codec, false /* prefer_hw_codec */); + SetDefaultVideoCodec(right_tab, video_codec, false /* prefer_hw_codec */); + } + if (opus_dtx) { + EnableOpusDtx(left_tab); + EnableOpusDtx(right_tab); + } + NegotiateCall(left_tab, right_tab); + + // Remote video will only play in one tab since the call is one-way. + StartDetectingVideo(right_tab, "remote-view"); + WaitForVideoToPlay(right_tab); + + // Let values stabilize, bandwidth ramp up, etc. + test::SleepInJavascript(left_tab, 60000); + + std::unique_ptr<base::DictionaryValue> all_data = + MeasureWebRtcInternalsData(10000); + ASSERT_TRUE(all_data.get() != NULL); + + // This assumes the sending peer connection is always listed first in the + // data store, and the receiving second. + const base::DictionaryValue* first_pc_dict = + GetDataOnPeerConnection(all_data.get(), 0); + ASSERT_TRUE(first_pc_dict != NULL); + test::PrintBweForVideoMetrics( + *first_pc_dict, MakePerfTestLabel("_sendonly", opus_dtx), video_codec); + test::PrintMetricsForSendStreams( + *first_pc_dict, MakePerfTestLabel("_sendonly", opus_dtx), video_codec); + + const base::DictionaryValue* second_pc_dict = + GetDataOnPeerConnection(all_data.get(), 1); + ASSERT_TRUE(second_pc_dict != NULL); + test::PrintBweForVideoMetrics( + *second_pc_dict, MakePerfTestLabel("_recvonly", opus_dtx), video_codec); + test::PrintMetricsForRecvStreams( + *second_pc_dict, MakePerfTestLabel("_recvonly", opus_dtx), video_codec); + + HangUp(left_tab); + HangUp(right_tab); + } +}; + +// This is manual for its long execution time. + +IN_PROC_BROWSER_TEST_F( + WebRtcInternalsPerfBrowserTest, + MANUAL_RunsAudioVideoCall60SecsAndLogsInternalMetricsVp8) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioVideoCall60SecsAndLogsInternalMetrics("VP8"); +} + +IN_PROC_BROWSER_TEST_F( + WebRtcInternalsPerfBrowserTest, + MANUAL_RunsAudioVideoCall60SecsAndLogsInternalMetricsVp9) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioVideoCall60SecsAndLogsInternalMetrics("VP9"); +} + +IN_PROC_BROWSER_TEST_F( + WebRtcInternalsPerfBrowserTest, + MANUAL_RunsAudioVideoCall60SecsAndLogsInternalMetricsVp9Profile2) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioVideoCall60SecsAndLogsInternalMetrics( + "VP9", true /* prefer_hw_video_codec */, + WebRtcTestBase::kVP9Profile2Specifier, "VP9p2"); +} + +#if BUILDFLAG(RTC_USE_H264) + +IN_PROC_BROWSER_TEST_F( + WebRtcInternalsPerfBrowserTest, + MANUAL_RunsAudioVideoCall60SecsAndLogsInternalMetricsH264) { + base::ScopedAllowBlockingForTesting allow_blocking; + // Only run test if run-time feature corresponding to |rtc_use_h264| is on. + if (!base::FeatureList::IsEnabled( + blink::features::kWebRtcH264WithOpenH264FFmpeg)) { + LOG(WARNING) + << "Run-time feature WebRTC-H264WithOpenH264FFmpeg disabled. " + "Skipping WebRtcInternalsPerfBrowserTest." + "MANUAL_RunsAudioVideoCall60SecsAndLogsInternalMetricsH264 (test " + "\"OK\")"; + return; + } + RunsAudioVideoCall60SecsAndLogsInternalMetrics( + "H264", true /* prefer_hw_video_codec */); +} + +#endif // BUILDFLAG(RTC_USE_H264) + +IN_PROC_BROWSER_TEST_F( + WebRtcInternalsPerfBrowserTest, + MANUAL_RunsOneWayCall60SecsAndLogsInternalMetricsDefault) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsOneWayCall60SecsAndLogsInternalMetrics("", false); +} + +IN_PROC_BROWSER_TEST_F( + WebRtcInternalsPerfBrowserTest, + MANUAL_RunsOneWayCall60SecsAndLogsInternalMetricsWithOpusDtx) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsOneWayCall60SecsAndLogsInternalMetrics("", true); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_log_buffer.cc b/chromium/chrome/browser/media/webrtc/webrtc_log_buffer.cc new file mode 100644 index 00000000000..14e87d6621d --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_log_buffer.cc @@ -0,0 +1,42 @@ +// Copyright 2019 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 "chrome/browser/media/webrtc/webrtc_log_buffer.h" + +#include "base/logging.h" + +WebRtcLogBuffer::WebRtcLogBuffer() + : buffer_(), + circular_(&buffer_[0], sizeof(buffer_), sizeof(buffer_) / 2, false), + read_only_(false) {} + +WebRtcLogBuffer::~WebRtcLogBuffer() { +#if DCHECK_IS_ON() + DCHECK(read_only_ || sequence_checker_.CalledOnValidSequence()); +#endif +} + +void WebRtcLogBuffer::Log(const std::string& message) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!read_only_); + circular_.Write(message.c_str(), message.length()); + const char eol = '\n'; + circular_.Write(&eol, 1); +} + +webrtc_logging::PartialCircularBuffer WebRtcLogBuffer::Read() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(read_only_); + return webrtc_logging::PartialCircularBuffer(&buffer_[0], sizeof(buffer_)); +} + +void WebRtcLogBuffer::SetComplete() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!read_only_) << "Already set? (programmer error)"; + read_only_ = true; + // Detach from the current sequence so that we can check reads on a different + // sequence. This is to make sure that Read()s still happen on one sequence + // only. + DETACH_FROM_SEQUENCE(sequence_checker_); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_log_buffer.h b/chromium/chrome/browser/media/webrtc/webrtc_log_buffer.h new file mode 100644 index 00000000000..66b3c283f60 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_log_buffer.h @@ -0,0 +1,47 @@ +// Copyright 2019 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOG_BUFFER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOG_BUFFER_H_ + +#include <string> + +#include "base/sequence_checker.h" +#include "build/build_config.h" +#include "components/webrtc_logging/common/partial_circular_buffer.h" + +#if defined(OS_ANDROID) +const size_t kWebRtcLogSize = 1 * 1024 * 1024; // 1 MB +#else +const size_t kWebRtcLogSize = 6 * 1024 * 1024; // 6 MB +#endif + +class WebRtcLogBuffer { + public: + WebRtcLogBuffer(); + ~WebRtcLogBuffer(); + + void Log(const std::string& message); + + // Returns a circular buffer instance for reading the internal log buffer. + // Must only be called after the log has been marked as complete + // (see SetComplete) and the caller must ensure that the WebRtcLogBuffer + // instance remains in scope for the lifetime of the returned circular buffer. + webrtc_logging::PartialCircularBuffer Read(); + + // Switches the buffer to read-only mode, where access to the internal + // buffer is allowed from different threads than were used to contribute + // to the log. Calls to Log() won't be allowed after calling + // SetComplete() and the call to SetComplete() must be done on the same + // thread as constructed the buffer and calls Log(). + void SetComplete(); + + private: + SEQUENCE_CHECKER(sequence_checker_); + uint8_t buffer_[kWebRtcLogSize]; + webrtc_logging::PartialCircularBuffer circular_; + bool read_only_; +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOG_BUFFER_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_log_uploader.cc b/chromium/chrome/browser/media/webrtc/webrtc_log_uploader.cc new file mode 100644 index 00000000000..1b386f4a803 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_log_uploader.cc @@ -0,0 +1,633 @@ +// 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 "chrome/browser/media/webrtc/webrtc_log_uploader.h" + +#include <stddef.h> +#include <cstdlib> +#include <utility> + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/metrics/histogram_functions.h" +#include "base/pickle.h" +#include "base/strings/strcat.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/task/post_task.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "components/version_info/version_info.h" +#include "components/webrtc_logging/browser/log_cleanup.h" +#include "components/webrtc_logging/browser/text_log_list.h" +#include "components/webrtc_logging/common/partial_circular_buffer.h" +#include "net/base/load_flags.h" +#include "net/base/mime_util.h" +#include "net/base/net_errors.h" +#include "net/http/http_status_code.h" +#include "net/traffic_annotation/network_traffic_annotation.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/cpp/simple_url_loader.h" +#include "services/network/public/mojom/url_loader_factory.mojom.h" +#include "third_party/zlib/zlib.h" + +namespace { + +const int kLogCountLimit = 5; +const uint32_t kIntermediateCompressionBufferBytes = 256 * 1024; // 256 KB +const int kLogListLimitLines = 50; + +const char kWebrtcLogUploadContentType[] = "multipart/form-data"; +const char kWebrtcLogMultipartBoundary[] = + "----**--yradnuoBgoLtrapitluMklaTelgooG--**----"; + +// Adds the header section for a gzip file to the multipart |post_data|. +void AddMultipartFileContentHeader(std::string* post_data, + const std::string& content_name) { + post_data->append("--"); + post_data->append(kWebrtcLogMultipartBoundary); + post_data->append("\r\nContent-Disposition: form-data; name=\""); + post_data->append(content_name); + post_data->append("\"; filename=\""); + post_data->append(content_name + ".gz"); + post_data->append("\"\r\nContent-Type: application/gzip\r\n\r\n"); +} + +// Adds |compressed_log| to |post_data|. +void AddLogData(std::string* post_data, const std::string& compressed_log) { + AddMultipartFileContentHeader(post_data, "webrtc_log"); + post_data->append(compressed_log); + post_data->append("\r\n"); +} + +// Adds the RTP dump data to |post_data|. +void AddRtpDumpData(std::string* post_data, + const std::string& name, + const std::string& dump_data) { + AddMultipartFileContentHeader(post_data, name); + post_data->append(dump_data.data(), dump_data.size()); + post_data->append("\r\n"); +} + +// Helper for WebRtcLogUploader::CompressLog(). +void ResizeForNextOutput(std::string* compressed_log, z_stream* stream) { + size_t old_size = compressed_log->size() - stream->avail_out; + compressed_log->resize(old_size + kIntermediateCompressionBufferBytes); + stream->next_out = + reinterpret_cast<unsigned char*>(&(*compressed_log)[old_size]); + stream->avail_out = kIntermediateCompressionBufferBytes; +} + +} // namespace + +WebRtcLogUploader::UploadDoneData::UploadDoneData() = default; +WebRtcLogUploader::UploadDoneData::UploadDoneData( + const WebRtcLogUploader::UploadDoneData& other) = default; +WebRtcLogUploader::UploadDoneData::~UploadDoneData() = default; + +WebRtcLogUploader::WebRtcLogUploader() + : main_task_runner_(base::SequencedTaskRunnerHandle::Get()), + background_task_runner_( + base::CreateSequencedTaskRunner({base::ThreadPool(), base::MayBlock(), + base::TaskPriority::BEST_EFFORT})) {} + +WebRtcLogUploader::~WebRtcLogUploader() { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_); + DCHECK(pending_uploads_.empty()); + DCHECK(shutdown_); +} + +bool WebRtcLogUploader::ApplyForStartLogging() { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_); + if (log_count_ < kLogCountLimit && !shutdown_) { + ++log_count_; + return true; + } + return false; +} + +void WebRtcLogUploader::LoggingStoppedDontUpload() { + DecreaseLogCount(); +} + +void WebRtcLogUploader::LoggingStoppedDoUpload( + std::unique_ptr<WebRtcLogBuffer> log_buffer, + std::unique_ptr<WebRtcLogMetaDataMap> meta_data, + const WebRtcLogUploader::UploadDoneData& upload_done_data) { + DCHECK(background_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(log_buffer.get()); + DCHECK(meta_data.get()); + DCHECK(!upload_done_data.paths.directory.empty()); + + std::string compressed_log = CompressLog(log_buffer.get()); + + std::string local_log_id; + + if (base::PathExists(upload_done_data.paths.directory)) { + webrtc_logging::DeleteOldWebRtcLogFiles(upload_done_data.paths.directory); + + local_log_id = base::NumberToString(base::Time::Now().ToDoubleT()); + base::FilePath log_file_path = + upload_done_data.paths.directory.AppendASCII(local_log_id) + .AddExtension(FILE_PATH_LITERAL(".gz")); + WriteCompressedLogToFile(compressed_log, log_file_path); + + base::FilePath log_list_path = + webrtc_logging::TextLogList::GetWebRtcLogListFileForDirectory( + upload_done_data.paths.directory); + AddLocallyStoredLogInfoToUploadListFile(log_list_path, local_log_id); + } + + UploadDoneData upload_done_data_with_log_id = upload_done_data; + upload_done_data_with_log_id.local_log_id = local_log_id; + PrepareMultipartPostData(compressed_log, std::move(meta_data), + upload_done_data_with_log_id); +} + +void WebRtcLogUploader::PrepareMultipartPostData( + const std::string& compressed_log, + std::unique_ptr<WebRtcLogMetaDataMap> meta_data, + const WebRtcLogUploader::UploadDoneData& upload_done_data) { + DCHECK(background_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(!compressed_log.empty()); + DCHECK(meta_data.get()); + + std::unique_ptr<std::string> post_data(new std::string()); + SetupMultipart(post_data.get(), compressed_log, + upload_done_data.paths.incoming_rtp_dump, + upload_done_data.paths.outgoing_rtp_dump, *meta_data.get()); + + // If a test has set the test string pointer, write to it and skip uploading. + // Still fire the upload callback so that we can run an extension API test + // using the test framework for that without hanging. + // TODO(grunell): Remove this when the api test for this feature is fully + // implemented according to the test plan. http://crbug.com/257329. + if (post_data_) { + *post_data_ = *post_data; + NotifyUploadDoneAndLogStats(net::HTTP_OK, net::OK, "", upload_done_data); + return; + } + + main_task_runner_->PostTask( + FROM_HERE, base::BindOnce(&WebRtcLogUploader::UploadCompressedLog, + base::Unretained(this), upload_done_data, + std::move(post_data))); +} + +void WebRtcLogUploader::UploadStoredLog( + const WebRtcLogUploader::UploadDoneData& upload_data) { + DCHECK(background_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(!upload_data.local_log_id.empty()); + DCHECK(!upload_data.paths.directory.empty()); + + base::FilePath native_log_path = + upload_data.paths.directory.AppendASCII(upload_data.local_log_id) + .AddExtension(FILE_PATH_LITERAL(".gz")); + + std::string compressed_log; + if (!base::ReadFileToString(native_log_path, &compressed_log)) { + DPLOG(WARNING) << "Could not read WebRTC log file."; + base::UmaHistogramSparse("WebRtcTextLogging.UploadFailed", + upload_data.web_app_id); + base::UmaHistogramSparse("WebRtcTextLogging.UploadFailureReason", + WebRtcLogUploadFailureReason::kStoredLogNotFound); + main_task_runner_->PostTask( + FROM_HERE, + base::BindOnce(upload_data.callback, false, "", "Log doesn't exist.")); + return; + } + + UploadDoneData upload_data_with_rtp = upload_data; + + // Optimistically set the rtp paths to what they should be if they exist. + upload_data_with_rtp.paths.incoming_rtp_dump = + upload_data.paths.directory.AppendASCII(upload_data.local_log_id) + .AddExtension(FILE_PATH_LITERAL(".rtp_in")); + + upload_data_with_rtp.paths.outgoing_rtp_dump = + upload_data.paths.directory.AppendASCII(upload_data.local_log_id) + .AddExtension(FILE_PATH_LITERAL(".rtp_out")); + + std::unique_ptr<WebRtcLogMetaDataMap> meta_data(new WebRtcLogMetaDataMap()); + { + std::string meta_data_contents; + base::FilePath meta_path = + upload_data.paths.directory.AppendASCII(upload_data.local_log_id) + .AddExtension(FILE_PATH_LITERAL(".meta")); + if (base::ReadFileToString(meta_path, &meta_data_contents) && + !meta_data_contents.empty()) { + base::Pickle pickle(&meta_data_contents[0], meta_data_contents.size()); + base::PickleIterator it(pickle); + std::string key, value; + while (it.ReadString(&key) && it.ReadString(&value)) + (*meta_data.get())[key] = value; + } + } + + PrepareMultipartPostData(compressed_log, std::move(meta_data), + upload_data_with_rtp); +} + +void WebRtcLogUploader::LoggingStoppedDoStore( + const WebRtcLogPaths& log_paths, + const std::string& log_id, + std::unique_ptr<WebRtcLogBuffer> log_buffer, + std::unique_ptr<WebRtcLogMetaDataMap> meta_data, + const GenericDoneCallback& done_callback) { + DCHECK(background_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(!log_id.empty()); + DCHECK(log_buffer.get()); + DCHECK(!log_paths.directory.empty()); + + webrtc_logging::DeleteOldWebRtcLogFiles(log_paths.directory); + + base::FilePath log_list_path = + webrtc_logging::TextLogList::GetWebRtcLogListFileForDirectory( + log_paths.directory); + + // Store the native log with a ".gz" extension. + std::string compressed_log = CompressLog(log_buffer.get()); + base::FilePath native_log_path = + log_paths.directory.AppendASCII(log_id).AddExtension( + FILE_PATH_LITERAL(".gz")); + WriteCompressedLogToFile(compressed_log, native_log_path); + AddLocallyStoredLogInfoToUploadListFile(log_list_path, log_id); + + // Move the rtp dump files to the log directory with a name of + // <log id>.rtp_[in|out]. + if (!log_paths.incoming_rtp_dump.empty()) { + base::FilePath rtp_path = + log_paths.directory.AppendASCII(log_id).AddExtension( + FILE_PATH_LITERAL(".rtp_in")); + base::Move(log_paths.incoming_rtp_dump, rtp_path); + } + + if (!log_paths.outgoing_rtp_dump.empty()) { + base::FilePath rtp_path = + log_paths.directory.AppendASCII(log_id).AddExtension( + FILE_PATH_LITERAL(".rtp_out")); + base::Move(log_paths.outgoing_rtp_dump, rtp_path); + } + + if (meta_data.get() && !meta_data->empty()) { + base::Pickle pickle; + for (const auto& it : *meta_data.get()) { + pickle.WriteString(it.first); + pickle.WriteString(it.second); + } + base::FilePath meta_path = + log_paths.directory.AppendASCII(log_id).AddExtension( + FILE_PATH_LITERAL(".meta")); + base::WriteFile(meta_path, static_cast<const char*>(pickle.data()), + pickle.size()); + } + + main_task_runner_->PostTask(FROM_HERE, + base::BindOnce(done_callback, true, "")); + + main_task_runner_->PostTask( + FROM_HERE, base::BindOnce(&WebRtcLogUploader::DecreaseLogCount, + base::Unretained(this))); +} + +void WebRtcLogUploader::Shutdown() { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_); + DCHECK(!shutdown_); + + // Clear the pending uploads list, which will reset all URL loaders. + pending_uploads_.clear(); + shutdown_ = true; +} + +void WebRtcLogUploader::OnSimpleLoaderComplete( + SimpleURLLoaderList::iterator it, + const WebRtcLogUploader::UploadDoneData& upload_done_data, + std::unique_ptr<std::string> response_body) { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_); + DCHECK(!shutdown_); + network::SimpleURLLoader* loader = it->get(); + base::Optional<int> response_code; + if (loader->ResponseInfo() && loader->ResponseInfo()->headers) { + response_code = loader->ResponseInfo()->headers->response_code(); + } + const int network_error_code = loader->NetError(); + pending_uploads_.erase(it); + std::string report_id; + if (response_body) + report_id = std::move(*response_body); + // The log path can be empty here if we failed getting it before. We still + // upload the log if that's the case. + if (!upload_done_data.paths.directory.empty()) { + // TODO(jiayl): Add the RTP dump records to chrome://webrtc-logs. + base::FilePath log_list_path = + webrtc_logging::TextLogList::GetWebRtcLogListFileForDirectory( + upload_done_data.paths.directory); + background_task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&WebRtcLogUploader::AddUploadedLogInfoToUploadListFile, + log_list_path, upload_done_data.local_log_id, + report_id)); + } + NotifyUploadDoneAndLogStats(response_code, network_error_code, report_id, + upload_done_data); +} + +void WebRtcLogUploader::SetupMultipart( + std::string* post_data, + const std::string& compressed_log, + const base::FilePath& incoming_rtp_dump, + const base::FilePath& outgoing_rtp_dump, + const std::map<std::string, std::string>& meta_data) { +#if defined(OS_WIN) + const char product[] = "Chrome"; +#elif defined(OS_MACOSX) + const char product[] = "Chrome_Mac"; +#elif defined(OS_LINUX) +#if !defined(ADDRESS_SANITIZER) + const char product[] = "Chrome_Linux"; +#else + const char product[] = "Chrome_Linux_ASan"; +#endif +#elif defined(OS_ANDROID) + const char product[] = "Chrome_Android"; +#elif defined(OS_CHROMEOS) + const char product[] = "Chrome_ChromeOS"; +#else +#error Platform not supported. +#endif + net::AddMultipartValueForUpload("prod", product, kWebrtcLogMultipartBoundary, + "", post_data); + net::AddMultipartValueForUpload("ver", + version_info::GetVersionNumber() + "-webrtc", + kWebrtcLogMultipartBoundary, "", post_data); + net::AddMultipartValueForUpload("guid", "0", kWebrtcLogMultipartBoundary, "", + post_data); + net::AddMultipartValueForUpload("type", "webrtc_log", + kWebrtcLogMultipartBoundary, "", post_data); + + // Add custom meta data. + for (const auto& it : meta_data) { + net::AddMultipartValueForUpload(it.first, it.second, + kWebrtcLogMultipartBoundary, "", post_data); + } + + AddLogData(post_data, compressed_log); + + // Add the rtp dumps if they exist. + base::FilePath rtp_dumps[2] = {incoming_rtp_dump, outgoing_rtp_dump}; + static const char* const kRtpDumpNames[2] = {"rtpdump_recv", "rtpdump_send"}; + + for (size_t i = 0; i < 2; ++i) { + if (!rtp_dumps[i].empty() && base::PathExists(rtp_dumps[i])) { + std::string dump_data; + if (base::ReadFileToString(rtp_dumps[i], &dump_data)) + AddRtpDumpData(post_data, kRtpDumpNames[i], dump_data); + } + } + + net::AddMultipartFinalDelimiterForUpload(kWebrtcLogMultipartBoundary, + post_data); +} + +std::string WebRtcLogUploader::CompressLog(WebRtcLogBuffer* buffer) { + z_stream stream = {0}; + int result = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + // windowBits = 15 is default, 16 is added to + // produce a gzip header + trailer. + 15 + 16, + 8, // memLevel = 8 is default. + Z_DEFAULT_STRATEGY); + DCHECK_EQ(Z_OK, result); + + std::string compressed_log; + ResizeForNextOutput(&compressed_log, &stream); + + uint8_t intermediate_buffer[kIntermediateCompressionBufferBytes] = {0}; + webrtc_logging::PartialCircularBuffer read_buffer(buffer->Read()); + do { + if (stream.avail_in == 0) { + uint32_t read = read_buffer.Read(&intermediate_buffer[0], + sizeof(intermediate_buffer)); + stream.next_in = &intermediate_buffer[0]; + stream.avail_in = read; + if (read != kIntermediateCompressionBufferBytes) + break; + } + result = deflate(&stream, Z_SYNC_FLUSH); + DCHECK_EQ(Z_OK, result); + if (stream.avail_out == 0) + ResizeForNextOutput(&compressed_log, &stream); + } while (true); + + // Ensure we have enough room in the output buffer. Easier to always just do a + // resize than looping around and resize if needed. + if (stream.avail_out < kIntermediateCompressionBufferBytes) + ResizeForNextOutput(&compressed_log, &stream); + + result = deflate(&stream, Z_FINISH); + DCHECK_EQ(Z_STREAM_END, result); + result = deflateEnd(&stream); + DCHECK_EQ(Z_OK, result); + + compressed_log.resize(compressed_log.size() - stream.avail_out); + return compressed_log; +} + +void WebRtcLogUploader::UploadCompressedLog( + const WebRtcLogUploader::UploadDoneData& upload_done_data, + std::unique_ptr<std::string> post_data) { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_); + + DecreaseLogCount(); + + // We don't log upload failure to UMA in case of shutting down for + // consistency, since there are other cases during shutdown were we don't get + // a chance to log. + if (shutdown_) + return; + + std::string content_type = kWebrtcLogUploadContentType; + content_type.append("; boundary="); + content_type.append(kWebrtcLogMultipartBoundary); + + // Create traffic annotation tag. + net::NetworkTrafficAnnotationTag traffic_annotation = + net::DefineNetworkTrafficAnnotation("webrtc_log_upload", R"( + semantics { + sender: "Webrtc Log Uploader" + description: "Uploads WebRTC debug logs for Hangouts." + trigger: + "When a Hangouts extension or Hangouts services extension signals " + "to upload via the private WebRTC logging extension API." + data: + "WebRTC specific log entries, additional system information, and " + "RTP packet headers for incoming and outgoing WebRTC streams. " + "Audio or video data is never sent." + destination: GOOGLE_OWNED_SERVICE + } + policy { + cookies_allowed: NO + setting: + "This feature can be disabled by unchecking 'Report additional " + "diagnostics to help improve Hangouts.' in Hangouts settings." + policy_exception_justification: + "Not implemented, it would be good to do so." + })"); + + constexpr char kUploadURL[] = "https://clients2.google.com/cr/report"; + auto resource_request = std::make_unique<network::ResourceRequest>(); + resource_request->url = !upload_url_for_testing_.is_empty() + ? upload_url_for_testing_ + : GURL(kUploadURL); + resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit; + resource_request->method = "POST"; + std::unique_ptr<network::SimpleURLLoader> simple_url_loader = + network::SimpleURLLoader::Create(std::move(resource_request), + traffic_annotation); + simple_url_loader->AttachStringForUpload(*post_data, content_type); + auto it = pending_uploads_.insert(pending_uploads_.begin(), + std::move(simple_url_loader)); + network::SimpleURLLoader* raw_loader = it->get(); + raw_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( + g_browser_process->shared_url_loader_factory().get(), + base::BindOnce(&WebRtcLogUploader::OnSimpleLoaderComplete, + base::Unretained(this), std::move(it), upload_done_data)); +} + +void WebRtcLogUploader::DecreaseLogCount() { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_); + --log_count_; +} + +void WebRtcLogUploader::WriteCompressedLogToFile( + const std::string& compressed_log, + const base::FilePath& log_file_path) { + DCHECK(background_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(!compressed_log.empty()); + base::WriteFile(log_file_path, &compressed_log[0], compressed_log.size()); +} + +void WebRtcLogUploader::AddLocallyStoredLogInfoToUploadListFile( + const base::FilePath& upload_list_path, + const std::string& local_log_id) { + DCHECK(background_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(!upload_list_path.empty()); + DCHECK(!local_log_id.empty()); + + std::string contents; + + if (base::PathExists(upload_list_path)) { + if (!base::ReadFileToString(upload_list_path, &contents)) { + DPLOG(WARNING) << "Could not read WebRTC log list file."; + return; + } + + // Limit the number of log entries to |kLogListLimitLines| - 1, to make room + // for the new entry. Each line including the last ends with a '\n', so hit + // n will be before line n-1 (from the back). + int lf_count = 0; + int i = contents.size() - 1; + for (; i >= 0 && lf_count < kLogListLimitLines; --i) { + if (contents[i] == '\n') + ++lf_count; + } + if (lf_count >= kLogListLimitLines) { + // + 1 to compensate for the for loop decrease before the conditional + // check and + 1 to get the length. + contents.erase(0, i + 2); + } + } + + // Write the log ID and capture time to the log list file. Leave the upload + // time and report ID empty. + contents += ",," + local_log_id + "," + + base::NumberToString(base::Time::Now().ToDoubleT()) + '\n'; + + int written = + base::WriteFile(upload_list_path, &contents[0], contents.size()); + if (written != static_cast<int>(contents.size())) { + DPLOG(WARNING) << "Could not write all data to WebRTC log list file: " + << written; + } +} + +// static +void WebRtcLogUploader::AddUploadedLogInfoToUploadListFile( + const base::FilePath& upload_list_path, + const std::string& local_log_id, + const std::string& report_id) { + DCHECK(!upload_list_path.empty()); + DCHECK(!local_log_id.empty()); + DCHECK(!report_id.empty()); + + std::string contents; + + if (base::PathExists(upload_list_path)) { + if (!base::ReadFileToString(upload_list_path, &contents)) { + DPLOG(WARNING) << "Could not read WebRTC log list file."; + return; + } + } + + // Write the Unix time and report ID to the log list file. We should be able + // to find the local log ID, in that case insert the data into the existing + // line. Otherwise add it in the end. + base::Time time_now = base::Time::Now(); + std::string time_now_str = base::NumberToString(time_now.ToDoubleT()); + size_t pos = contents.find(",," + local_log_id); + if (pos != std::string::npos) { + contents.insert(pos, time_now_str); + contents.insert(pos + time_now_str.length() + 1, report_id); + } else { + contents += time_now_str + "," + report_id + ",," + time_now_str + "\n"; + } + + int written = + base::WriteFile(upload_list_path, &contents[0], contents.size()); + if (written != static_cast<int>(contents.size())) { + DPLOG(WARNING) << "Could not write all data to WebRTC log list file: " + << written; + } +} + +void WebRtcLogUploader::NotifyUploadDoneAndLogStats( + base::Optional<int> response_code, + int network_error_code, + const std::string& report_id, + const WebRtcLogUploader::UploadDoneData& upload_done_data) { + if (upload_done_data.callback.is_null()) + return; + + const bool success = response_code == net::HTTP_OK; + std::string error_message; + if (success) { + base::UmaHistogramSparse("WebRtcTextLogging.UploadSuccessful", + upload_done_data.web_app_id); + } else { + base::UmaHistogramSparse("WebRtcTextLogging.UploadFailed", + upload_done_data.web_app_id); + if (response_code.has_value()) { + base::UmaHistogramSparse("WebRtcTextLogging.UploadFailureReason", + response_code.value()); + } else { + DCHECK_NE(network_error_code, net::OK); + base::UmaHistogramSparse("WebRtcTextLogging.UploadFailureReason", + WebRtcLogUploadFailureReason::kNetworkError); + base::UmaHistogramSparse("WebRtcTextLogging.UploadFailureNetErrorCode", + std::abs(network_error_code)); + } + error_message = base::StrCat( + {"Uploading failed, response code: ", + response_code.has_value() ? base::NumberToString(response_code.value()) + : "<no value>"}); + } + main_task_runner_->PostTask( + FROM_HERE, base::BindOnce(upload_done_data.callback, success, report_id, + error_message)); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_log_uploader.h b/chromium/chrome/browser/media/webrtc/webrtc_log_uploader.h new file mode 100644 index 00000000000..ea558ca1a7e --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_log_uploader.h @@ -0,0 +1,236 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOG_UPLOADER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOG_UPLOADER_H_ + +#include <stdint.h> + +#include <list> +#include <map> +#include <memory> +#include <string> + +#include "base/files/file_path.h" +#include "base/gtest_prod_util.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/optional.h" +#include "base/sequence_checker.h" +#include "base/sequenced_task_runner.h" +#include "chrome/browser/media/webrtc/webrtc_log_buffer.h" +#include "url/gurl.h" + +namespace network { +class SimpleURLLoader; +} + +typedef struct z_stream_s z_stream; + +struct WebRtcLogPaths { + base::FilePath directory; + base::FilePath incoming_rtp_dump; + base::FilePath outgoing_rtp_dump; +}; + +typedef std::map<std::string, std::string> WebRtcLogMetaDataMap; + +// Upload failure reasons used for UMA stats. A failure reason can be one of +// those listed here or a response code for the upload HTTP request. The +// values in this list must be less than 100 and cannot be changed. +struct WebRtcLogUploadFailureReason { + enum { + kInvalidState = 0, + kStoredLogNotFound = 1, + kNetworkError = 2, + }; +}; + +// WebRtcLogUploader uploads WebRTC logs, keeps count of how many logs have +// been started and denies further logs if a limit is reached. It also adds +// the timestamp and report ID of the uploded log to a text file. There must +// only be one object of this type. +class WebRtcLogUploader { + public: + typedef base::Callback<void(bool, const std::string&)> GenericDoneCallback; + typedef base::Callback<void(bool, const std::string&, const std::string&)> + UploadDoneCallback; + + // Used when uploading is done to perform post-upload actions. |paths| is + // also used pre-upload. + struct UploadDoneData { + UploadDoneData(); + UploadDoneData(const UploadDoneData& other); + ~UploadDoneData(); + + WebRtcLogPaths paths; + UploadDoneCallback callback; + std::string local_log_id; + // Used for statistics. See |WebRtcLoggingHandlerHost::web_app_id_|. + int web_app_id; + }; + + WebRtcLogUploader(); + ~WebRtcLogUploader(); + + // Returns true is number of logs limit is not reached yet. Increases log + // count if true is returned. Must be called before UploadLog(). + bool ApplyForStartLogging(); + + // Notifies that logging has stopped and that the log should not be uploaded. + // Decreases log count. May only be called if permission to log has been + // granted by calling ApplyForStartLogging() and getting true in return. + // After this function has been called, a new permission must be granted. + // Call either this function or LoggingStoppedDoUpload(). + void LoggingStoppedDontUpload(); + + // Notifies that that logging has stopped and that the log should be uploaded. + // Decreases log count. May only be called if permission to log has been + // granted by calling ApplyForStartLogging() and getting true in return. After + // this function has been called, a new permission must be granted. Call + // either this function or LoggingStoppedDontUpload(). + // |upload_done_data.local_log_id| is set and used internally and should be + // left empty. + void LoggingStoppedDoUpload(std::unique_ptr<WebRtcLogBuffer> log_buffer, + std::unique_ptr<WebRtcLogMetaDataMap> meta_data, + const UploadDoneData& upload_done_data); + + // Uploads a previously stored log (see LoggingStoppedDoStore()). + void UploadStoredLog(const UploadDoneData& upload_data); + + // Similarly to LoggingStoppedDoUpload(), we store the log in compressed + // format on disk but add the option to specify a unique |log_id| for later + // identification and potential upload. + void LoggingStoppedDoStore(const WebRtcLogPaths& log_paths, + const std::string& log_id, + std::unique_ptr<WebRtcLogBuffer> log_buffer, + std::unique_ptr<WebRtcLogMetaDataMap> meta_data, + const GenericDoneCallback& done_callback); + + // Cancels URL fetcher operation by deleting all URL fetchers. This cancels + // any pending uploads and releases SystemURLRequestContextGetter references. + // Sets |shutdown_| which prevents new fetchers from being created. + void Shutdown(); + + // For testing purposes. If called, the multipart will not be uploaded, but + // written to |post_data_| instead. + void OverrideUploadWithBufferForTesting(std::string* post_data) { + DCHECK((post_data && !post_data_) || (!post_data && post_data_)); + post_data_ = post_data; + } + + // For testing purposes. + void SetUploadUrlForTesting(const GURL& url) { + DCHECK((!url.is_empty() && upload_url_for_testing_.is_empty()) || + (url.is_empty() && !upload_url_for_testing_.is_empty())); + upload_url_for_testing_ = url; + } + + const scoped_refptr<base::SequencedTaskRunner>& background_task_runner() + const { + return background_task_runner_; + } + + private: + // Allow the test class to call AddLocallyStoredLogInfoToUploadListFile. + friend class WebRtcLogUploaderTest; + FRIEND_TEST_ALL_PREFIXES(WebRtcLogUploaderTest, + AddLocallyStoredLogInfoToUploadListFile); + FRIEND_TEST_ALL_PREFIXES(WebRtcLogUploaderTest, + AddUploadedLogInfoToUploadListFile); + + // Sets up a multipart body to be uploaded. The body is produced according + // to RFC 2046. + void SetupMultipart(std::string* post_data, + const std::string& compressed_log, + const base::FilePath& incoming_rtp_dump, + const base::FilePath& outgoing_rtp_dump, + const std::map<std::string, std::string>& meta_data); + + std::string CompressLog(WebRtcLogBuffer* buffer); + + void UploadCompressedLog(const UploadDoneData& upload_done_data, + std::unique_ptr<std::string> post_data); + + void DecreaseLogCount(); + + // Must be called on the FILE thread. + void WriteCompressedLogToFile(const std::string& compressed_log, + const base::FilePath& log_file_path); + + void PrepareMultipartPostData(const std::string& compressed_log, + std::unique_ptr<WebRtcLogMetaDataMap> meta_data, + const UploadDoneData& upload_done_data); + + // Append information (upload time, report ID and local ID) about a log to a + // log list file, limited to |kLogListLimitLines| entries. This list is used + // for viewing the logs under chrome://webrtc-logs, see WebRtcLogUploadList. + // The list has the format: + // [upload_time],[report_id],[local_id],[capture_time] + // Each line represents a log. + // * |upload_time| is the time when the log was uploaded in Unix time. + // * |report_id| is the ID reported back by the server. + // * |local_id| is the ID for the locally stored log. It's the time stored + // in Unix time and it's also used as file name. + // * |capture_time| is the Unix time when the log was captured. + // AddLocallyStoredLogInfoToUploadListFile() will first be called. + // |upload_time| and |report_id| will be left empty in the entry written to + // the list file. If uploading is successful, + // AddUploadedLogInfoToUploadListFile() will be called and those empty fields + // will be filled out. + // Must be called on the FILE thread. + void AddLocallyStoredLogInfoToUploadListFile( + const base::FilePath& upload_list_path, + const std::string& local_log_id); + static void AddUploadedLogInfoToUploadListFile( + const base::FilePath& upload_list_path, + const std::string& local_log_id, + const std::string& report_id); + + // Notifies users that upload has completed and logs UMA stats. + // |response_code| not having a value means that no response code could be + // retrieved, in which case |network_error_code| should be something other + // than net::OK. + void NotifyUploadDoneAndLogStats(base::Optional<int> response_code, + int network_error_code, + const std::string& report_id, + const UploadDoneData& upload_done_data); + + using SimpleURLLoaderList = + std::list<std::unique_ptr<network::SimpleURLLoader>>; + + void OnSimpleLoaderComplete(SimpleURLLoaderList::iterator it, + const UploadDoneData& upload_done_data, + std::unique_ptr<std::string> response_body); + + SEQUENCE_CHECKER(main_sequence_checker_); + + // Main sequence where this class was constructed. + scoped_refptr<base::SequencedTaskRunner> main_task_runner_; + + // Background sequence where we run background, potentially blocking, + // operations. + scoped_refptr<base::SequencedTaskRunner> background_task_runner_; + + // Keeps track of number of currently open logs. Must only be accessed from + // the main sequence. + int log_count_ = 0; + + // For testing purposes, see OverrideUploadWithBufferForTesting. Only accessed + // on the background sequence + std::string* post_data_ = nullptr; + + // For testing purposes. + GURL upload_url_for_testing_; + + // Only accessed on the main sequence. + SimpleURLLoaderList pending_uploads_; + + // When true, don't create new URL loaders. + bool shutdown_ = false; + + DISALLOW_COPY_AND_ASSIGN(WebRtcLogUploader); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOG_UPLOADER_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_log_uploader_unittest.cc b/chromium/chrome/browser/media/webrtc/webrtc_log_uploader_unittest.cc new file mode 100644 index 00000000000..b00d4b99f1a --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_log_uploader_unittest.cc @@ -0,0 +1,316 @@ +// 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 "chrome/browser/media/webrtc/webrtc_log_uploader.h" + +#include <stddef.h> + +#include <string> +#include <utility> + +#include "base/bind.h" +#include "base/files/file.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/logging.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/task/post_task.h" +#include "base/test/task_environment.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/time/time.h" +#include "testing/gtest/include/gtest/gtest.h" + +const char kTestTime[] = "time"; +const char kTestReportId[] = "report-id"; +const char kTestLocalId[] = "local-id"; + +class WebRtcLogUploaderTest : public testing::Test { + public: + WebRtcLogUploaderTest() {} + + bool VerifyNumberOfLines(int expected_lines) { + std::vector<std::string> lines = GetLinesFromListFile(); + EXPECT_EQ(expected_lines, static_cast<int>(lines.size())); + return expected_lines == static_cast<int>(lines.size()); + } + + bool VerifyLastLineHasAllInfo() { + std::string last_line = GetLastLineFromListFile(); + if (last_line.empty()) + return false; + std::vector<std::string> line_parts = base::SplitString( + last_line, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + EXPECT_EQ(4u, line_parts.size()); + if (4u != line_parts.size()) + return false; + // The times (indices 0 and 3) is the time when the info was written to the + // file which we don't know, so just verify that it's not empty. + EXPECT_FALSE(line_parts[0].empty()); + EXPECT_STREQ(kTestReportId, line_parts[1].c_str()); + EXPECT_STREQ(kTestLocalId, line_parts[2].c_str()); + EXPECT_FALSE(line_parts[3].empty()); + return true; + } + + // Verify that the last line contains the correct info for a local storage. + bool VerifyLastLineHasLocalStorageInfoOnly() { + std::string last_line = GetLastLineFromListFile(); + if (last_line.empty()) + return false; + std::vector<std::string> line_parts = base::SplitString( + last_line, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + EXPECT_EQ(4u, line_parts.size()); + if (4u != line_parts.size()) + return false; + EXPECT_TRUE(line_parts[0].empty()); + EXPECT_TRUE(line_parts[1].empty()); + EXPECT_STREQ(kTestLocalId, line_parts[2].c_str()); + EXPECT_FALSE(line_parts[3].empty()); + return true; + } + + // Verify that the last line contains the correct info for an upload. + bool VerifyLastLineHasUploadInfoOnly() { + std::string last_line = GetLastLineFromListFile(); + if (last_line.empty()) + return false; + std::vector<std::string> line_parts = base::SplitString( + last_line, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + EXPECT_EQ(4u, line_parts.size()); + if (4u != line_parts.size()) + return false; + EXPECT_FALSE(line_parts[0].empty()); + EXPECT_STREQ(kTestReportId, line_parts[1].c_str()); + EXPECT_TRUE(line_parts[2].empty()); + EXPECT_FALSE(line_parts[3].empty()); + return true; + } + + bool AddLinesToTestFile(int number_of_lines) { + base::File test_list_file(test_list_path_, + base::File::FLAG_OPEN | base::File::FLAG_APPEND); + EXPECT_TRUE(test_list_file.IsValid()); + if (!test_list_file.IsValid()) + return false; + + for (int i = 0; i < number_of_lines; ++i) { + EXPECT_EQ(static_cast<int>(sizeof(kTestTime)) - 1, + test_list_file.WriteAtCurrentPos(kTestTime, + sizeof(kTestTime) - 1)); + EXPECT_EQ(1, test_list_file.WriteAtCurrentPos(",", 1)); + EXPECT_EQ(static_cast<int>(sizeof(kTestReportId)) - 1, + test_list_file.WriteAtCurrentPos(kTestReportId, + sizeof(kTestReportId) - 1)); + EXPECT_EQ(1, test_list_file.WriteAtCurrentPos(",", 1)); + EXPECT_EQ(static_cast<int>(sizeof(kTestLocalId)) - 1, + test_list_file.WriteAtCurrentPos(kTestLocalId, + sizeof(kTestLocalId) - 1)); + EXPECT_EQ(1, test_list_file.WriteAtCurrentPos(",", 1)); + EXPECT_EQ(static_cast<int>(sizeof(kTestTime)) - 1, + test_list_file.WriteAtCurrentPos(kTestTime, + sizeof(kTestTime) - 1)); + EXPECT_EQ(1, test_list_file.WriteAtCurrentPos("\n", 1)); + } + return true; + } + + std::vector<std::string> GetLinesFromListFile() { + std::string contents; + int read = base::ReadFileToString(test_list_path_, &contents); + EXPECT_GT(read, 0); + if (read == 0) + return std::vector<std::string>(); + // Since every line should end with '\n', the last line should be empty. So + // we expect at least two lines including the final empty. Remove the empty + // line before returning. + std::vector<std::string> lines = base::SplitString( + contents, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + EXPECT_GT(lines.size(), 1u); + if (lines.size() < 2) + return std::vector<std::string>(); + EXPECT_TRUE(lines.back().empty()); + if (!lines.back().empty()) + return std::vector<std::string>(); + lines.pop_back(); + return lines; + } + + std::string GetLastLineFromListFile() { + std::vector<std::string> lines = GetLinesFromListFile(); + EXPECT_GT(lines.size(), 0u); + if (lines.empty()) + return std::string(); + return lines[lines.size() - 1]; + } + + void VerifyRtpDumpInMultipart(const std::string& post_data, + const std::string& dump_name, + const std::string& dump_content) { + std::vector<std::string> lines = base::SplitStringUsingSubstr( + post_data, "\r\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + + std::string name_line = "Content-Disposition: form-data; name=\""; + name_line.append(dump_name); + name_line.append("\""); + name_line.append("; filename=\""); + name_line.append(dump_name); + name_line.append(".gz\""); + + size_t i = 0; + for (; i < lines.size(); ++i) { + if (lines[i] == name_line) + break; + } + + // The RTP dump takes 4 lines: content-disposition, content-type, empty + // line, dump content. + EXPECT_LT(i, lines.size() - 3); + + EXPECT_EQ("Content-Type: application/gzip", lines[i + 1]); + EXPECT_EQ("", lines[i + 2]); + EXPECT_EQ(dump_content, lines[i + 3]); + } + + static void AddLocallyStoredLogInfoToUploadListFile( + WebRtcLogUploader* log_uploader, + const base::FilePath& upload_list_path, + const std::string& local_log_id) { + base::RunLoop run_loop; + log_uploader->background_task_runner()->PostTaskAndReply( + FROM_HERE, + base::BindOnce( + &WebRtcLogUploader::AddLocallyStoredLogInfoToUploadListFile, + base::Unretained(log_uploader), upload_list_path, local_log_id), + run_loop.QuitClosure()); + run_loop.Run(); + } + + void FlushRunLoop() { + base::RunLoop run_loop; + base::SequencedTaskRunnerHandle::Get()->PostTask(FROM_HERE, + run_loop.QuitClosure()); + run_loop.Run(); + } + + base::test::TaskEnvironment task_environment_; + base::FilePath test_list_path_; +}; + +TEST_F(WebRtcLogUploaderTest, AddLocallyStoredLogInfoToUploadListFile) { + // Get a temporary filename. We don't want the file to exist to begin with + // since that's the normal use case, hence the delete. + ASSERT_TRUE(base::CreateTemporaryFile(&test_list_path_)); + EXPECT_TRUE(base::DeleteFile(test_list_path_, false)); + std::unique_ptr<WebRtcLogUploader> webrtc_log_uploader( + new WebRtcLogUploader()); + + AddLocallyStoredLogInfoToUploadListFile(webrtc_log_uploader.get(), + test_list_path_, kTestLocalId); + AddLocallyStoredLogInfoToUploadListFile(webrtc_log_uploader.get(), + test_list_path_, kTestLocalId); + ASSERT_TRUE(VerifyNumberOfLines(2)); + ASSERT_TRUE(VerifyLastLineHasLocalStorageInfoOnly()); + + const int expected_line_limit = 50; + ASSERT_TRUE(AddLinesToTestFile(expected_line_limit - 2)); + ASSERT_TRUE(VerifyNumberOfLines(expected_line_limit)); + ASSERT_TRUE(VerifyLastLineHasAllInfo()); + + AddLocallyStoredLogInfoToUploadListFile(webrtc_log_uploader.get(), + test_list_path_, kTestLocalId); + ASSERT_TRUE(VerifyNumberOfLines(expected_line_limit)); + ASSERT_TRUE(VerifyLastLineHasLocalStorageInfoOnly()); + + ASSERT_TRUE(AddLinesToTestFile(10)); + ASSERT_TRUE(VerifyNumberOfLines(60)); + ASSERT_TRUE(VerifyLastLineHasAllInfo()); + + AddLocallyStoredLogInfoToUploadListFile(webrtc_log_uploader.get(), + test_list_path_, kTestLocalId); + ASSERT_TRUE(VerifyNumberOfLines(expected_line_limit)); + ASSERT_TRUE(VerifyLastLineHasLocalStorageInfoOnly()); + + webrtc_log_uploader->Shutdown(); + FlushRunLoop(); +} + +TEST_F(WebRtcLogUploaderTest, AddUploadedLogInfoToUploadListFile) { + // Get a temporary filename. We don't want the file to exist to begin with + // since that's the normal use case, hence the delete. + ASSERT_TRUE(base::CreateTemporaryFile(&test_list_path_)); + EXPECT_TRUE(base::DeleteFile(test_list_path_, false)); + std::unique_ptr<WebRtcLogUploader> webrtc_log_uploader( + new WebRtcLogUploader()); + + AddLocallyStoredLogInfoToUploadListFile(webrtc_log_uploader.get(), + test_list_path_, kTestLocalId); + ASSERT_TRUE(VerifyNumberOfLines(1)); + ASSERT_TRUE(VerifyLastLineHasLocalStorageInfoOnly()); + + webrtc_log_uploader->AddUploadedLogInfoToUploadListFile( + test_list_path_, kTestLocalId, kTestReportId); + ASSERT_TRUE(VerifyNumberOfLines(1)); + ASSERT_TRUE(VerifyLastLineHasAllInfo()); + + // Use a local ID that should not be found in the list. + webrtc_log_uploader->AddUploadedLogInfoToUploadListFile( + test_list_path_, "dummy id", kTestReportId); + ASSERT_TRUE(VerifyNumberOfLines(2)); + ASSERT_TRUE(VerifyLastLineHasUploadInfoOnly()); + + webrtc_log_uploader->Shutdown(); + FlushRunLoop(); +} + +TEST_F(WebRtcLogUploaderTest, AddRtpDumpsToPostedData) { + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + + std::unique_ptr<WebRtcLogUploader> webrtc_log_uploader( + new WebRtcLogUploader()); + + std::string post_data; + webrtc_log_uploader->OverrideUploadWithBufferForTesting(&post_data); + + // Create the fake dump files. + const base::FilePath incoming_dump = temp_dir.GetPath().AppendASCII("recv"); + const base::FilePath outgoing_dump = temp_dir.GetPath().AppendASCII("send"); + const std::string incoming_dump_content = "dummy incoming"; + const std::string outgoing_dump_content = "dummy outgoing"; + + base::WriteFile(incoming_dump, + &incoming_dump_content[0], + incoming_dump_content.size()); + base::WriteFile(outgoing_dump, + &outgoing_dump_content[0], + outgoing_dump_content.size()); + + WebRtcLogUploader::UploadDoneData upload_done_data; + upload_done_data.paths.directory = temp_dir.GetPath().AppendASCII("log"); + + upload_done_data.paths.incoming_rtp_dump = incoming_dump; + upload_done_data.paths.outgoing_rtp_dump = outgoing_dump; + + std::unique_ptr<WebRtcLogBuffer> log(new WebRtcLogBuffer()); + log->SetComplete(); + + base::RunLoop run_loop; + webrtc_log_uploader->background_task_runner()->PostTaskAndReply( + FROM_HERE, + base::BindOnce(&WebRtcLogUploader::LoggingStoppedDoUpload, + base::Unretained(webrtc_log_uploader.get()), + std::move(log), std::make_unique<WebRtcLogMetaDataMap>(), + upload_done_data), + run_loop.QuitClosure()); + run_loop.Run(); + + VerifyRtpDumpInMultipart(post_data, "rtpdump_recv", incoming_dump_content); + VerifyRtpDumpInMultipart(post_data, "rtpdump_send", outgoing_dump_content); + + webrtc_log_uploader->Shutdown(); + FlushRunLoop(); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_log_util.cc b/chromium/chrome/browser/media/webrtc/webrtc_log_util.cc new file mode 100644 index 00000000000..f724e04d6e1 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_log_util.cc @@ -0,0 +1,36 @@ +// Copyright 2014 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 "chrome/browser/media/webrtc/webrtc_log_util.h" + +#include <vector> + +#include "base/bind.h" +#include "base/task/post_task.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "components/webrtc_logging/browser/log_cleanup.h" +#include "components/webrtc_logging/browser/text_log_list.h" +#include "content/public/browser/browser_thread.h" + +// static +void WebRtcLogUtil::DeleteOldWebRtcLogFilesForAllProfiles() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + std::vector<ProfileAttributesEntry*> entries = + g_browser_process->profile_manager()->GetProfileAttributesStorage(). + GetAllProfilesAttributes(); + for (ProfileAttributesEntry* entry : entries) { + base::PostTask( + FROM_HERE, + {base::ThreadPool(), base::MayBlock(), base::TaskPriority::BEST_EFFORT}, + base::BindOnce( + &webrtc_logging::DeleteOldWebRtcLogFiles, + webrtc_logging::TextLogList:: + GetWebRtcLogDirectoryForBrowserContextPath(entry->GetPath()))); + } +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_log_util.h b/chromium/chrome/browser/media/webrtc/webrtc_log_util.h new file mode 100644 index 00000000000..8f06b483fbf --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_log_util.h @@ -0,0 +1,15 @@ +// Copyright 2014 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOG_UTIL_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOG_UTIL_H_ + +class WebRtcLogUtil { + public: + // Calls webrtc_logging::DeleteOldWebRtcLogFiles() for all profiles. Must be + // called on the UI thread. + static void DeleteOldWebRtcLogFilesForAllProfiles(); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOG_UTIL_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_logging_controller.cc b/chromium/chrome/browser/media/webrtc/webrtc_logging_controller.cc new file mode 100644 index 00000000000..824464ffdca --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_logging_controller.cc @@ -0,0 +1,589 @@ +// 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 "chrome/browser/media/webrtc/webrtc_logging_controller.h" + +#include <string> +#include <utility> + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/metrics/histogram_functions.h" +#include "base/supports_user_data.h" +#include "base/task/post_task.h" +#include "base/task_runner_util.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "chrome/browser/media/webrtc/webrtc_event_log_manager.h" +#include "chrome/browser/media/webrtc/webrtc_log_uploader.h" +#include "chrome/browser/media/webrtc/webrtc_rtp_dump_handler.h" +#include "components/webrtc_logging/browser/text_log_list.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/render_process_host.h" +#include "services/service_manager/public/cpp/connector.h" + +#if defined(OS_LINUX) || defined(OS_CHROMEOS) +#include "content/public/browser/child_process_security_policy.h" +#include "storage/browser/fileapi/isolated_context.h" +#endif // defined(OS_LINUX) || defined(OS_CHROMEOS) + +using webrtc_event_logging::WebRtcEventLogManager; + +namespace { + +// Key used to attach the handler to the RenderProcessHost. +constexpr char kRenderProcessHostKey[] = "kWebRtcLoggingControllerKey"; + +} // namespace + +// static +void WebRtcLoggingController::AttachToRenderProcessHost( + content::RenderProcessHost* host, + WebRtcLogUploader* log_uploader) { + host->SetUserData( + kRenderProcessHostKey, + std::make_unique<base::UserDataAdapter<WebRtcLoggingController>>( + new WebRtcLoggingController(host->GetID(), host->GetBrowserContext(), + log_uploader))); +} + +// static +WebRtcLoggingController* WebRtcLoggingController::FromRenderProcessHost( + content::RenderProcessHost* host) { + return base::UserDataAdapter<WebRtcLoggingController>::Get( + host, kRenderProcessHostKey); +} + +void WebRtcLoggingController::SetMetaData( + std::unique_ptr<WebRtcLogMetaDataMap> meta_data, + const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + // Set the web app ID if there's a "client" key, otherwise leave it unchanged. + for (const auto& it : *meta_data) { + if (it.first == "client") { + web_app_id_ = static_cast<int>(base::PersistentHash(it.second)); + text_log_handler_->SetWebAppId(web_app_id_); + break; + } + } + + text_log_handler_->SetMetaData(std::move(meta_data), callback); +} + +void WebRtcLoggingController::StartLogging( + const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + // Request a log_slot from the LogUploader and start logging. + if (text_log_handler_->StartLogging(log_uploader_, callback)) { + // Start logging in the renderer. The callback has already been fired since + // there is no acknowledgement when the renderer actually starts. + content::RenderProcessHost* host = + content::RenderProcessHost::FromID(render_process_id_); + + // OK for this to replace an existing logging_agent_ connection. + host->BindReceiver(logging_agent_.BindNewPipeAndPassReceiver()); + logging_agent_.set_disconnect_handler( + base::BindOnce(&WebRtcLoggingController::OnAgentDisconnected, this)); + logging_agent_->Start(receiver_.BindNewPipeAndPassRemote()); + } +} + +void WebRtcLoggingController::StopLogging(const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + // Change the state to STOPPING and disable logging in the browser. + if (text_log_handler_->StopLogging(callback)) { + // Stop logging in the renderer. OnStopped will be called when this is done + // to change the state from STOPPING to STOPPED and fire the callback. + logging_agent_->Stop(); + } +} + +void WebRtcLoggingController::UploadLog(const UploadDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + // This functions uploads both text logs (mandatory) and RTP dumps (optional). + // TODO(terelius): If there's no text log available (either because it hasn't + // been started or because it hasn't been stopped), the current implementation + // will fire an error callback and leave any RTP dumps in a local directory. + // Would it be better to upload whatever logs we have, or would the lack of + // an error callback make it harder to debug potential errors? + + base::UmaHistogramSparse("WebRtcTextLogging.UploadStarted", web_app_id_); + + base::PostTaskAndReplyWithResult( + log_uploader_->background_task_runner().get(), FROM_HERE, + base::BindOnce(log_directory_getter_), + base::BindOnce(&WebRtcLoggingController::TriggerUpload, this, callback)); +} + +void WebRtcLoggingController::UploadStoredLog( + const std::string& log_id, + const UploadDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + base::UmaHistogramSparse("WebRtcTextLogging.UploadStoredStarted", + web_app_id_); + + // Make this a method call on log_uploader_ + + WebRtcLogUploader::UploadDoneData upload_data; + upload_data.callback = callback; + upload_data.local_log_id = log_id; + upload_data.web_app_id = web_app_id_; + + log_uploader_->background_task_runner()->PostTask( + FROM_HERE, base::BindOnce( + [](WebRtcLogUploader* log_uploader, + WebRtcLogUploader::UploadDoneData upload_data, + base::RepeatingCallback<base::FilePath(void)> + log_directory_getter) { + upload_data.paths.directory = log_directory_getter.Run(); + log_uploader->UploadStoredLog(upload_data); + }, + log_uploader_, upload_data, log_directory_getter_)); +} + +void WebRtcLoggingController::DiscardLog(const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + if (!text_log_handler_->ExpectLoggingStateStopped(callback)) { + // The callback is fired with an error message by ExpectLoggingStateStopped. + return; + } + log_uploader_->LoggingStoppedDontUpload(); + text_log_handler_->DiscardLog(); + rtp_dump_handler_.reset(); + stop_rtp_dump_callback_.Reset(); + FireGenericDoneCallback(callback, true, ""); +} + +// Stores the log locally using a hash of log_id + security origin. +void WebRtcLoggingController::StoreLog(const std::string& log_id, + const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + if (!text_log_handler_->ExpectLoggingStateStopped(callback)) { + // The callback is fired with an error message by ExpectLoggingStateStopped. + return; + } + + if (rtp_dump_handler_) { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(stop_rtp_dump_callback_, true, true)); + + rtp_dump_handler_->StopOngoingDumps(base::Bind( + &WebRtcLoggingController::StoreLogContinue, this, log_id, callback)); + return; + } + + StoreLogContinue(log_id, callback); +} + +void WebRtcLoggingController::StoreLogContinue( + const std::string& log_id, + const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + std::unique_ptr<WebRtcLogPaths> log_paths(new WebRtcLogPaths()); + ReleaseRtpDumps(log_paths.get()); + + base::PostTaskAndReplyWithResult( + log_uploader_->background_task_runner().get(), FROM_HERE, + base::BindOnce(log_directory_getter_), + base::BindOnce(&WebRtcLoggingController::StoreLogInDirectory, this, + log_id, base::Passed(&log_paths), callback)); +} + +void WebRtcLoggingController::StartRtpDump( + RtpDumpType type, + const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(stop_rtp_dump_callback_.is_null()); + + content::RenderProcessHost* host = + content::RenderProcessHost::FromID(render_process_id_); + + // This call cannot fail. + stop_rtp_dump_callback_ = host->StartRtpDump( + type == RTP_DUMP_INCOMING || type == RTP_DUMP_BOTH, + type == RTP_DUMP_OUTGOING || type == RTP_DUMP_BOTH, + base::Bind(&WebRtcLoggingController::OnRtpPacket, this)); + + if (!rtp_dump_handler_) { + base::PostTaskAndReplyWithResult( + log_uploader_->background_task_runner().get(), FROM_HERE, + base::BindOnce(log_directory_getter_), + base::BindOnce(&WebRtcLoggingController::CreateRtpDumpHandlerAndStart, + this, type, callback)); + return; + } + + DoStartRtpDump(type, callback); +} + +void WebRtcLoggingController::StopRtpDump(RtpDumpType type, + const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + if (!rtp_dump_handler_) { + FireGenericDoneCallback(callback, false, "RTP dump has not been started."); + return; + } + + if (!stop_rtp_dump_callback_.is_null()) { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(stop_rtp_dump_callback_, + type == RTP_DUMP_INCOMING || type == RTP_DUMP_BOTH, + type == RTP_DUMP_OUTGOING || type == RTP_DUMP_BOTH)); + } + + rtp_dump_handler_->StopDump(type, callback); +} + +void WebRtcLoggingController::StartEventLogging( + const std::string& session_id, + size_t max_log_size_bytes, + int output_period_ms, + size_t web_app_id, + const StartEventLoggingCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + WebRtcEventLogManager::GetInstance()->StartRemoteLogging( + render_process_id_, session_id, max_log_size_bytes, output_period_ms, + web_app_id, callback); +} + +#if defined(OS_LINUX) || defined(OS_CHROMEOS) +void WebRtcLoggingController::GetLogsDirectory( + const LogsDirectoryCallback& callback, + const LogsDirectoryErrorCallback& error_callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + base::PostTaskAndReplyWithResult( + log_uploader_->background_task_runner().get(), FROM_HERE, + base::BindOnce(log_directory_getter_), + base::BindOnce(&WebRtcLoggingController::GrantLogsDirectoryAccess, this, + callback, error_callback)); +} + +void WebRtcLoggingController::GrantLogsDirectoryAccess( + const LogsDirectoryCallback& callback, + const LogsDirectoryErrorCallback& error_callback, + const base::FilePath& logs_path) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (logs_path.empty()) { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(error_callback, "Logs directory not available")); + return; + } + + storage::IsolatedContext* isolated_context = + storage::IsolatedContext::GetInstance(); + DCHECK(isolated_context); + + std::string registered_name; + storage::IsolatedContext::ScopedFSHandle file_system = + isolated_context->RegisterFileSystemForPath( + storage::kFileSystemTypeNativeLocal, std::string(), logs_path, + ®istered_name); + + // Only granting read and delete access to reduce contention with + // webrtcLogging APIs that modify files in that folder. + content::ChildProcessSecurityPolicy* policy = + content::ChildProcessSecurityPolicy::GetInstance(); + policy->GrantReadFileSystem(render_process_id_, file_system.id()); + // Delete is needed to prevent accumulation of files. + policy->GrantDeleteFromFileSystem(render_process_id_, file_system.id()); + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(callback, file_system.id(), registered_name)); +} +#endif // defined(OS_LINUX) || defined(OS_CHROMEOS) + +void WebRtcLoggingController::OnRtpPacket( + std::unique_ptr<uint8_t[]> packet_header, + size_t header_length, + size_t packet_length, + bool incoming) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // |rtp_dump_handler_| could be null if we are waiting for the FILE thread to + // create/ensure the log directory. + if (rtp_dump_handler_) { + rtp_dump_handler_->OnRtpPacket(packet_header.get(), header_length, + packet_length, incoming); + } +} + +void WebRtcLoggingController::OnAddMessages( + std::vector<chrome::mojom::WebRtcLoggingMessagePtr> messages) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (text_log_handler_->GetState() == WebRtcTextLogHandler::STARTED || + text_log_handler_->GetState() == WebRtcTextLogHandler::STOPPING) { + for (auto& message : messages) + text_log_handler_->LogWebRtcLoggingMessage(message.get()); + } +} + +void WebRtcLoggingController::OnStopped() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (text_log_handler_->GetState() != WebRtcTextLogHandler::STOPPING) { + // If an out-of-order response is received, stop_callback_ may be invalid, + // and must not be invoked. + DLOG(ERROR) << "OnStopped invoked in state " + << text_log_handler_->GetState(); + mojo::ReportBadMessage("WRLHH: OnStopped invoked in unexpected state."); + return; + } + text_log_handler_->StopDone(); +} + +WebRtcLoggingController::WebRtcLoggingController( + int render_process_id, + content::BrowserContext* browser_context, + WebRtcLogUploader* log_uploader) + : receiver_(this), + render_process_id_(render_process_id), + log_directory_getter_(base::BindRepeating( + &WebRtcLoggingController::GetLogDirectoryAndEnsureExists, + browser_context->GetPath())), + upload_log_on_render_close_(false), + text_log_handler_( + std::make_unique<WebRtcTextLogHandler>(render_process_id)), + rtp_dump_handler_(), + stop_rtp_dump_callback_(), + log_uploader_(log_uploader) { + DCHECK(log_uploader_); +} + +WebRtcLoggingController::~WebRtcLoggingController() { + // If we hit this, then we might be leaking a log reference count (see + // ApplyForStartLogging). + DCHECK_EQ(WebRtcTextLogHandler::CLOSED, text_log_handler_->GetState()); +} + +void WebRtcLoggingController::OnAgentDisconnected() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (text_log_handler_->GetChannelIsClosing()) + return; + + switch (text_log_handler_->GetState()) { + case WebRtcTextLogHandler::STARTING: + case WebRtcTextLogHandler::STARTED: + case WebRtcTextLogHandler::STOPPING: + case WebRtcTextLogHandler::STOPPED: + text_log_handler_->ChannelClosing(); + if (upload_log_on_render_close_) { + base::PostTaskAndReplyWithResult( + log_uploader_->background_task_runner().get(), FROM_HERE, + base::BindOnce(log_directory_getter_), + base::BindOnce(&WebRtcLoggingController::TriggerUpload, this, + UploadDoneCallback())); + } else { + log_uploader_->LoggingStoppedDontUpload(); + text_log_handler_->DiscardLog(); + } + break; + case WebRtcTextLogHandler::CLOSED: + // Do nothing + break; + default: + NOTREACHED(); + } +} + +void WebRtcLoggingController::TriggerUpload( + const UploadDoneCallback& callback, + const base::FilePath& log_directory) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (rtp_dump_handler_) { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(stop_rtp_dump_callback_, true, true)); + + rtp_dump_handler_->StopOngoingDumps( + base::Bind(&WebRtcLoggingController::DoUploadLogAndRtpDumps, this, + log_directory, callback)); + return; + } + + DoUploadLogAndRtpDumps(log_directory, callback); +} + +void WebRtcLoggingController::StoreLogInDirectory( + const std::string& log_id, + std::unique_ptr<WebRtcLogPaths> log_paths, + const GenericDoneCallback& done_callback, + const base::FilePath& directory) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // If channel is not closing, storing is only allowed when in STOPPED state. + // If channel is closing, storing is allowed for all states except CLOSED. + const WebRtcTextLogHandler::LoggingState text_logging_state = + text_log_handler_->GetState(); + const bool channel_is_closing = text_log_handler_->GetChannelIsClosing(); + if ((!channel_is_closing && + text_logging_state != WebRtcTextLogHandler::STOPPED) || + (channel_is_closing && + text_log_handler_->GetState() == WebRtcTextLogHandler::CLOSED)) { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(done_callback, false, + "Logging not stopped or no log open.")); + return; + } + + log_paths->directory = directory; + + std::unique_ptr<WebRtcLogBuffer> log_buffer; + std::unique_ptr<WebRtcLogMetaDataMap> meta_data; + text_log_handler_->ReleaseLog(&log_buffer, &meta_data); + CHECK(log_buffer.get()) << "State=" << text_log_handler_->GetState() + << ", uorc=" << upload_log_on_render_close_; + + log_uploader_->background_task_runner()->PostTask( + FROM_HERE, base::BindOnce(&WebRtcLogUploader::LoggingStoppedDoStore, + base::Unretained(log_uploader_), *log_paths, + log_id, std::move(log_buffer), + std::move(meta_data), done_callback)); +} + +void WebRtcLoggingController::DoUploadLogAndRtpDumps( + const base::FilePath& log_directory, + const UploadDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // If channel is not closing, upload is only allowed when in STOPPED state. + // If channel is closing, uploading is allowed for all states except CLOSED. + const WebRtcTextLogHandler::LoggingState text_logging_state = + text_log_handler_->GetState(); + const bool channel_is_closing = text_log_handler_->GetChannelIsClosing(); + if ((!channel_is_closing && + text_logging_state != WebRtcTextLogHandler::STOPPED) || + (channel_is_closing && + text_log_handler_->GetState() == WebRtcTextLogHandler::CLOSED)) { + // If the channel is not closing the log is expected to be uploaded, so + // it's considered a failure if it isn't. + // If the channel is closing we don't log failure to UMA for consistency, + // since there are other cases during shutdown were we don't get a chance + // to log. + if (!channel_is_closing) { + base::UmaHistogramSparse("WebRtcTextLogging.UploadFailed", web_app_id_); + base::UmaHistogramSparse("WebRtcTextLogging.UploadFailureReason", + WebRtcLogUploadFailureReason::kInvalidState); + } + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(callback, false, "", + "Logging not stopped or no log open.")); + return; + } + + WebRtcLogUploader::UploadDoneData upload_done_data; + upload_done_data.paths.directory = log_directory; + upload_done_data.callback = callback; + upload_done_data.web_app_id = web_app_id_; + ReleaseRtpDumps(&upload_done_data.paths); + + std::unique_ptr<WebRtcLogBuffer> log_buffer; + std::unique_ptr<WebRtcLogMetaDataMap> meta_data; + text_log_handler_->ReleaseLog(&log_buffer, &meta_data); + CHECK(log_buffer.get()) << "State=" << text_log_handler_->GetState() + << ", uorc=" << upload_log_on_render_close_; + + log_uploader_->background_task_runner()->PostTask( + FROM_HERE, + base::BindOnce(&WebRtcLogUploader::LoggingStoppedDoUpload, + base::Unretained(log_uploader_), std::move(log_buffer), + std::move(meta_data), upload_done_data)); +} + +void WebRtcLoggingController::CreateRtpDumpHandlerAndStart( + RtpDumpType type, + const GenericDoneCallback& callback, + const base::FilePath& dump_dir) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // |rtp_dump_handler_| may be non-null if StartRtpDump is called again before + // GetLogDirectoryAndEnsureExists returns on the FILE thread for a previous + // StartRtpDump. + if (!rtp_dump_handler_) + rtp_dump_handler_.reset(new WebRtcRtpDumpHandler(dump_dir)); + + DoStartRtpDump(type, callback); +} + +void WebRtcLoggingController::DoStartRtpDump( + RtpDumpType type, + const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(rtp_dump_handler_); + + std::string error; + bool result = rtp_dump_handler_->StartDump(type, &error); + FireGenericDoneCallback(callback, result, error); +} + +bool WebRtcLoggingController::ReleaseRtpDumps(WebRtcLogPaths* log_paths) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(log_paths); + + if (!rtp_dump_handler_) + return false; + + WebRtcRtpDumpHandler::ReleasedDumps rtp_dumps( + rtp_dump_handler_->ReleaseDumps()); + log_paths->incoming_rtp_dump = rtp_dumps.incoming_dump_path; + log_paths->outgoing_rtp_dump = rtp_dumps.outgoing_dump_path; + + rtp_dump_handler_.reset(); + stop_rtp_dump_callback_.Reset(); + + return true; +} + +void WebRtcLoggingController::FireGenericDoneCallback( + const GenericDoneCallback& callback, + bool success, + const std::string& error_message) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + DCHECK_EQ(success, error_message.empty()); + + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(callback, success, error_message)); +} + +// static +base::FilePath WebRtcLoggingController::GetLogDirectoryAndEnsureExists( + const base::FilePath& browser_context_directory_path) { + DCHECK(!browser_context_directory_path.empty()); + // Since we can be alive after the RenderProcessHost and the BrowserContext + // (profile) have gone away, we could create the log directory here after a + // profile has been deleted and removed from disk. If that happens it will be + // cleaned up (at a higher level) the next browser restart. + base::FilePath log_dir_path = + webrtc_logging::TextLogList::GetWebRtcLogDirectoryForBrowserContextPath( + browser_context_directory_path); + base::File::Error error; + if (!base::CreateDirectoryAndGetError(log_dir_path, &error)) { + DLOG(ERROR) << "Could not create WebRTC log directory, error: " << error; + return base::FilePath(); + } + return log_dir_path; +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_logging_controller.h b/chromium/chrome/browser/media/webrtc/webrtc_logging_controller.h new file mode 100644 index 00000000000..ee1eca46fba --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_logging_controller.h @@ -0,0 +1,242 @@ +// 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOGGING_CONTROLLER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOGGING_CONTROLLER_H_ + +#include <stddef.h> +#include <stdint.h> + +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/rtp_dump_type.h" +#include "chrome/browser/media/webrtc/webrtc_log_uploader.h" +#include "chrome/browser/media/webrtc/webrtc_text_log_handler.h" +#include "chrome/common/media/webrtc_logging.mojom.h" +#include "content/public/browser/render_process_host.h" +#include "mojo/public/cpp/bindings/remote.h" + +class WebRtcLogUploader; +class WebRtcRtpDumpHandler; + +namespace content { +class BrowserContext; +} // namespace content + +// WebRtcLoggingController handles operations regarding the WebRTC logging: +// - Opens a connection to a WebRtcLoggingAgent that runs in the render process +// and generates log messages. +// - Writes basic machine info to the log. +// - Informs the handler in the render process when to stop logging. +// - Closes the connection to the WebRtcLoggingAgent (and thereby discarding it) +// or triggers uploading of the log. +// - Detects when the agent (e.g., because of a tab closure or crash) is going +// away and possibly triggers uploading the log. +class WebRtcLoggingController + : public base::RefCounted<WebRtcLoggingController>, + public chrome::mojom::WebRtcLoggingClient { + public: + typedef WebRtcLogUploader::GenericDoneCallback GenericDoneCallback; + typedef WebRtcLogUploader::UploadDoneCallback UploadDoneCallback; + typedef base::Callback<void(const std::string&, const std::string&)> + LogsDirectoryCallback; + typedef base::Callback<void(const std::string&)> LogsDirectoryErrorCallback; + + // Argument #1: Indicate success/failure. + // Argument #2: If success, the log's ID. Otherwise, empty. + // Argument #3: If failure, the error message. Otherwise, empty. + typedef base::RepeatingCallback< + void(bool, const std::string&, const std::string&)> + StartEventLoggingCallback; + + static void AttachToRenderProcessHost(content::RenderProcessHost* host, + WebRtcLogUploader* log_uploader); + static WebRtcLoggingController* FromRenderProcessHost( + content::RenderProcessHost* host); + + // Sets meta data that will be uploaded along with the log and also written + // in the beginning of the log. Must be called on the IO thread before calling + // StartLogging. + void SetMetaData(std::unique_ptr<WebRtcLogMetaDataMap> meta_data, + const GenericDoneCallback& callback); + + // Opens a log and starts logging. Must be called on the IO thread. + void StartLogging(const GenericDoneCallback& callback); + + // Stops logging. Log will remain open until UploadLog or DiscardLog is + // called. Must be called on the IO thread. + void StopLogging(const GenericDoneCallback& callback); + + // Uploads the text log and the RTP dumps. Discards the local copy. May only + // be called after text logging has stopped. Must be called on the IO thread. + void UploadLog(const UploadDoneCallback& callback); + + // Uploads a log that was previously saved via a call to StoreLog(). + // Otherwise operates in the same way as UploadLog. + void UploadStoredLog(const std::string& log_id, + const UploadDoneCallback& callback); + + // Discards the log and the RTP dumps. May only be called after logging has + // stopped. Must be called on the IO thread. + void DiscardLog(const GenericDoneCallback& callback); + + // Stores the log locally using a hash of log_id + security origin. + void StoreLog(const std::string& log_id, const GenericDoneCallback& callback); + + // May be called on any thread. |upload_log_on_render_close_| is used + // for decision making and it's OK if it changes before the execution based + // on that decision has finished. + void set_upload_log_on_render_close(bool should_upload) { + upload_log_on_render_close_ = should_upload; + } + + // Starts dumping the RTP headers for the specified direction. Must be called + // on the UI thread. |type| specifies which direction(s) of RTP packets should + // be dumped. |callback| will be called when starting the dump is done. + void StartRtpDump(RtpDumpType type, const GenericDoneCallback& callback); + + // Stops dumping the RTP headers for the specified direction. Must be called + // on the UI thread. |type| specifies which direction(s) of RTP packet dumping + // should be stopped. |callback| will be called when stopping the dump is + // done. + void StopRtpDump(RtpDumpType type, const GenericDoneCallback& callback); + + // Called when an RTP packet is sent or received. Must be called on the UI + // thread. + void OnRtpPacket(std::unique_ptr<uint8_t[]> packet_header, + size_t header_length, + size_t packet_length, + bool incoming); + + // Start remote-bound event logging for a specific peer connection + // (indicated by its session description's ID). + // The callback will be posted back, indicating |true| if and only if an + // event log was successfully started, in which case the first of the string + // arguments will be set to the log-ID. Otherwise, the second of the string + // arguments will contain the error message. + // This function must be called on the UI thread. + void StartEventLogging(const std::string& session_id, + size_t max_log_size_bytes, + int output_period_ms, + size_t web_app_id, + const StartEventLoggingCallback& callback); + +#if defined(OS_LINUX) || defined(OS_CHROMEOS) + // Ensures that the WebRTC Logs directory exists and then grants render + // process access to the 'WebRTC Logs' directory, and invokes |callback| with + // the ids necessary to create a DirectoryEntry object. + void GetLogsDirectory(const LogsDirectoryCallback& callback, + const LogsDirectoryErrorCallback& error_callback); +#endif // defined(OS_LINUX) || defined(OS_CHROMEOS) + + // chrome::mojom::WebRtcLoggingClient methods: + void OnAddMessages( + std::vector<chrome::mojom::WebRtcLoggingMessagePtr> messages) override; + void OnStopped() override; + + private: + friend class base::RefCounted<WebRtcLoggingController>; + + WebRtcLoggingController(int render_process_id, + content::BrowserContext* browser_context, + WebRtcLogUploader* log_uploader); + ~WebRtcLoggingController() override; + + void OnAgentDisconnected(); + + // Called after stopping RTP dumps. + void StoreLogContinue(const std::string& log_id, + const GenericDoneCallback& callback); + + // Writes a formatted log |message| to the |circular_buffer_|. + void LogToCircularBuffer(const std::string& message); + + void TriggerUpload(const UploadDoneCallback& callback, + const base::FilePath& log_directory); + + void StoreLogInDirectory(const std::string& log_id, + std::unique_ptr<WebRtcLogPaths> log_paths, + const GenericDoneCallback& done_callback, + const base::FilePath& directory); + + // A helper for TriggerUpload to do the real work. + void DoUploadLogAndRtpDumps(const base::FilePath& log_directory, + const UploadDoneCallback& callback); + + // Create the RTP dump handler and start dumping. Must be called after making + // sure the log directory exists. + void CreateRtpDumpHandlerAndStart(RtpDumpType type, + const GenericDoneCallback& callback, + const base::FilePath& dump_dir); + + // A helper for starting RTP dump assuming the RTP dump handler has been + // created. + void DoStartRtpDump(RtpDumpType type, const GenericDoneCallback& callback); + + bool ReleaseRtpDumps(WebRtcLogPaths* log_paths); + + void FireGenericDoneCallback( + const WebRtcLoggingController::GenericDoneCallback& callback, + bool success, + const std::string& error_message); + +#if defined(OS_LINUX) || defined(OS_CHROMEOS) + // Grants the render process access to the 'WebRTC Logs' directory, and + // invokes |callback| with the ids necessary to create a DirectoryEntry + // object. If the |logs_path| couldn't be created or found, |error_callback| + // is run. + void GrantLogsDirectoryAccess( + const LogsDirectoryCallback& callback, + const LogsDirectoryErrorCallback& error_callback, + const base::FilePath& logs_path); +#endif // defined(OS_LINUX) || defined(OS_CHROMEOS) + + static base::FilePath GetLogDirectoryAndEnsureExists( + const base::FilePath& browser_context_directory_path); + + SEQUENCE_CHECKER(sequence_checker_); + + mojo::Receiver<chrome::mojom::WebRtcLoggingClient> receiver_; + mojo::Remote<chrome::mojom::WebRtcLoggingAgent> logging_agent_; + + // The render process ID this object belongs to. + const int render_process_id_; + + // A callback that needs to be run from a blocking worker pool and returns + // the browser context directory path associated with our renderer process. + base::RepeatingCallback<base::FilePath(void)> log_directory_getter_; + + // True if we should upload whatever log we have when the renderer closes. + bool upload_log_on_render_close_; + + // The text log handler owns the WebRtcLogBuffer object and keeps track of + // the logging state. + std::unique_ptr<WebRtcTextLogHandler> text_log_handler_; + + // The RTP dump handler responsible for creating the RTP header dump files. + std::unique_ptr<WebRtcRtpDumpHandler> rtp_dump_handler_; + + // The callback to call when StopRtpDump is called. + content::RenderProcessHost::WebRtcStopRtpDumpCallback stop_rtp_dump_callback_; + + // A pointer to the log uploader that's shared for all browser contexts. + // Ownership lies with the browser process. + WebRtcLogUploader* const log_uploader_; + + // Web app id used for statistics. Created as the hash of the value of a + // "client" meta data key, if exists. 0 means undefined, and is the hash of + // the empty string. + int web_app_id_ = 0; + + DISALLOW_COPY_AND_ASSIGN(WebRtcLoggingController); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_LOGGING_CONTROLLER_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_handler.cc b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_handler.cc new file mode 100644 index 00000000000..f502a329ef1 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_handler.cc @@ -0,0 +1,336 @@ +// Copyright 2014 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 "chrome/browser/media/webrtc/webrtc_rtp_dump_handler.h" + +#include <utility> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/task/post_task.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/time/time.h" +#include "chrome/browser/media/webrtc/webrtc_rtp_dump_writer.h" + +namespace { + +static const size_t kMaxOngoingRtpDumpsAllowed = 5; + +// The browser process wide total number of ongoing (i.e. started and not +// released) RTP dumps. Incoming and outgoing in one WebRtcDumpHandler are +// counted as one dump. +// Must be accessed on the browser IO thread. +static size_t g_ongoing_rtp_dumps = 0; + +void FireGenericDoneCallback( + const WebRtcRtpDumpHandler::GenericDoneCallback& callback, + bool success, + const std::string& error_message) { + DCHECK(!callback.is_null()); + + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(callback, success, error_message)); +} + +bool DumpTypeContainsIncoming(RtpDumpType type) { + return type == RTP_DUMP_INCOMING || type == RTP_DUMP_BOTH; +} + +bool DumpTypeContainsOutgoing(RtpDumpType type) { + return type == RTP_DUMP_OUTGOING || type == RTP_DUMP_BOTH; +} + +} // namespace + +WebRtcRtpDumpHandler::WebRtcRtpDumpHandler(const base::FilePath& dump_dir) + : dump_dir_(dump_dir), + incoming_state_(STATE_NONE), + outgoing_state_(STATE_NONE) {} + +WebRtcRtpDumpHandler::~WebRtcRtpDumpHandler() { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_); + + // Reset dump writer first to stop writing. + if (dump_writer_) { + --g_ongoing_rtp_dumps; + dump_writer_.reset(); + } + + if (incoming_state_ != STATE_NONE && !incoming_dump_path_.empty()) { + base::PostTask( + FROM_HERE, + {base::ThreadPool(), base::MayBlock(), base::TaskPriority::BEST_EFFORT}, + base::BindOnce(base::IgnoreResult(&base::DeleteFile), + incoming_dump_path_, false)); + } + + if (outgoing_state_ != STATE_NONE && !outgoing_dump_path_.empty()) { + base::PostTask( + FROM_HERE, + {base::ThreadPool(), base::MayBlock(), base::TaskPriority::BEST_EFFORT}, + base::BindOnce(base::IgnoreResult(&base::DeleteFile), + outgoing_dump_path_, false)); + } +} + +bool WebRtcRtpDumpHandler::StartDump(RtpDumpType type, + std::string* error_message) { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_); + + if (!dump_writer_ && g_ongoing_rtp_dumps >= kMaxOngoingRtpDumpsAllowed) { + *error_message = "Max RTP dump limit reached."; + DVLOG(2) << *error_message; + return false; + } + + // Returns an error if any type of dump specified by the caller cannot be + // started. + if ((DumpTypeContainsIncoming(type) && incoming_state_ != STATE_NONE) || + (DumpTypeContainsOutgoing(type) && outgoing_state_ != STATE_NONE)) { + *error_message = + "RTP dump already started for type " + base::NumberToString(type); + return false; + } + + if (DumpTypeContainsIncoming(type)) + incoming_state_ = STATE_STARTED; + + if (DumpTypeContainsOutgoing(type)) + outgoing_state_ = STATE_STARTED; + + DVLOG(2) << "Start RTP dumping: type = " << type; + + if (!dump_writer_) { + ++g_ongoing_rtp_dumps; + + static const char kRecvDumpFilePrefix[] = "rtpdump_recv_"; + static const char kSendDumpFilePrefix[] = "rtpdump_send_"; + static const size_t kMaxDumpSize = 5 * 1024 * 1024; // 5MB + + std::string dump_id = base::NumberToString(base::Time::Now().ToDoubleT()); + incoming_dump_path_ = + dump_dir_.AppendASCII(std::string(kRecvDumpFilePrefix) + dump_id) + .AddExtension(FILE_PATH_LITERAL(".gz")); + + outgoing_dump_path_ = + dump_dir_.AppendASCII(std::string(kSendDumpFilePrefix) + dump_id) + .AddExtension(FILE_PATH_LITERAL(".gz")); + + // WebRtcRtpDumpWriter does not support changing the dump path after it's + // created. So we assign both incoming and outgoing dump path even if only + // one type of dumping has been started. + // For "Unretained(this)", see comments StopDump. + dump_writer_.reset(new WebRtcRtpDumpWriter( + incoming_dump_path_, + outgoing_dump_path_, + kMaxDumpSize, + base::Bind(&WebRtcRtpDumpHandler::OnMaxDumpSizeReached, + base::Unretained(this)))); + } + + return true; +} + +void WebRtcRtpDumpHandler::StopDump(RtpDumpType type, + const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_); + + // Returns an error if any type of dump specified by the caller cannot be + // stopped. + if ((DumpTypeContainsIncoming(type) && incoming_state_ != STATE_STARTED) || + (DumpTypeContainsOutgoing(type) && outgoing_state_ != STATE_STARTED)) { + if (!callback.is_null()) { + FireGenericDoneCallback( + callback, false, + "RTP dump not started or already stopped for type " + + base::NumberToString(type)); + } + return; + } + + DVLOG(2) << "Stopping RTP dumping: type = " << type; + + if (DumpTypeContainsIncoming(type)) + incoming_state_ = STATE_STOPPING; + + if (DumpTypeContainsOutgoing(type)) + outgoing_state_ = STATE_STOPPING; + + // Using "Unretained(this)" because the this object owns the writer and the + // writer is guaranteed to cancel the callback before it goes away. Same for + // the other posted tasks bound to the writer. + dump_writer_->EndDump( + type, + base::Bind(&WebRtcRtpDumpHandler::OnDumpEnded, + base::Unretained(this), + callback.is_null() + ? base::Closure() + : base::Bind(&FireGenericDoneCallback, callback, true, ""), + type)); +} + +bool WebRtcRtpDumpHandler::ReadyToRelease() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_); + + return incoming_state_ != STATE_STARTED && + incoming_state_ != STATE_STOPPING && + outgoing_state_ != STATE_STARTED && outgoing_state_ != STATE_STOPPING; +} + +WebRtcRtpDumpHandler::ReleasedDumps WebRtcRtpDumpHandler::ReleaseDumps() { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_); + DCHECK(ReadyToRelease()); + + base::FilePath incoming_dump, outgoing_dump; + + if (incoming_state_ == STATE_STOPPED) { + DVLOG(2) << "Incoming RTP dumps released: " << incoming_dump_path_.value(); + + incoming_state_ = STATE_NONE; + incoming_dump = incoming_dump_path_; + } + + if (outgoing_state_ == STATE_STOPPED) { + DVLOG(2) << "Outgoing RTP dumps released: " << outgoing_dump_path_.value(); + + outgoing_state_ = STATE_NONE; + outgoing_dump = outgoing_dump_path_; + } + return ReleasedDumps(incoming_dump, outgoing_dump); +} + +void WebRtcRtpDumpHandler::OnRtpPacket(const uint8_t* packet_header, + size_t header_length, + size_t packet_length, + bool incoming) { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_); + + if ((incoming && incoming_state_ != STATE_STARTED) || + (!incoming && outgoing_state_ != STATE_STARTED)) { + return; + } + + dump_writer_->WriteRtpPacket( + packet_header, header_length, packet_length, incoming); +} + +void WebRtcRtpDumpHandler::StopOngoingDumps(const base::Closure& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_); + DCHECK(!callback.is_null()); + + // No ongoing dumps, return directly. + if ((incoming_state_ == STATE_NONE || incoming_state_ == STATE_STOPPED) && + (outgoing_state_ == STATE_NONE || outgoing_state_ == STATE_STOPPED)) { + callback.Run(); + return; + } + + // If the background task runner is working on stopping the dumps, wait for it + // to complete and then check the states again. + if (incoming_state_ == STATE_STOPPING || outgoing_state_ == STATE_STOPPING) { + dump_writer_->background_task_runner()->PostTaskAndReply( + FROM_HERE, base::DoNothing(), + base::BindOnce(&WebRtcRtpDumpHandler::StopOngoingDumps, + weak_ptr_factory_.GetWeakPtr(), callback)); + return; + } + + // Either incoming or outgoing dump must be ongoing. + RtpDumpType type = + (incoming_state_ == STATE_STARTED) + ? (outgoing_state_ == STATE_STARTED ? RTP_DUMP_BOTH + : RTP_DUMP_INCOMING) + : RTP_DUMP_OUTGOING; + + if (incoming_state_ == STATE_STARTED) + incoming_state_ = STATE_STOPPING; + + if (outgoing_state_ == STATE_STARTED) + outgoing_state_ = STATE_STOPPING; + + DVLOG(2) << "Stopping ongoing dumps: type = " << type; + + dump_writer_->EndDump(type, + base::Bind(&WebRtcRtpDumpHandler::OnDumpEnded, + base::Unretained(this), + callback, + type)); +} + +void WebRtcRtpDumpHandler::SetDumpWriterForTesting( + std::unique_ptr<WebRtcRtpDumpWriter> writer) { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_); + + dump_writer_ = std::move(writer); + ++g_ongoing_rtp_dumps; + + incoming_dump_path_ = dump_dir_.AppendASCII("recv"); + outgoing_dump_path_ = dump_dir_.AppendASCII("send"); +} + +void WebRtcRtpDumpHandler::OnMaxDumpSizeReached() { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_); + + RtpDumpType type = + (incoming_state_ == STATE_STARTED) + ? (outgoing_state_ == STATE_STARTED ? RTP_DUMP_BOTH + : RTP_DUMP_INCOMING) + : RTP_DUMP_OUTGOING; + StopDump(type, GenericDoneCallback()); +} + +void WebRtcRtpDumpHandler::OnDumpEnded(const base::Closure& callback, + RtpDumpType ended_type, + bool incoming_success, + bool outgoing_success) { + DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_); + + if (DumpTypeContainsIncoming(ended_type)) { + DCHECK_EQ(STATE_STOPPING, incoming_state_); + incoming_state_ = STATE_STOPPED; + + if (!incoming_success) { + base::PostTask(FROM_HERE, + {base::ThreadPool(), base::MayBlock(), + base::TaskPriority::BEST_EFFORT}, + base::BindOnce(base::IgnoreResult(&base::DeleteFile), + incoming_dump_path_, false)); + + DVLOG(2) << "Deleted invalid incoming dump " + << incoming_dump_path_.value(); + incoming_dump_path_.clear(); + } + } + + if (DumpTypeContainsOutgoing(ended_type)) { + DCHECK_EQ(STATE_STOPPING, outgoing_state_); + outgoing_state_ = STATE_STOPPED; + + if (!outgoing_success) { + base::PostTask(FROM_HERE, + {base::ThreadPool(), base::MayBlock(), + base::TaskPriority::BEST_EFFORT}, + base::BindOnce(base::IgnoreResult(&base::DeleteFile), + outgoing_dump_path_, false)); + + DVLOG(2) << "Deleted invalid outgoing dump " + << outgoing_dump_path_.value(); + outgoing_dump_path_.clear(); + } + } + + // Release the writer when it's no longer needed. + if (incoming_state_ != STATE_STOPPING && outgoing_state_ != STATE_STOPPING && + incoming_state_ != STATE_STARTED && outgoing_state_ != STATE_STARTED) { + dump_writer_.reset(); + --g_ongoing_rtp_dumps; + } + + // This object might be deleted after running the callback. + if (!callback.is_null()) + callback.Run(); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_handler.h b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_handler.h new file mode 100644 index 00000000000..3c40044ea5a --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_handler.h @@ -0,0 +1,139 @@ +// Copyright 2014 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_RTP_DUMP_HANDLER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_RTP_DUMP_HANDLER_H_ + +#include <stddef.h> +#include <stdint.h> + +#include <memory> + +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/sequence_checker.h" +#include "chrome/browser/media/webrtc/rtp_dump_type.h" + +class WebRtcRtpDumpWriter; + +// WebRtcRtpDumpHandler handles operations regarding the WebRTC RTP dump: +// - Starts or stops the RTP dumping on behalf of the client. +// - Stops the RTP dumping when the max dump file size is reached. +// - Writes the dump file. +// - Provides the dump file to the client code to be uploaded when +// ReleaseRtpDump is called. +// - Cleans up the dump file if not transferred to the client before the object +// is destroyed. +// +// Must be created/used/destroyed on the browser IO thread. +class WebRtcRtpDumpHandler { + public: + typedef base::Callback<void(bool, const std::string&)> GenericDoneCallback; + + struct ReleasedDumps { + ReleasedDumps(const base::FilePath& incoming_dump, + const base::FilePath& outgoing_dump) + : incoming_dump_path(incoming_dump), + outgoing_dump_path(outgoing_dump) {} + + const base::FilePath incoming_dump_path; + const base::FilePath outgoing_dump_path; + }; + + // The caller must make sure |dump_dir| exists. RTP dump files are saved under + // |dump_dir| as "rtpdump_$DIRECTION_$TIMESTAMP.gz", where $DIRECTION is + // 'send' for outgoing dump or 'recv' for incoming dump. $TIMESTAMP is the + // dump started time converted to a double number in microsecond precision, + // which should guarantee the uniqueness across tabs and dump streams in + // practice. + explicit WebRtcRtpDumpHandler(const base::FilePath& dump_dir); + ~WebRtcRtpDumpHandler(); + + // Starts the specified type of dumping. Incoming/outgoing dumping can be + // started separately. Returns true if called in a valid state, i.e. the + // specified type of dump has not been started. + bool StartDump(RtpDumpType type, std::string* error_message); + + // Stops the specified type of dumping. Incoming/outgoing dumping can be + // stopped separately. Returns asynchronously through |callback|, where + // |success| is true if StopDump is called in a valid state. The callback is + // called when the writer finishes writing the dumps. + void StopDump(RtpDumpType type, const GenericDoneCallback& callback); + + // Returns true if it's valid to call ReleaseDumps, i.e. no dumping is ongoing + // or being stopped. + bool ReadyToRelease() const; + + // Releases all the dumps and resets the state. + // It should only be called when both incoming and outgoing dumping has been + // stopped, i.e. ReadyToRelease() returns true. Returns the dump file paths. + // + // The caller will own the dump file after the method returns. If ReleaseDump + // is not called before this object goes away, the dump file will be deleted + // by this object. + ReleasedDumps ReleaseDumps(); + + // Adds an RTP packet to the dump. The caller must make sure it's a valid RTP + // packet. + void OnRtpPacket(const uint8_t* packet_header, + size_t header_length, + size_t packet_length, + bool incoming); + + // Stops all ongoing dumps and call |callback| when finished. + void StopOngoingDumps(const base::Closure& callback); + + private: + friend class WebRtcRtpDumpHandlerTest; + + // State transitions: + // initial --> STATE_NONE + // StartDump --> STATE_STARTED + // StopDump --> STATE_STOPPED + // ReleaseDump --> STATE_RELEASING + // ReleaseDump done --> STATE_NONE + enum State { + STATE_NONE, + STATE_STARTED, + STATE_STOPPING, + STATE_STOPPED, + }; + + // For unit test to inject a fake writer. + void SetDumpWriterForTesting(std::unique_ptr<WebRtcRtpDumpWriter> writer); + + // Callback from the dump writer when the max dump size is reached. + void OnMaxDumpSizeReached(); + + // Callback from the dump writer when ending dumps finishes. Calls |callback| + // when finished. + void OnDumpEnded(const base::Closure& callback, + RtpDumpType ended_type, + bool incoming_succeeded, + bool outgoing_succeeded); + + SEQUENCE_CHECKER(main_sequence_); + + // The absolute path to the directory containing the incoming/outgoing dumps. + const base::FilePath dump_dir_; + + // The dump file paths. + base::FilePath incoming_dump_path_; + base::FilePath outgoing_dump_path_; + + // The states of the incoming and outgoing dump. + State incoming_state_; + State outgoing_state_; + + // The object used to create and write the dump file. + std::unique_ptr<WebRtcRtpDumpWriter> dump_writer_; + + base::WeakPtrFactory<WebRtcRtpDumpHandler> weak_ptr_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(WebRtcRtpDumpHandler); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_RTP_DUMP_HANDLER_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_handler_unittest.cc b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_handler_unittest.cc new file mode 100644 index 00000000000..01c742b0968 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_handler_unittest.cc @@ -0,0 +1,432 @@ +// Copyright 2014 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 "chrome/browser/media/webrtc/webrtc_rtp_dump_handler.h" + +#include <stddef.h> +#include <stdint.h> + +#include <memory> +#include <utility> + +#include "base/bind.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/location.h" +#include "base/run_loop.h" +#include "base/sequenced_task_runner.h" +#include "base/single_thread_task_runner.h" +#include "base/stl_util.h" +#include "base/task/thread_pool/thread_pool_instance.h" +#include "base/threading/thread_task_runner_handle.h" +#include "chrome/browser/media/webrtc/webrtc_rtp_dump_writer.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +class FakeDumpWriter : public WebRtcRtpDumpWriter { + public: + FakeDumpWriter(size_t max_dump_size, + const base::Closure& max_size_reached_callback, + bool end_dump_success) + : WebRtcRtpDumpWriter(base::FilePath(), + base::FilePath(), + max_dump_size, + base::Closure()), + max_dump_size_(max_dump_size), + current_dump_size_(0), + max_size_reached_callback_(max_size_reached_callback), + end_dump_success_(end_dump_success) {} + + void WriteRtpPacket(const uint8_t* packet_header, + size_t header_length, + size_t packet_length, + bool incoming) override { + current_dump_size_ += header_length; + if (current_dump_size_ > max_dump_size_) + max_size_reached_callback_.Run(); + } + + void EndDump(RtpDumpType type, + const EndDumpCallback& finished_callback) override { + bool incoming_success = end_dump_success_; + bool outgoing_success = end_dump_success_; + + if (type == RTP_DUMP_INCOMING) + outgoing_success = false; + else if (type == RTP_DUMP_OUTGOING) + incoming_success = false; + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(finished_callback, incoming_success, outgoing_success)); + } + + private: + size_t max_dump_size_; + size_t current_dump_size_; + base::Closure max_size_reached_callback_; + bool end_dump_success_; +}; + +class WebRtcRtpDumpHandlerTest : public testing::Test { + public: + WebRtcRtpDumpHandlerTest() + : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP) { + ResetDumpHandler(base::FilePath(), true); + } + + void ResetDumpHandler(const base::FilePath& dir, bool end_dump_success) { + handler_.reset(new WebRtcRtpDumpHandler( + dir.empty() ? base::FilePath(FILE_PATH_LITERAL("dummy")) : dir)); + + std::unique_ptr<WebRtcRtpDumpWriter> writer(new FakeDumpWriter( + 10, base::Bind(&WebRtcRtpDumpHandler::OnMaxDumpSizeReached, + base::Unretained(handler_.get())), + end_dump_success)); + + handler_->SetDumpWriterForTesting(std::move(writer)); + } + + void DeleteDumpHandler() { handler_.reset(); } + + void WriteFakeDumpFiles(const base::FilePath& dir, + base::FilePath* incoming_dump, + base::FilePath* outgoing_dump) { + *incoming_dump = dir.AppendASCII("recv"); + *outgoing_dump = dir.AppendASCII("send"); + const char dummy[] = "dummy"; + EXPECT_GT(base::WriteFile(*incoming_dump, dummy, base::size(dummy)), 0); + EXPECT_GT(base::WriteFile(*outgoing_dump, dummy, base::size(dummy)), 0); + } + + void FlushTaskRunners() { + base::ThreadPoolInstance::Get()->FlushForTesting(); + base::RunLoop().RunUntilIdle(); + } + + MOCK_METHOD2(OnStopDumpFinished, + void(bool success, const std::string& error)); + + MOCK_METHOD0(OnStopOngoingDumpsFinished, void(void)); + + protected: + content::BrowserTaskEnvironment task_environment_; + std::unique_ptr<WebRtcRtpDumpHandler> handler_; +}; + +TEST_F(WebRtcRtpDumpHandlerTest, StateTransition) { + std::string error; + + RtpDumpType types[3]; + types[0] = RTP_DUMP_INCOMING; + types[1] = RTP_DUMP_OUTGOING; + types[2] = RTP_DUMP_BOTH; + + for (size_t i = 0; i < base::size(types); ++i) { + DVLOG(2) << "Verifying state transition: type = " << types[i]; + + // Only StartDump is allowed in STATE_NONE. + EXPECT_CALL(*this, OnStopDumpFinished(false, testing::_)); + handler_->StopDump(types[i], + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + + WebRtcRtpDumpHandler::ReleasedDumps empty_dumps(handler_->ReleaseDumps()); + EXPECT_TRUE(empty_dumps.incoming_dump_path.empty()); + EXPECT_TRUE(empty_dumps.outgoing_dump_path.empty()); + EXPECT_TRUE(handler_->StartDump(types[i], &error)); + base::RunLoop().RunUntilIdle(); + + // Only StopDump is allowed in STATE_STARTED. + EXPECT_FALSE(handler_->StartDump(types[i], &error)); + EXPECT_FALSE(handler_->ReadyToRelease()); + + EXPECT_CALL(*this, OnStopDumpFinished(true, testing::_)); + handler_->StopDump(types[i], + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + base::RunLoop().RunUntilIdle(); + + // Only ReleaseDump is allowed in STATE_STOPPED. + EXPECT_FALSE(handler_->StartDump(types[i], &error)); + + EXPECT_CALL(*this, OnStopDumpFinished(false, testing::_)); + handler_->StopDump(types[i], + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + EXPECT_TRUE(handler_->ReadyToRelease()); + + WebRtcRtpDumpHandler::ReleasedDumps dumps(handler_->ReleaseDumps()); + if (types[i] == RTP_DUMP_INCOMING || types[i] == RTP_DUMP_BOTH) + EXPECT_FALSE(dumps.incoming_dump_path.empty()); + + if (types[i] == RTP_DUMP_OUTGOING || types[i] == RTP_DUMP_BOTH) + EXPECT_FALSE(dumps.outgoing_dump_path.empty()); + + base::RunLoop().RunUntilIdle(); + ResetDumpHandler(base::FilePath(), true); + } +} + +TEST_F(WebRtcRtpDumpHandlerTest, StoppedWhenMaxSizeReached) { + std::string error; + + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_INCOMING, &error)); + + std::vector<uint8_t> buffer(100, 0); + handler_->OnRtpPacket(&buffer[0], buffer.size(), buffer.size(), true); + base::RunLoop().RunUntilIdle(); + + // Dumping should have been stopped, so ready to release. + WebRtcRtpDumpHandler::ReleasedDumps dumps = handler_->ReleaseDumps(); + EXPECT_FALSE(dumps.incoming_dump_path.empty()); +} + +TEST_F(WebRtcRtpDumpHandlerTest, PacketIgnoredIfDumpingNotStarted) { + std::vector<uint8_t> buffer(100, 0); + handler_->OnRtpPacket(&buffer[0], buffer.size(), buffer.size(), true); + handler_->OnRtpPacket(&buffer[0], buffer.size(), buffer.size(), false); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(WebRtcRtpDumpHandlerTest, PacketIgnoredIfDumpingStopped) { + std::string error; + + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_INCOMING, &error)); + + EXPECT_CALL(*this, OnStopDumpFinished(true, testing::_)); + handler_->StopDump(RTP_DUMP_INCOMING, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + + std::vector<uint8_t> buffer(100, 0); + handler_->OnRtpPacket(&buffer[0], buffer.size(), buffer.size(), true); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(WebRtcRtpDumpHandlerTest, CannotStartMoreThanFiveDumps) { + std::string error; + + handler_.reset(); + + std::unique_ptr<WebRtcRtpDumpHandler> handlers[6]; + + for (size_t i = 0; i < base::size(handlers); ++i) { + handlers[i].reset(new WebRtcRtpDumpHandler(base::FilePath())); + + if (i < base::size(handlers) - 1) { + EXPECT_TRUE(handlers[i]->StartDump(RTP_DUMP_INCOMING, &error)); + } else { + EXPECT_FALSE(handlers[i]->StartDump(RTP_DUMP_INCOMING, &error)); + } + } +} + +TEST_F(WebRtcRtpDumpHandlerTest, StartStopIncomingThenStartStopOutgoing) { + std::string error; + + EXPECT_CALL(*this, OnStopDumpFinished(true, testing::_)).Times(2); + + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_INCOMING, &error)); + handler_->StopDump(RTP_DUMP_INCOMING, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_OUTGOING, &error)); + handler_->StopDump(RTP_DUMP_OUTGOING, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + + base::RunLoop().RunUntilIdle(); +} + +TEST_F(WebRtcRtpDumpHandlerTest, StartIncomingStartOutgoingThenStopBoth) { + std::string error; + + EXPECT_CALL(*this, OnStopDumpFinished(true, testing::_)); + + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_INCOMING, &error)); + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_OUTGOING, &error)); + + handler_->StopDump(RTP_DUMP_INCOMING, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + + base::RunLoop().RunUntilIdle(); +} + +TEST_F(WebRtcRtpDumpHandlerTest, StartBothThenStopIncomingStopOutgoing) { + std::string error; + + EXPECT_CALL(*this, OnStopDumpFinished(true, testing::_)).Times(2); + + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_BOTH, &error)); + + handler_->StopDump(RTP_DUMP_INCOMING, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + handler_->StopDump(RTP_DUMP_OUTGOING, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + + base::RunLoop().RunUntilIdle(); +} + +TEST_F(WebRtcRtpDumpHandlerTest, DumpsCleanedUpIfNotReleased) { + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + ResetDumpHandler(temp_dir.GetPath(), true); + + base::FilePath incoming_dump, outgoing_dump; + WriteFakeDumpFiles(temp_dir.GetPath(), &incoming_dump, &outgoing_dump); + + std::string error; + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_BOTH, &error)); + + EXPECT_CALL(*this, OnStopDumpFinished(true, testing::_)); + handler_->StopDump(RTP_DUMP_BOTH, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + base::RunLoop().RunUntilIdle(); + FlushTaskRunners(); + + handler_.reset(); + FlushTaskRunners(); + + EXPECT_FALSE(base::PathExists(incoming_dump)); + EXPECT_FALSE(base::PathExists(outgoing_dump)); +} + +TEST_F(WebRtcRtpDumpHandlerTest, DumpDeletedIfEndDumpFailed) { + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + + // Make the writer return failure on EndStream. + ResetDumpHandler(temp_dir.GetPath(), false); + + base::FilePath incoming_dump, outgoing_dump; + WriteFakeDumpFiles(temp_dir.GetPath(), &incoming_dump, &outgoing_dump); + + std::string error; + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_BOTH, &error)); + EXPECT_CALL(*this, OnStopDumpFinished(true, testing::_)).Times(2); + + handler_->StopDump(RTP_DUMP_INCOMING, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + base::RunLoop().RunUntilIdle(); + FlushTaskRunners(); + + EXPECT_FALSE(base::PathExists(incoming_dump)); + EXPECT_TRUE(base::PathExists(outgoing_dump)); + + handler_->StopDump(RTP_DUMP_OUTGOING, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + base::RunLoop().RunUntilIdle(); + FlushTaskRunners(); + EXPECT_FALSE(base::PathExists(outgoing_dump)); +} + +TEST_F(WebRtcRtpDumpHandlerTest, StopOngoingDumpsWhileStoppingDumps) { + std::string error; + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_BOTH, &error)); + + testing::InSequence s; + EXPECT_CALL(*this, OnStopDumpFinished(true, testing::_)); + EXPECT_CALL(*this, OnStopOngoingDumpsFinished()); + + handler_->StopDump(RTP_DUMP_BOTH, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + base::RunLoop().RunUntilIdle(); + + handler_->StopOngoingDumps( + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopOngoingDumpsFinished, + base::Unretained(this))); + + FlushTaskRunners(); + + WebRtcRtpDumpHandler::ReleasedDumps dumps(handler_->ReleaseDumps()); + EXPECT_FALSE(dumps.incoming_dump_path.empty()); + EXPECT_FALSE(dumps.outgoing_dump_path.empty()); +} + +TEST_F(WebRtcRtpDumpHandlerTest, StopOngoingDumpsWhileDumping) { + std::string error; + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_BOTH, &error)); + + EXPECT_CALL(*this, OnStopOngoingDumpsFinished()); + + handler_->StopOngoingDumps( + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopOngoingDumpsFinished, + base::Unretained(this))); + + FlushTaskRunners(); + + WebRtcRtpDumpHandler::ReleasedDumps dumps(handler_->ReleaseDumps()); + EXPECT_FALSE(dumps.incoming_dump_path.empty()); + EXPECT_FALSE(dumps.outgoing_dump_path.empty()); +} + +TEST_F(WebRtcRtpDumpHandlerTest, StopOngoingDumpsWhenAlreadyStopped) { + std::string error; + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_BOTH, &error)); + + { + EXPECT_CALL(*this, OnStopDumpFinished(true, testing::_)); + + handler_->StopDump(RTP_DUMP_BOTH, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + base::RunLoop().RunUntilIdle(); + FlushTaskRunners(); + } + + EXPECT_CALL(*this, OnStopOngoingDumpsFinished()); + handler_->StopOngoingDumps( + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopOngoingDumpsFinished, + base::Unretained(this))); +} + +TEST_F(WebRtcRtpDumpHandlerTest, StopOngoingDumpsWhileStoppingOneDump) { + std::string error; + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_BOTH, &error)); + + testing::InSequence s; + EXPECT_CALL(*this, OnStopDumpFinished(true, testing::_)); + EXPECT_CALL(*this, OnStopOngoingDumpsFinished()); + + handler_->StopDump(RTP_DUMP_INCOMING, + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopDumpFinished, + base::Unretained(this))); + base::RunLoop().RunUntilIdle(); + + handler_->StopOngoingDumps( + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopOngoingDumpsFinished, + base::Unretained(this))); + + FlushTaskRunners(); + + WebRtcRtpDumpHandler::ReleasedDumps dumps(handler_->ReleaseDumps()); + EXPECT_FALSE(dumps.incoming_dump_path.empty()); + EXPECT_FALSE(dumps.outgoing_dump_path.empty()); +} + +TEST_F(WebRtcRtpDumpHandlerTest, DeleteHandlerBeforeStopCallback) { + std::string error; + + EXPECT_CALL(*this, OnStopOngoingDumpsFinished()) + .WillOnce(testing::InvokeWithoutArgs( + this, &WebRtcRtpDumpHandlerTest::DeleteDumpHandler)); + + EXPECT_TRUE(handler_->StartDump(RTP_DUMP_BOTH, &error)); + + handler_->StopOngoingDumps( + base::Bind(&WebRtcRtpDumpHandlerTest::OnStopOngoingDumpsFinished, + base::Unretained(this))); + + base::RunLoop().RunUntilIdle(); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_writer.cc b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_writer.cc new file mode 100644 index 00000000000..d676f226252 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_writer.cc @@ -0,0 +1,451 @@ +// Copyright 2014 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 "chrome/browser/media/webrtc/webrtc_rtp_dump_writer.h" + +#include <string.h> + +#include "base/big_endian.h" +#include "base/bind.h" +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/stl_util.h" +#include "base/task/post_task.h" +#include "content/public/browser/browser_thread.h" +#include "third_party/zlib/zlib.h" + +namespace { + +static const size_t kMinimumGzipOutputBufferSize = 256; // In bytes. + +const unsigned char kRtpDumpFileHeaderFirstLine[] = "#!rtpplay1.0 0.0.0.0/0\n"; +static const size_t kRtpDumpFileHeaderSize = 16; // In bytes. + +// A helper for writing the header of the dump file. +void WriteRtpDumpFileHeaderBigEndian(base::TimeTicks start, + std::vector<uint8_t>* output) { + size_t buffer_start_pos = output->size(); + output->resize(output->size() + kRtpDumpFileHeaderSize); + + char* buffer = reinterpret_cast<char*>(&(*output)[buffer_start_pos]); + + base::TimeDelta delta = start - base::TimeTicks(); + uint32_t start_sec = delta.InSeconds(); + base::WriteBigEndian(buffer, start_sec); + buffer += sizeof(start_sec); + + uint32_t start_usec = + delta.InMilliseconds() * base::Time::kMicrosecondsPerMillisecond; + base::WriteBigEndian(buffer, start_usec); + buffer += sizeof(start_usec); + + // Network source, always 0. + base::WriteBigEndian(buffer, uint32_t(0)); + buffer += sizeof(uint32_t); + + // UDP port, always 0. + base::WriteBigEndian(buffer, uint16_t(0)); + buffer += sizeof(uint16_t); + + // 2 bytes padding. + base::WriteBigEndian(buffer, uint16_t(0)); +} + +// The header size for each packet dump. +static const size_t kPacketDumpHeaderSize = 8; // In bytes. + +// A helper for writing the header for each packet dump. +// |start| is the time when the recording is started. +// |dump_length| is the length of the packet dump including this header. +// |packet_length| is the length of the RTP packet header. +void WritePacketDumpHeaderBigEndian(const base::TimeTicks& start, + uint16_t dump_length, + uint16_t packet_length, + std::vector<uint8_t>* output) { + size_t buffer_start_pos = output->size(); + output->resize(output->size() + kPacketDumpHeaderSize); + + char* buffer = reinterpret_cast<char*>(&(*output)[buffer_start_pos]); + + base::WriteBigEndian(buffer, dump_length); + buffer += sizeof(dump_length); + + base::WriteBigEndian(buffer, packet_length); + buffer += sizeof(packet_length); + + uint32_t elapsed = + static_cast<uint32_t>((base::TimeTicks::Now() - start).InMilliseconds()); + base::WriteBigEndian(buffer, elapsed); +} + +// Append |src_len| bytes from |src| to |dest|. +void AppendToBuffer(const uint8_t* src, + size_t src_len, + std::vector<uint8_t>* dest) { + size_t old_dest_size = dest->size(); + dest->resize(old_dest_size + src_len); + memcpy(&(*dest)[old_dest_size], src, src_len); +} + +} // namespace + +// This class runs on the backround task runner, compresses and writes the +// dump buffer to disk. +class WebRtcRtpDumpWriter::FileWorker { + public: + explicit FileWorker(const base::FilePath& dump_path) : dump_path_(dump_path) { + DETACH_FROM_SEQUENCE(sequence_checker_); + + memset(&stream_, 0, sizeof(stream_)); + int result = deflateInit2(&stream_, + Z_DEFAULT_COMPRESSION, + Z_DEFLATED, + // windowBits = 15 is default, 16 is added to + // produce a gzip header + trailer. + 15 + 16, + 8, // memLevel = 8 is default. + Z_DEFAULT_STRATEGY); + DCHECK_EQ(Z_OK, result); + } + + ~FileWorker() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // Makes sure all allocations are freed. + deflateEnd(&stream_); + } + + // Compresses the data in |buffer| and write to the dump file. If |end_stream| + // is true, the compression stream will be ended and the dump file cannot be + // written to any more. + void CompressAndWriteToFileOnFileThread( + std::unique_ptr<std::vector<uint8_t>> buffer, + bool end_stream, + FlushResult* result, + size_t* bytes_written) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // This is called either when the in-memory buffer is full or the dump + // should be ended. + DCHECK(!buffer->empty() || end_stream); + + *result = FLUSH_RESULT_SUCCESS; + *bytes_written = 0; + + // There may be nothing to compress/write if there is no RTP packet since + // the last flush. + if (!buffer->empty()) { + *bytes_written = CompressAndWriteBufferToFile(buffer.get(), result); + } else if (!base::PathExists(dump_path_)) { + // If the dump does not exist, it means there is no RTP packet recorded. + // Return FLUSH_RESULT_NO_DATA to indicate no dump file created. + *result = FLUSH_RESULT_NO_DATA; + } + + if (end_stream && !EndDumpFile()) + *result = FLUSH_RESULT_FAILURE; + } + + private: + // Helper for CompressAndWriteToFileOnFileThread to compress and write one + // dump. + size_t CompressAndWriteBufferToFile(std::vector<uint8_t>* buffer, + FlushResult* result) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(buffer->size()); + + *result = FLUSH_RESULT_SUCCESS; + + std::vector<uint8_t> compressed_buffer; + if (!Compress(buffer, &compressed_buffer)) { + DVLOG(2) << "Compressing buffer failed."; + *result = FLUSH_RESULT_FAILURE; + return 0; + } + + int bytes_written = -1; + + if (base::PathExists(dump_path_)) { + bytes_written = + base::AppendToFile(dump_path_, reinterpret_cast<const char*>( + compressed_buffer.data()), + compressed_buffer.size()) + ? compressed_buffer.size() + : -1; + } else { + bytes_written = base::WriteFile( + dump_path_, + reinterpret_cast<const char*>(&compressed_buffer[0]), + compressed_buffer.size()); + } + + if (bytes_written == -1) { + DVLOG(2) << "Writing file failed: " << dump_path_.value(); + *result = FLUSH_RESULT_FAILURE; + return 0; + } + + DCHECK_EQ(static_cast<size_t>(bytes_written), compressed_buffer.size()); + return bytes_written; + } + + // Compresses |input| into |output|. + bool Compress(std::vector<uint8_t>* input, std::vector<uint8_t>* output) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + int result = Z_OK; + + output->resize(std::max(kMinimumGzipOutputBufferSize, input->size())); + + stream_.next_in = &(*input)[0]; + stream_.avail_in = input->size(); + stream_.next_out = &(*output)[0]; + stream_.avail_out = output->size(); + + result = deflate(&stream_, Z_SYNC_FLUSH); + DCHECK_EQ(Z_OK, result); + DCHECK_EQ(0U, stream_.avail_in); + + output->resize(output->size() - stream_.avail_out); + + stream_.next_in = NULL; + stream_.next_out = NULL; + stream_.avail_out = 0; + return true; + } + + // Ends the compression stream and completes the dump file. + bool EndDumpFile() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + std::vector<uint8_t> output_buffer; + output_buffer.resize(kMinimumGzipOutputBufferSize); + + stream_.next_in = NULL; + stream_.avail_in = 0; + stream_.next_out = &output_buffer[0]; + stream_.avail_out = output_buffer.size(); + + int result = deflate(&stream_, Z_FINISH); + DCHECK_EQ(Z_STREAM_END, result); + + result = deflateEnd(&stream_); + DCHECK_EQ(Z_OK, result); + + output_buffer.resize(output_buffer.size() - stream_.avail_out); + + memset(&stream_, 0, sizeof(z_stream)); + + DCHECK(!output_buffer.empty()); + return base::AppendToFile( + dump_path_, reinterpret_cast<const char*>(output_buffer.data()), + output_buffer.size()); + } + + const base::FilePath dump_path_; + + z_stream stream_; + + SEQUENCE_CHECKER(sequence_checker_); + + DISALLOW_COPY_AND_ASSIGN(FileWorker); +}; + +WebRtcRtpDumpWriter::WebRtcRtpDumpWriter( + const base::FilePath& incoming_dump_path, + const base::FilePath& outgoing_dump_path, + size_t max_dump_size, + const base::Closure& max_dump_size_reached_callback) + : max_dump_size_(max_dump_size), + max_dump_size_reached_callback_(max_dump_size_reached_callback), + total_dump_size_on_disk_(0), + background_task_runner_( + base::CreateSequencedTaskRunner({base::ThreadPool(), base::MayBlock(), + base::TaskPriority::BEST_EFFORT})), + incoming_file_thread_worker_(new FileWorker(incoming_dump_path)), + outgoing_file_thread_worker_(new FileWorker(outgoing_dump_path)) {} + +WebRtcRtpDumpWriter::~WebRtcRtpDumpWriter() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + bool success = background_task_runner_->DeleteSoon( + FROM_HERE, incoming_file_thread_worker_.release()); + DCHECK(success); + + success = background_task_runner_->DeleteSoon( + FROM_HERE, outgoing_file_thread_worker_.release()); + DCHECK(success); +} + +void WebRtcRtpDumpWriter::WriteRtpPacket(const uint8_t* packet_header, + size_t header_length, + size_t packet_length, + bool incoming) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + static const size_t kMaxInMemoryBufferSize = 65536; + + std::vector<uint8_t>* dest_buffer = + incoming ? &incoming_buffer_ : &outgoing_buffer_; + + // We use the capacity of the buffer to indicate if the buffer has been + // initialized and if the dump file header has been created. + if (!dest_buffer->capacity()) { + dest_buffer->reserve(std::min(kMaxInMemoryBufferSize, max_dump_size_)); + + start_time_ = base::TimeTicks::Now(); + + // Writes the dump file header. + AppendToBuffer(kRtpDumpFileHeaderFirstLine, + base::size(kRtpDumpFileHeaderFirstLine) - 1, dest_buffer); + WriteRtpDumpFileHeaderBigEndian(start_time_, dest_buffer); + } + + size_t packet_dump_length = kPacketDumpHeaderSize + header_length; + + // Flushes the buffer to disk if the buffer is full. + if (dest_buffer->size() + packet_dump_length > dest_buffer->capacity()) + FlushBuffer(incoming, false, FlushDoneCallback()); + + WritePacketDumpHeaderBigEndian( + start_time_, packet_dump_length, packet_length, dest_buffer); + + // Writes the actual RTP packet header. + AppendToBuffer(packet_header, header_length, dest_buffer); +} + +void WebRtcRtpDumpWriter::EndDump(RtpDumpType type, + const EndDumpCallback& finished_callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(type == RTP_DUMP_OUTGOING || incoming_file_thread_worker_ != NULL); + DCHECK(type == RTP_DUMP_INCOMING || outgoing_file_thread_worker_ != NULL); + + bool incoming = (type == RTP_DUMP_BOTH || type == RTP_DUMP_INCOMING); + EndDumpContext context(type, finished_callback); + + // End the incoming dump first if required. OnDumpEnded will continue to end + // the outgoing dump if necessary. + FlushBuffer(incoming, + true, + base::Bind(&WebRtcRtpDumpWriter::OnDumpEnded, + weak_ptr_factory_.GetWeakPtr(), + context, + incoming)); +} + +size_t WebRtcRtpDumpWriter::max_dump_size() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return max_dump_size_; +} + +WebRtcRtpDumpWriter::EndDumpContext::EndDumpContext( + RtpDumpType type, + const EndDumpCallback& callback) + : type(type), + incoming_succeeded(false), + outgoing_succeeded(false), + callback(callback) { +} + +WebRtcRtpDumpWriter::EndDumpContext::EndDumpContext( + const EndDumpContext& other) = default; + +WebRtcRtpDumpWriter::EndDumpContext::~EndDumpContext() { +} + +void WebRtcRtpDumpWriter::FlushBuffer(bool incoming, + bool end_stream, + const FlushDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + std::unique_ptr<std::vector<uint8_t>> new_buffer(new std::vector<uint8_t>()); + + if (incoming) { + new_buffer->reserve(incoming_buffer_.capacity()); + new_buffer->swap(incoming_buffer_); + } else { + new_buffer->reserve(outgoing_buffer_.capacity()); + new_buffer->swap(outgoing_buffer_); + } + + std::unique_ptr<FlushResult> result(new FlushResult(FLUSH_RESULT_FAILURE)); + + std::unique_ptr<size_t> bytes_written(new size_t(0)); + + FileWorker* worker = incoming ? incoming_file_thread_worker_.get() + : outgoing_file_thread_worker_.get(); + + // Using "Unretained(worker)" because |worker| is owner by this object and it + // guaranteed to be deleted on the backround task runner before this object + // goes away. + base::OnceClosure task = base::BindOnce( + &FileWorker::CompressAndWriteToFileOnFileThread, base::Unretained(worker), + std::move(new_buffer), end_stream, result.get(), bytes_written.get()); + + // OnFlushDone is necessary to avoid running the callback after this + // object is gone. + base::OnceClosure reply = base::BindOnce( + &WebRtcRtpDumpWriter::OnFlushDone, weak_ptr_factory_.GetWeakPtr(), + callback, std::move(result), std::move(bytes_written)); + + // Define the task and reply outside the method call so that getting and + // passing the scoped_ptr does not depend on the argument evaluation order. + background_task_runner_->PostTaskAndReply(FROM_HERE, std::move(task), + std::move(reply)); + + if (end_stream) { + bool success = background_task_runner_->DeleteSoon( + FROM_HERE, incoming ? incoming_file_thread_worker_.release() + : outgoing_file_thread_worker_.release()); + DCHECK(success); + } +} + +void WebRtcRtpDumpWriter::OnFlushDone( + const FlushDoneCallback& callback, + const std::unique_ptr<FlushResult>& result, + const std::unique_ptr<size_t>& bytes_written) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + total_dump_size_on_disk_ += *bytes_written; + + if (total_dump_size_on_disk_ >= max_dump_size_ && + !max_dump_size_reached_callback_.is_null()) { + max_dump_size_reached_callback_.Run(); + } + + // Returns success for FLUSH_RESULT_MAX_SIZE_REACHED since the dump is still + // valid. + if (!callback.is_null()) { + callback.Run(*result != FLUSH_RESULT_FAILURE && + *result != FLUSH_RESULT_NO_DATA); + } +} + +void WebRtcRtpDumpWriter::OnDumpEnded(EndDumpContext context, + bool incoming, + bool success) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + DVLOG(2) << "Dump ended, incoming = " << incoming + << ", succeeded = " << success; + + if (incoming) + context.incoming_succeeded = success; + else + context.outgoing_succeeded = success; + + // End the outgoing dump if needed. + if (incoming && context.type == RTP_DUMP_BOTH) { + FlushBuffer(false, + true, + base::Bind(&WebRtcRtpDumpWriter::OnDumpEnded, + weak_ptr_factory_.GetWeakPtr(), + context, + false)); + return; + } + + // This object might be deleted after running the callback. + context.callback.Run(context.incoming_succeeded, context.outgoing_succeeded); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_writer.h b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_writer.h new file mode 100644 index 00000000000..39af6dfa1b5 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_writer.h @@ -0,0 +1,148 @@ +// Copyright 2014 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_RTP_DUMP_WRITER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_RTP_DUMP_WRITER_H_ + +#include <stddef.h> +#include <stdint.h> + +#include <memory> + +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/sequence_checker.h" +#include "base/sequenced_task_runner.h" +#include "base/time/time.h" +#include "chrome/browser/media/webrtc/rtp_dump_type.h" + +// This class is responsible for creating the compressed RTP header dump file: +// - Adds the RTP headers to an in-memory buffer. +// - When the in-memory buffer is full, compresses it, and writes it to the +// disk. +// - Notifies the caller when the on-disk file size reaches the max limit. +// - The uncompressed dump follows the standard RTPPlay format +// (http://www.cs.columbia.edu/irt/software/rtptools/). +// - The caller is always responsible for cleaning up the dump file in all +// cases. +// - WebRtcRtpDumpWriter does not stop writing to the dump after the max size +// limit is reached. The caller must stop calling WriteRtpPacket instead. +// +// This object must run on the IO thread. +class WebRtcRtpDumpWriter { + public: + typedef base::Callback<void(bool incoming_succeeded, bool outgoing_succeeded)> + EndDumpCallback; + + // |incoming_dump_path| and |outgoing_dump_path| are the file paths of the + // compressed dump files for incoming and outgoing packets respectively. + // |max_dump_size| is the max size of the compressed dump file in bytes. + // |max_dump_size_reached_callback| will be called when the on-disk file size + // reaches |max_dump_size|. + WebRtcRtpDumpWriter(const base::FilePath& incoming_dump_path, + const base::FilePath& outgoing_dump_path, + size_t max_dump_size, + const base::Closure& max_dump_size_reached_callback); + + virtual ~WebRtcRtpDumpWriter(); + + // Adds a RTP packet to the dump. The caller must make sure it's a valid RTP + // packet. No validation is done by this method. + virtual void WriteRtpPacket(const uint8_t* packet_header, + size_t header_length, + size_t packet_length, + bool incoming); + + // Flushes the in-memory buffer to the disk and ends the dump. The caller must + // make sure the dump has not already been ended. + // |finished_callback| will be called to indicate whether the dump is valid. + // If this object is destroyed before the operation is finished, the callback + // will be canceled and the dump files will be deleted. + virtual void EndDump(RtpDumpType type, + const EndDumpCallback& finished_callback); + + size_t max_dump_size() const; + + const scoped_refptr<base::SequencedTaskRunner>& background_task_runner() + const { + return background_task_runner_; + } + + private: + enum FlushResult { + // Flushing has succeeded and the dump size is under the max limit. + FLUSH_RESULT_SUCCESS, + // Nothing has been written to disk and the dump is empty. + FLUSH_RESULT_NO_DATA, + // Flushing has failed for other reasons. + FLUSH_RESULT_FAILURE + }; + + class FileWorker; + + typedef base::Callback<void(bool)> FlushDoneCallback; + + // Used by EndDump to cache the input and intermediate results. + struct EndDumpContext { + EndDumpContext(RtpDumpType type, const EndDumpCallback& callback); + EndDumpContext(const EndDumpContext& other); + ~EndDumpContext(); + + RtpDumpType type; + bool incoming_succeeded; + bool outgoing_succeeded; + EndDumpCallback callback; + }; + + // Flushes the in-memory buffer to disk. If |incoming| is true, the incoming + // buffer will be flushed; otherwise, the outgoing buffer will be flushed. + // The dump file will be ended if |end_stream| is true. |callback| will be + // called when flushing is done. + void FlushBuffer(bool incoming, + bool end_stream, + const FlushDoneCallback& callback); + + // Called when FlushBuffer finishes. Checks the max dump size limit and + // maybe calls the |max_dump_size_reached_callback_|. Also calls |callback| + // with the flush result. + void OnFlushDone(const FlushDoneCallback& callback, + const std::unique_ptr<FlushResult>& result, + const std::unique_ptr<size_t>& bytes_written); + + // Called when one type of dump has been ended. It continues to end the other + // dump if needed based on |context| and |incoming|, or calls the callback in + // |context| if no more dump needs to be ended. + void OnDumpEnded(EndDumpContext context, bool incoming, bool success); + + // The max limit on the total size of incoming and outgoing dumps on disk. + const size_t max_dump_size_; + + // The callback to call when the max size limit is reached. + const base::Closure max_dump_size_reached_callback_; + + // The in-memory buffers for the uncompressed dumps. + std::vector<uint8_t> incoming_buffer_; + std::vector<uint8_t> outgoing_buffer_; + + // The time when the first packet is dumped. + base::TimeTicks start_time_; + + // The total on-disk size of the compressed incoming and outgoing dumps. + size_t total_dump_size_on_disk_; + + // File workers must be called and deleted on the backround task runner. + scoped_refptr<base::SequencedTaskRunner> background_task_runner_; + std::unique_ptr<FileWorker> incoming_file_thread_worker_; + std::unique_ptr<FileWorker> outgoing_file_thread_worker_; + + SEQUENCE_CHECKER(sequence_checker_); + + base::WeakPtrFactory<WebRtcRtpDumpWriter> weak_ptr_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(WebRtcRtpDumpWriter); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_RTP_DUMP_WRITER_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_writer_unittest.cc b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_writer_unittest.cc new file mode 100644 index 00000000000..606aca3fec2 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_rtp_dump_writer_unittest.cc @@ -0,0 +1,375 @@ +// Copyright 2014 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 "chrome/browser/media/webrtc/webrtc_rtp_dump_writer.h" + +#include <stddef.h> +#include <stdint.h> +#include <string.h> + +#include <memory> + +#include "base/big_endian.h" +#include "base/bind.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "base/sequenced_task_runner.h" +#include "base/stl_util.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/test/browser_task_environment.h" +#include "content/public/test/test_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/zlib/zlib.h" + +static const size_t kMinimumRtpHeaderLength = 12; + +static void CreateFakeRtpPacketHeader(size_t csrc_count, + size_t extension_header_count, + std::vector<uint8_t>* packet_header) { + packet_header->resize(kMinimumRtpHeaderLength + + csrc_count * sizeof(uint32_t) + + (extension_header_count + 1) * sizeof(uint32_t)); + + memset(&(*packet_header)[0], 0, packet_header->size()); + + // First byte format: vvpxcccc, where 'vv' is the version, 'p' is padding, 'x' + // is the extension bit, 'cccc' is the CSRC count. + (*packet_header)[0] = 0; + (*packet_header)[0] |= (0x2 << 6); // version. + // The extension bit. + (*packet_header)[0] |= (extension_header_count > 0 ? (0x1 << 4) : 0); + (*packet_header)[0] |= (csrc_count & 0xf); + + // Set extension length. + size_t offset = kMinimumRtpHeaderLength + + (csrc_count & 0xf) * sizeof(uint32_t) + sizeof(uint16_t); + base::WriteBigEndian(reinterpret_cast<char*>(&(*packet_header)[offset]), + static_cast<uint16_t>(extension_header_count)); +} + +static void FlushTaskRunner(base::SequencedTaskRunner* task_runner) { + base::RunLoop run_loop; + task_runner->PostTask(FROM_HERE, run_loop.QuitClosure()); + run_loop.Run(); +} + +class WebRtcRtpDumpWriterTest : public testing::Test { + public: + WebRtcRtpDumpWriterTest() + : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP), + temp_dir_(new base::ScopedTempDir()) {} + + void SetUp() override { + ASSERT_TRUE(temp_dir_->CreateUniqueTempDir()); + + incoming_dump_path_ = temp_dir_->GetPath().AppendASCII("rtpdump_recv"); + outgoing_dump_path_ = temp_dir_->GetPath().AppendASCII("rtpdump_send"); + writer_.reset(new WebRtcRtpDumpWriter( + incoming_dump_path_, + outgoing_dump_path_, + 4 * 1024 * 1024, + base::Bind(&WebRtcRtpDumpWriterTest::OnMaxSizeReached, + base::Unretained(this)))); + } + + // Verifies that the dump contains records of |rtp_packet| repeated + // |packet_count| times. + void VerifyDumps(size_t incoming_packet_count, size_t outgoing_packet_count) { + std::string incoming_dump; + std::string outgoing_dump; + + if (incoming_packet_count) { + EXPECT_TRUE(base::ReadFileToString(incoming_dump_path_, &incoming_dump)); + EXPECT_TRUE(VerifyCompressedDump(&incoming_dump, incoming_packet_count)); + } else { + EXPECT_FALSE(base::PathExists(incoming_dump_path_)); + } + + if (outgoing_packet_count) { + EXPECT_TRUE(base::ReadFileToString(outgoing_dump_path_, &outgoing_dump)); + EXPECT_TRUE(VerifyCompressedDump(&outgoing_dump, outgoing_packet_count)); + } else { + EXPECT_FALSE(base::PathExists(outgoing_dump_path_)); + } + } + + MOCK_METHOD2(OnEndDumpDone, void(bool, bool)); + MOCK_METHOD0(OnMaxSizeReached, void(void)); + + protected: + // Verifies the compressed dump file contains the expected number of packets. + bool VerifyCompressedDump(std::string* dump, size_t expected_packet_count) { + EXPECT_GT(dump->size(), 0U); + + std::vector<uint8_t> decompressed_dump; + EXPECT_TRUE(Decompress(dump, &decompressed_dump)); + + size_t actual_packet_count = 0; + EXPECT_TRUE(ReadDecompressedDump(decompressed_dump, &actual_packet_count)); + EXPECT_EQ(expected_packet_count, actual_packet_count); + + return true; + } + + // Decompresses the |input| into |output|. + bool Decompress(std::string* input, std::vector<uint8_t>* output) { + z_stream stream = {0}; + + int result = inflateInit2(&stream, 15 + 16); + EXPECT_EQ(Z_OK, result); + + output->resize(input->size() * 100); + + stream.next_in = + reinterpret_cast<unsigned char*>(const_cast<char*>(&(*input)[0])); + stream.avail_in = input->size(); + stream.next_out = &(*output)[0]; + stream.avail_out = output->size(); + + result = inflate(&stream, Z_FINISH); + DCHECK_EQ(Z_STREAM_END, result); + result = inflateEnd(&stream); + DCHECK_EQ(Z_OK, result); + + output->resize(output->size() - stream.avail_out); + return true; + } + + // Tries to read |dump| as a rtpplay dump file and returns the number of + // packets found in the dump. + bool ReadDecompressedDump(const std::vector<uint8_t>& dump, + size_t* packet_count) { + static const char kFirstLine[] = "#!rtpplay1.0 0.0.0.0/0\n"; + static const size_t kDumpFileHeaderSize = 4 * sizeof(uint32_t); + + *packet_count = 0; + size_t dump_pos = 0; + + // Verifies the first line. + EXPECT_EQ(memcmp(&dump[0], kFirstLine, base::size(kFirstLine) - 1), 0); + + dump_pos += base::size(kFirstLine) - 1; + EXPECT_GT(dump.size(), dump_pos); + + // Skips the file header. + dump_pos += kDumpFileHeaderSize; + EXPECT_GT(dump.size(), dump_pos); + + // Reads each packet dump. + while (dump_pos < dump.size()) { + size_t packet_dump_length = 0; + if (!VerifyPacketDump(&dump[dump_pos], + dump.size() - dump_pos, + &packet_dump_length)) { + DVLOG(0) << "Failed to read the packet dump for packet " + << *packet_count << ", dump_pos = " << dump_pos + << ", dump_length = " << dump.size(); + return false; + } + + EXPECT_GE(dump.size(), dump_pos + packet_dump_length); + dump_pos += packet_dump_length; + + (*packet_count)++; + } + return true; + } + + // Tries to read one packet dump starting at |dump| and returns the size of + // the packet dump. + bool VerifyPacketDump(const uint8_t* dump, + size_t dump_length, + size_t* packet_dump_length) { + static const size_t kDumpHeaderLength = 8; + + size_t dump_pos = 0; + base::ReadBigEndian(reinterpret_cast<const char*>(dump + dump_pos), + reinterpret_cast<uint16_t*>(packet_dump_length)); + if (*packet_dump_length < kDumpHeaderLength + kMinimumRtpHeaderLength) + return false; + + EXPECT_GE(dump_length, *packet_dump_length); + dump_pos += sizeof(uint16_t); + + uint16_t rtp_packet_length = 0; + base::ReadBigEndian(reinterpret_cast<const char*>(dump + dump_pos), + &rtp_packet_length); + if (rtp_packet_length < kMinimumRtpHeaderLength) + return false; + + dump_pos += sizeof(uint16_t); + + // Skips the elapsed time field. + dump_pos += sizeof(uint32_t); + + return IsValidRtpHeader(dump + dump_pos, + *packet_dump_length - kDumpHeaderLength); + } + + // Returns true if |header| is a valid RTP header. + bool IsValidRtpHeader(const uint8_t* header, size_t length) { + if ((header[0] & 0xC0) != 0x80) + return false; + + size_t cc_count = header[0] & 0x0F; + size_t header_length_without_extn = kMinimumRtpHeaderLength + 4 * cc_count; + + if (length < header_length_without_extn) + return false; + + uint16_t extension_count = 0; + base::ReadBigEndian( + reinterpret_cast<const char*>(header + header_length_without_extn + 2), + &extension_count); + + if (length < (extension_count + 1) * 4 + header_length_without_extn) + return false; + + return true; + } + + content::BrowserTaskEnvironment task_environment_; + std::unique_ptr<base::ScopedTempDir> temp_dir_; + base::FilePath incoming_dump_path_; + base::FilePath outgoing_dump_path_; + std::unique_ptr<WebRtcRtpDumpWriter> writer_; +}; + +TEST_F(WebRtcRtpDumpWriterTest, NoDumpFileIfNoPacketDumped) { + // The scope is used to make sure the EXPECT_CALL is checked before exiting + // the scope. + { + EXPECT_CALL(*this, OnEndDumpDone(false, false)); + + writer_->EndDump(RTP_DUMP_BOTH, + base::Bind(&WebRtcRtpDumpWriterTest::OnEndDumpDone, + base::Unretained(this))); + + FlushTaskRunner(writer_->background_task_runner().get()); + base::RunLoop().RunUntilIdle(); + FlushTaskRunner(writer_->background_task_runner().get()); + base::RunLoop().RunUntilIdle(); + } + EXPECT_FALSE(base::PathExists(incoming_dump_path_)); + EXPECT_FALSE(base::PathExists(outgoing_dump_path_)); +} + +TEST_F(WebRtcRtpDumpWriterTest, WriteAndFlushSmallSizeDump) { + std::vector<uint8_t> packet_header; + CreateFakeRtpPacketHeader(1, 2, &packet_header); + + writer_->WriteRtpPacket( + &packet_header[0], packet_header.size(), 100, true); + writer_->WriteRtpPacket( + &packet_header[0], packet_header.size(), 100, false); + + // The scope is used to make sure the EXPECT_CALL is checked before exiting + // the scope. + { + EXPECT_CALL(*this, OnEndDumpDone(true, true)); + + writer_->EndDump(RTP_DUMP_BOTH, + base::Bind(&WebRtcRtpDumpWriterTest::OnEndDumpDone, + base::Unretained(this))); + + FlushTaskRunner(writer_->background_task_runner().get()); + base::RunLoop().RunUntilIdle(); + FlushTaskRunner(writer_->background_task_runner().get()); + base::RunLoop().RunUntilIdle(); + } + + VerifyDumps(1, 1); +} + +TEST_F(WebRtcRtpDumpWriterTest, WriteOverMaxLimit) { + // Reset the writer with a small max size limit. + writer_.reset(new WebRtcRtpDumpWriter( + incoming_dump_path_, + outgoing_dump_path_, + 100, + base::Bind(&WebRtcRtpDumpWriterTest::OnMaxSizeReached, + base::Unretained(this)))); + + std::vector<uint8_t> packet_header; + CreateFakeRtpPacketHeader(3, 4, &packet_header); + + const size_t kPacketCount = 200; + // The scope is used to make sure the EXPECT_CALL is checked before exiting + // the scope. + { + EXPECT_CALL(*this, OnMaxSizeReached()).Times(testing::AtLeast(1)); + + // Write enough packets to overflow the in-memory buffer and max limit. + for (size_t i = 0; i < kPacketCount; ++i) { + writer_->WriteRtpPacket( + &packet_header[0], packet_header.size(), 100, true); + + writer_->WriteRtpPacket( + &packet_header[0], packet_header.size(), 100, false); + } + + EXPECT_CALL(*this, OnEndDumpDone(true, true)); + + writer_->EndDump(RTP_DUMP_BOTH, + base::Bind(&WebRtcRtpDumpWriterTest::OnEndDumpDone, + base::Unretained(this))); + + FlushTaskRunner(writer_->background_task_runner().get()); + base::RunLoop().RunUntilIdle(); + FlushTaskRunner(writer_->background_task_runner().get()); + base::RunLoop().RunUntilIdle(); + } + VerifyDumps(kPacketCount, kPacketCount); +} + +TEST_F(WebRtcRtpDumpWriterTest, DestroyWriterBeforeEndDumpCallback) { + EXPECT_CALL(*this, OnEndDumpDone(testing::_, testing::_)).Times(0); + + writer_->EndDump(RTP_DUMP_BOTH, + base::Bind(&WebRtcRtpDumpWriterTest::OnEndDumpDone, + base::Unretained(this))); + + writer_.reset(); + + // Two |RunUntilIdle()| calls are needed as the first run posts a task that + // we need to give a chance to run with the second call. + base::RunLoop().RunUntilIdle(); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(WebRtcRtpDumpWriterTest, EndDumpsSeparately) { + std::vector<uint8_t> packet_header; + CreateFakeRtpPacketHeader(1, 2, &packet_header); + + writer_->WriteRtpPacket( + &packet_header[0], packet_header.size(), 100, true); + writer_->WriteRtpPacket( + &packet_header[0], packet_header.size(), 100, true); + writer_->WriteRtpPacket( + &packet_header[0], packet_header.size(), 100, false); + + // The scope is used to make sure the EXPECT_CALL is checked before exiting + // the scope. + { + EXPECT_CALL(*this, OnEndDumpDone(true, false)); + EXPECT_CALL(*this, OnEndDumpDone(false, true)); + + writer_->EndDump(RTP_DUMP_INCOMING, + base::Bind(&WebRtcRtpDumpWriterTest::OnEndDumpDone, + base::Unretained(this))); + + writer_->EndDump(RTP_DUMP_OUTGOING, + base::Bind(&WebRtcRtpDumpWriterTest::OnEndDumpDone, + base::Unretained(this))); + + FlushTaskRunner(writer_->background_task_runner().get()); + base::RunLoop().RunUntilIdle(); + FlushTaskRunner(writer_->background_task_runner().get()); + base::RunLoop().RunUntilIdle(); + } + + VerifyDumps(2, 1); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_simulcast_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_simulcast_browsertest.cc new file mode 100644 index 00000000000..582f04f5474 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_simulcast_browsertest.cc @@ -0,0 +1,68 @@ +// Copyright 2015 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/files/file_path.h" +#include "base/path_service.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "media/base/media_switches.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "testing/perf/perf_test.h" +#include "ui/gl/gl_switches.h" + +static const char kSimulcastTestPage[] = "/webrtc/webrtc-simulcast.html"; + +// Simulcast integration test. This test ensures 'a=x-google-flag:conference' +// is working and that Chrome is capable of sending simulcast streams. +class WebRtcSimulcastBrowserTest : public WebRtcTestBase { + public: + // TODO(phoglund): Make it possible to enable DetectErrorsInJavaScript() here. + + void SetUpCommandLine(base::CommandLine* command_line) override { + // Just answer 'allow' to GetUserMedia invocations. + command_line->AppendSwitch(switches::kUseFakeUIForMediaStream); + + // The video playback will not work without a GPU, so force its use here. + command_line->AppendSwitch(switches::kUseGpuInTests); + + // Use fake devices in order to run on VMs. + command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); + } +}; + +// Fails/times out on Windows and Chrome OS. Flaky on Mac. +// http://crbug.com/452623 +// http://crbug.com/1004546 +// MSan reports errors. http://crbug.com/452892 +#if defined(OS_WIN) || defined(OS_MACOSX) || defined(OS_CHROMEOS) || \ + defined(MEMORY_SANITIZER) +#define MAYBE_TestVgaReturnsTwoSimulcastStreams DISABLED_TestVgaReturnsTwoSimulcastStreams +#else +#define MAYBE_TestVgaReturnsTwoSimulcastStreams TestVgaReturnsTwoSimulcastStreams +#endif +IN_PROC_BROWSER_TEST_F(WebRtcSimulcastBrowserTest, + MAYBE_TestVgaReturnsTwoSimulcastStreams) { + ASSERT_TRUE(embedded_test_server()->Start()); + + ui_test_utils::NavigateToURL( + browser(), embedded_test_server()->GetURL(kSimulcastTestPage)); + + content::WebContents* tab_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + + ASSERT_EQ("OK", ExecuteJavascript("testVgaReturnsTwoSimulcastStreams()", + tab_contents)); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_stats_perf_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_stats_perf_browsertest.cc new file mode 100644 index 00000000000..049fc2ce87e --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_stats_perf_browsertest.cc @@ -0,0 +1,391 @@ +// Copyright 2016 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 <string> + +#include "base/command_line.h" +#include "base/test/test_timeouts.h" +#include "base/threading/thread_restrictions.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/test_stats_dictionary.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "content/public/common/content_switches.h" +#include "media/base/media_switches.h" +#include "testing/perf/perf_result_reporter.h" +#include "third_party/blink/public/common/features.h" + +namespace content { + +namespace { + +const char kMainWebrtcTestHtmlPage[] = "/webrtc/webrtc_jsep01_test.html"; + +const char kInboundRtp[] = "inbound-rtp"; +const char kOutboundRtp[] = "outbound-rtp"; + +constexpr int kBitsPerByte = 8; + +constexpr char kMetricPrefixAudioStats[] = "WebRtcAudioStats."; +constexpr char kMetricPrefixVideoStats[] = "WebRtcVideoStats."; +constexpr char kMetricPrefixGetStats[] = "WebRtcGetStats."; +constexpr char kMetricSendRateBitsPerS[] = "send_rate"; +constexpr char kMetricReceiveRateBitsPerS[] = "receive_rate"; +constexpr char kMetricInvocationTimeMs[] = "invocation_time"; + +perf_test::PerfResultReporter SetUpAudioReporter(const std::string& story) { + perf_test::PerfResultReporter reporter(kMetricPrefixAudioStats, story); + reporter.RegisterFyiMetric(kMetricSendRateBitsPerS, "bits/s"); + reporter.RegisterFyiMetric(kMetricReceiveRateBitsPerS, "bits/s"); + return reporter; +} + +perf_test::PerfResultReporter SetUpVideoReporter(const std::string& story) { + perf_test::PerfResultReporter reporter(kMetricPrefixVideoStats, story); + reporter.RegisterFyiMetric(kMetricSendRateBitsPerS, "bits/s"); + reporter.RegisterFyiMetric(kMetricReceiveRateBitsPerS, "bits/s"); + return reporter; +} + +perf_test::PerfResultReporter SetUpGetStatsReporter(const std::string& story) { + perf_test::PerfResultReporter reporter(kMetricPrefixGetStats, story); + reporter.RegisterFyiMetric(kMetricInvocationTimeMs, "ms"); + return reporter; +} + +enum class GetStatsVariation { + PROMISE_BASED, + CALLBACK_BASED +}; + +// Sums up "RTC[In/Out]boundRTPStreamStats.bytes_[received/sent]" values. +double GetTotalRTPStreamBytes( + TestStatsReportDictionary* report, const char* type, + const char* media_type) { + DCHECK(type == kInboundRtp || type == kOutboundRtp); + const char* bytes_name = + (type == kInboundRtp) ? "bytesReceived" : "bytesSent"; + double total_bytes = 0.0; + report->ForEach([&type, &bytes_name, &media_type, &total_bytes]( + const TestStatsDictionary& stats) { + if (stats.GetString("type") == type && + stats.GetString("mediaType") == media_type) { + total_bytes += stats.GetNumber(bytes_name); + } + }); + return total_bytes; +} + +double GetAudioBytesSent(TestStatsReportDictionary* report) { + return GetTotalRTPStreamBytes(report, kOutboundRtp, "audio"); +} + +double GetAudioBytesReceived(TestStatsReportDictionary* report) { + return GetTotalRTPStreamBytes(report, kInboundRtp, "audio"); +} + +double GetVideoBytesSent(TestStatsReportDictionary* report) { + return GetTotalRTPStreamBytes(report, kOutboundRtp, "video"); +} + +double GetVideoBytesReceived(TestStatsReportDictionary* report) { + return GetTotalRTPStreamBytes(report, kInboundRtp, "video"); +} + +// Performance browsertest for WebRTC. This test is manual since it takes long +// to execute and requires the reference files provided by the webrtc.DEPS +// solution (which is only available on WebRTC internal bots). +// Gets its metrics from the standards conformant "RTCPeerConnection.getStats". +class WebRtcStatsPerfBrowserTest : public WebRtcTestBase { + public: + void SetUpInProcessBrowserTestFixture() override { + DetectErrorsInJavaScript(); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + // Ensure the infobar is enabled, since we expect that in this test. + EXPECT_FALSE(command_line->HasSwitch(switches::kUseFakeUIForMediaStream)); + + // Play a suitable, somewhat realistic video file. + base::FilePath input_video = test::GetReferenceFilesDir() + .Append(test::kReferenceFileName360p) + .AddExtension(test::kY4mFileExtension); + command_line->AppendSwitchPath(switches::kUseFileForFakeVideoCapture, + input_video); + command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); + } + + void StartCall(const std::string& audio_codec, + const std::string& video_codec, + bool prefer_hw_video_codec, + const std::string& video_codec_profile) { + ASSERT_TRUE(test::HasReferenceFilesInCheckout()); + ASSERT_TRUE(embedded_test_server()->Start()); + + ASSERT_GE(TestTimeouts::test_launcher_timeout().InSeconds(), 100) + << "This is a long-running test; you must specify " + "--test-launcher-timeout to have a value of at least 100000."; + + ASSERT_GE(TestTimeouts::action_max_timeout().InSeconds(), 100) + << "This is a long-running test; you must specify " + "--ui-test-action-max-timeout to have a value of at least 100000."; + + ASSERT_LT(TestTimeouts::action_max_timeout(), + TestTimeouts::test_launcher_timeout()) + << "action_max_timeout needs to be strictly-less-than " + "test_launcher_timeout"; + + left_tab_ = OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + right_tab_ = OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage); + + SetupPeerconnectionWithLocalStream(left_tab_); + SetupPeerconnectionWithLocalStream(right_tab_); + SetDefaultAudioCodec(left_tab_, audio_codec); + SetDefaultAudioCodec(right_tab_, audio_codec); + SetDefaultVideoCodec(left_tab_, video_codec, prefer_hw_video_codec, + video_codec_profile); + SetDefaultVideoCodec(right_tab_, video_codec, prefer_hw_video_codec, + video_codec_profile); + CreateDataChannel(left_tab_, "data"); + CreateDataChannel(right_tab_, "data"); + NegotiateCall(left_tab_, right_tab_); + StartDetectingVideo(left_tab_, "remote-view"); + StartDetectingVideo(right_tab_, "remote-view"); + WaitForVideoToPlay(left_tab_); + WaitForVideoToPlay(right_tab_); + } + + void EndCall() { + if (left_tab_) + HangUp(left_tab_); + if (right_tab_) + HangUp(right_tab_); + } + + void RunsAudioAndVideoCallCollectingMetricsWithAudioCodec( + const std::string& audio_codec) { + RunsAudioAndVideoCallCollectingMetrics( + audio_codec, kUseDefaultVideoCodec, false /* prefer_hw_video_codec */, + "" /* video_codec_profile */, "" /* video_codec_print_modifier */); + } + + void RunsAudioAndVideoCallCollectingMetricsWithVideoCodec( + const std::string& video_codec, + bool prefer_hw_video_codec = false, + const std::string& video_codec_profile = std::string(), + const std::string& video_codec_print_modifier = std::string()) { + RunsAudioAndVideoCallCollectingMetrics( + kUseDefaultAudioCodec, video_codec, prefer_hw_video_codec, + video_codec_profile, video_codec_print_modifier); + } + + void RunsAudioAndVideoCallCollectingMetrics( + const std::string& audio_codec, + const std::string& video_codec, + bool prefer_hw_video_codec, + const std::string& video_codec_profile, + const std::string& video_codec_print_modifier) { + StartCall(audio_codec, video_codec, prefer_hw_video_codec, + video_codec_profile); + + // Call for 60 seconds so that values may stabilize, bandwidth ramp up, etc. + test::SleepInJavascript(left_tab_, 60000); + + // The ramp-up may vary greatly and impact the resulting total bytes, to get + // reliable measurements we do two measurements, at 60 and 70 seconds and + // look at the average bytes/second in that window. + double audio_bytes_sent_before = 0.0; + double audio_bytes_received_before = 0.0; + double video_bytes_sent_before = 0.0; + double video_bytes_received_before = 0.0; + + scoped_refptr<TestStatsReportDictionary> report = + GetStatsReportDictionary(left_tab_); + if (audio_codec != kUseDefaultAudioCodec) { + audio_bytes_sent_before = GetAudioBytesSent(report.get()); + audio_bytes_received_before = GetAudioBytesReceived(report.get()); + + } + if (video_codec != kUseDefaultVideoCodec) { + video_bytes_sent_before = GetVideoBytesSent(report.get()); + video_bytes_received_before = GetVideoBytesReceived(report.get()); + } + + double measure_duration_seconds = 10.0; + test::SleepInJavascript(left_tab_, static_cast<int>( + measure_duration_seconds * base::Time::kMillisecondsPerSecond)); + + report = GetStatsReportDictionary(left_tab_); + if (audio_codec != kUseDefaultAudioCodec) { + double audio_bytes_sent_after = GetAudioBytesSent(report.get()); + double audio_bytes_received_after = GetAudioBytesReceived(report.get()); + + double audio_send_rate = + (audio_bytes_sent_after - audio_bytes_sent_before) / + measure_duration_seconds; + double audio_receive_rate = + (audio_bytes_received_after - audio_bytes_received_before) / + measure_duration_seconds; + + auto reporter = SetUpAudioReporter(audio_codec); + reporter.AddResult(kMetricSendRateBitsPerS, + audio_send_rate * kBitsPerByte); + reporter.AddResult(kMetricReceiveRateBitsPerS, + audio_receive_rate * kBitsPerByte); + } + if (video_codec != kUseDefaultVideoCodec) { + double video_bytes_sent_after = GetVideoBytesSent(report.get()); + double video_bytes_received_after = GetVideoBytesReceived(report.get()); + + double video_send_rate = + (video_bytes_sent_after - video_bytes_sent_before) / + measure_duration_seconds; + double video_receive_rate = + (video_bytes_received_after - video_bytes_received_before) / + measure_duration_seconds; + + std::string story = + (video_codec_print_modifier.empty() ? video_codec + : video_codec_print_modifier); + auto reporter = SetUpVideoReporter(story); + reporter.AddResult(kMetricSendRateBitsPerS, + video_send_rate * kBitsPerByte); + reporter.AddResult(kMetricReceiveRateBitsPerS, + video_receive_rate * kBitsPerByte); + } + + EndCall(); + } + + void RunsAudioAndVideoCallMeasuringGetStatsPerformance( + GetStatsVariation variation) { + EXPECT_TRUE(base::TimeTicks::IsHighResolution()); + + StartCall(kUseDefaultAudioCodec, kUseDefaultVideoCodec, + false /* prefer_hw_video_codec */, ""); + + double invocation_time = 0.0; + switch (variation) { + case GetStatsVariation::PROMISE_BASED: + invocation_time = (MeasureGetStatsPerformance(left_tab_) + + MeasureGetStatsPerformance(right_tab_)) / 2.0; + break; + case GetStatsVariation::CALLBACK_BASED: + invocation_time = + (MeasureGetStatsCallbackPerformance(left_tab_) + + MeasureGetStatsCallbackPerformance(right_tab_)) / 2.0; + break; + } + auto reporter = SetUpGetStatsReporter( + variation == GetStatsVariation::PROMISE_BASED ? "promise" : "callback"); + reporter.AddResult(kMetricInvocationTimeMs, invocation_time); + + EndCall(); + } + + private: + content::WebContents* left_tab_ = nullptr; + content::WebContents* right_tab_ = nullptr; +}; + +IN_PROC_BROWSER_TEST_F( + WebRtcStatsPerfBrowserTest, + MANUAL_RunsAudioAndVideoCallCollectingMetrics_AudioCodec_opus) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioAndVideoCallCollectingMetricsWithAudioCodec("opus"); +} + +IN_PROC_BROWSER_TEST_F( + WebRtcStatsPerfBrowserTest, + MANUAL_RunsAudioAndVideoCallCollectingMetrics_AudioCodec_ISAC) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioAndVideoCallCollectingMetricsWithAudioCodec("ISAC"); +} + +IN_PROC_BROWSER_TEST_F( + WebRtcStatsPerfBrowserTest, + MANUAL_RunsAudioAndVideoCallCollectingMetrics_AudioCodec_G722) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioAndVideoCallCollectingMetricsWithAudioCodec("G722"); +} + +IN_PROC_BROWSER_TEST_F( + WebRtcStatsPerfBrowserTest, + MANUAL_RunsAudioAndVideoCallCollectingMetrics_AudioCodec_PCMU) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioAndVideoCallCollectingMetricsWithAudioCodec("PCMU"); +} + +IN_PROC_BROWSER_TEST_F( + WebRtcStatsPerfBrowserTest, + MANUAL_RunsAudioAndVideoCallCollectingMetrics_AudioCodec_PCMA) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioAndVideoCallCollectingMetricsWithAudioCodec("PCMA"); +} + +IN_PROC_BROWSER_TEST_F( + WebRtcStatsPerfBrowserTest, + MANUAL_RunsAudioAndVideoCallCollectingMetrics_VideoCodec_VP8) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioAndVideoCallCollectingMetricsWithVideoCodec("VP8"); +} + +IN_PROC_BROWSER_TEST_F( + WebRtcStatsPerfBrowserTest, + MANUAL_RunsAudioAndVideoCallCollectingMetrics_VideoCodec_VP9) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioAndVideoCallCollectingMetricsWithVideoCodec("VP9"); +} + +IN_PROC_BROWSER_TEST_F( + WebRtcStatsPerfBrowserTest, + MANUAL_RunsAudioAndVideoCallCollectingMetrics_VideoCodec_VP9Profile2) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioAndVideoCallCollectingMetricsWithVideoCodec( + "VP9", true /* prefer_hw_video_codec */, + WebRtcTestBase::kVP9Profile2Specifier, "VP9p2"); +} + +#if BUILDFLAG(RTC_USE_H264) + +IN_PROC_BROWSER_TEST_F( + WebRtcStatsPerfBrowserTest, + MANUAL_RunsAudioAndVideoCallCollectingMetrics_VideoCodec_H264) { + base::ScopedAllowBlockingForTesting allow_blocking; + // Only run test if run-time feature corresponding to |rtc_use_h264| is on. + if (!base::FeatureList::IsEnabled( + blink::features::kWebRtcH264WithOpenH264FFmpeg)) { + LOG(WARNING) << "Run-time feature WebRTC-H264WithOpenH264FFmpeg disabled. " + "Skipping WebRtcPerfBrowserTest." + "MANUAL_RunsAudioAndVideoCallCollectingMetrics_VideoCodec_" + "H264 (test " + "\"OK\")"; + return; + } + RunsAudioAndVideoCallCollectingMetricsWithVideoCodec( + "H264", true /* prefer_hw_video_codec */); +} + +#endif // BUILDFLAG(RTC_USE_H264) + +IN_PROC_BROWSER_TEST_F( + WebRtcStatsPerfBrowserTest, + MANUAL_RunsAudioAndVideoCallMeasuringGetStatsPerformance_Promise) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioAndVideoCallMeasuringGetStatsPerformance( + GetStatsVariation::PROMISE_BASED); +} + +IN_PROC_BROWSER_TEST_F( + WebRtcStatsPerfBrowserTest, + MANUAL_RunsAudioAndVideoCallMeasuringGetStatsPerformance_Callback) { + base::ScopedAllowBlockingForTesting allow_blocking; + RunsAudioAndVideoCallMeasuringGetStatsPerformance( + GetStatsVariation::CALLBACK_BASED); +} + +} // namespace + +} // namespace content diff --git a/chromium/chrome/browser/media/webrtc/webrtc_text_log_handler.cc b/chromium/chrome/browser/media/webrtc/webrtc_text_log_handler.cc new file mode 100644 index 00000000000..c0126266d57 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_text_log_handler.cc @@ -0,0 +1,539 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/webrtc_text_log_handler.h" + +#include <map> +#include <string> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/cpu.h" +#include "base/feature_list.h" +#include "base/logging.h" +#include "base/metrics/histogram_functions.h" +#include "base/strings/strcat.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/system/sys_info.h" +#include "base/task/post_task.h" +#include "base/time/time.h" +#include "chrome/common/channel_info.h" +#include "chrome/common/media/webrtc_logging.mojom.h" +#include "components/version_info/version_info.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/gpu_data_manager.h" +#include "content/public/browser/network_service_instance.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/webrtc_log.h" +#include "content/public/common/content_features.h" +#include "gpu/config/gpu_info.h" +#include "media/audio/audio_manager.h" +#include "media/webrtc/webrtc_switches.h" +#include "net/base/ip_address.h" +#include "net/base/network_change_notifier.h" +#include "net/base/network_interfaces.h" +#include "services/network/public/mojom/network_service.mojom.h" +#include "services/service_manager/sandbox/features.h" +#include "services/service_manager/sandbox/sandbox_type.h" + +#if defined(OS_LINUX) +#include "base/linux_util.h" +#endif + +#if defined(OS_MACOSX) +#include "base/mac/mac_util.h" +#endif + +#if defined(OS_CHROMEOS) +#include "chromeos/system/statistics_provider.h" +#endif + +using base::NumberToString; + +namespace { + +void ForwardMessageViaTaskRunner( + scoped_refptr<base::SequencedTaskRunner> task_runner, + base::Callback<void(const std::string&)> callback, + const std::string& message) { + task_runner->PostTask(FROM_HERE, + base::BindOnce(std::move(callback), message)); +} + +std::string Format(const std::string& message, + base::Time timestamp, + base::Time start_time) { + int32_t interval_ms = + static_cast<int32_t>((timestamp - start_time).InMilliseconds()); + return base::StringPrintf("[%03d:%03d] %s", interval_ms / 1000, + interval_ms % 1000, message.c_str()); +} + +std::string FormatMetaDataAsLogMessage(const WebRtcLogMetaDataMap& meta_data) { + std::string message; + for (auto& kv : meta_data) { + message += kv.first + ": " + kv.second + '\n'; + } + // Remove last '\n'. + if (!message.empty()) + message.erase(message.size() - 1, 1); // TODO(terelius): Use pop_back() + return message; +} + +// For privacy reasons when logging IP addresses. The returned "sensitive +// string" is for release builds a string with the end stripped away. Last +// octet for IPv4 and last 80 bits (5 groups) for IPv6. String will be +// "1.2.3.x" and "1.2.3::" respectively. For debug builds, the string is +// not stripped. +std::string IPAddressToSensitiveString(const net::IPAddress& address) { +#if defined(NDEBUG) + std::string sensitive_address; + switch (address.size()) { + case net::IPAddress::kIPv4AddressSize: { + sensitive_address = address.ToString(); + size_t find_pos = sensitive_address.rfind('.'); + if (find_pos == std::string::npos) + return std::string(); + sensitive_address.resize(find_pos); + sensitive_address += ".x"; + break; + } + case net::IPAddress::kIPv6AddressSize: { + // TODO(grunell): Create a string of format "1:2:3:x:x:x:x:x" to clarify + // that the end has been stripped out. + net::IPAddressBytes stripped = address.bytes(); + std::fill(stripped.begin() + 6, stripped.end(), 0); + sensitive_address = net::IPAddress(stripped).ToString(); + break; + } + default: { break; } + } + return sensitive_address; +#else + return address.ToString(); +#endif +} + +} // namespace + +WebRtcTextLogHandler::WebRtcTextLogHandler(int render_process_id) + : render_process_id_(render_process_id), logging_state_(CLOSED) {} + +WebRtcTextLogHandler::~WebRtcTextLogHandler() { + // If the log isn't closed that means we haven't decremented the log count + // in the LogUploader. + DCHECK(logging_state_ == CLOSED || channel_is_closing_); + DCHECK(!log_buffer_); +} + +WebRtcTextLogHandler::LoggingState WebRtcTextLogHandler::GetState() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return logging_state_; +} + +bool WebRtcTextLogHandler::GetChannelIsClosing() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return channel_is_closing_; +} + +void WebRtcTextLogHandler::SetMetaData( + std::unique_ptr<WebRtcLogMetaDataMap> meta_data, + const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + if (channel_is_closing_) { + FireGenericDoneCallback(callback, false, "The renderer is closing."); + return; + } + + if (logging_state_ != CLOSED && logging_state_ != STARTED) { + FireGenericDoneCallback(callback, false, + "Meta data must be set before stop or upload."); + return; + } + + if (logging_state_ == LoggingState::STARTED) { + std::string meta_data_message = FormatMetaDataAsLogMessage(*meta_data); + LogToCircularBuffer(meta_data_message); + } + + if (!meta_data_) { + // If no meta data has been set previously, set it now. + meta_data_ = std::move(meta_data); + } else if (meta_data) { + // If there is existing meta data, update it and any new fields. The meta + // data is kept around to be uploaded separately from the log. + for (const auto& it : *meta_data) + (*meta_data_)[it.first] = it.second; + } + + FireGenericDoneCallback(callback, true, ""); +} + +bool WebRtcTextLogHandler::StartLogging(WebRtcLogUploader* log_uploader, + const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + if (channel_is_closing_) { + FireGenericDoneCallback(callback, false, "The renderer is closing."); + return false; + } + + if (logging_state_ != CLOSED) { + FireGenericDoneCallback(callback, false, "A log is already open."); + return false; + } + + if (!log_uploader->ApplyForStartLogging()) { + FireGenericDoneCallback(callback, false, + "Cannot start, maybe the maximum number of " + "simultaneuos logs has been reached."); + return false; + } + + logging_state_ = STARTING; + + DCHECK(!log_buffer_); + log_buffer_.reset(new WebRtcLogBuffer()); + if (!meta_data_) + meta_data_.reset(new WebRtcLogMetaDataMap()); + + content::GetNetworkService()->GetNetworkList( + net::EXCLUDE_HOST_SCOPE_VIRTUAL_INTERFACES, + base::BindOnce(&WebRtcTextLogHandler::OnGetNetworkInterfaceList, + weak_factory_.GetWeakPtr(), std::move(callback))); + return true; +} + +void WebRtcTextLogHandler::StartDone(const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + if (channel_is_closing_) { + FireGenericDoneCallback(callback, false, + "Failed to start log. Renderer is closing."); + return; + } + + DCHECK_EQ(STARTING, logging_state_); + + base::UmaHistogramSparse("WebRtcTextLogging.Start", web_app_id_); + + logging_started_time_ = base::Time::Now(); + logging_state_ = STARTED; + FireGenericDoneCallback(callback, true, ""); +} + +bool WebRtcTextLogHandler::StopLogging(const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + if (channel_is_closing_) { + FireGenericDoneCallback(callback, false, + "Can't stop log. Renderer is closing."); + return false; + } + + if (logging_state_ != STARTED) { + FireGenericDoneCallback(callback, false, "Logging not started."); + return false; + } + + stop_callback_ = callback; + logging_state_ = STOPPING; + + base::PostTask(FROM_HERE, {content::BrowserThread::IO}, + base::BindOnce(&content::WebRtcLog::ClearLogMessageCallback, + render_process_id_)); + return true; +} + +void WebRtcTextLogHandler::StopDone() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(stop_callback_); + + if (channel_is_closing_) { + FireGenericDoneCallback(stop_callback_, false, + "Failed to stop log. Renderer is closing."); + return; + } + + // If we aren't in STOPPING state, then there is a bug in the caller, since + // it is responsible for checking the state before making the call. If we do + // enter here in a bad state, then we can't use the stop_callback_ or we + // might fire the same callback multiple times. + DCHECK_EQ(STOPPING, logging_state_); + if (logging_state_ == STOPPING) { + logging_started_time_ = base::Time(); + logging_state_ = STOPPED; + FireGenericDoneCallback(stop_callback_, true, ""); + stop_callback_.Reset(); + } +} + +void WebRtcTextLogHandler::ChannelClosing() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (logging_state_ == STARTING || logging_state_ == STARTED) { + base::PostTask(FROM_HERE, {content::BrowserThread::IO}, + base::BindOnce(&content::WebRtcLog::ClearLogMessageCallback, + render_process_id_)); + } + channel_is_closing_ = true; +} + +void WebRtcTextLogHandler::DiscardLog() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(logging_state_ == STOPPED || + (channel_is_closing_ && logging_state_ != CLOSED)); + + base::UmaHistogramSparse("WebRtcTextLogging.Discard", web_app_id_); + + log_buffer_.reset(); + meta_data_.reset(); + logging_state_ = LoggingState::CLOSED; +} + +void WebRtcTextLogHandler::ReleaseLog( + std::unique_ptr<WebRtcLogBuffer>* log_buffer, + std::unique_ptr<WebRtcLogMetaDataMap>* meta_data) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(logging_state_ == STOPPED || + (channel_is_closing_ && logging_state_ != CLOSED)); + DCHECK(log_buffer_); + DCHECK(meta_data_); + + // Checking log_buffer_ here due to seeing some crashes out in the wild. + // See crbug/699960 for more details. + // TODO(crbug/807547): Remove if condition. + if (log_buffer_) { + log_buffer_->SetComplete(); + *log_buffer = std::move(log_buffer_); + } + + if (meta_data_) + *meta_data = std::move(meta_data_); + + logging_state_ = LoggingState::CLOSED; +} + +void WebRtcTextLogHandler::LogToCircularBuffer(const std::string& message) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK_NE(logging_state_, CLOSED); + if (log_buffer_) { + log_buffer_->Log(message); + } +} + +void WebRtcTextLogHandler::LogMessage(const std::string& message) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (logging_state_ == STARTED && !channel_is_closing_) { + LogToCircularBuffer( + Format(message, base::Time::Now(), logging_started_time_)); + } +} + +void WebRtcTextLogHandler::LogWebRtcLoggingMessage( + const chrome::mojom::WebRtcLoggingMessage* message) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + LogToCircularBuffer( + Format(message->data, message->timestamp, logging_started_time_)); +} + +bool WebRtcTextLogHandler::ExpectLoggingStateStopped( + const GenericDoneCallback& callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (logging_state_ != STOPPED) { + FireGenericDoneCallback(callback, false, + "Logging not stopped or no log open."); + return false; + } + return true; +} + +void WebRtcTextLogHandler::FireGenericDoneCallback( + const GenericDoneCallback& callback, + bool success, + const std::string& error_message) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!callback.is_null()); + + if (error_message.empty()) { + DCHECK(success); + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(callback, success, error_message)); + return; + } + + DCHECK(!success); + + // Add current logging state to error message. + auto state_string = [&] { + switch (logging_state_) { + case LoggingState::CLOSED: + return "closed"; + case LoggingState::STARTING: + return "starting"; + case LoggingState::STARTED: + return "started"; + case LoggingState::STOPPING: + return "stopping"; + case LoggingState::STOPPED: + return "stopped"; + } + NOTREACHED(); + return ""; + }; + + std::string error_message_with_state = + base::StrCat({error_message, ". State=", state_string(), ". Channel is ", + channel_is_closing_ ? "" : "not ", "closing."}); + + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(callback, success, error_message_with_state)); +} + +void WebRtcTextLogHandler::SetWebAppId(int web_app_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + web_app_id_ = web_app_id; +} + +void WebRtcTextLogHandler::OnGetNetworkInterfaceList( + const GenericDoneCallback& callback, + const base::Optional<net::NetworkInterfaceList>& networks) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (logging_state_ != STARTING || channel_is_closing_) { + FireGenericDoneCallback(callback, false, "Logging cancelled."); + return; + } + + // Log start time (current time). We don't use base/i18n/time_formatting.h + // here because we don't want the format of the current locale. + base::Time::Exploded now = {0}; + base::Time::Now().LocalExplode(&now); + LogToCircularBuffer(base::StringPrintf("Start %d-%02d-%02d %02d:%02d:%02d", + now.year, now.month, now.day_of_month, + now.hour, now.minute, now.second)); + + // Write metadata if received before logging started. + if (meta_data_ && !meta_data_->empty()) { + std::string info = FormatMetaDataAsLogMessage(*meta_data_); + LogToCircularBuffer(info); + } + + // Chrome version + LogToCircularBuffer("Chrome version: " + version_info::GetVersionNumber() + + " " + chrome::GetChannelName()); + + // OS + LogToCircularBuffer(base::SysInfo::OperatingSystemName() + " " + + base::SysInfo::OperatingSystemVersion() + " " + + base::SysInfo::OperatingSystemArchitecture()); +#if defined(OS_LINUX) + LogToCircularBuffer("Linux distribution: " + base::GetLinuxDistro()); +#endif + + // CPU + base::CPU cpu; + LogToCircularBuffer( + "Cpu: " + NumberToString(cpu.family()) + "." + + NumberToString(cpu.model()) + "." + NumberToString(cpu.stepping()) + + ", x" + NumberToString(base::SysInfo::NumberOfProcessors()) + ", " + + NumberToString(base::SysInfo::AmountOfPhysicalMemoryMB()) + "MB"); + LogToCircularBuffer("Cpu brand: " + cpu.cpu_brand()); + + // Computer model + std::string computer_model = "Not available"; +#if defined(OS_MACOSX) + computer_model = base::mac::GetModelIdentifier(); +#elif defined(OS_CHROMEOS) + chromeos::system::StatisticsProvider::GetInstance()->GetMachineStatistic( + chromeos::system::kHardwareClassKey, &computer_model); +#endif + LogToCircularBuffer("Computer model: " + computer_model); + + // GPU + gpu::GPUInfo gpu_info = content::GpuDataManager::GetInstance()->GetGPUInfo(); + const gpu::GPUInfo::GPUDevice& active_gpu = gpu_info.active_gpu(); + LogToCircularBuffer( + "Gpu: machine-model-name=" + gpu_info.machine_model_name + + ", machine-model-version=" + gpu_info.machine_model_version + + ", vendor-id=" + base::NumberToString(active_gpu.vendor_id) + + ", device-id=" + base::NumberToString(active_gpu.device_id) + + ", driver-vendor=" + active_gpu.driver_vendor + + ", driver-version=" + active_gpu.driver_version); + LogToCircularBuffer("OpenGL: gl-vendor=" + gpu_info.gl_vendor + + ", gl-renderer=" + gpu_info.gl_renderer + + ", gl-version=" + gpu_info.gl_version); + + // AudioService features + auto enabled_or_disabled_feature_string = [](auto& feature) { + return base::FeatureList::IsEnabled(feature) ? "enabled" : "disabled"; + }; + auto enabled_or_disabled_bool_string = [](bool value) { + return value ? "enabled" : "disabled"; + }; + LogToCircularBuffer(base::StrCat( + {"AudioService: AudioStreams=", + enabled_or_disabled_feature_string(features::kAudioServiceAudioStreams), + ", OutOfProcess=", + enabled_or_disabled_feature_string(features::kAudioServiceOutOfProcess), + ", LaunchOnStartup=", + enabled_or_disabled_feature_string( + features::kAudioServiceLaunchOnStartup), + ", Sandbox=", + enabled_or_disabled_bool_string( + service_manager::IsAudioSandboxEnabled()), + ", ApmInAudioService=", + enabled_or_disabled_bool_string( + media::IsWebRtcApmInAudioServiceEnabled())})); + + // Audio manager + // On some platforms, this can vary depending on build flags and failure + // fallbacks. On Linux for example, we fallback on ALSA if PulseAudio fails to + // initialize. TODO(http://crbug/843202): access AudioManager name via Audio + // service interface. + media::AudioManager* audio_manager = media::AudioManager::Get(); + LogToCircularBuffer(base::StringPrintf( + "Audio manager: %s", + audio_manager ? audio_manager->GetName() : "Out of process")); + + // Network interfaces + const net::NetworkInterfaceList empty_network_list; + const net::NetworkInterfaceList& network_list = + networks.has_value() ? *networks : empty_network_list; + LogToCircularBuffer("Discovered " + + base::NumberToString(network_list.size()) + + " network interfaces:"); + for (const auto& network : network_list) { + LogToCircularBuffer( + "Name: " + network.friendly_name + ", Address: " + + IPAddressToSensitiveString(network.address) + ", Type: " + + net::NetworkChangeNotifier::ConnectionTypeToString(network.type)); + } + + StartDone(callback); + + // After the above data has been written, tell the browser to enable logging. + // TODO(terelius): Once we have moved over to Mojo, we could tell the + // renderer to start logging here, but for the time being + // WebRtcLoggingHandlerHost::StartLogging will be responsible for sending + // that IPC message. + + // TODO(darin): Change SetLogMessageCallback to run on the UI thread. + + auto log_message_callback = base::Bind( + &ForwardMessageViaTaskRunner, base::SequencedTaskRunnerHandle::Get(), + base::Bind(&WebRtcTextLogHandler::LogMessage, + weak_factory_.GetWeakPtr())); + base::PostTask( + FROM_HERE, {content::BrowserThread::IO}, + base::BindOnce(&content::WebRtcLog::SetLogMessageCallback, + render_process_id_, std::move(log_message_callback))); +} diff --git a/chromium/chrome/browser/media/webrtc/webrtc_text_log_handler.h b/chromium/chrome/browser/media/webrtc/webrtc_text_log_handler.h new file mode 100644 index 00000000000..02c5f50cc32 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_text_log_handler.h @@ -0,0 +1,149 @@ +// Copyright 2016 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 CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_TEXT_LOG_HANDLER_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_TEXT_LOG_HANDLER_H_ + +#include <map> +#include <memory> +#include <string> + +#include "base/callback.h" +#include "base/memory/weak_ptr.h" +#include "base/sequence_checker.h" +#include "chrome/browser/media/webrtc/webrtc_log_uploader.h" +#include "net/base/network_interfaces.h" + +namespace chrome { +namespace mojom { +class WebRtcLoggingMessage; +} // namespace mojom +} // namespace chrome + +class WebRtcLogBuffer; + +class WebRtcTextLogHandler { + public: + // States used for protecting from function calls made at non-allowed points + // in time. For example, StartLogging() is only allowed in CLOSED state. + // See also comment on |channel_is_closing_| below. + // Transitions: SetMetaData(): CLOSED -> CLOSED, or + // STARTED -> STARTED + // StartLogging(): CLOSED -> STARTING. + // StartDone(): STARTING -> STARTED. + // StopLogging(): STARTED -> STOPPING. + // StopDone(): STOPPING -> STOPPED. + // DiscardLog(): STOPPED -> CLOSED. + // ReleaseLog(): STOPPED -> CLOSED. + enum LoggingState { + CLOSED, // Logging not started, no log in memory. + STARTING, // Start logging is in progress. + STARTED, // Logging started. + STOPPING, // Stop logging is in progress. + STOPPED, // Logging has been stopped, log still open in memory. + }; + + typedef base::Callback<void(bool, const std::string&)> GenericDoneCallback; + + explicit WebRtcTextLogHandler(int render_process_id); + ~WebRtcTextLogHandler(); + + // Returns the current state of the log. + LoggingState GetState() const; + + // Returns true if channel is closing. + bool GetChannelIsClosing() const; + + // Sets meta data for log uploading. Merged with any already set meta data. + // Values for existing keys are overwritten. The meta data already set at log + // start is written to the beginning of the log. Meta data set after log start + // is written to the log at that time. + void SetMetaData(std::unique_ptr<WebRtcLogMetaDataMap> meta_data, + const GenericDoneCallback& callback); + + // Opens a log and starts logging if allowed by the LogUploader. + // Returns false if logging could not be started. + bool StartLogging(WebRtcLogUploader* log_uploader, + const GenericDoneCallback& callback); + + // Stops logging. Log will remain open until UploadLog or DiscardLog is + // called. + bool StopLogging(const GenericDoneCallback& callback); + + // Called by the WebRtcLoggingHandlerHost when logging has stopped in the + // renderer. Should only be called in response to a + // WebRtcLoggingMsg_LoggingStopped IPC message. + void StopDone(); + + // Signals that the renderer is closing, which de facto stops logging but + // keeps the log in memory. + // Can be called in any state except CLOSED. + void ChannelClosing(); + + // Discards a stopped log. + void DiscardLog(); + + // Releases a stopped log to the caller. + void ReleaseLog(std::unique_ptr<WebRtcLogBuffer>* log_buffer, + std::unique_ptr<WebRtcLogMetaDataMap>* meta_data); + + // Adds a message to the log. + void LogMessage(const std::string& message); + + // Adds a message to the log. + void LogWebRtcLoggingMessage( + const chrome::mojom::WebRtcLoggingMessage* message); + + // Returns true if the logging state is CLOSED and fires an the callback + // with an error message otherwise. + bool ExpectLoggingStateStopped(const GenericDoneCallback& callback); + + void FireGenericDoneCallback(const GenericDoneCallback& callback, + bool success, + const std::string& error_message); + + void SetWebAppId(int web_app_id); + + private: + void StartDone(const GenericDoneCallback& callback); + + void LogToCircularBuffer(const std::string& message); + + void OnGetNetworkInterfaceList( + const GenericDoneCallback& callback, + const base::Optional<net::NetworkInterfaceList>& networks); + + SEQUENCE_CHECKER(sequence_checker_); + + // The render process ID this object belongs to. + const int render_process_id_; + + // Should be created by StartLogging(). + std::unique_ptr<WebRtcLogBuffer> log_buffer_; + + // Should be created by StartLogging(). + std::unique_ptr<WebRtcLogMetaDataMap> meta_data_; + + GenericDoneCallback stop_callback_; + LoggingState logging_state_; + + // True if renderer is closing. The log (if there is one) can still be + // released or discarded (i.e. closed). No new logs can be created. The only + // state change possible when channel is closing is from any state to CLOSED. + bool channel_is_closing_ = false; + + // The system time in ms when logging is started. Reset when logging_state_ + // changes to STOPPED. + base::Time logging_started_time_; + + // Web app id used for statistics. See + // |WebRtcLoggingHandlerHost::web_app_id_|. + int web_app_id_ = 0; + + base::WeakPtrFactory<WebRtcTextLogHandler> weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(WebRtcTextLogHandler); +}; + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WEBRTC_TEXT_LOG_HANDLER_H_ diff --git a/chromium/chrome/browser/media/webrtc/webrtc_video_display_perf_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_video_display_perf_browsertest.cc new file mode 100644 index 00000000000..1b90baade1a --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_video_display_perf_browsertest.cc @@ -0,0 +1,497 @@ +// Copyright 2018 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 <algorithm> + +#include "base/json/json_reader.h" +#include "base/strings/string_tokenizer.h" +#include "base/strings/stringprintf.h" +#include "base/test/trace_event_analyzer.h" +#include "build/build_config.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/test/base/tracing.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/render_view_host.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "media/base/media_switches.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "testing/perf/perf_result_reporter.h" +#include "third_party/blink/public/common/features.h" +#include "ui/gl/gl_switches.h" + +using trace_analyzer::TraceEvent; +using trace_analyzer::TraceEventVector; +using trace_analyzer::Query; + +namespace { + +// Trace events. +static const char kStartRenderEventName[] = + "RemoteVideoSourceDelegate::RenderFrame"; +static const char kEnqueueFrameEventName[] = + "WebMediaPlayerMSCompositor::EnqueueFrame"; +static const char kSetFrameEventName[] = + "WebMediaPlayerMSCompositor::SetCurrentFrame"; +static const char kGetFrameEventName[] = + "WebMediaPlayerMSCompositor::GetCurrentFrame"; +static const char kVideoResourceEventName[] = + "VideoResourceUpdater::ObtainFrameResources"; +static const char kVsyncEventName[] = "Display::DrawAndSwap"; + +// VideoFrameSubmitter dumps the delay from the handover of a decoded remote +// VideoFrame from webrtc to the moment the OS acknowledges the swap buffers. +static const char kVideoFrameSubmitterEventName[] = "VideoFrameSubmitter"; + +static const char kEventMatchKey[] = "Timestamp"; +static const char kMainWebrtcTestHtmlPage[] = + "/webrtc/webrtc_video_display_perf_test.html"; + +constexpr char kMetricPrefixVideoDisplayPerf[] = "WebRtcVideoDisplayPerf."; +constexpr char kMetricSkippedFramesPercent[] = "skipped_frames"; +constexpr char kMetricPassingToRenderAlgoLatencyUs[] = + "passing_to_render_algorithm_latency"; +constexpr char kMetricRenderAlgoLatencyUs[] = "render_algorithm_latency"; +constexpr char kMetricCompositorPickingFrameLatencyUs[] = + "compositor_picking_frame_latency"; +constexpr char kMetricCompositorResourcePreparationLatencyUs[] = + "compositor_resource_preparation_latency"; +constexpr char kMetricVsyncLatencyUs[] = "vsync_latency"; +constexpr char kMetricTotalControlledLatencyUs[] = "total_controlled_latency"; +constexpr char kMetricTotalLatencyUs[] = "total_latency"; +constexpr char kMetricPostDecodeToRasterLatencyUs[] = + "post_decode_to_raster_latency"; +constexpr char kMetricWebRtcDecodeLatencyUs[] = "webrtc_decode_latency"; + +perf_test::PerfResultReporter SetUpReporter(const std::string& story) { + perf_test::PerfResultReporter reporter(kMetricPrefixVideoDisplayPerf, story); + reporter.RegisterImportantMetric(kMetricSkippedFramesPercent, "percent"); + reporter.RegisterImportantMetric(kMetricPassingToRenderAlgoLatencyUs, "us"); + reporter.RegisterImportantMetric(kMetricRenderAlgoLatencyUs, "us"); + reporter.RegisterImportantMetric(kMetricCompositorPickingFrameLatencyUs, + "us"); + reporter.RegisterImportantMetric( + kMetricCompositorResourcePreparationLatencyUs, "us"); + reporter.RegisterImportantMetric(kMetricVsyncLatencyUs, "us"); + reporter.RegisterImportantMetric(kMetricTotalControlledLatencyUs, "us"); + reporter.RegisterImportantMetric(kMetricTotalLatencyUs, "us"); + reporter.RegisterImportantMetric(kMetricPostDecodeToRasterLatencyUs, "us"); + reporter.RegisterImportantMetric(kMetricWebRtcDecodeLatencyUs, "us"); + return reporter; +} + +struct VideoDisplayPerfTestConfig { + int width; + int height; + int fps; + bool disable_render_smoothness_algorithm; +}; + +std::string VectorToString(const std::vector<double>& values) { + std::string ret = ""; + for (double val : values) { + ret += base::StringPrintf("%.0lf,", val); + } + // Strip of trailing comma. + return ret.substr(0, ret.length() - 1); +} + +void FindEvents(trace_analyzer::TraceAnalyzer* analyzer, + const std::string& event_name, + const Query& base_query, + TraceEventVector* events) { + Query query = Query::EventNameIs(event_name) && base_query; + analyzer->FindEvents(query, events); +} + +void AssociateEvents(trace_analyzer::TraceAnalyzer* analyzer, + const std::vector<std::string>& event_names, + const std::string& match_string, + const Query& base_query) { + for (size_t i = 0; i < event_names.size() - 1; ++i) { + Query begin = Query::EventNameIs(event_names[i]); + Query end = Query::EventNameIs(event_names[i + 1]); + Query match(Query::EventArg(match_string) == Query::OtherArg(match_string)); + analyzer->AssociateEvents(begin, end, base_query && match); + } +} + +content::WebContents* OpenWebrtcInternalsTab(Browser* browser) { + chrome::AddTabAt(browser, GURL(), -1, true); + ui_test_utils::NavigateToURL(browser, GURL("chrome://webrtc-internals")); + return browser->tab_strip_model()->GetActiveWebContents(); +} + +std::vector<double> ParseGoogMaxDecodeFromWebrtcInternalsTab( + const std::string& webrtc_internals_stats_json) { + std::vector<double> goog_decode_ms; + + std::unique_ptr<base::Value> parsed_json = + base::JSONReader::ReadDeprecated(webrtc_internals_stats_json); + base::DictionaryValue* dictionary = nullptr; + if (!parsed_json.get() || !parsed_json->GetAsDictionary(&dictionary)) + return goog_decode_ms; + ignore_result(parsed_json.release()); + + // |dictionary| should have exactly two entries, one per ssrc. + if (!dictionary || dictionary->size() != 2u) + return goog_decode_ms; + + // Only a given |dictionary| entry will have a "stats" entry that has a key + // that ends with "recv-googMaxDecodeMs" inside (it will start with the ssrc + // id, but we don't care about that). Then collect the string of "values" out + // of that key and convert those into the |goog_decode_ms| vector of doubles. + for (const auto& dictionary_entry : *dictionary) { + for (const auto& ssrc_entry : dictionary_entry.second->DictItems()) { + if (ssrc_entry.first != "stats") + continue; + + for (const auto& stat_entry : ssrc_entry.second.DictItems()) { + if (!base::EndsWith(stat_entry.first, "recv-googMaxDecodeMs", + base::CompareCase::SENSITIVE)) { + continue; + } + base::Value* values_entry = stat_entry.second.FindKey({"values"}); + if (!values_entry) + continue; + base::StringTokenizer values_tokenizer(values_entry->GetString(), + "[,]"); + while (values_tokenizer.GetNext()) { + if (values_tokenizer.token_is_delim()) + continue; + goog_decode_ms.push_back(atof(values_tokenizer.token().c_str()) * + base::Time::kMicrosecondsPerMillisecond); + } + } + } + } + return goog_decode_ms; +} + +} // anonymous namespace + +// Tests the performance of Chrome displaying remote video. +// +// This test creates a WebRTC peer connection between two tabs and measures the +// trace events listed in the beginning of this file on the tab receiving +// remote video. In order to cut down from the encode cost, the tab receiving +// remote video does not send any video to its peer. +// +// This test traces certain categories for a period of time. It follows the +// lifetime of a single video frame by synchronizing on the timestamps values +// attached to trace events. Then, it calculates the duration and related stats. +class WebRtcVideoDisplayPerfBrowserTest + : public WebRtcTestBase, + public testing::WithParamInterface< + std::tuple<gfx::Size /* resolution */, + int /* fps */, + bool /* disable_render_smoothness_algorithm */>> { + public: + WebRtcVideoDisplayPerfBrowserTest() { + const auto& params = GetParam(); + const gfx::Size& resolution = std::get<0>(params); + test_config_ = {resolution.width(), resolution.height(), + std::get<1>(params), std::get<2>(params)}; + } + + void SetUpInProcessBrowserTestFixture() override { + DetectErrorsInJavaScript(); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + command_line->AppendSwitch(switches::kUseFakeUIForMediaStream); + command_line->AppendSwitchASCII( + switches::kUseFakeDeviceForMediaStream, + base::StringPrintf("fps=%d", test_config_.fps)); + if (test_config_.disable_render_smoothness_algorithm) + command_line->AppendSwitch(switches::kDisableRTCSmoothnessAlgorithm); + command_line->AppendSwitch(switches::kUseGpuInTests); + } + + void TestVideoDisplayPerf(const std::string& video_codec) { + ASSERT_TRUE(embedded_test_server()->Start()); + // chrome:webrtc-internals doesn't start tracing anything until the + // connection(s) are up. + content::WebContents* webrtc_internals_tab = + OpenWebrtcInternalsTab(browser()); + EXPECT_TRUE(content::ExecuteScript( + webrtc_internals_tab, + "currentGetStatsMethod = OPTION_GETSTATS_LEGACY")); + + content::WebContents* left_tab = + OpenPageAndGetUserMediaInNewTabWithConstraints( + embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage), + base::StringPrintf( + "{audio: true, video: {mandatory: {minWidth: %d, maxWidth: %d, " + "minHeight: %d, maxHeight: %d}}}", + test_config_.width, test_config_.width, test_config_.height, + test_config_.height)); + content::WebContents* right_tab = + OpenPageAndGetUserMediaInNewTabWithConstraints( + embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage), + "{audio: true, video: false}"); + const int process_id = + right_tab->GetRenderViewHost()->GetProcess()->GetProcess().Pid(); + + const std::string disable_cpu_adaptation_constraint( + "{'optional': [{'googCpuOveruseDetection': false}]}"); + SetupPeerconnectionWithConstraintsAndLocalStream( + left_tab, disable_cpu_adaptation_constraint); + SetupPeerconnectionWithConstraintsAndLocalStream( + right_tab, disable_cpu_adaptation_constraint); + + if (!video_codec.empty()) { + constexpr bool kPreferHwVideoCodec = true; + SetDefaultVideoCodec(left_tab, video_codec, kPreferHwVideoCodec); + SetDefaultVideoCodec(right_tab, video_codec, kPreferHwVideoCodec); + } + NegotiateCall(left_tab, right_tab); + + StartDetectingVideo(right_tab, "remote-view"); + WaitForVideoToPlay(right_tab); + // Run the connection a bit to ramp up. + test::SleepInJavascript(left_tab, 10000); + + ASSERT_TRUE(tracing::BeginTracing("media,viz,webrtc")); + // Run the connection for 5 seconds to collect metrics. + test::SleepInJavascript(left_tab, 5000); + + const std::string webrtc_internals_stats_json = ExecuteJavascript( + "window.domAutomationController.send(" + " JSON.stringify(peerConnectionDataStore));", + webrtc_internals_tab); + webrtc_decode_latencies_ = + ParseGoogMaxDecodeFromWebrtcInternalsTab(webrtc_internals_stats_json); + chrome::CloseWebContents(browser(), webrtc_internals_tab, false); + + std::string json_events; + ASSERT_TRUE(tracing::EndTracing(&json_events)); + std::unique_ptr<trace_analyzer::TraceAnalyzer> analyzer( + trace_analyzer::TraceAnalyzer::Create(json_events)); + analyzer->AssociateAsyncBeginEndEvents(); + + HangUp(left_tab); + HangUp(right_tab); + chrome::CloseWebContents(browser(), left_tab, false); + chrome::CloseWebContents(browser(), right_tab, false); + + ASSERT_TRUE(CalculatePerfResults(analyzer.get(), process_id)); + PrintResults(video_codec); + } + + private: + bool CalculatePerfResults(trace_analyzer::TraceAnalyzer* analyzer, + int render_process_id) { + Query match_process_id = Query::EventPidIs(render_process_id); + const std::vector<std::string> chain_of_events = { + kStartRenderEventName, kEnqueueFrameEventName, kSetFrameEventName, + kGetFrameEventName, kVideoResourceEventName}; + AssociateEvents(analyzer, chain_of_events, + kEventMatchKey, match_process_id); + + TraceEventVector start_render_events; + FindEvents(analyzer, kStartRenderEventName, match_process_id, + &start_render_events); + if (start_render_events.empty()) + return false; + + // We are only interested in vsync events coming after the first render + // event. Earlier ones are already missed. + Query after_first_render_event = + Query::EventTime() > + Query::Double(start_render_events.front()->timestamp); + TraceEventVector vsync_events; + FindEvents(analyzer, kVsyncEventName, after_first_render_event, + &vsync_events); + if (vsync_events.empty()) + return false; + + size_t found_vsync_index = 0; + size_t skipped_frame_count = 0; + for (const auto* event : start_render_events) { + const double start = event->timestamp; + + const TraceEvent* enqueue_frame_event = event->other_event; + if (!enqueue_frame_event) { + skipped_frame_count++; + continue; + } + const double enqueue_frame_duration = + enqueue_frame_event->timestamp - start; + + const TraceEvent* set_frame_event = enqueue_frame_event->other_event; + if (!set_frame_event) { + skipped_frame_count++; + continue; + } + const double set_frame_duration = + set_frame_event->timestamp - enqueue_frame_event->timestamp; + + const TraceEvent* get_frame_event = set_frame_event->other_event; + if (!get_frame_event) { + skipped_frame_count++; + continue; + } + const double get_frame_duration = + get_frame_event->timestamp - set_frame_event->timestamp; + + const TraceEvent* video_resource_event = get_frame_event->other_event; + if (!video_resource_event) { + skipped_frame_count++; + continue; + } + const double resource_ready_duration = + video_resource_event->timestamp - get_frame_event->timestamp; + + // We try to find the closest vsync event after video resource is ready. + const bool found_vsync = FindFirstOf( + vsync_events, + Query::EventTime() > Query::Double(video_resource_event->timestamp + + video_resource_event->duration), + found_vsync_index, &found_vsync_index); + if (!found_vsync) { + skipped_frame_count++; + continue; + } + const double vsync_duration = vsync_events[found_vsync_index]->timestamp - + video_resource_event->timestamp; + const double total_duration = + vsync_events[found_vsync_index]->timestamp - start; + + enqueue_frame_durations_.push_back(enqueue_frame_duration); + set_frame_durations_.push_back(set_frame_duration); + get_frame_durations_.push_back(get_frame_duration); + resource_ready_durations_.push_back(resource_ready_duration); + vsync_durations_.push_back(vsync_duration); + total_controlled_durations_.push_back(total_duration - + set_frame_duration); + total_durations_.push_back(total_duration); + } + + if (start_render_events.size() == skipped_frame_count) + return false; + + // Calculate the percentage by dividing by the number of frames received. + skipped_frame_percentage_ = + 100.0 * skipped_frame_count / start_render_events.size(); + + // |kVideoFrameSubmitterEventName| is in itself an ASYNC latency measurement + // from the point where the remote video decode is available (i.e. + // kStartRenderEventName) until the platform-dependent swap buffers, so by + // definition is larger than the |total_duration|. + TraceEventVector video_frame_submitter_events; + analyzer->FindEvents(Query::MatchAsyncBeginWithNext() && + Query::EventNameIs(kVideoFrameSubmitterEventName), + &video_frame_submitter_events); + for (const auto* event : video_frame_submitter_events) { + // kVideoFrameSubmitterEventName is divided into a BEGIN, a PAST and an + // END steps. AssociateAsyncBeginEndEvents paired BEGIN with PAST, but we + // have to get to the END. Note that if there's no intermediate PAST, it + // means this wasn't a remote feed VideoFrame, we should not have those in + // this test. If there's no END, then tracing was cut short. + if (!event->has_other_event() || + event->other_event->phase != TRACE_EVENT_PHASE_ASYNC_STEP_PAST || + !event->other_event->has_other_event()) { + continue; + } + const auto begin = event->timestamp; + const auto end = event->other_event->other_event->timestamp; + video_frame_submmitter_latencies_.push_back(end - begin); + } + + return true; + } + + void PrintResults(const std::string& video_codec) { + std::string smoothness_indicator = + test_config_.disable_render_smoothness_algorithm ? "_DisableSmoothness" + : ""; + std::string story = base::StringPrintf( + "%s_%dp%df%s", video_codec.c_str(), test_config_.height, + test_config_.fps, smoothness_indicator.c_str()); + auto reporter = SetUpReporter(story); + reporter.AddResult(kMetricSkippedFramesPercent, + base::StringPrintf("%.2lf", skipped_frame_percentage_)); + // We identify intervals in a way that can help us easily bisect the source + // of added latency in case of a regression. From these intervals, "Render + // Algorithm" can take random amount of times based on the vsync cycle it is + // closest to. Therefore, "Total Controlled Latency" refers to the total + // times without that section for semi-consistent results. + reporter.AddResultList(kMetricPassingToRenderAlgoLatencyUs, + VectorToString(enqueue_frame_durations_)); + reporter.AddResultList(kMetricRenderAlgoLatencyUs, + VectorToString(set_frame_durations_)); + reporter.AddResultList(kMetricCompositorPickingFrameLatencyUs, + VectorToString(get_frame_durations_)); + reporter.AddResultList(kMetricCompositorResourcePreparationLatencyUs, + VectorToString(resource_ready_durations_)); + reporter.AddResultList(kMetricVsyncLatencyUs, + VectorToString(vsync_durations_)); + reporter.AddResultList(kMetricTotalControlledLatencyUs, + VectorToString(total_controlled_durations_)); + reporter.AddResultList(kMetricTotalLatencyUs, + VectorToString(total_durations_)); + + reporter.AddResultList(kMetricPostDecodeToRasterLatencyUs, + VectorToString(video_frame_submmitter_latencies_)); + reporter.AddResultList(kMetricWebRtcDecodeLatencyUs, + VectorToString(webrtc_decode_latencies_)); + } + + VideoDisplayPerfTestConfig test_config_; + // Containers for test results. + double skipped_frame_percentage_ = 0; + std::vector<double> enqueue_frame_durations_; + std::vector<double> set_frame_durations_; + std::vector<double> get_frame_durations_; + std::vector<double> resource_ready_durations_; + std::vector<double> vsync_durations_; + std::vector<double> total_controlled_durations_; + std::vector<double> total_durations_; + + // These two put together represent the whole delay from encoded video frames + // to OS swap buffers call (or callback, depending on the platform). + std::vector<double> video_frame_submmitter_latencies_; + std::vector<double> webrtc_decode_latencies_; +}; + +// TODO(https://crbug.com/993020): Fix flakes on Windows bots. +#if defined(OS_WIN) +#define MAYBE_WebRtcVideoDisplayPerfBrowserTests \ + DISABLED_WebRtcVideoDisplayPerfBrowserTests +#else +#define MAYBE_WebRtcVideoDisplayPerfBrowserTests \ + WebRtcVideoDisplayPerfBrowserTests +#endif +INSTANTIATE_TEST_SUITE_P(MAYBE_WebRtcVideoDisplayPerfBrowserTests, + WebRtcVideoDisplayPerfBrowserTest, + testing::Combine(testing::Values(gfx::Size(1280, 720), + gfx::Size(1920, + 1080)), + testing::Values(30, 60), + testing::Bool())); + +IN_PROC_BROWSER_TEST_P(WebRtcVideoDisplayPerfBrowserTest, + MANUAL_TestVideoDisplayPerfVP9) { + TestVideoDisplayPerf("VP9"); +} + +#if BUILDFLAG(RTC_USE_H264) +IN_PROC_BROWSER_TEST_P(WebRtcVideoDisplayPerfBrowserTest, + MANUAL_TestVideoDisplayPerfH264) { + if (!base::FeatureList::IsEnabled( + blink::features::kWebRtcH264WithOpenH264FFmpeg)) { + LOG(WARNING) << "Run-time feature WebRTC-H264WithOpenH264FFmpeg disabled. " + "Skipping WebRtcVideoDisplayPerfBrowserTest.MANUAL_" + "TestVideoDisplayPerfH264 " + "(test \"OK\")"; + return; + } + TestVideoDisplayPerf("H264"); +} +#endif // BUILDFLAG(RTC_USE_H264) diff --git a/chromium/chrome/browser/media/webrtc/webrtc_video_high_bitrate_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_video_high_bitrate_browsertest.cc new file mode 100644 index 00000000000..a8eebc633b3 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_video_high_bitrate_browsertest.cc @@ -0,0 +1,163 @@ +// Copyright 2018 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/test/test_timeouts.h" +#include "base/time/time.h" +#include "chrome/browser/media/webrtc/test_stats_dictionary.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_switches.h" +#include "media/base/media_switches.h" +#include "testing/perf/perf_result_reporter.h" +#include "ui/gl/gl_switches.h" + +namespace { + +static const char kMainWebrtcTestHtmlPage[] = + "/webrtc/webrtc_video_display_perf_test.html"; +static const char kInboundRtp[] = "inbound-rtp"; +static const char kOutboundRtp[] = "outbound-rtp"; + +constexpr int kBitsPerByte = 8; + +constexpr char kMetricPrefixHighBitrate[] = "WebRtcHighBitrateVideo."; +constexpr char kMetricSendRateBitsPerS[] = "send_rate"; +constexpr char kMetricReceiveRateBitsPerS[] = "receive_rate"; + +perf_test::PerfResultReporter SetUpReporter(const std::string& story) { + perf_test::PerfResultReporter reporter(kMetricPrefixHighBitrate, story); + reporter.RegisterFyiMetric(kMetricSendRateBitsPerS, "bits/s"); + reporter.RegisterFyiMetric(kMetricReceiveRateBitsPerS, "bits/s"); + return reporter; +} + +// Sums up "RTC[In/Out]boundRTPStreamStats.bytes_[received/sent]" values. +double GetTotalRTPStreamBytes(content::TestStatsReportDictionary* report, + const char* type, + const char* media_type) { + DCHECK(type == kInboundRtp || type == kOutboundRtp); + const char* bytes_name = + (type == kInboundRtp) ? "bytesReceived" : "bytesSent"; + double total_bytes = 0.0; + report->ForEach([&type, &bytes_name, &media_type, + &total_bytes](const content::TestStatsDictionary& stats) { + if (stats.GetString("type") == type && + stats.GetString("mediaType") == media_type) { + total_bytes += stats.GetNumber(bytes_name); + } + }); + return total_bytes; +} + +double GetVideoBytesSent(content::TestStatsReportDictionary* report) { + return GetTotalRTPStreamBytes(report, kOutboundRtp, "video"); +} + +double GetVideoBytesReceived(content::TestStatsReportDictionary* report) { + return GetTotalRTPStreamBytes(report, kInboundRtp, "video"); +} + +} // anonymous namespace + +namespace content { + +// Tests the performance of WebRTC peer connection with high bitrate +// +// This test creates a WebRTC peer connection between two tabs and sets a very +// high target bitrate to observe any perf regressions/improvements for such +// cases. In order to achieve this, we use a fake codec that creates a dummy +// output for the given bitrate. +class WebRtcVideoHighBitrateBrowserTest : public WebRtcTestBase { + public: + void SetUpInProcessBrowserTestFixture() override { + DetectErrorsInJavaScript(); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + command_line->AppendSwitch(switches::kUseFakeCodecForPeerConnection); + command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); + command_line->AppendSwitch(switches::kUseFakeUIForMediaStream); + command_line->AppendSwitch(switches::kUseGpuInTests); + } + + protected: + void SetDefaultVideoTargetBitrate(content::WebContents* tab, + int bits_per_second) { + EXPECT_EQ("ok", ExecuteJavascript( + base::StringPrintf("setDefaultVideoTargetBitrate(%d)", + bits_per_second), + tab)); + } +}; + +IN_PROC_BROWSER_TEST_F(WebRtcVideoHighBitrateBrowserTest, + MANUAL_HighBitrateEncodeDecode) { + ASSERT_TRUE(embedded_test_server()->Start()); + ASSERT_GE(TestTimeouts::test_launcher_timeout().InSeconds(), 30) + << "This is a long-running test; you must specify " + "--test-launcher-timeout to have a value of at least 30000."; + ASSERT_GE(TestTimeouts::action_max_timeout().InSeconds(), 30) + << "This is a long-running test; you must specify " + "--ui-test-action-max-timeout to have a value of at least 30000."; + ASSERT_LT(TestTimeouts::action_max_timeout(), + TestTimeouts::test_launcher_timeout()) + << "action_max_timeout needs to be strictly-less-than " + "test_launcher_timeout"; + + content::WebContents* left_tab = + OpenPageAndGetUserMediaInNewTabWithConstraints( + embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage), + "{audio: true, video: true}"); + content::WebContents* right_tab = + OpenPageAndGetUserMediaInNewTabWithConstraints( + embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage), + "{audio: true, video: false}"); + SetupPeerconnectionWithLocalStream(left_tab); + SetupPeerconnectionWithLocalStream(right_tab); + const int target_bits_per_second = 80000; + SetDefaultVideoTargetBitrate(left_tab, target_bits_per_second); + SetDefaultVideoTargetBitrate(right_tab, target_bits_per_second); + NegotiateCall(left_tab, right_tab); + + // Run the connection a bit to ramp up. + test::SleepInJavascript(left_tab, 10000); + + scoped_refptr<TestStatsReportDictionary> sender_report = + GetStatsReportDictionary(left_tab); + const double video_bytes_sent_before = GetVideoBytesSent(sender_report.get()); + scoped_refptr<TestStatsReportDictionary> receiver_report = + GetStatsReportDictionary(right_tab); + const double video_bytes_received_before = + GetVideoBytesReceived(receiver_report.get()); + + // Collect stats. + const double duration_in_seconds = 5.0; + test::SleepInJavascript( + left_tab, duration_in_seconds * base::Time::kMillisecondsPerSecond); + + sender_report = GetStatsReportDictionary(left_tab); + const double video_bytes_sent_after = GetVideoBytesSent(sender_report.get()); + receiver_report = GetStatsReportDictionary(right_tab); + const double video_bytes_received_after = + GetVideoBytesReceived(receiver_report.get()); + + const double video_send_rate = + (video_bytes_sent_after - video_bytes_sent_before) / duration_in_seconds; + const double video_receive_rate = + (video_bytes_received_after - video_bytes_received_before) / + duration_in_seconds; + + auto reporter = SetUpReporter("baseline_story"); + reporter.AddResult(kMetricSendRateBitsPerS, video_send_rate * kBitsPerByte); + reporter.AddResult(kMetricReceiveRateBitsPerS, + video_receive_rate * kBitsPerByte); + + HangUp(left_tab); + HangUp(right_tab); +} + +} // namespace content diff --git a/chromium/chrome/browser/media/webrtc/webrtc_video_quality_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_video_quality_browsertest.cc new file mode 100644 index 00000000000..e09ff308c75 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_video_quality_browsertest.cc @@ -0,0 +1,367 @@ +// 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 <stddef.h> + +#include "base/base64.h" +#include "base/command_line.h" +#include "base/environment.h" +#include "base/files/file.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/path_service.h" +#include "base/process/launch.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/stringprintf.h" +#include "base/test/test_timeouts.h" +#include "base/threading/thread_restrictions.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/chrome_notification_types.h" +#include "chrome/browser/infobars/infobar_service.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "components/infobars/core/infobar.h" +#include "content/public/browser/notification_service.h" +#include "content/public/test/browser_test_utils.h" +#include "media/base/media_switches.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/python_utils.h" +#include "testing/perf/perf_test.h" +#include "third_party/blink/public/common/features.h" +#include "ui/gl/gl_switches.h" + +namespace { +std::string MakeLabel(const char* test_name, const std::string& video_codec) { + std::string codec_label = video_codec.empty() ? "" : "_" + video_codec; + return base::StringPrintf("%s%s", test_name, codec_label.c_str()); +} +} // namespace + +static const base::FilePath::CharType kFrameAnalyzerExecutable[] = +#if defined(OS_WIN) + FILE_PATH_LITERAL("frame_analyzer.exe"); +#else + FILE_PATH_LITERAL("frame_analyzer"); +#endif + +static const base::FilePath::CharType kCapturedYuvFileName[] = + FILE_PATH_LITERAL("captured_video.yuv"); +static const base::FilePath::CharType kCapturedWebmFileName[] = + FILE_PATH_LITERAL("captured_video.webm"); +static const char kMainWebrtcTestHtmlPage[] = + "/webrtc/webrtc_jsep01_test.html"; +static const char kCapturingWebrtcHtmlPage[] = + "/webrtc/webrtc_video_quality_test.html"; + +static const struct VideoQualityTestConfig { + const char* test_name; + int width; + int height; + const base::FilePath::CharType* reference_video; + const char* constraints; +} kVideoConfigurations[] = { + { "360p", 640, 360, + test::kReferenceFileName360p, + WebRtcTestBase::kAudioVideoCallConstraints360p }, + { "720p", 1280, 720, + test::kReferenceFileName720p, + WebRtcTestBase::kAudioVideoCallConstraints720p }, +}; + +// Test the video quality of the WebRTC output. +// +// Prerequisites: This test case must run on a machine with a chrome playing +// the video from the reference files located in GetReferenceFilesDir(). +// The file kReferenceY4mFileName.kY4mFileExtension is played using a +// FileVideoCaptureDevice and its sibling with kYuvFileExtension is used for +// comparison. +// +// You must also compile the frame_analyzer target before you run this +// test to get all the tools built. +// +// The test runs several custom binaries - rgba_to_i420 converter and +// frame_analyzer. Both tools can be found under third_party/webrtc/rtc_tools. +// The test also runs a stand alone Python implementation of a WebSocket server +// (pywebsocket) and a barcode_decoder script. +class WebRtcVideoQualityBrowserTest : public WebRtcTestBase, + public testing::WithParamInterface<VideoQualityTestConfig> { + public: + WebRtcVideoQualityBrowserTest() + : environment_(base::Environment::Create()) { + test_config_ = GetParam(); + } + + void SetUpInProcessBrowserTestFixture() override { + DetectErrorsInJavaScript(); // Look for errors in our rather complex js. + + ASSERT_TRUE(temp_working_dir_.CreateUniqueTempDir()); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + // Set up the command line option with the expected file name. We will check + // its existence in HasAllRequiredResources(). + webrtc_reference_video_y4m_ = test::GetReferenceFilesDir() + .Append(test_config_.reference_video) + .AddExtension(test::kY4mFileExtension); + command_line->AppendSwitchPath(switches::kUseFileForFakeVideoCapture, + webrtc_reference_video_y4m_); + command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); + + // The video playback will not work without a GPU, so force its use here. + command_line->AppendSwitch(switches::kUseGpuInTests); + } + + // Writes the captured video to a webm file. + void WriteCapturedWebmVideo(content::WebContents* capturing_tab, + const base::FilePath& webm_video_filename) { + std::string base64_encoded_video = + ExecuteJavascript("getRecordedVideoAsBase64()", capturing_tab); + std::string recorded_video; + ASSERT_TRUE(base::Base64Decode(base64_encoded_video, &recorded_video)); + base::File video_file(webm_video_filename, + base::File::FLAG_CREATE | base::File::FLAG_WRITE); + size_t written = + video_file.Write(0, recorded_video.c_str(), recorded_video.length()); + ASSERT_EQ(recorded_video.length(), written); + } + + // Runs ffmpeg on the captured webm video and writes it to a yuv video file. + bool RunWebmToI420Converter(const base::FilePath& webm_video_filename, + const base::FilePath& yuv_video_filename, + const int width, + const int height) { + base::FilePath path_to_ffmpeg = test::GetToolForPlatform("ffmpeg"); + if (!base::PathExists(path_to_ffmpeg)) { + LOG(ERROR) << "Missing ffmpeg: should be in " << path_to_ffmpeg.value(); + return false; + } + + // Set up ffmpeg to output at a certain resolution (-s) and bitrate (-b:v). + // This is needed because WebRTC is free to start the call at a lower + // resolution before ramping up. Without these flags, ffmpeg would output a + // video in the inital lower resolution, causing the SSIM and PSNR results + // to become meaningless. + base::CommandLine ffmpeg_command(path_to_ffmpeg); + ffmpeg_command.AppendArg("-i"); + ffmpeg_command.AppendArgPath(webm_video_filename); + ffmpeg_command.AppendArg("-s"); + ffmpeg_command.AppendArg(base::StringPrintf("%dx%d", width, height)); + ffmpeg_command.AppendArg("-b:v"); + ffmpeg_command.AppendArg(base::StringPrintf("%d", 120 * width * height)); + ffmpeg_command.AppendArgPath(yuv_video_filename); + + // We produce an output file that will later be used as an input to the + // barcode decoder and frame analyzer tools. + DVLOG(0) << "Running " << ffmpeg_command.GetCommandLineString(); + std::string result; + bool ok = base::GetAppOutputAndError(ffmpeg_command, &result); + DVLOG(0) << "Output was:\n\n" << result; + return ok; + } + + // Compares the |captured_video_filename| with the |reference_video_filename|. + // + // The barcode decoder decodes the captured video containing barcodes overlaid + // into every frame of the video. It produces a set of PNG images. + // The frames should be of size |width| x |height|. + // All measurements calculated are printed as perf parsable numbers to stdout. + bool CompareVideosAndPrintResult( + const std::string& test_label, + int width, + int height, + const base::FilePath& captured_video_filename, + const base::FilePath& reference_video_filename) { + base::FilePath path_to_analyzer = base::MakeAbsoluteFilePath( + GetBrowserDir().Append(kFrameAnalyzerExecutable)); + base::FilePath path_to_compare_script = GetSourceDir().Append( + FILE_PATH_LITERAL("third_party/webrtc/rtc_tools/compare_videos.py")); + + if (!base::PathExists(path_to_analyzer)) { + LOG(ERROR) << "Missing frame analyzer: should be in " + << path_to_analyzer.value() + << ". Try building the frame_analyzer target."; + return false; + } + if (!base::PathExists(path_to_compare_script)) { + LOG(ERROR) << "Missing video compare script: should be in " + << path_to_compare_script.value(); + return false; + } + + // Note: don't append switches to this command since it will mess up the + // -u in the python invocation! + base::CommandLine compare_command(base::CommandLine::NO_PROGRAM); + EXPECT_TRUE(GetPythonCommand(&compare_command)); + + compare_command.AppendArgPath(path_to_compare_script); + compare_command.AppendArg("--label=" + test_label); + compare_command.AppendArg("--ref_video"); + compare_command.AppendArgPath(reference_video_filename); + compare_command.AppendArg("--test_video"); + compare_command.AppendArgPath(captured_video_filename); + compare_command.AppendArg("--frame_analyzer"); + compare_command.AppendArgPath(path_to_analyzer); + compare_command.AppendArg("--yuv_frame_width"); + compare_command.AppendArg(base::NumberToString(width)); + compare_command.AppendArg("--yuv_frame_height"); + compare_command.AppendArg(base::NumberToString(height)); + + DVLOG(0) << "Running " << compare_command.GetCommandLineString(); + std::string output; + bool ok = base::GetAppOutput(compare_command, &output); + + // Print to stdout to ensure the perf numbers are parsed properly by the + // buildbot step. The tool should print a handful RESULT lines. + printf("Output was:\n\n%s\n", output.c_str()); + bool has_result_lines = output.find("RESULT") != std::string::npos; + if (!ok || !has_result_lines) { + LOG(ERROR) << "Failed to compare videos; see output above to see what " + << "the error was."; + return false; + } + return true; + } + + void TestVideoQuality(const std::string& video_codec, + bool prefer_hw_video_codec) { + ASSERT_GE(TestTimeouts::test_launcher_timeout().InSeconds(), 150) + << "This is a long-running test; you must specify " + "--test-launcher-timeout to have a value of at least 150000."; + ASSERT_GE(TestTimeouts::action_max_timeout().InSeconds(), 150) + << "This is a long-running test; you must specify " + "--ui-test-action-max-timeout to have a value of at least 150000."; + ASSERT_LT(TestTimeouts::action_max_timeout(), + TestTimeouts::test_launcher_timeout()) + << "action_max_timeout needs to be strictly-less-than " + "test_launcher_timeout"; + ASSERT_TRUE(test::HasReferenceFilesInCheckout()); + ASSERT_TRUE(embedded_test_server()->Start()); + + content::WebContents* left_tab = + OpenPageAndGetUserMediaInNewTabWithConstraints( + embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage), + test_config_.constraints); + content::WebContents* right_tab = + OpenPageAndGetUserMediaInNewTabWithConstraints( + embedded_test_server()->GetURL(kCapturingWebrtcHtmlPage), + test_config_.constraints); + + SetupPeerconnectionWithLocalStream(left_tab); + SetupPeerconnectionWithLocalStream(right_tab); + + if (!video_codec.empty()) { + SetDefaultVideoCodec(left_tab, video_codec, prefer_hw_video_codec); + SetDefaultVideoCodec(right_tab, video_codec, prefer_hw_video_codec); + } + NegotiateCall(left_tab, right_tab); + + // Poll slower here to avoid flooding the log with messages: capturing and + // sending frames take quite a bit of time. + int polling_interval_msec = 1000; + + EXPECT_TRUE(test::PollingWaitUntil("doneFrameCapturing()", "done-capturing", + right_tab, polling_interval_msec)); + + HangUp(left_tab); + + WriteCapturedWebmVideo(right_tab, + GetWorkingDir().Append(kCapturedWebmFileName)); + + // Shut everything down to avoid having the javascript race with the + // analysis tools. For instance, dont have console log printouts interleave + // with the RESULT lines from the analysis tools (crbug.com/323200). + chrome::CloseWebContents(browser(), left_tab, false); + chrome::CloseWebContents(browser(), right_tab, false); + + RunWebmToI420Converter(GetWorkingDir().Append(kCapturedWebmFileName), + GetWorkingDir().Append(kCapturedYuvFileName), + test_config_.width, test_config_.height); + + ASSERT_TRUE(CompareVideosAndPrintResult( + MakeLabel(test_config_.test_name, video_codec), test_config_.width, + test_config_.height, GetWorkingDir().Append(kCapturedYuvFileName), + test::GetReferenceFilesDir() + .Append(test_config_.reference_video) + .AddExtension(test::kYuvFileExtension))); + } + + protected: + VideoQualityTestConfig test_config_; + + base::FilePath GetWorkingDir() { return temp_working_dir_.GetPath(); } + + private: + base::FilePath GetSourceDir() { + base::FilePath source_dir; + base::PathService::Get(base::DIR_SOURCE_ROOT, &source_dir); + return source_dir; + } + + base::FilePath GetBrowserDir() { + base::FilePath browser_dir; + EXPECT_TRUE(base::PathService::Get(base::DIR_MODULE, &browser_dir)); + return browser_dir; + } + + std::unique_ptr<base::Environment> environment_; + base::FilePath webrtc_reference_video_y4m_; + base::ScopedTempDir temp_working_dir_; +}; + +INSTANTIATE_TEST_SUITE_P(WebRtcVideoQualityBrowserTests, + WebRtcVideoQualityBrowserTest, + testing::ValuesIn(kVideoConfigurations)); + +IN_PROC_BROWSER_TEST_P(WebRtcVideoQualityBrowserTest, + MANUAL_TestVideoQualityVp8) { + base::ScopedAllowBlockingForTesting allow_blocking; + TestVideoQuality("VP8", false /* prefer_hw_video_codec */); +} + +// Flaky on windows. +// TODO(crbug.com/1008766): re-enable when flakiness is investigated, diagnosed +// and resolved. +#if defined(OS_WIN) +#define MAYBE_MANUAL_TestVideoQualityVp9 DISABLED_MANUAL_TestVideoQualityVp9 +#else +#define MAYBE_MANUAL_TestVideoQualityVp9 MANUAL_TestVideoQualityVp9 +#endif +IN_PROC_BROWSER_TEST_P(WebRtcVideoQualityBrowserTest, + MAYBE_MANUAL_TestVideoQualityVp9) { + base::ScopedAllowBlockingForTesting allow_blocking; + TestVideoQuality("VP9", true /* prefer_hw_video_codec */); +} + +#if BUILDFLAG(RTC_USE_H264) + +// Flaky on mac: crbug.com/754684 +#if defined(OS_MACOSX) +#define MAYBE_MANUAL_TestVideoQualityH264 DISABLED_MANUAL_TestVideoQualityH264 +#else +#define MAYBE_MANUAL_TestVideoQualityH264 MANUAL_TestVideoQualityH264 +#endif + +IN_PROC_BROWSER_TEST_P(WebRtcVideoQualityBrowserTest, + MAYBE_MANUAL_TestVideoQualityH264) { + base::ScopedAllowBlockingForTesting allow_blocking; + // Only run test if run-time feature corresponding to |rtc_use_h264| is on. + if (!base::FeatureList::IsEnabled( + blink::features::kWebRtcH264WithOpenH264FFmpeg)) { + LOG(WARNING) << "Run-time feature WebRTC-H264WithOpenH264FFmpeg disabled. " + "Skipping WebRtcVideoQualityBrowserTest.MANUAL_TestVideoQualityH264 " + "(test \"OK\")"; + return; + } + TestVideoQuality("H264", true /* prefer_hw_video_codec */); +} + +#endif // BUILDFLAG(RTC_USE_H264) diff --git a/chromium/chrome/browser/media/webrtc/webrtc_webcam_browsertest.cc b/chromium/chrome/browser/media/webrtc/webrtc_webcam_browsertest.cc new file mode 100644 index 00000000000..db5eeb4e8eb --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_webcam_browsertest.cc @@ -0,0 +1,118 @@ +// Copyright 2014 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 "build/build_config.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" +#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/common/content_features.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "media/base/media_switches.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "testing/gtest/include/gtest/gtest-param-test.h" + +static const char kMainWebrtcTestHtmlPage[] = + "/webrtc/webrtc_jsep01_test.html"; + +enum class TargetVideoCaptureImplementation { + DEFAULT, +#if defined(OS_WIN) + WIN_MEDIA_FOUNDATION +#endif +}; + +const TargetVideoCaptureImplementation kTargetVideoCaptureImplementations[] = { + TargetVideoCaptureImplementation::DEFAULT, +#if defined(OS_WIN) + TargetVideoCaptureImplementation::WIN_MEDIA_FOUNDATION +#endif +}; + +// These tests runs on real webcams and ensure WebRTC can acquire webcams +// correctly. They will do nothing if there are no webcams on the system. +// The webcam on the system must support up to 1080p, or the test will fail. +// This test is excellent for testing the various capture paths of WebRTC +// on all desktop platforms. +class WebRtcWebcamBrowserTest + : public WebRtcTestBase, + public testing::WithParamInterface<TargetVideoCaptureImplementation> { + public: + WebRtcWebcamBrowserTest() { +#if defined(OS_WIN) + if (GetParam() == TargetVideoCaptureImplementation::WIN_MEDIA_FOUNDATION) { + scoped_feature_list_.InitAndEnableFeature( + media::kMediaFoundationVideoCapture); + } else { + scoped_feature_list_.InitAndDisableFeature( + media::kMediaFoundationVideoCapture); + } +#endif + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + EXPECT_FALSE(command_line->HasSwitch( + switches::kUseFakeDeviceForMediaStream)); + EXPECT_FALSE(command_line->HasSwitch( + switches::kUseFakeUIForMediaStream)); + } + + protected: + void SetUpInProcessBrowserTestFixture() override { + DetectErrorsInJavaScript(); // Look for errors in our rather complex js. + } + + std::string GetUserMediaAndGetStreamSize(content::WebContents* tab, + const std::string& constraints) { + std::string actual_stream_size; + if (GetUserMediaWithSpecificConstraintsAndAcceptIfPrompted(tab, + constraints)) { + StartDetectingVideo(tab, "local-view"); + if (WaitForVideoToPlay(tab)) + actual_stream_size = GetStreamSize(tab, "local-view"); + CloseLastLocalStream(tab); + } + return actual_stream_size; + } + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +// This test is manual because the test results can vary heavily depending on +// which webcam or drivers you have on the system. +IN_PROC_BROWSER_TEST_P(WebRtcWebcamBrowserTest, + MANUAL_TestAcquiringAndReacquiringWebcam) { + ASSERT_TRUE(embedded_test_server()->Start()); + GURL url(embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage)); + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* tab = + browser()->tab_strip_model()->GetActiveWebContents(); + + if (!content::IsWebcamAvailableOnSystem(tab)) { + DVLOG(0) << "No webcam found on bot: skipping..."; + return; + } + + EXPECT_EQ("320x240", + GetUserMediaAndGetStreamSize(tab, kVideoCallConstraintsQVGA)); + EXPECT_EQ("640x480", + GetUserMediaAndGetStreamSize(tab, kVideoCallConstraintsVGA)); + EXPECT_EQ("640x360", + GetUserMediaAndGetStreamSize(tab, kVideoCallConstraints360p)); + EXPECT_EQ("1280x720", + GetUserMediaAndGetStreamSize(tab, kVideoCallConstraints720p)); + EXPECT_EQ("1920x1080", + GetUserMediaAndGetStreamSize(tab, kVideoCallConstraints1080p)); +} + +INSTANTIATE_TEST_SUITE_P(WebRtcWebcamBrowserTests, + WebRtcWebcamBrowserTest, + testing::ValuesIn(kTargetVideoCaptureImplementations)); diff --git a/chromium/chrome/browser/media/webrtc/window_icon_util.h b/chromium/chrome/browser/media/webrtc/window_icon_util.h new file mode 100644 index 00000000000..5bbb199bb56 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/window_icon_util.h @@ -0,0 +1,14 @@ +// Copyright 2016 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 CHROME_BROWSER_MEDIA_WEBRTC_WINDOW_ICON_UTIL_H_ +#define CHROME_BROWSER_MEDIA_WEBRTC_WINDOW_ICON_UTIL_H_ + +#include "content/public/browser/desktop_media_id.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_capture_options.h" +#include "ui/gfx/image/image_skia.h" + +gfx::ImageSkia GetWindowIcon(content::DesktopMediaID id); + +#endif // CHROME_BROWSER_MEDIA_WEBRTC_WINDOW_ICON_UTIL_H_ diff --git a/chromium/chrome/browser/media/webrtc/window_icon_util_chromeos.cc b/chromium/chrome/browser/media/webrtc/window_icon_util_chromeos.cc new file mode 100644 index 00000000000..4a826a5b573 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/window_icon_util_chromeos.cc @@ -0,0 +1,21 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/window_icon_util.h" + +#include "content/public/browser/desktop_media_id.h" +#include "ui/aura/client/aura_constants.h" +#include "ui/aura/window.h" + +gfx::ImageSkia GetWindowIcon(content::DesktopMediaID id) { + DCHECK_EQ(content::DesktopMediaID::TYPE_WINDOW, id.type); + aura::Window* window = content::DesktopMediaID::GetNativeWindowById(id); + if (!window) + return gfx::ImageSkia(); + + gfx::ImageSkia* image = window->GetProperty(aura::client::kWindowIconKey); + if (!image) + image = window->GetProperty(aura::client::kAppIconKey); + return image ? *image : gfx::ImageSkia(); +} diff --git a/chromium/chrome/browser/media/webrtc/window_icon_util_mac.mm b/chromium/chrome/browser/media/webrtc/window_icon_util_mac.mm new file mode 100644 index 00000000000..0f4b122633b --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/window_icon_util_mac.mm @@ -0,0 +1,78 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/window_icon_util.h" + +#import <Cocoa/Cocoa.h> + +#include "base/mac/foundation_util.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/stl_util.h" +#include "third_party/libyuv/include/libyuv/convert_argb.h" + +gfx::ImageSkia GetWindowIcon(content::DesktopMediaID id) { + DCHECK(id.type == content::DesktopMediaID::TYPE_WINDOW); + + CGWindowID ids[1]; + ids[0] = id.id; + base::ScopedCFTypeRef<CFArrayRef> window_id_array(CFArrayCreate( + nullptr, reinterpret_cast<const void**>(&ids), base::size(ids), nullptr)); + base::ScopedCFTypeRef<CFArrayRef> window_array( + CGWindowListCreateDescriptionFromArray(window_id_array)); + if (!window_array || 0 == CFArrayGetCount(window_array)) { + return gfx::ImageSkia(); + } + + CFDictionaryRef window = base::mac::CFCastStrict<CFDictionaryRef>( + CFArrayGetValueAtIndex(window_array, 0)); + CFNumberRef pid_ref = + base::mac::GetValueFromDictionary<CFNumberRef>(window, kCGWindowOwnerPID); + + int pid; + CFNumberGetValue(pid_ref, kCFNumberIntType, &pid); + + NSImage* icon_image = + [[NSRunningApplication runningApplicationWithProcessIdentifier:pid] icon]; + + // Icon's NSImage defaults to the smallest which can be only 32x32. + NSRect proposed_rect = NSMakeRect(0, 0, 128, 128); + CGImageRef cg_icon_image = + [icon_image CGImageForProposedRect:&proposed_rect context:nil hints:nil]; + + // 4 components of 8 bits each. + if (CGImageGetBitsPerPixel(cg_icon_image) != 32 || + CGImageGetBitsPerComponent(cg_icon_image) != 8) { + return gfx::ImageSkia(); + } + + // Premultiplied alpha and last (alpha channel is next to the blue channel) + if (CGImageGetAlphaInfo(cg_icon_image) != kCGImageAlphaPremultipliedLast) { + return gfx::ImageSkia(); + } + + // Ensure BGR like. + int byte_order = CGImageGetBitmapInfo(cg_icon_image) & kCGBitmapByteOrderMask; + if (byte_order != kCGBitmapByteOrderDefault && + byte_order != kCGBitmapByteOrder32Big) { + return gfx::ImageSkia(); + } + + CGDataProviderRef provider = CGImageGetDataProvider(cg_icon_image); + base::ScopedCFTypeRef<CFDataRef> cf_data(CGDataProviderCopyData(provider)); + + int width = CGImageGetWidth(cg_icon_image); + int height = CGImageGetHeight(cg_icon_image); + int src_stride = CGImageGetBytesPerRow(cg_icon_image); + const uint8_t* src_data = CFDataGetBytePtr(cf_data); + + SkBitmap result; + result.allocN32Pixels(width, height, false /* no-premultiplied */); + + uint8_t* pixels_data = reinterpret_cast<uint8_t*>(result.getPixels()); + + libyuv::ABGRToARGB(src_data, src_stride, pixels_data, result.rowBytes(), + width, height); + + return gfx::ImageSkia::CreateFrom1xBitmap(result); +} diff --git a/chromium/chrome/browser/media/webrtc/window_icon_util_ozone.cc b/chromium/chrome/browser/media/webrtc/window_icon_util_ozone.cc new file mode 100644 index 00000000000..f519648c489 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/window_icon_util_ozone.cc @@ -0,0 +1,17 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/window_icon_util.h" + +#include "content/public/browser/desktop_media_id.h" +#include "ui/aura/client/aura_constants.h" + +gfx::ImageSkia GetWindowIcon(content::DesktopMediaID id) { + DCHECK_EQ(content::DesktopMediaID::TYPE_WINDOW, id.type); + // TODO(tonikitoo): can we make the implementation of + // chrome/browser/media/webrtc/window_icon_util_chromeos.cc generic + // enough so we can reuse it here? + NOTIMPLEMENTED(); + return gfx::ImageSkia(); +} diff --git a/chromium/chrome/browser/media/webrtc/window_icon_util_win.cc b/chromium/chrome/browser/media/webrtc/window_icon_util_win.cc new file mode 100644 index 00000000000..0a14a8afc7c --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/window_icon_util_win.cc @@ -0,0 +1,40 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/window_icon_util.h" + +#include "ui/gfx/icon_util.h" + +gfx::ImageSkia GetWindowIcon(content::DesktopMediaID id) { + DCHECK(id.type == content::DesktopMediaID::TYPE_WINDOW); + + HWND hwnd = reinterpret_cast<HWND>(id.id); + HICON icon_handle = 0; + + SendMessageTimeout(hwnd, WM_GETICON, ICON_BIG, 0, SMTO_ABORTIFHUNG, 5, + (PDWORD_PTR)&icon_handle); + if (!icon_handle) + icon_handle = reinterpret_cast<HICON>(GetClassLongPtr(hwnd, GCLP_HICON)); + + if (!icon_handle) { + SendMessageTimeout(hwnd, WM_GETICON, ICON_SMALL, 0, SMTO_ABORTIFHUNG, 5, + (PDWORD_PTR)&icon_handle); + } + if (!icon_handle) { + SendMessageTimeout(hwnd, WM_GETICON, ICON_SMALL2, 0, SMTO_ABORTIFHUNG, 5, + (PDWORD_PTR)&icon_handle); + } + if (!icon_handle) + icon_handle = reinterpret_cast<HICON>(GetClassLongPtr(hwnd, GCLP_HICONSM)); + + if (!icon_handle) + return gfx::ImageSkia(); + + const SkBitmap icon_bitmap = IconUtil::CreateSkBitmapFromHICON(icon_handle); + + if (icon_bitmap.isNull()) + return gfx::ImageSkia(); + + return gfx::ImageSkia::CreateFrom1xBitmap(icon_bitmap); +} diff --git a/chromium/chrome/browser/media/webrtc/window_icon_util_x11.cc b/chromium/chrome/browser/media/webrtc/window_icon_util_x11.cc new file mode 100644 index 00000000000..272d0109da6 --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/window_icon_util_x11.cc @@ -0,0 +1,74 @@ +// Copyright 2016 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 "chrome/browser/media/webrtc/window_icon_util.h" + +#include "ui/base/x/x11_util.h" +#include "ui/gfx/x/x11.h" +#include "ui/gfx/x/x11_atom_cache.h" +#include "ui/gfx/x/x11_error_tracker.h" +#include "ui/gfx/x/x11_types.h" + +gfx::ImageSkia GetWindowIcon(content::DesktopMediaID id) { + DCHECK(id.type == content::DesktopMediaID::TYPE_WINDOW); + + Display* display = gfx::GetXDisplay(); + Atom property = gfx::GetAtom("_NET_WM_ICON"); + Atom actual_type; + int actual_format; + unsigned long bytes_after; // NOLINT: type required by XGetWindowProperty + unsigned long size; + long* data; + + // The |error_tracker| essentially provides an empty X error handler for + // the call of XGetWindowProperty. The motivation is to guard against crash + // for any reason that XGetWindowProperty fails. For example, at the time that + // XGetWindowProperty is called, the window handler (a.k.a |id.id|) may + // already be invalid due to the fact that the end user has closed the + // corresponding window, etc. + std::unique_ptr<gfx::X11ErrorTracker> error_tracker( + new gfx::X11ErrorTracker()); + int status = XGetWindowProperty(display, id.id, property, 0L, ~0L, x11::False, + AnyPropertyType, &actual_type, &actual_format, + &size, &bytes_after, + reinterpret_cast<unsigned char**>(&data)); + error_tracker.reset(); + + if (status != x11::Success) { + return gfx::ImageSkia(); + } + + // The format of |data| is concatenation of sections like + // [width, height, pixel data of size width * height], and the total bytes + // number of |data| is |size|. And here we are picking the largest icon. + int width = 0; + int height = 0; + int start = 0; + int i = 0; + while (i + 1 < static_cast<int>(size)) { + if ((i == 0 || static_cast<int>(data[i] * data[i + 1]) > width * height) && + (i + 1 + data[i] * data[i + 1] < static_cast<int>(size))) { + width = static_cast<int>(data[i]); + height = static_cast<int>(data[i + 1]); + start = i + 2; + } + i = i + 2 + static_cast<int>(data[i] * data[i + 1]); + } + + SkBitmap result; + SkImageInfo info = SkImageInfo::MakeN32(width, height, kUnpremul_SkAlphaType); + result.allocPixels(info); + + uint32_t* pixels_data = reinterpret_cast<uint32_t*>(result.getPixels()); + + for (long y = 0; y < height; ++y) { + for (long x = 0; x < width; ++x) { + pixels_data[result.rowBytesAsPixels() * y + x] = + static_cast<uint32_t>(data[start + width * y + x]); + } + } + + XFree(data); + return gfx::ImageSkia::CreateFrom1xBitmap(result); +} |