Chromium Code Reviews| Index: chrome/browser/media/desktop_capture_access_handler.cc |
| diff --git a/chrome/browser/media/desktop_capture_access_handler.cc b/chrome/browser/media/desktop_capture_access_handler.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..241584b5029649e183a266afcdfb3925d8dec146 |
| --- /dev/null |
| +++ b/chrome/browser/media/desktop_capture_access_handler.cc |
| @@ -0,0 +1,409 @@ |
| +// 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/desktop_capture_access_handler.h" |
| + |
| +#include "base/command_line.h" |
| +#include "base/strings/string_util.h" |
| +#include "base/strings/utf_string_conversions.h" |
| +#include "chrome/browser/media/desktop_streams_registry.h" |
| +#include "chrome/browser/media/media_capture_devices_dispatcher.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_switches.h" |
| +#include "chrome/grit/generated_resources.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/common/media_stream_request.h" |
| +#include "extensions/common/constants.h" |
| +#include "media/audio/audio_manager_base.h" |
| +#include "net/base/net_util.h" |
| +#include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h" |
| +#include "ui/base/l10n/l10n_util.h" |
| + |
| +#if defined(ENABLE_EXTENSIONS) |
|
Sergey Ulanov
2015/06/01 23:39:17
This file doesn't need to be compiled when extensi
changbin
2015/06/02 14:20:09
Thanks! Remove ifdefs check from desktop_capture_a
|
| +#include "extensions/browser/app_window/app_window.h" |
| +#include "extensions/browser/app_window/app_window_registry.h" |
| +#include "extensions/common/extension.h" |
| +#endif |
| + |
| +using content::BrowserThread; |
| + |
| +namespace { |
| + |
| +#if defined(ENABLE_EXTENSIONS) |
| +bool IsExtensionWhitelistedForScreenCapture( |
| + const extensions::Extension* extension) { |
| +#if defined(OS_CHROMEOS) |
| + std::string hash = base::SHA1HashString(extension->id()); |
| + std::string hex_hash = base::HexEncode(hash.c_str(), hash.length()); |
| + |
| + // crbug.com/446688 |
| + return hex_hash == "4F25792AF1AA7483936DE29C07806F203C7170A0" || |
| + hex_hash == "BD8781D757D830FC2E85470A1B6E8A718B7EE0D9" || |
| + hex_hash == "4AC2B6C63C6480D150DFDA13E4A5956EB1D0DDBB" || |
| + hex_hash == "81986D4F846CEDDDB962643FA501D1780DD441BB"; |
| +#else |
| + return false; |
| +#endif // defined(OS_CHROMEOS) |
| +} |
| +#endif // defined(ENABLE_EXTENSIONS) |
| + |
| +bool IsBuiltInExtension(const GURL& origin) { |
| + return |
| + // Feedback Extension. |
| + origin.spec() == "chrome-extension://gfdkimpbcpahaombhbimeihdjnejgicl/"; |
| +} |
| + |
| +// Returns true of the security origin is associated with casting. |
| +bool IsOriginForCasting(const GURL& origin) { |
| + // Whitelisted tab casting extensions. |
| + return |
| + // Dev |
| + origin.spec() == "chrome-extension://enhhojjnijigcajfphajepfemndkmdlo/" || |
| + // Canary |
| + origin.spec() == "chrome-extension://hfaagokkkhdbgiakmmlclaapfelnkoah/" || |
| + // Beta (internal) |
| + origin.spec() == "chrome-extension://fmfcbgogabcbclcofgocippekhfcmgfj/" || |
| + // Google Cast Beta |
| + origin.spec() == "chrome-extension://dliochdbjfkdbacpmhlcpmleaejidimm/" || |
| + // Google Cast Stable |
| + origin.spec() == "chrome-extension://boadgeojelhgndaghljhdicfkmllpafd/" || |
| + // http://crbug.com/457908 |
| + origin.spec() == "chrome-extension://ekpaaapppgpmolpcldedioblbkmijaca/" || |
| + origin.spec() == "chrome-extension://fjhoaacokmgbjemoflkofnenfaiekifl/"; |
| +} |
| + |
| +// 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 defined(ENABLE_EXTENSIONS) |
| + if (extension) { |
| + title = extension->name(); |
| + return base::UTF8ToUTF16(title); |
| + } |
| +#endif |
| + GURL url = web_contents->GetURL(); |
| + title = url.SchemeIsSecure() ? net::GetHostAndOptionalPort(url) |
| + : url.GetOrigin().spec(); |
| + return base::UTF8ToUTF16(title); |
| +} |
| + |
| +// 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. |
| +scoped_ptr<content::MediaStreamUI> GetDevicesForDesktopCapture( |
| + content::MediaStreamDevices* devices, |
| + content::DesktopMediaID media_id, |
| + bool capture_audio, |
| + bool display_notification, |
| + const base::string16& application_title, |
| + const base::string16& registered_extension_name) { |
| + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| + scoped_ptr<content::MediaStreamUI> ui; |
| + |
| + // Add selected desktop source to the list. |
| + devices->push_back(content::MediaStreamDevice( |
| + content::MEDIA_DESKTOP_VIDEO_CAPTURE, media_id.ToString(), "Screen")); |
| + if (capture_audio) { |
| + // Use the special loopback device ID for system audio capture. |
| + devices->push_back(content::MediaStreamDevice( |
| + content::MEDIA_DESKTOP_AUDIO_CAPTURE, |
| + media::AudioManagerBase::kLoopbackInputDeviceId, "System Audio")); |
| + } |
| + |
| + // If required, register to display the notification for stream capture. |
| + if (display_notification) { |
| + if (application_title == registered_extension_name) { |
| + ui = ScreenCaptureNotificationUI::Create(l10n_util::GetStringFUTF16( |
| + IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_TEXT, application_title)); |
| + } else { |
| + ui = ScreenCaptureNotificationUI::Create(l10n_util::GetStringFUTF16( |
| + IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_TEXT_DELEGATED, |
| + registered_extension_name, application_title)); |
| + } |
| + } |
| + |
| + return ui.Pass(); |
| +} |
| + |
| +#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 (extensions::AppWindowRegistry::AppWindowList::const_iterator iter = |
| + window_list.begin(); |
| + iter != window_list.end(); ++iter) { |
| + if ((*iter)->web_contents() == web_contents) |
| + return (*iter)->GetNativeWindow(); |
| + } |
| + |
| + return NULL; |
| +} |
| +#endif |
| + |
| +} // namespace |
| + |
| +DesktopCaptureAccessHandler::DesktopCaptureAccessHandler() { |
| +} |
| + |
| +DesktopCaptureAccessHandler::~DesktopCaptureAccessHandler() { |
| +} |
| + |
| +void DesktopCaptureAccessHandler::ProcessScreenCaptureAccessRequest( |
| + content::WebContents* web_contents, |
| + const content::MediaStreamRequest& request, |
| + const content::MediaResponseCallback& callback, |
| + const extensions::Extension* extension) { |
| + content::MediaStreamDevices devices; |
| + scoped_ptr<content::MediaStreamUI> ui; |
| + |
| + DCHECK_EQ(request.video_type, content::MEDIA_DESKTOP_VIDEO_CAPTURE); |
| + |
| + 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 component_extension = false; |
| +#if defined(ENABLE_EXTENSIONS) |
| + component_extension = |
| + extension && extension->location() == extensions::Manifest::COMPONENT; |
| +#endif |
| + |
| + bool screen_capture_enabled = |
| + base::CommandLine::ForCurrentProcess()->HasSwitch( |
| + switches::kEnableUserMediaScreenCapturing); |
| +#if defined(ENABLE_EXTENSIONS) |
| + screen_capture_enabled |= IsOriginForCasting(request.security_origin) || |
| + IsExtensionWhitelistedForScreenCapture(extension) || |
| + IsBuiltInExtension(request.security_origin); |
| +#endif |
| + |
| + const bool origin_is_secure = |
| + request.security_origin.SchemeIsSecure() || |
| + request.security_origin.SchemeIs(extensions::kExtensionScheme) || |
| + 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. |
| + content::MediaStreamRequestResult result = |
| + content::MEDIA_DEVICE_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::ShowMessageBox() starts a nested message loop which may allow |
| + // |web_contents| to be destroyed on the UI thread before the message 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; |
| + |
| + bool whitelisted_extension = false; |
| +#if defined(ENABLE_EXTENSIONS) |
| + whitelisted_extension = IsExtensionWhitelistedForScreenCapture(extension); |
| +#endif |
| + |
| + // For whitelisted or component extensions, bypass message box. |
| + bool user_approved = false; |
| + if (!whitelisted_extension && !component_extension) { |
| + base::string16 application_name = |
| + base::UTF8ToUTF16(request.security_origin.spec()); |
| +#if defined(ENABLE_EXTENSIONS) |
| + if (extension) |
| + application_name = base::UTF8ToUTF16(extension->name()); |
| +#endif |
| + base::string16 confirmation_text = l10n_util::GetStringFUTF16( |
| + request.audio_type == content::MEDIA_NO_SERVICE |
| + ? IDS_MEDIA_SCREEN_CAPTURE_CONFIRMATION_TEXT |
| + : IDS_MEDIA_SCREEN_AND_AUDIO_CAPTURE_CONFIRMATION_TEXT, |
| + application_name); |
| + chrome::MessageBoxResult result = chrome::ShowMessageBox( |
| + parent_window, |
| + l10n_util::GetStringFUTF16( |
| + IDS_MEDIA_SCREEN_CAPTURE_CONFIRMATION_TITLE, application_name), |
| + confirmation_text, chrome::MESSAGE_BOX_TYPE_QUESTION); |
| + user_approved = (result == chrome::MESSAGE_BOX_RESULT_YES); |
| + } |
| + |
| + if (user_approved || component_extension || whitelisted_extension) { |
| + content::DesktopMediaID screen_id; |
| +#if defined(OS_CHROMEOS) |
| + screen_id = content::DesktopMediaID::RegisterAuraWindow( |
| + ash::Shell::GetInstance()->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 == content::MEDIA_DESKTOP_AUDIO_CAPTURE && |
| + loopback_audio_supported); |
| + |
| + // Unless we're being invoked from a component extension, register to |
| + // display the notification for stream capture. |
| + bool display_notification = !component_extension; |
| + |
| + ui = GetDevicesForDesktopCapture(&devices, screen_id, capture_audio, |
| + 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() ? content::MEDIA_DEVICE_PERMISSION_DENIED |
| + : content::MEDIA_DEVICE_OK; |
| + } |
| + |
| + callback.Run(devices, result, ui.Pass()); |
| +} |
| + |
| +bool DesktopCaptureAccessHandler::SupportsStreamType( |
| + const content::MediaStreamType type, |
| + const extensions::Extension* extension) { |
| + return type == content::MEDIA_DESKTOP_VIDEO_CAPTURE || |
| + type == content::MEDIA_DESKTOP_AUDIO_CAPTURE; |
| +} |
| + |
| +bool DesktopCaptureAccessHandler::CheckMediaAccessPermission( |
| + content::WebContents* web_contents, |
| + const GURL& security_origin, |
| + content::MediaStreamType type, |
| + const extensions::Extension* extension) { |
| + return false; |
| +} |
| + |
| +void DesktopCaptureAccessHandler::HandleRequest( |
| + content::WebContents* web_contents, |
| + const content::MediaStreamRequest& request, |
| + const content::MediaResponseCallback& callback, |
| + const extensions::Extension* extension) { |
| + content::MediaStreamDevices devices; |
| + scoped_ptr<content::MediaStreamUI> ui; |
| + |
| + if (request.video_type != content::MEDIA_DESKTOP_VIDEO_CAPTURE) { |
| + callback.Run(devices, content::MEDIA_DEVICE_INVALID_STATE, ui.Pass()); |
| + 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()) { |
| + ProcessScreenCaptureAccessRequest(web_contents, request, callback, |
| + extension); |
| + return; |
| + } |
| + |
| + // The extension name that the stream is registered with. |
| + std::string original_extension_name; |
| + // 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 = MediaCaptureDevicesDispatcher::GetInstance() |
| + ->GetDesktopStreamsRegistry() |
| + ->RequestMediaForStreamId(request.requested_video_device_id, |
| + main_frame->GetProcess()->GetID(), |
| + main_frame->GetRoutingID(), |
| + request.security_origin, |
| + &original_extension_name); |
| + } |
| + |
| + // Received invalid device id. |
| + if (media_id.type == content::DesktopMediaID::TYPE_NONE) { |
| + callback.Run(devices, content::MEDIA_DEVICE_INVALID_STATE, ui.Pass()); |
| + return; |
| + } |
| + |
| + 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 |
| + |
| + // Audio is only supported for screen capture streams. |
| + bool capture_audio = |
| + (media_id.type == content::DesktopMediaID::TYPE_SCREEN && |
| + request.audio_type == content::MEDIA_DESKTOP_AUDIO_CAPTURE && |
| + loopback_audio_supported); |
| + |
| + ui = GetDevicesForDesktopCapture(&devices, media_id, capture_audio, true, |
| + GetApplicationTitle(web_contents, extension), |
| + base::UTF8ToUTF16(original_extension_name)); |
| + |
| + callback.Run(devices, content::MEDIA_DEVICE_OK, ui.Pass()); |
| +} |
| + |
| +void DesktopCaptureAccessHandler::UpdateMediaRequest( |
| + int render_process_id, |
| + int render_frame_id, |
| + int page_request_id, |
| + content::MediaStreamType stream_type, |
| + content::MediaRequestState state) { |
| + // Track desktop capture sessions. Tracking is necessary to avoid unbalanced |
| + // session counts since not all requests will reach MEDIA_REQUEST_STATE_DONE, |
| + // but they will all reach MEDIA_REQUEST_STATE_CLOSING. |
| + if (stream_type != content::MEDIA_DESKTOP_VIDEO_CAPTURE) |
| + return; |
| + |
| + if (state == content::MEDIA_REQUEST_STATE_DONE) { |
| + DesktopCaptureSession session = { |
| + render_process_id, render_frame_id, page_request_id}; |
| + desktop_capture_sessions_.push_back(session); |
| + } else if (state == content::MEDIA_REQUEST_STATE_CLOSING) { |
| + for (DesktopCaptureSessions::iterator it = |
| + desktop_capture_sessions_.begin(); |
| + it != desktop_capture_sessions_.end(); ++it) { |
| + if (it->render_process_id == render_process_id && |
| + it->render_frame_id == render_frame_id && |
| + it->page_request_id == page_request_id) { |
| + desktop_capture_sessions_.erase(it); |
| + break; |
| + } |
| + } |
| + } |
| +} |
| + |
| +bool DesktopCaptureAccessHandler::IsCaptureInProgress() { |
| + return desktop_capture_sessions_.size() > 0; |
| +} |