| OLD | NEW |
| (Empty) |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #include "chrome/browser/media/desktop_capture_access_handler.h" | |
| 6 | |
| 7 #include <utility> | |
| 8 | |
| 9 #include "base/command_line.h" | |
| 10 #include "base/strings/string_number_conversions.h" | |
| 11 #include "base/strings/string_util.h" | |
| 12 #include "base/strings/utf_string_conversions.h" | |
| 13 #include "build/build_config.h" | |
| 14 #include "chrome/browser/media/desktop_streams_registry.h" | |
| 15 #include "chrome/browser/media/media_capture_devices_dispatcher.h" | |
| 16 #include "chrome/browser/ui/browser.h" | |
| 17 #include "chrome/browser/ui/browser_finder.h" | |
| 18 #include "chrome/browser/ui/browser_window.h" | |
| 19 #include "chrome/browser/ui/screen_capture_notification_ui.h" | |
| 20 #include "chrome/browser/ui/simple_message_box.h" | |
| 21 #include "chrome/common/chrome_switches.h" | |
| 22 #include "chrome/grit/generated_resources.h" | |
| 23 #include "content/public/browser/browser_thread.h" | |
| 24 #include "content/public/browser/desktop_media_id.h" | |
| 25 #include "content/public/browser/render_frame_host.h" | |
| 26 #include "content/public/browser/render_process_host.h" | |
| 27 #include "content/public/common/content_switches.h" | |
| 28 #include "content/public/common/media_stream_request.h" | |
| 29 #include "content/public/common/origin_util.h" | |
| 30 #include "extensions/browser/app_window/app_window.h" | |
| 31 #include "extensions/browser/app_window/app_window_registry.h" | |
| 32 #include "extensions/common/constants.h" | |
| 33 #include "extensions/common/extension.h" | |
| 34 #include "extensions/common/switches.h" | |
| 35 #include "media/audio/audio_device_description.h" | |
| 36 #include "net/base/url_util.h" | |
| 37 #include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h" | |
| 38 #include "ui/base/l10n/l10n_util.h" | |
| 39 | |
| 40 #if defined(OS_CHROMEOS) | |
| 41 #include "ash/shell.h" | |
| 42 #endif // defined(OS_CHROMEOS) | |
| 43 | |
| 44 using content::BrowserThread; | |
| 45 | |
| 46 namespace { | |
| 47 | |
| 48 // Helper to get title of the calling application shown in the screen capture | |
| 49 // notification. | |
| 50 base::string16 GetApplicationTitle(content::WebContents* web_contents, | |
| 51 const extensions::Extension* extension) { | |
| 52 // Use extension name as title for extensions and host/origin for drive-by | |
| 53 // web. | |
| 54 std::string title; | |
| 55 if (extension) { | |
| 56 title = extension->name(); | |
| 57 return base::UTF8ToUTF16(title); | |
| 58 } | |
| 59 GURL url = web_contents->GetURL(); | |
| 60 title = content::IsOriginSecure(url) ? net::GetHostAndOptionalPort(url) | |
| 61 : url.GetOrigin().spec(); | |
| 62 return base::UTF8ToUTF16(title); | |
| 63 } | |
| 64 | |
| 65 base::string16 GetStopSharingUIString( | |
| 66 const base::string16& application_title, | |
| 67 const base::string16& registered_extension_name, | |
| 68 bool capture_audio, | |
| 69 content::DesktopMediaID::Type capture_type) { | |
| 70 if (!capture_audio) { | |
| 71 if (application_title == registered_extension_name) { | |
| 72 switch (capture_type) { | |
| 73 case content::DesktopMediaID::TYPE_SCREEN: | |
| 74 return l10n_util::GetStringFUTF16( | |
| 75 IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_TEXT, application_title); | |
| 76 case content::DesktopMediaID::TYPE_WINDOW: | |
| 77 return l10n_util::GetStringFUTF16( | |
| 78 IDS_MEDIA_WINDOW_CAPTURE_NOTIFICATION_TEXT, application_title); | |
| 79 case content::DesktopMediaID::TYPE_WEB_CONTENTS: | |
| 80 return l10n_util::GetStringFUTF16( | |
| 81 IDS_MEDIA_TAB_CAPTURE_NOTIFICATION_TEXT, application_title); | |
| 82 case content::DesktopMediaID::TYPE_NONE: | |
| 83 NOTREACHED(); | |
| 84 } | |
| 85 } else { | |
| 86 switch (capture_type) { | |
| 87 case content::DesktopMediaID::TYPE_SCREEN: | |
| 88 return l10n_util::GetStringFUTF16( | |
| 89 IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_TEXT_DELEGATED, | |
| 90 registered_extension_name, application_title); | |
| 91 case content::DesktopMediaID::TYPE_WINDOW: | |
| 92 return l10n_util::GetStringFUTF16( | |
| 93 IDS_MEDIA_WINDOW_CAPTURE_NOTIFICATION_TEXT_DELEGATED, | |
| 94 registered_extension_name, application_title); | |
| 95 case content::DesktopMediaID::TYPE_WEB_CONTENTS: | |
| 96 return l10n_util::GetStringFUTF16( | |
| 97 IDS_MEDIA_TAB_CAPTURE_NOTIFICATION_TEXT_DELEGATED, | |
| 98 registered_extension_name, application_title); | |
| 99 case content::DesktopMediaID::TYPE_NONE: | |
| 100 NOTREACHED(); | |
| 101 } | |
| 102 } | |
| 103 } else { // The case with audio | |
| 104 if (application_title == registered_extension_name) { | |
| 105 switch (capture_type) { | |
| 106 case content::DesktopMediaID::TYPE_SCREEN: | |
| 107 return l10n_util::GetStringFUTF16( | |
| 108 IDS_MEDIA_SCREEN_CAPTURE_WITH_AUDIO_NOTIFICATION_TEXT, | |
| 109 application_title); | |
| 110 case content::DesktopMediaID::TYPE_WEB_CONTENTS: | |
| 111 return l10n_util::GetStringFUTF16( | |
| 112 IDS_MEDIA_TAB_CAPTURE_WITH_AUDIO_NOTIFICATION_TEXT, | |
| 113 application_title); | |
| 114 case content::DesktopMediaID::TYPE_NONE: | |
| 115 case content::DesktopMediaID::TYPE_WINDOW: | |
| 116 NOTREACHED(); | |
| 117 } | |
| 118 } else { | |
| 119 switch (capture_type) { | |
| 120 case content::DesktopMediaID::TYPE_SCREEN: | |
| 121 return l10n_util::GetStringFUTF16( | |
| 122 IDS_MEDIA_SCREEN_CAPTURE_WITH_AUDIO_NOTIFICATION_TEXT_DELEGATED, | |
| 123 registered_extension_name, application_title); | |
| 124 case content::DesktopMediaID::TYPE_WEB_CONTENTS: | |
| 125 return l10n_util::GetStringFUTF16( | |
| 126 IDS_MEDIA_TAB_CAPTURE_WITH_AUDIO_NOTIFICATION_TEXT_DELEGATED, | |
| 127 registered_extension_name, application_title); | |
| 128 case content::DesktopMediaID::TYPE_NONE: | |
| 129 case content::DesktopMediaID::TYPE_WINDOW: | |
| 130 NOTREACHED(); | |
| 131 } | |
| 132 } | |
| 133 } | |
| 134 return base::string16(); | |
| 135 } | |
| 136 // Helper to get list of media stream devices for desktop capture in |devices|. | |
| 137 // Registers to display notification if |display_notification| is true. | |
| 138 // Returns an instance of MediaStreamUI to be passed to content layer. | |
| 139 std::unique_ptr<content::MediaStreamUI> GetDevicesForDesktopCapture( | |
| 140 content::MediaStreamDevices* devices, | |
| 141 content::DesktopMediaID media_id, | |
| 142 bool capture_audio, | |
| 143 bool mute_system_audio, | |
| 144 bool display_notification, | |
| 145 const base::string16& application_title, | |
| 146 const base::string16& registered_extension_name) { | |
| 147 DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
| 148 std::unique_ptr<content::MediaStreamUI> ui; | |
| 149 | |
| 150 // Add selected desktop source to the list. | |
| 151 devices->push_back(content::MediaStreamDevice( | |
| 152 content::MEDIA_DESKTOP_VIDEO_CAPTURE, media_id.ToString(), "Screen")); | |
| 153 if (capture_audio) { | |
| 154 if (media_id.type == content::DesktopMediaID::TYPE_WEB_CONTENTS) { | |
| 155 devices->push_back( | |
| 156 content::MediaStreamDevice(content::MEDIA_DESKTOP_AUDIO_CAPTURE, | |
| 157 media_id.ToString(), "Tab audio")); | |
| 158 } else if (mute_system_audio) { | |
| 159 // Use the special loopback device ID for system audio capture. | |
| 160 devices->push_back(content::MediaStreamDevice( | |
| 161 content::MEDIA_DESKTOP_AUDIO_CAPTURE, | |
| 162 media::AudioDeviceDescription::kLoopbackWithMuteDeviceId, | |
| 163 "System Audio")); | |
| 164 } else { | |
| 165 // Use the special loopback device ID for system audio capture. | |
| 166 devices->push_back(content::MediaStreamDevice( | |
| 167 content::MEDIA_DESKTOP_AUDIO_CAPTURE, | |
| 168 media::AudioDeviceDescription::kLoopbackInputDeviceId, | |
| 169 "System Audio")); | |
| 170 } | |
| 171 } | |
| 172 | |
| 173 // If required, register to display the notification for stream capture. | |
| 174 if (!display_notification) { | |
| 175 return ui; | |
| 176 } | |
| 177 | |
| 178 ui = ScreenCaptureNotificationUI::Create(GetStopSharingUIString( | |
| 179 application_title, registered_extension_name, capture_audio, | |
| 180 media_id.type)); | |
| 181 | |
| 182 return ui; | |
| 183 } | |
| 184 | |
| 185 #if !defined(OS_ANDROID) | |
| 186 // Find browser or app window from a given |web_contents|. | |
| 187 gfx::NativeWindow FindParentWindowForWebContents( | |
| 188 content::WebContents* web_contents) { | |
| 189 Browser* browser = chrome::FindBrowserWithWebContents(web_contents); | |
| 190 if (browser && browser->window()) | |
| 191 return browser->window()->GetNativeWindow(); | |
| 192 | |
| 193 const extensions::AppWindowRegistry::AppWindowList& window_list = | |
| 194 extensions::AppWindowRegistry::Get(web_contents->GetBrowserContext()) | |
| 195 ->app_windows(); | |
| 196 for (extensions::AppWindowRegistry::AppWindowList::const_iterator iter = | |
| 197 window_list.begin(); | |
| 198 iter != window_list.end(); ++iter) { | |
| 199 if ((*iter)->web_contents() == web_contents) | |
| 200 return (*iter)->GetNativeWindow(); | |
| 201 } | |
| 202 | |
| 203 return NULL; | |
| 204 } | |
| 205 #endif | |
| 206 | |
| 207 } // namespace | |
| 208 | |
| 209 DesktopCaptureAccessHandler::DesktopCaptureAccessHandler() { | |
| 210 } | |
| 211 | |
| 212 DesktopCaptureAccessHandler::~DesktopCaptureAccessHandler() { | |
| 213 } | |
| 214 | |
| 215 void DesktopCaptureAccessHandler::ProcessScreenCaptureAccessRequest( | |
| 216 content::WebContents* web_contents, | |
| 217 const content::MediaStreamRequest& request, | |
| 218 const content::MediaResponseCallback& callback, | |
| 219 const extensions::Extension* extension) { | |
| 220 content::MediaStreamDevices devices; | |
| 221 std::unique_ptr<content::MediaStreamUI> ui; | |
| 222 | |
| 223 DCHECK_EQ(request.video_type, content::MEDIA_DESKTOP_VIDEO_CAPTURE); | |
| 224 | |
| 225 UpdateExtensionTrusted(request, extension); | |
| 226 | |
| 227 bool loopback_audio_supported = false; | |
| 228 #if defined(USE_CRAS) || defined(OS_WIN) | |
| 229 // Currently loopback audio capture is supported only on Windows and ChromeOS. | |
| 230 loopback_audio_supported = true; | |
| 231 #endif | |
| 232 | |
| 233 bool component_extension = false; | |
| 234 component_extension = | |
| 235 extension && extension->location() == extensions::Manifest::COMPONENT; | |
| 236 | |
| 237 bool screen_capture_enabled = | |
| 238 base::CommandLine::ForCurrentProcess()->HasSwitch( | |
| 239 switches::kEnableUserMediaScreenCapturing) || | |
| 240 MediaCaptureDevicesDispatcher::IsOriginForCasting( | |
| 241 request.security_origin) || | |
| 242 IsExtensionWhitelistedForScreenCapture(extension) || | |
| 243 IsBuiltInExtension(request.security_origin); | |
| 244 | |
| 245 const bool origin_is_secure = | |
| 246 content::IsOriginSecure(request.security_origin) || | |
| 247 base::CommandLine::ForCurrentProcess()->HasSwitch( | |
| 248 switches::kAllowHttpScreenCapture); | |
| 249 | |
| 250 // If basic conditions (screen capturing is enabled and origin is secure) | |
| 251 // aren't fulfilled, we'll use "invalid state" as result. Otherwise, we set | |
| 252 // it after checking permission. | |
| 253 // TODO(grunell): It would be good to change this result for something else, | |
| 254 // probably a new one. | |
| 255 content::MediaStreamRequestResult result = | |
| 256 content::MEDIA_DEVICE_INVALID_STATE; | |
| 257 | |
| 258 // Approve request only when the following conditions are met: | |
| 259 // 1. Screen capturing is enabled via command line switch or white-listed for | |
| 260 // the given origin. | |
| 261 // 2. Request comes from a page with a secure origin or from an extension. | |
| 262 if (screen_capture_enabled && origin_is_secure) { | |
| 263 // Get title of the calling application prior to showing the message box. | |
| 264 // chrome::ShowQuestionMessageBox() starts a nested message loop which may | |
| 265 // allow |web_contents| to be destroyed on the UI thread before the messag | |
| 266 // box is closed. See http://crbug.com/326690. | |
| 267 base::string16 application_title = | |
| 268 GetApplicationTitle(web_contents, extension); | |
| 269 #if !defined(OS_ANDROID) | |
| 270 gfx::NativeWindow parent_window = | |
| 271 FindParentWindowForWebContents(web_contents); | |
| 272 #else | |
| 273 gfx::NativeWindow parent_window = NULL; | |
| 274 #endif | |
| 275 web_contents = NULL; | |
| 276 | |
| 277 bool whitelisted_extension = | |
| 278 IsExtensionWhitelistedForScreenCapture(extension); | |
| 279 | |
| 280 // For whitelisted or component extensions, bypass message box. | |
| 281 bool user_approved = false; | |
| 282 if (!whitelisted_extension && !component_extension) { | |
| 283 base::string16 application_name = | |
| 284 base::UTF8ToUTF16(request.security_origin.spec()); | |
| 285 if (extension) | |
| 286 application_name = base::UTF8ToUTF16(extension->name()); | |
| 287 base::string16 confirmation_text = l10n_util::GetStringFUTF16( | |
| 288 request.audio_type == content::MEDIA_NO_SERVICE | |
| 289 ? IDS_MEDIA_SCREEN_CAPTURE_CONFIRMATION_TEXT | |
| 290 : IDS_MEDIA_SCREEN_AND_AUDIO_CAPTURE_CONFIRMATION_TEXT, | |
| 291 application_name); | |
| 292 chrome::MessageBoxResult result = chrome::ShowQuestionMessageBox( | |
| 293 parent_window, | |
| 294 l10n_util::GetStringFUTF16( | |
| 295 IDS_MEDIA_SCREEN_CAPTURE_CONFIRMATION_TITLE, application_name), | |
| 296 confirmation_text); | |
| 297 user_approved = (result == chrome::MESSAGE_BOX_RESULT_YES); | |
| 298 } | |
| 299 | |
| 300 if (user_approved || component_extension || whitelisted_extension) { | |
| 301 content::DesktopMediaID screen_id; | |
| 302 #if defined(OS_CHROMEOS) | |
| 303 screen_id = content::DesktopMediaID::RegisterAuraWindow( | |
| 304 content::DesktopMediaID::TYPE_SCREEN, | |
| 305 ash::Shell::GetInstance()->GetPrimaryRootWindow()); | |
| 306 #else // defined(OS_CHROMEOS) | |
| 307 screen_id = content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, | |
| 308 webrtc::kFullDesktopScreenId); | |
| 309 #endif // !defined(OS_CHROMEOS) | |
| 310 | |
| 311 bool capture_audio = | |
| 312 (request.audio_type == content::MEDIA_DESKTOP_AUDIO_CAPTURE && | |
| 313 loopback_audio_supported); | |
| 314 | |
| 315 // Unless we're being invoked from a component extension, register to | |
| 316 // display the notification for stream capture. | |
| 317 bool display_notification = !component_extension; | |
| 318 | |
| 319 ui = GetDevicesForDesktopCapture(&devices, screen_id, capture_audio, true, | |
| 320 display_notification, application_title, | |
| 321 application_title); | |
| 322 DCHECK(!devices.empty()); | |
| 323 } | |
| 324 | |
| 325 // The only case when devices can be empty is if the user has denied | |
| 326 // permission. | |
| 327 result = devices.empty() ? content::MEDIA_DEVICE_PERMISSION_DENIED | |
| 328 : content::MEDIA_DEVICE_OK; | |
| 329 } | |
| 330 | |
| 331 callback.Run(devices, result, std::move(ui)); | |
| 332 } | |
| 333 | |
| 334 bool DesktopCaptureAccessHandler::SupportsStreamType( | |
| 335 const content::MediaStreamType type, | |
| 336 const extensions::Extension* extension) { | |
| 337 return type == content::MEDIA_DESKTOP_VIDEO_CAPTURE || | |
| 338 type == content::MEDIA_DESKTOP_AUDIO_CAPTURE; | |
| 339 } | |
| 340 | |
| 341 bool DesktopCaptureAccessHandler::CheckMediaAccessPermission( | |
| 342 content::WebContents* web_contents, | |
| 343 const GURL& security_origin, | |
| 344 content::MediaStreamType type, | |
| 345 const extensions::Extension* extension) { | |
| 346 return false; | |
| 347 } | |
| 348 | |
| 349 void DesktopCaptureAccessHandler::HandleRequest( | |
| 350 content::WebContents* web_contents, | |
| 351 const content::MediaStreamRequest& request, | |
| 352 const content::MediaResponseCallback& callback, | |
| 353 const extensions::Extension* extension) { | |
| 354 content::MediaStreamDevices devices; | |
| 355 std::unique_ptr<content::MediaStreamUI> ui; | |
| 356 | |
| 357 if (request.video_type != content::MEDIA_DESKTOP_VIDEO_CAPTURE) { | |
| 358 callback.Run(devices, content::MEDIA_DEVICE_INVALID_STATE, std::move(ui)); | |
| 359 return; | |
| 360 } | |
| 361 | |
| 362 // If the device id wasn't specified then this is a screen capture request | |
| 363 // (i.e. chooseDesktopMedia() API wasn't used to generate device id). | |
| 364 if (request.requested_video_device_id.empty()) { | |
| 365 ProcessScreenCaptureAccessRequest(web_contents, request, callback, | |
| 366 extension); | |
| 367 return; | |
| 368 } | |
| 369 | |
| 370 // The extension name that the stream is registered with. | |
| 371 std::string original_extension_name; | |
| 372 // Resolve DesktopMediaID for the specified device id. | |
| 373 content::DesktopMediaID media_id; | |
| 374 // TODO(miu): Replace "main RenderFrame" IDs with the request's actual | |
| 375 // RenderFrame IDs once the desktop capture extension API implementation is | |
| 376 // fixed. http://crbug.com/304341 | |
| 377 content::WebContents* const web_contents_for_stream = | |
| 378 content::WebContents::FromRenderFrameHost( | |
| 379 content::RenderFrameHost::FromID(request.render_process_id, | |
| 380 request.render_frame_id)); | |
| 381 content::RenderFrameHost* const main_frame = | |
| 382 web_contents_for_stream ? web_contents_for_stream->GetMainFrame() : NULL; | |
| 383 if (main_frame) { | |
| 384 media_id = MediaCaptureDevicesDispatcher::GetInstance() | |
| 385 ->GetDesktopStreamsRegistry() | |
| 386 ->RequestMediaForStreamId(request.requested_video_device_id, | |
| 387 main_frame->GetProcess()->GetID(), | |
| 388 main_frame->GetRoutingID(), | |
| 389 request.security_origin, | |
| 390 &original_extension_name); | |
| 391 } | |
| 392 | |
| 393 // Received invalid device id. | |
| 394 if (media_id.type == content::DesktopMediaID::TYPE_NONE) { | |
| 395 callback.Run(devices, content::MEDIA_DEVICE_INVALID_STATE, std::move(ui)); | |
| 396 return; | |
| 397 } | |
| 398 | |
| 399 bool loopback_audio_supported = false; | |
| 400 #if defined(USE_CRAS) || defined(OS_WIN) | |
| 401 // Currently loopback audio capture is supported only on Windows and ChromeOS. | |
| 402 loopback_audio_supported = true; | |
| 403 #endif | |
| 404 | |
| 405 // This value essentially from the checkbox on picker window, so it | |
| 406 // corresponds to user permission. | |
| 407 const bool audio_permitted = media_id.audio_share; | |
| 408 | |
| 409 // This value essentially from whether getUserMedia requests audio stream. | |
| 410 const bool audio_requested = | |
| 411 request.audio_type == content::MEDIA_DESKTOP_AUDIO_CAPTURE; | |
| 412 | |
| 413 // This value shows for a given capture type, whether the system or our code | |
| 414 // can support audio sharing. Currently audio is only supported for screen and | |
| 415 // tab/webcontents capture streams. | |
| 416 const bool audio_supported = | |
| 417 (media_id.type == content::DesktopMediaID::TYPE_SCREEN && | |
| 418 loopback_audio_supported) || | |
| 419 media_id.type == content::DesktopMediaID::TYPE_WEB_CONTENTS; | |
| 420 | |
| 421 const bool check_audio_permission = | |
| 422 !base::CommandLine::ForCurrentProcess()->HasSwitch( | |
| 423 extensions::switches::kDisableDesktopCaptureAudio); | |
| 424 const bool capture_audio = | |
| 425 (check_audio_permission ? audio_permitted : true) && audio_requested && | |
| 426 audio_supported; | |
| 427 | |
| 428 ui = GetDevicesForDesktopCapture(&devices, media_id, capture_audio, false, | |
| 429 true, | |
| 430 GetApplicationTitle(web_contents, extension), | |
| 431 base::UTF8ToUTF16(original_extension_name)); | |
| 432 UpdateExtensionTrusted(request, extension); | |
| 433 callback.Run(devices, content::MEDIA_DEVICE_OK, std::move(ui)); | |
| 434 } | |
| 435 | |
| OLD | NEW |