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/extensions/api/tab_capture/offscreen_presentation.h"
10 #include "chrome/browser/extensions/api/tab_capture/tab_capture_registry.h"
11 #include "chrome/browser/profiles/profile.h"
12 #include "chrome/browser/ui/web_contents_sizer.h"
13 #include "content/public/browser/render_widget_host_view.h"
14 #include "content/public/browser/web_contents.h"
15 #include "extensions/browser/extension_host.h"
16 #include "extensions/browser/process_manager.h"
19 #include "base/memory/weak_ptr.h"
20 #include "base/thread_task_runner_handle.h"
21 #include "chrome/browser/ui/browser.h"
22 #include "chrome/browser/ui/browser_list.h"
23 #include "chrome/browser/ui/browser_window.h"
24 #include "ui/aura/window.h"
25 #include "ui/gfx/native_widget_types.h"
28 using content::WebContents
;
30 DEFINE_WEB_CONTENTS_USER_DATA_KEY(extensions::OffscreenPresentationsOwner
);
34 // Upper limit on the number of simultaneous off-screen presentations per
35 // extension instance.
36 const int kMaxPresentationsPerExtension
= 3;
38 // Time intervals used by the logic that detects when the capture of a
39 // presentation has stopped, to automatically tear it down and free resources.
40 const int kMaxSecondsToWaitForCapture
= 60;
41 const int kPollIntervalInSeconds
= 1;
45 // A WindowObserver that automatically finds a root Window to adopt the
46 // WebContents native view containing the OffscreenPresentation content. This
47 // is a workaround for Aura, which requires the WebContents native view be
48 // attached somewhere in the window tree in order to gain access to the
49 // compositing and capture functionality. The WebContents native view, although
50 // attached to the window tree, will never become visible on-screen.
51 class WindowAdoptionAgent
: protected aura::WindowObserver
{
53 static void Start(aura::Window
* offscreen_window
) {
54 new WindowAdoptionAgent(offscreen_window
);
55 // WindowAdoptionAgent destroys itself when the Window calls
56 // OnWindowDestroyed().
60 void OnWindowParentChanged(aura::Window
* target
,
61 aura::Window
* parent
) final
{
62 if (target
!= offscreen_window_
|| parent
!= nullptr)
65 // Post a task to return to the event loop before finding a new parent, to
66 // avoid clashing with the currently-in-progress window tree hierarchy
68 base::ThreadTaskRunnerHandle::Get()->PostTask(
70 base::Bind(&WindowAdoptionAgent::FindNewParent
,
71 weak_ptr_factory_
.GetWeakPtr()));
74 void OnWindowDestroyed(aura::Window
* window
) final
{
79 explicit WindowAdoptionAgent(aura::Window
* offscreen_window
)
80 : offscreen_window_(offscreen_window
),
81 weak_ptr_factory_(this) {
82 DVLOG(2) << "WindowAdoptionAgent for offscreen window " << offscreen_window_
83 << " is being created.";
84 offscreen_window
->AddObserver(this);
85 OnWindowParentChanged(offscreen_window_
, offscreen_window_
->parent());
88 ~WindowAdoptionAgent() final
{
89 DVLOG(2) << "WindowAdoptionAgent for offscreen window " << offscreen_window_
90 << " is self-destructing.";
93 void FindNewParent() {
94 BrowserList
* const browsers
=
95 BrowserList::GetInstance(chrome::GetActiveDesktop());
96 Browser
* const active_browser
=
97 browsers
? browsers
->GetLastActive() : nullptr;
98 BrowserWindow
* const active_window
=
99 active_browser
? active_browser
->window() : nullptr;
100 aura::Window
* const native_window
=
101 active_window
? active_window
->GetNativeWindow() : nullptr;
102 aura::Window
* const root_window
=
103 native_window
? native_window
->GetRootWindow() : nullptr;
105 DVLOG(2) << "Root window " << root_window
106 << " adopts the offscreen window " << offscreen_window_
<< '.';
107 root_window
->AddChild(offscreen_window_
);
109 LOG(DFATAL
) << "Unable to find an aura root window. "
110 "OffscreenPresentation compositing may be halted!";
114 aura::Window
* const offscreen_window_
;
115 base::WeakPtrFactory
<WindowAdoptionAgent
> weak_ptr_factory_
;
117 DISALLOW_COPY_AND_ASSIGN(WindowAdoptionAgent
);
124 namespace extensions
{
126 OffscreenPresentationsOwner::OffscreenPresentationsOwner(WebContents
* contents
)
127 : extension_web_contents_(contents
) {
128 DCHECK(extension_web_contents_
);
131 OffscreenPresentationsOwner::~OffscreenPresentationsOwner() {}
134 OffscreenPresentationsOwner
* OffscreenPresentationsOwner::Get(
135 content::WebContents
* extension_web_contents
) {
136 // CreateForWebContents() really means "create if not exists."
137 CreateForWebContents(extension_web_contents
);
138 return FromWebContents(extension_web_contents
);
141 OffscreenPresentation
* OffscreenPresentationsOwner::FindOrStartPresentation(
142 const GURL
& start_url
,
143 const std::string
& presentation_id
,
144 const gfx::Size
& initial_size
) {
145 OffscreenPresentation
* presentation
=
146 FindPresentation(start_url
, presentation_id
);
148 DVLOG(1) << "Returning already-running OffscreenPresentation for start_url="
149 << presentation
->start_url();
153 if (presentations_
.size() >= kMaxPresentationsPerExtension
)
154 return nullptr; // Maximum number of presentations reached.
156 presentation
= new OffscreenPresentation(this, start_url
, presentation_id
);
157 presentations_
.push_back(presentation
);
158 presentation
->Start(initial_size
);
162 void OffscreenPresentationsOwner::ClosePresentation(
163 OffscreenPresentation
* presentation
) {
165 std::find(presentations_
.begin(), presentations_
.end(), presentation
);
166 if (it
!= presentations_
.end())
167 presentations_
.erase(it
);
170 OffscreenPresentation
* OffscreenPresentationsOwner::FindPresentation(
171 const GURL
& start_url
, const std::string
& presentation_id
) const {
172 for (OffscreenPresentation
* presentation
: presentations_
) {
173 if (presentation
->start_url() == start_url
&&
174 presentation
->presentation_id() == presentation_id
)
180 OffscreenPresentation::OffscreenPresentation(OffscreenPresentationsOwner
* owner
,
181 const GURL
& start_url
,
182 const std::string
& id
)
184 start_url_(start_url
),
185 presentation_id_(id
),
186 profile_(Profile::FromBrowserContext(
187 owner
->extension_web_contents()->GetBrowserContext())
188 ->CreateOffTheRecordProfile()),
189 capture_poll_timer_(false, false),
190 content_capture_was_detected_(false) {
194 OffscreenPresentation::~OffscreenPresentation() {
195 DVLOG(1) << "Destroying OffscreenPresentation for start_url="
196 << start_url_
.spec();
199 void OffscreenPresentation::Start(const gfx::Size
& initial_size
) {
200 DCHECK(start_time_
.is_null());
201 DVLOG(1) << "Starting OffscreenPresentation with initial size of "
202 << initial_size
.ToString() << " for start_url=" << start_url_
.spec();
204 // Create the WebContents to contain the off-screen presentation page.
205 presentation_web_contents_
.reset(
206 WebContents::Create(WebContents::CreateParams(profile_
.get())));
207 presentation_web_contents_
->SetDelegate(this);
208 WebContentsObserver::Observe(presentation_web_contents_
.get());
210 #if defined(USE_AURA)
211 WindowAdoptionAgent::Start(presentation_web_contents_
->GetNativeView());
214 // Set initial size, if specified.
215 if (!initial_size
.IsEmpty())
216 ResizeWebContents(presentation_web_contents_
.get(), initial_size
);
218 // Mute audio output. When tab capture starts, the audio will be
219 // automatically unmuted, but will be captured into the MediaStream.
220 presentation_web_contents_
->SetAudioMuted(true);
222 // Navigate to the initial URL of the presentation.
223 content::NavigationController::LoadURLParams
load_params(start_url_
);
224 load_params
.should_replace_current_entry
= true;
225 load_params
.should_clear_history_list
= true;
226 presentation_web_contents_
->GetController().LoadURLWithParams(load_params
);
228 start_time_
= base::TimeTicks::Now();
229 DieIfContentCaptureEnded();
232 void OffscreenPresentation::CloseContents(WebContents
* source
) {
233 DCHECK_EQ(presentation_web_contents_
.get(), source
);
234 // Javascript in the page called window.close().
235 DVLOG(1) << "OffscreenPresentation will die at renderer's request for "
236 "start_url=" << start_url_
.spec();
237 owner_
->ClosePresentation(this);
240 bool OffscreenPresentation::ShouldSuppressDialogs(WebContents
* source
) {
241 DCHECK_EQ(presentation_web_contents_
.get(), source
);
242 // Suppress all because there is no possible direct user interaction with
247 bool OffscreenPresentation::ShouldFocusLocationBarByDefault(
248 WebContents
* source
) {
249 DCHECK_EQ(presentation_web_contents_
.get(), source
);
250 // Indicate the location bar should be focused instead of the page, even
251 // though there is no location bar. This will prevent the page from
252 // automatically receiving input focus, which should never occur since there
253 // is not supposed to be any direct user interaction.
257 bool OffscreenPresentation::ShouldFocusPageAfterCrash() {
258 // Never focus the page. Not even after a crash.
262 void OffscreenPresentation::CanDownload(
264 const std::string
& request_method
,
265 const base::Callback
<void(bool)>& callback
) {
266 // Presentation pages are not allowed to download files.
270 bool OffscreenPresentation::HandleContextMenu(
271 const content::ContextMenuParams
& params
) {
272 // Context menus should never be shown. Do nothing, but indicate the context
273 // menu was shown so that default implementation in libcontent does not
274 // attempt to do so on its own.
278 bool OffscreenPresentation::PreHandleKeyboardEvent(
280 const content::NativeWebKeyboardEvent
& event
,
281 bool* is_keyboard_shortcut
) {
282 DCHECK_EQ(presentation_web_contents_
.get(), source
);
283 // Intercept and silence all keyboard events before they can be sent to the
285 *is_keyboard_shortcut
= false;
289 bool OffscreenPresentation::PreHandleGestureEvent(
291 const blink::WebGestureEvent
& event
) {
292 DCHECK_EQ(presentation_web_contents_
.get(), source
);
293 // Intercept and silence all gesture events before they can be sent to the
298 bool OffscreenPresentation::CanDragEnter(
300 const content::DropData
& data
,
301 blink::WebDragOperationsMask operations_allowed
) {
302 DCHECK_EQ(presentation_web_contents_
.get(), source
);
303 // Halt all drag attempts onto the page since there should be no direct user
304 // interaction with it.
308 bool OffscreenPresentation::ShouldCreateWebContents(
309 WebContents
* contents
,
311 int main_frame_route_id
,
312 WindowContainerType window_container_type
,
313 const std::string
& frame_name
,
314 const GURL
& target_url
,
315 const std::string
& partition_id
,
316 content::SessionStorageNamespace
* session_storage_namespace
) {
317 DCHECK_EQ(presentation_web_contents_
.get(), contents
);
318 // Disallow creating separate WebContentses. The WebContents implementation
319 // uses this to spawn new windows/tabs, which is also not allowed for
320 // presentation pages.
324 bool OffscreenPresentation::EmbedsFullscreenWidget() const {
325 // OffscreenPresentation will manage fullscreen widgets.
329 void OffscreenPresentation::EnterFullscreenModeForTab(WebContents
* contents
,
330 const GURL
& origin
) {
331 DCHECK_EQ(presentation_web_contents_
.get(), contents
);
333 if (in_fullscreen_mode())
336 // TODO(miu): Refine fullscreen handling behavior once the Presentation API
337 // spec group defines this behavior.
339 non_fullscreen_size_
=
340 contents
->GetRenderWidgetHostView()->GetViewBounds().size();
341 if (contents
->GetCapturerCount() >= 0 &&
342 !contents
->GetPreferredSize().IsEmpty()) {
343 ResizeWebContents(contents
, contents
->GetPreferredSize());
347 void OffscreenPresentation::ExitFullscreenModeForTab(WebContents
* contents
) {
348 DCHECK_EQ(presentation_web_contents_
.get(), contents
);
350 if (!in_fullscreen_mode())
353 ResizeWebContents(contents
, non_fullscreen_size_
);
354 non_fullscreen_size_
= gfx::Size();
357 bool OffscreenPresentation::IsFullscreenForTabOrPending(
358 const WebContents
* contents
) const {
359 DCHECK_EQ(presentation_web_contents_
.get(), contents
);
360 return in_fullscreen_mode();
363 blink::WebDisplayMode
OffscreenPresentation::GetDisplayMode(
364 const WebContents
* contents
) const {
365 DCHECK_EQ(presentation_web_contents_
.get(), contents
);
366 return in_fullscreen_mode() ?
367 blink::WebDisplayModeFullscreen
: blink::WebDisplayModeBrowser
;
370 void OffscreenPresentation::RequestMediaAccessPermission(
371 WebContents
* contents
,
372 const content::MediaStreamRequest
& request
,
373 const content::MediaResponseCallback
& callback
) {
374 DCHECK_EQ(presentation_web_contents_
.get(), contents
);
376 // This method is being called to check whether an extension is permitted to
377 // capture the page. Verify that the request is being made by the extension
378 // that spawned this OffscreenPresentation.
380 // Find the extension ID associated with the extension background page's
382 content::BrowserContext
* const extension_browser_context
=
383 owner_
->extension_web_contents()->GetBrowserContext();
384 std::string extension_id
;
385 for (const ExtensionHost
* host
:
386 ProcessManager::Get(extension_browser_context
)->background_hosts()) {
387 if (host
->host_contents() == owner_
->extension_web_contents()) {
388 extension_id
= host
->extension_id();
393 // If verified, allow any tab capture audio/video devices that were requested.
394 extensions::TabCaptureRegistry
* const tab_capture_registry
=
395 extensions::TabCaptureRegistry::Get(extension_browser_context
);
396 content::MediaStreamDevices devices
;
397 if (tab_capture_registry
&& tab_capture_registry
->VerifyRequest(
398 request
.render_process_id
,
399 request
.render_frame_id
,
401 if (request
.audio_type
== content::MEDIA_TAB_AUDIO_CAPTURE
) {
402 devices
.push_back(content::MediaStreamDevice(
403 content::MEDIA_TAB_AUDIO_CAPTURE
, std::string(), std::string()));
405 if (request
.video_type
== content::MEDIA_TAB_VIDEO_CAPTURE
) {
406 devices
.push_back(content::MediaStreamDevice(
407 content::MEDIA_TAB_VIDEO_CAPTURE
, std::string(), std::string()));
411 DVLOG(2) << "Allowing " << devices
.size()
412 << " capture devices for OffscreenPresentation content.";
416 devices
.empty() ? content::MEDIA_DEVICE_INVALID_STATE
:
417 content::MEDIA_DEVICE_OK
,
418 scoped_ptr
<content::MediaStreamUI
>(nullptr));
421 bool OffscreenPresentation::CheckMediaAccessPermission(
422 WebContents
* contents
,
423 const GURL
& security_origin
,
424 content::MediaStreamType type
) {
425 DCHECK_EQ(presentation_web_contents_
.get(), contents
);
426 return type
== content::MEDIA_TAB_AUDIO_CAPTURE
||
427 type
== content::MEDIA_TAB_VIDEO_CAPTURE
;
430 void OffscreenPresentation::DidShowFullscreenWidget(int routing_id
) {
431 if (presentation_web_contents_
->GetCapturerCount() == 0 ||
432 presentation_web_contents_
->GetPreferredSize().IsEmpty())
433 return; // Do nothing, since no preferred size is specified.
434 content::RenderWidgetHostView
* const current_fs_view
=
435 presentation_web_contents_
->GetFullscreenRenderWidgetHostView();
437 current_fs_view
->SetSize(presentation_web_contents_
->GetPreferredSize());
440 void OffscreenPresentation::DieIfContentCaptureEnded() {
441 DCHECK(presentation_web_contents_
.get());
443 if (content_capture_was_detected_
) {
444 if (presentation_web_contents_
->GetCapturerCount() == 0) {
445 DVLOG(2) << "Capture of OffscreenPresentation content has stopped for "
446 "start_url=" << start_url_
.spec();
447 owner_
->ClosePresentation(this);
450 DVLOG(3) << "Capture of OffscreenPresentation content continues for "
451 "start_url=" << start_url_
.spec();
453 } else if (presentation_web_contents_
->GetCapturerCount() > 0) {
454 DVLOG(2) << "Capture of OffscreenPresentation content has started for "
455 "start_url=" << start_url_
.spec();
456 content_capture_was_detected_
= true;
457 } else if (base::TimeTicks::Now() - start_time_
>
458 base::TimeDelta::FromSeconds(kMaxSecondsToWaitForCapture
)) {
459 // More than a minute has elapsed since this OffscreenPresentation was
460 // started and content capture still hasn't started. As a safety
461 // precaution, assume that content capture is never going to start and die
462 // to free up resources.
463 LOG(WARNING
) << "Capture of OffscreenPresentation content did not start "
464 "within timeout for start_url=" << start_url_
.spec();
465 owner_
->ClosePresentation(this);
469 // Schedule the timer to check again in a second.
470 capture_poll_timer_
.Start(
472 base::TimeDelta::FromSeconds(kPollIntervalInSeconds
),
473 base::Bind(&OffscreenPresentation::DieIfContentCaptureEnded
,
474 base::Unretained(this)));
477 } // namespace extensions