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.
5 #include "chrome/browser/media/desktop_capture_access_handler.h"
7 #include "base/command_line.h"
8 #include "base/strings/string_util.h"
9 #include "base/strings/utf_string_conversions.h"
10 #include "chrome/browser/media/desktop_streams_registry.h"
11 #include "chrome/browser/media/media_capture_devices_dispatcher.h"
12 #include "chrome/browser/ui/browser.h"
13 #include "chrome/browser/ui/browser_finder.h"
14 #include "chrome/browser/ui/browser_window.h"
15 #include "chrome/browser/ui/screen_capture_notification_ui.h"
16 #include "chrome/browser/ui/simple_message_box.h"
17 #include "chrome/common/chrome_switches.h"
18 #include "chrome/grit/generated_resources.h"
19 #include "content/public/browser/browser_thread.h"
20 #include "content/public/browser/desktop_media_id.h"
21 #include "content/public/browser/render_frame_host.h"
22 #include "content/public/browser/render_process_host.h"
23 #include "content/public/common/media_stream_request.h"
24 #include "extensions/browser/app_window/app_window.h"
25 #include "extensions/browser/app_window/app_window_registry.h"
26 #include "extensions/common/constants.h"
27 #include "extensions/common/extension.h"
28 #include "media/audio/audio_manager_base.h"
29 #include "net/base/net_util.h"
30 #include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h"
31 #include "ui/base/l10n/l10n_util.h"
33 #if defined(OS_CHROMEOS)
34 #include "ash/shell.h"
35 #include "base/sha1.h"
36 #endif // defined(OS_CHROMEOS)
38 using content::BrowserThread
;
42 bool IsExtensionWhitelistedForScreenCapture(
43 const extensions::Extension
* extension
) {
47 #if defined(OS_CHROMEOS)
48 std::string hash
= base::SHA1HashString(extension
->id());
49 std::string hex_hash
= base::HexEncode(hash
.c_str(), hash
.length());
52 return hex_hash
== "4F25792AF1AA7483936DE29C07806F203C7170A0" ||
53 hex_hash
== "BD8781D757D830FC2E85470A1B6E8A718B7EE0D9" ||
54 hex_hash
== "4AC2B6C63C6480D150DFDA13E4A5956EB1D0DDBB" ||
55 hex_hash
== "81986D4F846CEDDDB962643FA501D1780DD441BB";
58 #endif // defined(OS_CHROMEOS)
61 bool IsBuiltInExtension(const GURL
& origin
) {
63 // Feedback Extension.
64 origin
.spec() == "chrome-extension://gfdkimpbcpahaombhbimeihdjnejgicl/";
67 // Helper to get title of the calling application shown in the screen capture
69 base::string16
GetApplicationTitle(content::WebContents
* web_contents
,
70 const extensions::Extension
* extension
) {
71 // Use extension name as title for extensions and host/origin for drive-by
75 title
= extension
->name();
76 return base::UTF8ToUTF16(title
);
78 GURL url
= web_contents
->GetURL();
79 title
= url
.SchemeIsSecure() ? net::GetHostAndOptionalPort(url
)
80 : url
.GetOrigin().spec();
81 return base::UTF8ToUTF16(title
);
84 // Helper to get list of media stream devices for desktop capture in |devices|.
85 // Registers to display notification if |display_notification| is true.
86 // Returns an instance of MediaStreamUI to be passed to content layer.
87 scoped_ptr
<content::MediaStreamUI
> GetDevicesForDesktopCapture(
88 content::MediaStreamDevices
* devices
,
89 content::DesktopMediaID media_id
,
91 bool display_notification
,
92 const base::string16
& application_title
,
93 const base::string16
& registered_extension_name
) {
94 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
95 scoped_ptr
<content::MediaStreamUI
> ui
;
97 // Add selected desktop source to the list.
98 devices
->push_back(content::MediaStreamDevice(
99 content::MEDIA_DESKTOP_VIDEO_CAPTURE
, media_id
.ToString(), "Screen"));
101 // Use the special loopback device ID for system audio capture.
102 devices
->push_back(content::MediaStreamDevice(
103 content::MEDIA_DESKTOP_AUDIO_CAPTURE
,
104 media::AudioManagerBase::kLoopbackInputDeviceId
, "System Audio"));
107 // If required, register to display the notification for stream capture.
108 if (display_notification
) {
109 if (application_title
== registered_extension_name
) {
110 ui
= ScreenCaptureNotificationUI::Create(l10n_util::GetStringFUTF16(
111 IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_TEXT
, application_title
));
113 ui
= ScreenCaptureNotificationUI::Create(l10n_util::GetStringFUTF16(
114 IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_TEXT_DELEGATED
,
115 registered_extension_name
, application_title
));
122 #if !defined(OS_ANDROID)
123 // Find browser or app window from a given |web_contents|.
124 gfx::NativeWindow
FindParentWindowForWebContents(
125 content::WebContents
* web_contents
) {
126 Browser
* browser
= chrome::FindBrowserWithWebContents(web_contents
);
127 if (browser
&& browser
->window())
128 return browser
->window()->GetNativeWindow();
130 const extensions::AppWindowRegistry::AppWindowList
& window_list
=
131 extensions::AppWindowRegistry::Get(web_contents
->GetBrowserContext())
133 for (extensions::AppWindowRegistry::AppWindowList::const_iterator iter
=
135 iter
!= window_list
.end(); ++iter
) {
136 if ((*iter
)->web_contents() == web_contents
)
137 return (*iter
)->GetNativeWindow();
146 DesktopCaptureAccessHandler::DesktopCaptureAccessHandler() {
149 DesktopCaptureAccessHandler::~DesktopCaptureAccessHandler() {
152 void DesktopCaptureAccessHandler::ProcessScreenCaptureAccessRequest(
153 content::WebContents
* web_contents
,
154 const content::MediaStreamRequest
& request
,
155 const content::MediaResponseCallback
& callback
,
156 const extensions::Extension
* extension
) {
157 content::MediaStreamDevices devices
;
158 scoped_ptr
<content::MediaStreamUI
> ui
;
160 DCHECK_EQ(request
.video_type
, content::MEDIA_DESKTOP_VIDEO_CAPTURE
);
162 bool loopback_audio_supported
= false;
163 #if defined(USE_CRAS) || defined(OS_WIN)
164 // Currently loopback audio capture is supported only on Windows and ChromeOS.
165 loopback_audio_supported
= true;
168 bool component_extension
= false;
169 component_extension
=
170 extension
&& extension
->location() == extensions::Manifest::COMPONENT
;
172 bool screen_capture_enabled
=
173 base::CommandLine::ForCurrentProcess()->HasSwitch(
174 switches::kEnableUserMediaScreenCapturing
) ||
175 MediaCaptureDevicesDispatcher::IsOriginForCasting(
176 request
.security_origin
) ||
177 IsExtensionWhitelistedForScreenCapture(extension
) ||
178 IsBuiltInExtension(request
.security_origin
);
180 const bool origin_is_secure
=
181 request
.security_origin
.SchemeIsSecure() ||
182 request
.security_origin
.SchemeIs(extensions::kExtensionScheme
) ||
183 base::CommandLine::ForCurrentProcess()->HasSwitch(
184 switches::kAllowHttpScreenCapture
);
186 // If basic conditions (screen capturing is enabled and origin is secure)
187 // aren't fulfilled, we'll use "invalid state" as result. Otherwise, we set
188 // it after checking permission.
189 // TODO(grunell): It would be good to change this result for something else,
190 // probably a new one.
191 content::MediaStreamRequestResult result
=
192 content::MEDIA_DEVICE_INVALID_STATE
;
194 // Approve request only when the following conditions are met:
195 // 1. Screen capturing is enabled via command line switch or white-listed for
197 // 2. Request comes from a page with a secure origin or from an extension.
198 if (screen_capture_enabled
&& origin_is_secure
) {
199 // Get title of the calling application prior to showing the message box.
200 // chrome::ShowMessageBox() starts a nested message loop which may allow
201 // |web_contents| to be destroyed on the UI thread before the message box
202 // is closed. See http://crbug.com/326690.
203 base::string16 application_title
=
204 GetApplicationTitle(web_contents
, extension
);
205 #if !defined(OS_ANDROID)
206 gfx::NativeWindow parent_window
=
207 FindParentWindowForWebContents(web_contents
);
209 gfx::NativeWindow parent_window
= NULL
;
213 bool whitelisted_extension
=
214 IsExtensionWhitelistedForScreenCapture(extension
);
216 // For whitelisted or component extensions, bypass message box.
217 bool user_approved
= false;
218 if (!whitelisted_extension
&& !component_extension
) {
219 base::string16 application_name
=
220 base::UTF8ToUTF16(request
.security_origin
.spec());
222 application_name
= base::UTF8ToUTF16(extension
->name());
223 base::string16 confirmation_text
= l10n_util::GetStringFUTF16(
224 request
.audio_type
== content::MEDIA_NO_SERVICE
225 ? IDS_MEDIA_SCREEN_CAPTURE_CONFIRMATION_TEXT
226 : IDS_MEDIA_SCREEN_AND_AUDIO_CAPTURE_CONFIRMATION_TEXT
,
228 chrome::MessageBoxResult result
= chrome::ShowMessageBox(
230 l10n_util::GetStringFUTF16(
231 IDS_MEDIA_SCREEN_CAPTURE_CONFIRMATION_TITLE
, application_name
),
232 confirmation_text
, chrome::MESSAGE_BOX_TYPE_QUESTION
);
233 user_approved
= (result
== chrome::MESSAGE_BOX_RESULT_YES
);
236 if (user_approved
|| component_extension
|| whitelisted_extension
) {
237 content::DesktopMediaID screen_id
;
238 #if defined(OS_CHROMEOS)
239 screen_id
= content::DesktopMediaID::RegisterAuraWindow(
240 ash::Shell::GetInstance()->GetPrimaryRootWindow());
241 #else // defined(OS_CHROMEOS)
242 screen_id
= content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN
,
243 webrtc::kFullDesktopScreenId
);
244 #endif // !defined(OS_CHROMEOS)
247 (request
.audio_type
== content::MEDIA_DESKTOP_AUDIO_CAPTURE
&&
248 loopback_audio_supported
);
250 // Unless we're being invoked from a component extension, register to
251 // display the notification for stream capture.
252 bool display_notification
= !component_extension
;
254 ui
= GetDevicesForDesktopCapture(&devices
, screen_id
, capture_audio
,
255 display_notification
, application_title
,
257 DCHECK(!devices
.empty());
260 // The only case when devices can be empty is if the user has denied
262 result
= devices
.empty() ? content::MEDIA_DEVICE_PERMISSION_DENIED
263 : content::MEDIA_DEVICE_OK
;
266 callback
.Run(devices
, result
, ui
.Pass());
269 bool DesktopCaptureAccessHandler::SupportsStreamType(
270 const content::MediaStreamType type
,
271 const extensions::Extension
* extension
) {
272 return type
== content::MEDIA_DESKTOP_VIDEO_CAPTURE
||
273 type
== content::MEDIA_DESKTOP_AUDIO_CAPTURE
;
276 bool DesktopCaptureAccessHandler::CheckMediaAccessPermission(
277 content::WebContents
* web_contents
,
278 const GURL
& security_origin
,
279 content::MediaStreamType type
,
280 const extensions::Extension
* extension
) {
284 void DesktopCaptureAccessHandler::HandleRequest(
285 content::WebContents
* web_contents
,
286 const content::MediaStreamRequest
& request
,
287 const content::MediaResponseCallback
& callback
,
288 const extensions::Extension
* extension
) {
289 content::MediaStreamDevices devices
;
290 scoped_ptr
<content::MediaStreamUI
> ui
;
292 if (request
.video_type
!= content::MEDIA_DESKTOP_VIDEO_CAPTURE
) {
293 callback
.Run(devices
, content::MEDIA_DEVICE_INVALID_STATE
, ui
.Pass());
297 // If the device id wasn't specified then this is a screen capture request
298 // (i.e. chooseDesktopMedia() API wasn't used to generate device id).
299 if (request
.requested_video_device_id
.empty()) {
300 ProcessScreenCaptureAccessRequest(web_contents
, request
, callback
,
305 // The extension name that the stream is registered with.
306 std::string original_extension_name
;
307 // Resolve DesktopMediaID for the specified device id.
308 content::DesktopMediaID media_id
;
309 // TODO(miu): Replace "main RenderFrame" IDs with the request's actual
310 // RenderFrame IDs once the desktop capture extension API implementation is
311 // fixed. http://crbug.com/304341
312 content::WebContents
* const web_contents_for_stream
=
313 content::WebContents::FromRenderFrameHost(
314 content::RenderFrameHost::FromID(request
.render_process_id
,
315 request
.render_frame_id
));
316 content::RenderFrameHost
* const main_frame
=
317 web_contents_for_stream
? web_contents_for_stream
->GetMainFrame() : NULL
;
319 media_id
= MediaCaptureDevicesDispatcher::GetInstance()
320 ->GetDesktopStreamsRegistry()
321 ->RequestMediaForStreamId(request
.requested_video_device_id
,
322 main_frame
->GetProcess()->GetID(),
323 main_frame
->GetRoutingID(),
324 request
.security_origin
,
325 &original_extension_name
);
328 // Received invalid device id.
329 if (media_id
.type
== content::DesktopMediaID::TYPE_NONE
) {
330 callback
.Run(devices
, content::MEDIA_DEVICE_INVALID_STATE
, ui
.Pass());
334 bool loopback_audio_supported
= false;
335 #if defined(USE_CRAS) || defined(OS_WIN)
336 // Currently loopback audio capture is supported only on Windows and ChromeOS.
337 loopback_audio_supported
= true;
340 // Audio is only supported for screen capture streams.
342 (media_id
.type
== content::DesktopMediaID::TYPE_SCREEN
&&
343 request
.audio_type
== content::MEDIA_DESKTOP_AUDIO_CAPTURE
&&
344 loopback_audio_supported
);
346 ui
= GetDevicesForDesktopCapture(&devices
, media_id
, capture_audio
, true,
347 GetApplicationTitle(web_contents
, extension
),
348 base::UTF8ToUTF16(original_extension_name
));
350 callback
.Run(devices
, content::MEDIA_DEVICE_OK
, ui
.Pass());
353 void DesktopCaptureAccessHandler::UpdateMediaRequestState(
354 int render_process_id
,
357 content::MediaStreamType stream_type
,
358 content::MediaRequestState state
) {
359 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
360 // Track desktop capture sessions. Tracking is necessary to avoid unbalanced
361 // session counts since not all requests will reach MEDIA_REQUEST_STATE_DONE,
362 // but they will all reach MEDIA_REQUEST_STATE_CLOSING.
363 if (stream_type
!= content::MEDIA_DESKTOP_VIDEO_CAPTURE
)
366 if (state
== content::MEDIA_REQUEST_STATE_DONE
) {
367 DesktopCaptureSession session
= {
368 render_process_id
, render_frame_id
, page_request_id
};
369 desktop_capture_sessions_
.push_back(session
);
370 } else if (state
== content::MEDIA_REQUEST_STATE_CLOSING
) {
371 for (DesktopCaptureSessions::iterator it
=
372 desktop_capture_sessions_
.begin();
373 it
!= desktop_capture_sessions_
.end(); ++it
) {
374 if (it
->render_process_id
== render_process_id
&&
375 it
->render_frame_id
== render_frame_id
&&
376 it
->page_request_id
== page_request_id
) {
377 desktop_capture_sessions_
.erase(it
);
384 bool DesktopCaptureAccessHandler::IsCaptureInProgress() {
385 return desktop_capture_sessions_
.size() > 0;