Enable Picture-in-Picture for extensions.
This CL enables Picture-in-Picture in extensions by implementing
EnterPictureInPicture and ExitPictureInPicture in ExtensionHostDelegate.
Change-Id: I77522e345b7fed7d4515e81204a635a5ed8ecb6c
Bug: 726619
Reviewed-on: https://chromium-review.googlesource.com/1145261
Commit-Queue: François Beaufort <[email protected]>
Reviewed-by: Sadrul Chowdhury <[email protected]>
Reviewed-by: Devlin <[email protected]>
Reviewed-by: Sergey Volk <[email protected]>
Cr-Commit-Position: refs/heads/master@{#586632}
diff --git a/chrome/browser/extensions/api/extension_action/browser_action_apitest.cc b/chrome/browser/extensions/api/extension_action/browser_action_apitest.cc
index 889510d0..cac23b1 100644
--- a/chrome/browser/extensions/api/extension_action/browser_action_apitest.cc
+++ b/chrome/browser/extensions/api/extension_action/browser_action_apitest.cc
@@ -36,6 +36,7 @@
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/notification_service.h"
+#include "content/public/browser/picture_in_picture_window_controller.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
@@ -1115,5 +1116,48 @@
#endif
}
+// Verify video can enter and exit Picture-in_Picture when browser action icon
+// is clicked.
+IN_PROC_BROWSER_TEST_F(BrowserActionApiTest,
+ TestPictureInPictureOnBrowserActionIconClick) {
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ ASSERT_TRUE(
+ RunExtensionTest("trigger_actions/browser_action_picture_in_picture"))
+ << message_;
+ const Extension* extension = GetSingleLoadedExtension();
+ ASSERT_TRUE(extension) << message_;
+
+ // Test that there is a browser action in the toolbar.
+ ASSERT_EQ(1, GetBrowserActionsBar()->NumberOfBrowserActions());
+
+ ExtensionAction* browser_action = GetBrowserAction(*extension);
+ EXPECT_TRUE(browser_action);
+
+ // Find the background page.
+ ProcessManager* process_manager =
+ extensions::ProcessManager::Get(browser()->profile());
+ content::WebContents* web_contents =
+ process_manager->GetBackgroundHostForExtension(extension->id())
+ ->web_contents();
+ ASSERT_TRUE(web_contents);
+ content::PictureInPictureWindowController* window_controller =
+ content::PictureInPictureWindowController::GetOrCreateForWebContents(
+ web_contents);
+ ASSERT_TRUE(window_controller->GetWindowForTesting());
+ EXPECT_FALSE(window_controller->GetWindowForTesting()->IsVisible());
+
+ // Click on the browser action icon to enter Picture-in-Picture.
+ ResultCatcher catcher;
+ GetBrowserActionsBar()->Press(0);
+ EXPECT_TRUE(catcher.GetNextResult());
+ EXPECT_TRUE(window_controller->GetWindowForTesting()->IsVisible());
+
+ // Click on the browser action icon to exit Picture-in-Picture.
+ GetBrowserActionsBar()->Press(0);
+ EXPECT_TRUE(catcher.GetNextResult());
+ EXPECT_FALSE(window_controller->GetWindowForTesting()->IsVisible());
+}
+
} // namespace
} // namespace extensions
diff --git a/chrome/browser/extensions/chrome_extension_host_delegate.cc b/chrome/browser/extensions/chrome_extension_host_delegate.cc
index 6736efb..13d09c9 100644
--- a/chrome/browser/extensions/chrome_extension_host_delegate.cc
+++ b/chrome/browser/extensions/chrome_extension_host_delegate.cc
@@ -14,6 +14,7 @@
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
+#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
#include "chrome/browser/ui/prefs/prefs_tab_helper.h"
#include "components/app_modal/javascript_dialog_manager.h"
#include "extensions/browser/extension_host.h"
@@ -102,4 +103,16 @@
return g_queue.Get().queue.get();
}
+gfx::Size ChromeExtensionHostDelegate::EnterPictureInPicture(
+ content::WebContents* web_contents,
+ const viz::SurfaceId& surface_id,
+ const gfx::Size& natural_size) {
+ return PictureInPictureWindowManager::GetInstance()->EnterPictureInPicture(
+ web_contents, surface_id, natural_size);
+}
+
+void ChromeExtensionHostDelegate::ExitPictureInPicture() {
+ PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture();
+}
+
} // namespace extensions
diff --git a/chrome/browser/extensions/chrome_extension_host_delegate.h b/chrome/browser/extensions/chrome_extension_host_delegate.h
index 03d3727..3dd24a7f 100644
--- a/chrome/browser/extensions/chrome_extension_host_delegate.h
+++ b/chrome/browser/extensions/chrome_extension_host_delegate.h
@@ -33,6 +33,10 @@
content::MediaStreamType type,
const Extension* extension) override;
ExtensionHostQueue* GetExtensionHostQueue() const override;
+ gfx::Size EnterPictureInPicture(content::WebContents* web_contents,
+ const viz::SurfaceId& surface_id,
+ const gfx::Size& natural_size) override;
+ void ExitPictureInPicture() override;
};
} // namespace extensions
diff --git a/chrome/test/data/extensions/api_test/trigger_actions/browser_action_picture_in_picture/background.js b/chrome/test/data/extensions/api_test/trigger_actions/browser_action_picture_in_picture/background.js
new file mode 100644
index 0000000..6ed0f2f
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/trigger_actions/browser_action_picture_in_picture/background.js
@@ -0,0 +1,40 @@
+// Copyright 2018 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.
+
+const video = document.createElement('video');
+
+chrome.test.getConfig(function(config) {
+ video.src = 'http://localhost:' + config.testServer.port +
+ '/media/bigbuck.webm';
+ video.load();
+ video.addEventListener('loadedmetadata', function() {
+ chrome.test.notifyPass();
+ })
+});
+
+// Toggle Picture-in-Picture when the user clicks on the browser action.
+chrome.browserAction.onClicked.addListener(function(tab) {
+ if (video !== document.pictureInPictureElement) {
+ enterPictureInPicture();
+ } else {
+ exitPictureInPicture();
+ }
+});
+
+function enterPictureInPicture() {
+ video.requestPictureInPicture()
+ .then(pipWindow => {
+ if (pipWindow.width === 0 || pipWindow.height === 0) {
+ return Promise.reject('Picture-in-Picture window size is not set.');
+ }
+ chrome.test.notifyPass();
+ })
+ .catch(error => { chrome.test.notifyFail('Error: ' + error); })
+}
+
+function exitPictureInPicture() {
+ document.exitPictureInPicture()
+ .then(() => { chrome.test.notifyPass(); })
+ .catch(error => { chrome.test.notifyFail('Error: ' + error); })
+}
diff --git a/chrome/test/data/extensions/api_test/trigger_actions/browser_action_picture_in_picture/manifest.json b/chrome/test/data/extensions/api_test/trigger_actions/browser_action_picture_in_picture/manifest.json
new file mode 100644
index 0000000..a1f17d1
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/trigger_actions/browser_action_picture_in_picture/manifest.json
@@ -0,0 +1,11 @@
+{
+ "name": "A browser action that toggles Picture-in-Picture",
+ "version": "1.0",
+ "manifest_version": 2,
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "browser_action": {
+ "default_title": "Toggle Picture-in-Picture"
+ }
+}
diff --git a/chromecast/browser/extensions/cast_extension_host_delegate.cc b/chromecast/browser/extensions/cast_extension_host_delegate.cc
index d58d416c..1c1f6ee 100644
--- a/chromecast/browser/extensions/cast_extension_host_delegate.cc
+++ b/chromecast/browser/extensions/cast_extension_host_delegate.cc
@@ -62,4 +62,16 @@
return queue.get();
}
+gfx::Size CastExtensionHostDelegate::EnterPictureInPicture(
+ content::WebContents* web_contents,
+ const viz::SurfaceId& surface_id,
+ const gfx::Size& natural_size) {
+ NOTREACHED();
+ return gfx::Size();
+}
+
+void CastExtensionHostDelegate::ExitPictureInPicture() {
+ NOTREACHED();
+}
+
} // namespace extensions
diff --git a/chromecast/browser/extensions/cast_extension_host_delegate.h b/chromecast/browser/extensions/cast_extension_host_delegate.h
index c471edf..d548eed 100644
--- a/chromecast/browser/extensions/cast_extension_host_delegate.h
+++ b/chromecast/browser/extensions/cast_extension_host_delegate.h
@@ -34,6 +34,10 @@
content::MediaStreamType type,
const Extension* extension) override;
ExtensionHostQueue* GetExtensionHostQueue() const override;
+ gfx::Size EnterPictureInPicture(content::WebContents* web_contents,
+ const viz::SurfaceId& surface_id,
+ const gfx::Size& natural_size) override;
+ void ExitPictureInPicture() override;
private:
DISALLOW_COPY_AND_ASSIGN(CastExtensionHostDelegate);
diff --git a/extensions/browser/extension_host.cc b/extensions/browser/extension_host.cc
index 999df3d..56bf6a8 100644
--- a/extensions/browser/extension_host.cc
+++ b/extensions/browser/extension_host.cc
@@ -455,6 +455,18 @@
return view_type == extensions::VIEW_TYPE_EXTENSION_BACKGROUND_PAGE;
}
+gfx::Size ExtensionHost::EnterPictureInPicture(const viz::SurfaceId& surface_id,
+ const gfx::Size& natural_size) {
+ // TODO(crbug.com/870609): Increment the keepalive count of the background
+ // page to avoid it shutting down while playing video.
+ return delegate_->EnterPictureInPicture(web_contents(), surface_id,
+ natural_size);
+}
+
+void ExtensionHost::ExitPictureInPicture() {
+ delegate_->ExitPictureInPicture();
+}
+
void ExtensionHost::RecordStopLoadingUMA() {
CHECK(load_start_.get());
if (extension_host_type_ == VIEW_TYPE_EXTENSION_BACKGROUND_PAGE) {
diff --git a/extensions/browser/extension_host.h b/extensions/browser/extension_host.h
index bd05034..db4489f 100644
--- a/extensions/browser/extension_host.h
+++ b/extensions/browser/extension_host.h
@@ -127,6 +127,9 @@
const GURL& security_origin,
content::MediaStreamType type) override;
bool IsNeverVisible(content::WebContents* web_contents) override;
+ gfx::Size EnterPictureInPicture(const viz::SurfaceId& surface_id,
+ const gfx::Size& natural_size) override;
+ void ExitPictureInPicture() override;
// ExtensionRegistryObserver:
void OnExtensionReady(content::BrowserContext* browser_context,
diff --git a/extensions/browser/extension_host_delegate.h b/extensions/browser/extension_host_delegate.h
index 548ea387..e648dc2 100644
--- a/extensions/browser/extension_host_delegate.h
+++ b/extensions/browser/extension_host_delegate.h
@@ -18,6 +18,11 @@
namespace gfx {
class Rect;
+class Size;
+} // namespace gfx
+
+namespace viz {
+class SurfaceId;
}
namespace extensions {
@@ -71,6 +76,17 @@
// Returns the ExtensionHostQueue implementation to use for creating
// ExtensionHost renderers.
virtual ExtensionHostQueue* GetExtensionHostQueue() const = 0;
+
+ // Notifies the Picture-in-Picture controller that there is a new player
+ // entering Picture-in-Picture.
+ // Returns the size of the Picture-in-Picture window.
+ virtual gfx::Size EnterPictureInPicture(content::WebContents* web_contents,
+ const viz::SurfaceId& surface_id,
+ const gfx::Size& natural_size) = 0;
+
+ // Updates the Picture-in-Picture controller with a signal that
+ // Picture-in-Picture mode has ended.
+ virtual void ExitPictureInPicture() = 0;
};
} // namespace extensions
diff --git a/extensions/shell/browser/shell_extension_host_delegate.cc b/extensions/shell/browser/shell_extension_host_delegate.cc
index ad7e362..686c912 100644
--- a/extensions/shell/browser/shell_extension_host_delegate.cc
+++ b/extensions/shell/browser/shell_extension_host_delegate.cc
@@ -71,4 +71,16 @@
return g_queue.Pointer();
}
+gfx::Size ShellExtensionHostDelegate::EnterPictureInPicture(
+ content::WebContents* web_contents,
+ const viz::SurfaceId& surface_id,
+ const gfx::Size& natural_size) {
+ NOTREACHED();
+ return gfx::Size();
+}
+
+void ShellExtensionHostDelegate::ExitPictureInPicture() {
+ NOTREACHED();
+}
+
} // namespace extensions
diff --git a/extensions/shell/browser/shell_extension_host_delegate.h b/extensions/shell/browser/shell_extension_host_delegate.h
index 832c9dd..1fe1ef66 100644
--- a/extensions/shell/browser/shell_extension_host_delegate.h
+++ b/extensions/shell/browser/shell_extension_host_delegate.h
@@ -34,6 +34,10 @@
content::MediaStreamType type,
const Extension* extension) override;
ExtensionHostQueue* GetExtensionHostQueue() const override;
+ gfx::Size EnterPictureInPicture(content::WebContents* web_contents,
+ const viz::SurfaceId& surface_id,
+ const gfx::Size& natural_size) override;
+ void ExitPictureInPicture() override;
private:
DISALLOW_COPY_AND_ASSIGN(ShellExtensionHostDelegate);
diff --git a/testing/buildbot/filters/chromeos.mash.fyi.browser_tests.filter b/testing/buildbot/filters/chromeos.mash.fyi.browser_tests.filter
index 31f1d06..c182a61 100644
--- a/testing/buildbot/filters/chromeos.mash.fyi.browser_tests.filter
+++ b/testing/buildbot/filters/chromeos.mash.fyi.browser_tests.filter
@@ -278,6 +278,7 @@
# https://crbug.com/827327
-PictureInPictureWindowControllerBrowserTest.*
-ControlPictureInPictureWindowControllerBrowserTest.*
+-BrowserActionApiTest.TestPictureInPictureOnBrowserActionIconClick
# These started failing with the switch to ws2.
# https:://crbug.com/855767
diff --git a/ui/views/test/widget_test.cc b/ui/views/test/widget_test.cc
index d94091c..91f1691 100644
--- a/ui/views/test/widget_test.cc
+++ b/ui/views/test/widget_test.cc
@@ -99,6 +99,9 @@
TestDesktopWidgetDelegate::TestDesktopWidgetDelegate() : widget_(new Widget) {
}
+TestDesktopWidgetDelegate::TestDesktopWidgetDelegate(Widget* widget)
+ : widget_(widget) {}
+
TestDesktopWidgetDelegate::~TestDesktopWidgetDelegate() {
if (widget_)
widget_->CloseNow();
diff --git a/ui/views/test/widget_test.h b/ui/views/test/widget_test.h
index 80c690a2..2e871d9 100644
--- a/ui/views/test/widget_test.h
+++ b/ui/views/test/widget_test.h
@@ -118,6 +118,7 @@
class TestDesktopWidgetDelegate : public WidgetDelegate {
public:
TestDesktopWidgetDelegate();
+ TestDesktopWidgetDelegate(Widget* widget);
~TestDesktopWidgetDelegate() override;
// Initialize the Widget, adding some meaningful default InitParams.
diff --git a/ui/views/widget/desktop_aura/desktop_native_widget_aura.cc b/ui/views/widget/desktop_aura/desktop_native_widget_aura.cc
index 09612ee..01a429a 100644
--- a/ui/views/widget/desktop_aura/desktop_native_widget_aura.cc
+++ b/ui/views/widget/desktop_aura/desktop_native_widget_aura.cc
@@ -321,8 +321,12 @@
desktop_window_tree_host_ = NULL;
content_window_ = NULL;
+ // |OnNativeWidgetDestroyed| may delete |this| if the object does not own
+ // itself.
+ bool should_delete_this =
+ (ownership_ == Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET);
native_widget_delegate_->OnNativeWidgetDestroyed();
- if (ownership_ == Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET)
+ if (should_delete_this)
delete this;
}
diff --git a/ui/views/widget/widget_unittest.cc b/ui/views/widget/widget_unittest.cc
index 1bd99e8..24c23336 100644
--- a/ui/views/widget/widget_unittest.cc
+++ b/ui/views/widget/widget_unittest.cc
@@ -1944,6 +1944,29 @@
EXPECT_TRUE(observer.widget_closed());
}
+// Widget used to destroy itself when OnNativeWidgetDestroyed is called.
+class TestNativeWidgetDestroyedWidget : public Widget {
+ public:
+ // Overridden from NativeWidgetDelegate:
+ void OnNativeWidgetDestroyed() override;
+};
+
+void TestNativeWidgetDestroyedWidget::OnNativeWidgetDestroyed() {
+ Widget::OnNativeWidgetDestroyed();
+ delete this;
+}
+
+// Verifies that widget destroyed itself in OnNativeWidgetDestroyed does not
+// crash in ASan.
+TEST_F(WidgetTest, WidgetDestroyedItselfDoesNotCrash) {
+ TestDesktopWidgetDelegate delegate(new TestNativeWidgetDestroyedWidget);
+ Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
+ params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
+ delegate.InitWidget(params);
+ delegate.GetWidget()->Show();
+ delegate.GetWidget()->CloseNow();
+}
+
// Verifies WindowClosing() is invoked correctly on the delegate when a Widget
// is closed.
TEST_F(WidgetTest, SingleWindowClosing) {