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_number_conversions.h"
9 #include "base/strings/string_util.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "chrome/browser/media/desktop_streams_registry.h"
12 #include "chrome/browser/media/media_capture_devices_dispatcher.h"
13 #include "chrome/browser/ui/browser.h"
14 #include "chrome/browser/ui/browser_finder.h"
15 #include "chrome/browser/ui/browser_window.h"
16 #include "chrome/browser/ui/screen_capture_notification_ui.h"
17 #include "chrome/browser/ui/simple_message_box.h"
18 #include "chrome/common/chrome_switches.h"
19 #include "chrome/grit/generated_resources.h"
20 #include "content/public/browser/browser_thread.h"
21 #include "content/public/browser/desktop_media_id.h"
22 #include "content/public/browser/render_frame_host.h"
23 #include "content/public/browser/render_process_host.h"
24 #include "content/public/common/media_stream_request.h"
25 #include "content/public/common/origin_util.h"
26 #include "extensions/browser/app_window/app_window.h"
27 #include "extensions/browser/app_window/app_window_registry.h"
28 #include "extensions/common/constants.h"
29 #include "extensions/common/extension.h"
30 #include "media/audio/audio_manager_base.h"
31 #include "net/base/net_util.h"
32 #include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h"
33 #include "ui/base/l10n/l10n_util.h"
35 #if defined(OS_CHROMEOS)
36 #include "ash/shell.h"
37 #include "base/sha1.h"
38 #endif // defined(OS_CHROMEOS)
40 using content::BrowserThread
;
44 bool IsExtensionWhitelistedForScreenCapture(
45 const extensions::Extension
* extension
) {
49 #if defined(OS_CHROMEOS)
50 std::string hash
= base::SHA1HashString(extension
->id());
51 std::string hex_hash
= base::HexEncode(hash
.c_str(), hash
.length());
54 return hex_hash
== "4F25792AF1AA7483936DE29C07806F203C7170A0" ||
55 hex_hash
== "BD8781D757D830FC2E85470A1B6E8A718B7EE0D9" ||
56 hex_hash
== "4AC2B6C63C6480D150DFDA13E4A5956EB1D0DDBB" ||
57 hex_hash
== "81986D4F846CEDDDB962643FA501D1780DD441BB";
60 #endif // defined(OS_CHROMEOS)
63 bool IsBuiltInExtension(const GURL
& origin
) {
65 // Feedback Extension.
66 origin
.spec() == "chrome-extension://gfdkimpbcpahaombhbimeihdjnejgicl/";
69 // Helper to get title of the calling application shown in the screen capture
71 base::string16
GetApplicationTitle(content::WebContents
* web_contents
,
72 const extensions::Extension
* extension
) {
73 // Use extension name as title for extensions and host/origin for drive-by
77 title
= extension
->name();
78 return base::UTF8ToUTF16(title
);
80 GURL url
= web_contents
->GetURL();
81 title
= content::IsOriginSecure(url
) ? net::GetHostAndOptionalPort(url
)
82 : url
.GetOrigin().spec();
83 return base::UTF8ToUTF16(title
);
86 // Helper to get list of media stream devices for desktop capture in |devices|.
87 // Registers to display notification if |display_notification| is true.
88 // Returns an instance of MediaStreamUI to be passed to content layer.
89 scoped_ptr
<content::MediaStreamUI
> GetDevicesForDesktopCapture(
90 content::MediaStreamDevices
* devices
,
91 content::DesktopMediaID media_id
,
93 bool display_notification
,
94 const base::string16
& application_title
,
95 const base::string16
& registered_extension_name
) {
96 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
97 scoped_ptr
<content::MediaStreamUI
> ui
;
99 // Add selected desktop source to the list.
100 devices
->push_back(content::MediaStreamDevice(
101 content::MEDIA_DESKTOP_VIDEO_CAPTURE
, media_id
.ToString(), "Screen"));
103 // Use the special loopback device ID for system audio capture.
104 devices
->push_back(content::MediaStreamDevice(
105 content::MEDIA_DESKTOP_AUDIO_CAPTURE
,
106 media::AudioManagerBase::kLoopbackInputDeviceId
, "System Audio"));
109 // If required, register to display the notification for stream capture.
110 if (display_notification
) {
111 if (application_title
== registered_extension_name
) {
112 ui
= ScreenCaptureNotificationUI::Create(l10n_util::GetStringFUTF16(
113 IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_TEXT
, application_title
));
115 ui
= ScreenCaptureNotificationUI::Create(l10n_util::GetStringFUTF16(
116 IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_TEXT_DELEGATED
,
117 registered_extension_name
, application_title
));
124 #if !defined(OS_ANDROID)
125 // Find browser or app window from a given |web_contents|.
126 gfx::NativeWindow
FindParentWindowForWebContents(
127 content::WebContents
* web_contents
) {
128 Browser
* browser
= chrome::FindBrowserWithWebContents(web_contents
);
129 if (browser
&& browser
->window())
130 return browser
->window()->GetNativeWindow();
132 const extensions::AppWindowRegistry::AppWindowList
& window_list
=
133 extensions::AppWindowRegistry::Get(web_contents
->GetBrowserContext())
135 for (extensions::AppWindowRegistry::AppWindowList::const_iterator iter
=
137 iter
!= window_list
.end(); ++iter
) {
138 if ((*iter
)->web_contents() == web_contents
)
139 return (*iter
)->GetNativeWindow();
148 DesktopCaptureAccessHandler::DesktopCaptureAccessHandler() {
151 DesktopCaptureAccessHandler::~DesktopCaptureAccessHandler() {
154 void DesktopCaptureAccessHandler::ProcessScreenCaptureAccessRequest(
155 content::WebContents
* web_contents
,
156 const content::MediaStreamRequest
& request
,
157 const content::MediaResponseCallback
& callback
,
158 const extensions::Extension
* extension
) {
159 content::MediaStreamDevices devices
;
160 scoped_ptr
<content::MediaStreamUI
> ui
;
162 DCHECK_EQ(request
.video_type
, content::MEDIA_DESKTOP_VIDEO_CAPTURE
);
164 bool loopback_audio_supported
= false;
165 #if defined(USE_CRAS) || defined(OS_WIN)
166 // Currently loopback audio capture is supported only on Windows and ChromeOS.
167 loopback_audio_supported
= true;
170 bool component_extension
= false;
171 component_extension
=
172 extension
&& extension
->location() == extensions::Manifest::COMPONENT
;
174 bool screen_capture_enabled
=
175 base::CommandLine::ForCurrentProcess()->HasSwitch(
176 switches::kEnableUserMediaScreenCapturing
) ||
177 MediaCaptureDevicesDispatcher::IsOriginForCasting(
178 request
.security_origin
) ||
179 IsExtensionWhitelistedForScreenCapture(extension
) ||
180 IsBuiltInExtension(request
.security_origin
);
182 const bool origin_is_secure
=
183 content::IsOriginSecure(request
.security_origin
) ||
184 base::CommandLine::ForCurrentProcess()->HasSwitch(
185 switches::kAllowHttpScreenCapture
);
187 // If basic conditions (screen capturing is enabled and origin is secure)
188 // aren't fulfilled, we'll use "invalid state" as result. Otherwise, we set
189 // it after checking permission.
190 // TODO(grunell): It would be good to change this result for something else,
191 // probably a new one.
192 content::MediaStreamRequestResult result
=
193 content::MEDIA_DEVICE_INVALID_STATE
;
195 // Approve request only when the following conditions are met:
196 // 1. Screen capturing is enabled via command line switch or white-listed for
198 // 2. Request comes from a page with a secure origin or from an extension.
199 if (screen_capture_enabled
&& origin_is_secure
) {
200 // Get title of the calling application prior to showing the message box.
201 // chrome::ShowMessageBox() starts a nested message loop which may allow
202 // |web_contents| to be destroyed on the UI thread before the message box
203 // is closed. See http://crbug.com/326690.
204 base::string16 application_title
=
205 GetApplicationTitle(web_contents
, extension
);
206 #if !defined(OS_ANDROID)
207 gfx::NativeWindow parent_window
=
208 FindParentWindowForWebContents(web_contents
);
210 gfx::NativeWindow parent_window
= NULL
;
214 bool whitelisted_extension
=
215 IsExtensionWhitelistedForScreenCapture(extension
);
217 // For whitelisted or component extensions, bypass message box.
218 bool user_approved
= false;
219 if (!whitelisted_extension
&& !component_extension
) {
220 base::string16 application_name
=
221 base::UTF8ToUTF16(request
.security_origin
.spec());
223 application_name
= base::UTF8ToUTF16(extension
->name());
224 base::string16 confirmation_text
= l10n_util::GetStringFUTF16(
225 request
.audio_type
== content::MEDIA_NO_SERVICE
226 ? IDS_MEDIA_SCREEN_CAPTURE_CONFIRMATION_TEXT
227 : IDS_MEDIA_SCREEN_AND_AUDIO_CAPTURE_CONFIRMATION_TEXT
,
229 chrome::MessageBoxResult result
= chrome::ShowMessageBox(
231 l10n_util::GetStringFUTF16(
232 IDS_MEDIA_SCREEN_CAPTURE_CONFIRMATION_TITLE
, application_name
),
233 confirmation_text
, chrome::MESSAGE_BOX_TYPE_QUESTION
);
234 user_approved
= (result
== chrome::MESSAGE_BOX_RESULT_YES
);
237 if (user_approved
|| component_extension
|| whitelisted_extension
) {
238 content::DesktopMediaID screen_id
;
239 #if defined(OS_CHROMEOS)
240 screen_id
= content::DesktopMediaID::RegisterAuraWindow(
241 content::DesktopMediaID::TYPE_SCREEN
,
242 ash::Shell::GetInstance()->GetPrimaryRootWindow());
243 #else // defined(OS_CHROMEOS)
244 screen_id
= content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN
,
245 webrtc::kFullDesktopScreenId
);
246 #endif // !defined(OS_CHROMEOS)
249 (request
.audio_type
== content::MEDIA_DESKTOP_AUDIO_CAPTURE
&&
250 loopback_audio_supported
);
252 // Unless we're being invoked from a component extension, register to
253 // display the notification for stream capture.
254 bool display_notification
= !component_extension
;
256 ui
= GetDevicesForDesktopCapture(&devices
, screen_id
, capture_audio
,
257 display_notification
, application_title
,
259 DCHECK(!devices
.empty());
262 // The only case when devices can be empty is if the user has denied
264 result
= devices
.empty() ? content::MEDIA_DEVICE_PERMISSION_DENIED
265 : content::MEDIA_DEVICE_OK
;
268 callback
.Run(devices
, result
, ui
.Pass());
271 bool DesktopCaptureAccessHandler::SupportsStreamType(
272 const content::MediaStreamType type
,
273 const extensions::Extension
* extension
) {
274 return type
== content::MEDIA_DESKTOP_VIDEO_CAPTURE
||
275 type
== content::MEDIA_DESKTOP_AUDIO_CAPTURE
;
278 bool DesktopCaptureAccessHandler::CheckMediaAccessPermission(
279 content::WebContents
* web_contents
,
280 const GURL
& security_origin
,
281 content::MediaStreamType type
,
282 const extensions::Extension
* extension
) {
286 void DesktopCaptureAccessHandler::HandleRequest(
287 content::WebContents
* web_contents
,
288 const content::MediaStreamRequest
& request
,
289 const content::MediaResponseCallback
& callback
,
290 const extensions::Extension
* extension
) {
291 content::MediaStreamDevices devices
;
292 scoped_ptr
<content::MediaStreamUI
> ui
;
294 if (request
.video_type
!= content::MEDIA_DESKTOP_VIDEO_CAPTURE
) {
295 callback
.Run(devices
, content::MEDIA_DEVICE_INVALID_STATE
, ui
.Pass());
299 // If the device id wasn't specified then this is a screen capture request
300 // (i.e. chooseDesktopMedia() API wasn't used to generate device id).
301 if (request
.requested_video_device_id
.empty()) {
302 ProcessScreenCaptureAccessRequest(web_contents
, request
, callback
,
307 // The extension name that the stream is registered with.
308 std::string original_extension_name
;
309 // Resolve DesktopMediaID for the specified device id.
310 content::DesktopMediaID media_id
;
311 // TODO(miu): Replace "main RenderFrame" IDs with the request's actual
312 // RenderFrame IDs once the desktop capture extension API implementation is
313 // fixed. http://crbug.com/304341
314 content::WebContents
* const web_contents_for_stream
=
315 content::WebContents::FromRenderFrameHost(
316 content::RenderFrameHost::FromID(request
.render_process_id
,
317 request
.render_frame_id
));
318 content::RenderFrameHost
* const main_frame
=
319 web_contents_for_stream
? web_contents_for_stream
->GetMainFrame() : NULL
;
321 media_id
= MediaCaptureDevicesDispatcher::GetInstance()
322 ->GetDesktopStreamsRegistry()
323 ->RequestMediaForStreamId(request
.requested_video_device_id
,
324 main_frame
->GetProcess()->GetID(),
325 main_frame
->GetRoutingID(),
326 request
.security_origin
,
327 &original_extension_name
);
330 // Received invalid device id.
331 if (media_id
.type
== content::DesktopMediaID::TYPE_NONE
) {
332 callback
.Run(devices
, content::MEDIA_DEVICE_INVALID_STATE
, ui
.Pass());
336 bool loopback_audio_supported
= false;
337 #if defined(USE_CRAS) || defined(OS_WIN)
338 // Currently loopback audio capture is supported only on Windows and ChromeOS.
339 loopback_audio_supported
= true;
342 // Audio is only supported for screen capture streams.
344 (media_id
.type
== content::DesktopMediaID::TYPE_SCREEN
&&
345 request
.audio_type
== content::MEDIA_DESKTOP_AUDIO_CAPTURE
&&
346 loopback_audio_supported
);
348 ui
= GetDevicesForDesktopCapture(&devices
, media_id
, capture_audio
, true,
349 GetApplicationTitle(web_contents
, extension
),
350 base::UTF8ToUTF16(original_extension_name
));
352 callback
.Run(devices
, content::MEDIA_DEVICE_OK
, ui
.Pass());
355 void DesktopCaptureAccessHandler::UpdateMediaRequestState(
356 int render_process_id
,
359 content::MediaStreamType stream_type
,
360 content::MediaRequestState state
) {
361 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
362 // Track desktop capture sessions. Tracking is necessary to avoid unbalanced
363 // session counts since not all requests will reach MEDIA_REQUEST_STATE_DONE,
364 // but they will all reach MEDIA_REQUEST_STATE_CLOSING.
365 if (stream_type
!= content::MEDIA_DESKTOP_VIDEO_CAPTURE
)
368 if (state
== content::MEDIA_REQUEST_STATE_DONE
) {
369 DesktopCaptureSession session
= {
370 render_process_id
, render_frame_id
, page_request_id
};
371 desktop_capture_sessions_
.push_back(session
);
372 } else if (state
== content::MEDIA_REQUEST_STATE_CLOSING
) {
373 for (DesktopCaptureSessions::iterator it
=
374 desktop_capture_sessions_
.begin();
375 it
!= desktop_capture_sessions_
.end(); ++it
) {
376 if (it
->render_process_id
== render_process_id
&&
377 it
->render_frame_id
== render_frame_id
&&
378 it
->page_request_id
== page_request_id
) {
379 desktop_capture_sessions_
.erase(it
);
386 bool DesktopCaptureAccessHandler::IsCaptureInProgress() {
387 return desktop_capture_sessions_
.size() > 0;