Commit 324ae62d authored by Mounir Lamouri's avatar Mounir Lamouri Committed by Commit Bot

Picture-in-Picture: move all logic to the PictureInPictureWindowControllerImpl.

This removes the duality between PictureInPictureServiceImpl and
PictureInPictureWindowControllerImpl that made handling the
PictureInPictureSession object more complicated than it should. Now, the service
is simply a shell passing the requests to the controller which then handles the
current session at the WebContents level.

This CL also introduces a content_browsertest test in order to allow for
integration tests that do not include the //chrome layer. Follow-ups may include
moving some browser_tests to it as they tend to be flaky and they are not
testing the actual UI.

Also adds a check for surface_id being null when the session creation is
requested.

Bug: 1067152, 1069858
FIXES: 1067152, 1069858
Change-Id: I1c389c513cc227e6d0f9d5e447926186600cdc2b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2197996
Commit-Queue: Mounir Lamouri <mlamouri@chromium.org>
Reviewed-by: default avatarBecca Hughes <beccahughes@chromium.org>
Reviewed-by: default avatarFrançois Beaufort <beaufort.francois@gmail.com>
Cr-Commit-Position: refs/heads/master@{#772029}
parent d5a923d3
// Copyright 2020 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.
#include "content/browser/picture_in_picture/picture_in_picture_service_impl.h"
#include "content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/overlay_window.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/mojom/picture_in_picture/picture_in_picture.mojom.h"
using testing::Mock;
using testing::NiceMock;
namespace content {
namespace {
class TestOverlayWindow : public OverlayWindow {
public:
TestOverlayWindow() = default;
~TestOverlayWindow() override = default;
static std::unique_ptr<OverlayWindow> Create(
PictureInPictureWindowController* controller) {
return std::unique_ptr<OverlayWindow>(new TestOverlayWindow());
}
bool IsActive() override { return false; }
void Close() override {}
void ShowInactive() override {}
void Hide() override {}
bool IsVisible() override { return false; }
bool IsAlwaysOnTop() override { return false; }
gfx::Rect GetBounds() override { return gfx::Rect(size_); }
void UpdateVideoSize(const gfx::Size& natural_size) override {
size_ = natural_size;
}
void SetPlaybackState(PlaybackState playback_state) override {}
void SetAlwaysHidePlayPauseButton(bool is_visible) override {}
void SetSkipAdButtonVisibility(bool is_visible) override {}
void SetNextTrackButtonVisibility(bool is_visible) override {}
void SetPreviousTrackButtonVisibility(bool is_visible) override {}
void SetSurfaceId(const viz::SurfaceId& surface_id) override {}
cc::Layer* GetLayerForTesting() override { return nullptr; }
private:
gfx::Size size_;
DISALLOW_COPY_AND_ASSIGN(TestOverlayWindow);
};
class TestContentBrowserClient : public ContentBrowserClient {
public:
TestContentBrowserClient() = default;
~TestContentBrowserClient() override = default;
std::unique_ptr<OverlayWindow> CreateWindowForPictureInPicture(
PictureInPictureWindowController* controller) override {
return TestOverlayWindow::Create(controller);
}
};
class TestWebContentsDelegate : public WebContentsDelegate {
public:
TestWebContentsDelegate() = default;
~TestWebContentsDelegate() override = default;
PictureInPictureResult EnterPictureInPicture(
WebContents* web_contents,
const viz::SurfaceId&,
const gfx::Size& natural_size) override {
return PictureInPictureResult::kSuccess;
}
MOCK_METHOD0(ExitPictureInPicture, void());
};
class PictureInPictureContentBrowserTest : public ContentBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
ContentBrowserTest::SetUpCommandLine(command_line);
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
switches::kEnableBlinkFeatures, "PictureInPictureAPI");
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
old_browser_client_ = SetBrowserClientForTesting(&content_browser_client_);
shell()->web_contents()->SetDelegate(&web_contents_delegate_);
}
void TearDownOnMainThread() override {
SetBrowserClientForTesting(old_browser_client_);
ContentBrowserTest::TearDownOnMainThread();
}
protected:
NiceMock<TestWebContentsDelegate> web_contents_delegate_;
private:
ContentBrowserClient* old_browser_client_ = nullptr;
TestContentBrowserClient content_browser_client_;
};
} // namespace
IN_PROC_BROWSER_TEST_F(PictureInPictureContentBrowserTest,
RequestSecondVideoInSameRFHDoesNotCloseWindow) {
EXPECT_CALL(web_contents_delegate_, ExitPictureInPicture()).Times(0);
EXPECT_TRUE(NavigateToURL(
shell(), GetTestUrl("media/picture_in_picture", "two-videos.html")));
// Play first video.
ASSERT_TRUE(ExecuteScript(shell()->web_contents(), "videos[0].play();"));
base::string16 expected_title = base::ASCIIToUTF16("videos[0] playing");
EXPECT_EQ(
expected_title,
TitleWatcher(shell()->web_contents(), expected_title).WaitAndGetTitle());
// Play second video.
ASSERT_TRUE(ExecuteScript(shell()->web_contents(), "videos[1].play();"));
expected_title = base::ASCIIToUTF16("videos[1] playing");
EXPECT_EQ(
expected_title,
TitleWatcher(shell()->web_contents(), expected_title).WaitAndGetTitle());
// Send first video in Picture-in-Picture.
ASSERT_TRUE(ExecuteScript(shell()->web_contents(),
"videos[0].requestPictureInPicture();"));
expected_title = base::ASCIIToUTF16("videos[0] entered picture-in-picture");
EXPECT_EQ(
expected_title,
TitleWatcher(shell()->web_contents(), expected_title).WaitAndGetTitle());
// Send second video in Picture-in-Picture.
ASSERT_TRUE(ExecuteScript(shell()->web_contents(),
"videos[1].requestPictureInPicture();"));
expected_title = base::ASCIIToUTF16("videos[1] entered picture-in-picture");
EXPECT_EQ(
expected_title,
TitleWatcher(shell()->web_contents(), expected_title).WaitAndGetTitle());
// The session should still be active and ExitPictureInPicture() never called.
EXPECT_NE(nullptr, PictureInPictureWindowControllerImpl::FromWebContents(
shell()->web_contents())
->active_session_for_testing());
Mock::VerifyAndClearExpectations(&web_contents_delegate_);
}
IN_PROC_BROWSER_TEST_F(PictureInPictureContentBrowserTest,
RequestSecondVideoInDifferentRFHDoesNotCloseWindow) {
EXPECT_CALL(web_contents_delegate_, ExitPictureInPicture()).Times(0);
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(NavigateToURL(
shell(),
embedded_test_server()->GetURL(
"example.com", "/media/picture_in_picture/two-videos.html")));
base::string16 expected_title = base::ASCIIToUTF16("iframe loaded");
EXPECT_EQ(
expected_title,
TitleWatcher(shell()->web_contents(), expected_title).WaitAndGetTitle());
// Play first video.
ASSERT_TRUE(ExecuteScript(shell()->web_contents(), "videos[0].play();"));
expected_title = base::ASCIIToUTF16("videos[0] playing");
EXPECT_EQ(
expected_title,
TitleWatcher(shell()->web_contents(), expected_title).WaitAndGetTitle());
// Play second video (in iframe).
ASSERT_TRUE(
ExecuteScript(shell()->web_contents(), "iframeVideos[0].play();"));
expected_title = base::ASCIIToUTF16("iframeVideos[0] playing");
EXPECT_EQ(
expected_title,
TitleWatcher(shell()->web_contents(), expected_title).WaitAndGetTitle());
// Send first video in Picture-in-Picture.
ASSERT_TRUE(ExecuteScript(shell()->web_contents(),
"videos[0].requestPictureInPicture();"));
expected_title = base::ASCIIToUTF16("videos[0] entered picture-in-picture");
EXPECT_EQ(
expected_title,
TitleWatcher(shell()->web_contents(), expected_title).WaitAndGetTitle());
// Send second video in Picture-in-Picture.
ASSERT_TRUE(ExecuteScript(shell()->web_contents(),
"iframeVideos[0].requestPictureInPicture();"));
expected_title =
base::ASCIIToUTF16("iframeVideos[0] entered picture-in-picture");
EXPECT_EQ(
expected_title,
TitleWatcher(shell()->web_contents(), expected_title).WaitAndGetTitle());
// The session should still be active and ExitPictureInPicture() never called.
EXPECT_NE(nullptr, PictureInPictureWindowControllerImpl::FromWebContents(
shell()->web_contents())
->active_session_for_testing());
Mock::VerifyAndClearExpectations(&web_contents_delegate_);
}
} // namespace content
...@@ -6,8 +6,9 @@ ...@@ -6,8 +6,9 @@
#include <utility> #include <utility>
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/picture_in_picture/picture_in_picture_session.h" #include "content/browser/picture_in_picture/picture_in_picture_session.h"
#include "content/browser/web_contents/web_contents_impl.h" #include "content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h"
#include "content/public/browser/web_contents_delegate.h" #include "content/public/browser/web_contents_delegate.h"
namespace content { namespace content {
...@@ -36,30 +37,22 @@ void PictureInPictureServiceImpl::StartSession( ...@@ -36,30 +37,22 @@ void PictureInPictureServiceImpl::StartSession(
mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver> observer, mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver> observer,
StartSessionCallback callback) { StartSessionCallback callback) {
gfx::Size window_size; gfx::Size window_size;
WebContentsImpl* web_contents_impl = static_cast<WebContentsImpl*>(
WebContents::FromRenderFrameHost(render_frame_host()));
auto result = web_contents_impl->EnterPictureInPicture(surface_id.value(),
natural_size);
mojo::PendingRemote<blink::mojom::PictureInPictureSession> session_remote; mojo::PendingRemote<blink::mojom::PictureInPictureSession> session_remote;
// Picture-in-Picture may not be supported by all embedders, so we should only if (surface_id.has_value()) {
// create the session if the EnterPictureInPicture request was successful. auto result = GetController().StartSession(
if (result == PictureInPictureResult::kSuccess) { this, MediaPlayerId(render_frame_host(), player_id), surface_id.value(),
active_session_ = std::make_unique<PictureInPictureSession>( natural_size, show_play_pause_button, std::move(observer),
this, MediaPlayerId(render_frame_host_, player_id), surface_id, &session_remote, &window_size);
natural_size, show_play_pause_button,
session_remote.InitWithNewPipeAndPassReceiver(), std::move(observer), if (result == PictureInPictureResult::kSuccess) {
&window_size); // Frames are to be blocklisted from the back-forward cache because the
// picture-in-picture continues to be displayed while the page is in the
// Frames are to be blocklisted from the back-forward cache because the // cache instead of closing.
// picture-in-picture continues to be displayed while the page is in the static_cast<RenderFrameHostImpl*>(render_frame_host())
// cache instead of closing. ->OnSchedulerTrackedFeatureUsed(
static_cast<RenderFrameHostImpl*>(render_frame_host_) blink::scheduler::WebSchedulerTrackedFeature::kPictureInPicture);
->OnSchedulerTrackedFeatureUsed( }
blink::scheduler::WebSchedulerTrackedFeature::kPictureInPicture);
} }
std::move(callback).Run(std::move(session_remote), window_size); std::move(callback).Run(std::move(session_remote), window_size);
...@@ -68,14 +61,18 @@ void PictureInPictureServiceImpl::StartSession( ...@@ -68,14 +61,18 @@ void PictureInPictureServiceImpl::StartSession(
PictureInPictureServiceImpl::PictureInPictureServiceImpl( PictureInPictureServiceImpl::PictureInPictureServiceImpl(
RenderFrameHost* render_frame_host, RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::PictureInPictureService> receiver) mojo::PendingReceiver<blink::mojom::PictureInPictureService> receiver)
: FrameServiceBase(render_frame_host, std::move(receiver)), : FrameServiceBase(render_frame_host, std::move(receiver)) {}
render_frame_host_(render_frame_host) {}
PictureInPictureServiceImpl::~PictureInPictureServiceImpl() { PictureInPictureServiceImpl::~PictureInPictureServiceImpl() {
// If the service is destroyed because the frame was destroyed, the session // If the service is destroyed because the frame was destroyed, the session
// may still be active and it has to be shutdown before its dtor runs. // may still be active and it has to be shutdown before its dtor runs.
if (active_session_) GetController().OnServiceDeleted(this);
active_session_->Shutdown(); }
PictureInPictureWindowControllerImpl&
PictureInPictureServiceImpl::GetController() {
return *PictureInPictureWindowControllerImpl::GetOrCreateForWebContents(
WebContents::FromRenderFrameHost(render_frame_host()));
} }
} // namespace content } // namespace content
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
#include <memory> #include <memory>
#include "base/containers/unique_ptr_adapters.h"
#include "content/common/content_export.h" #include "content/common/content_export.h"
#include "content/public/browser/frame_service_base.h" #include "content/public/browser/frame_service_base.h"
#include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/pending_receiver.h"
...@@ -16,13 +15,16 @@ ...@@ -16,13 +15,16 @@
namespace content { namespace content {
class PictureInPictureSession; class PictureInPictureWindowControllerImpl;
// Receives Picture-in-Picture messages from a given RenderFrame. There is one // Receives Picture-in-Picture messages from a given RenderFrame. There is one
// PictureInPictureServiceImpl per RenderFrameHost. The service gets a hold of // PictureInPictureServiceImpl per RenderFrameHost. The service pipes the
// a PictureInPictureSession to which it delegates most of the interactions with // `StartSession()` call to the PictureInPictureWindowControllerImpl which owns
// the rest of the Picture-in-Picture classes such as // the created session. The same object will get notified when the service is
// PictureInPictureWindowController. // killed given that the PictureInPictureWindowControllerImpl is
// WebContents-bound instead of RenderFrameHost.
// PictureInPictureServiceImpl owns itself. It self-destruct as needed, see the
// FrameServiceBase's documentation for more information.
class CONTENT_EXPORT PictureInPictureServiceImpl final class CONTENT_EXPORT PictureInPictureServiceImpl final
: public content::FrameServiceBase<blink::mojom::PictureInPictureService> { : public content::FrameServiceBase<blink::mojom::PictureInPictureService> {
public: public:
...@@ -43,10 +45,6 @@ class CONTENT_EXPORT PictureInPictureServiceImpl final ...@@ -43,10 +45,6 @@ class CONTENT_EXPORT PictureInPictureServiceImpl final
mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver>, mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver>,
StartSessionCallback) final; StartSessionCallback) final;
PictureInPictureSession* active_session_for_testing() const {
return active_session_.get();
}
private: private:
friend class PictureInPictureSession; friend class PictureInPictureSession;
...@@ -55,9 +53,7 @@ class CONTENT_EXPORT PictureInPictureServiceImpl final ...@@ -55,9 +53,7 @@ class CONTENT_EXPORT PictureInPictureServiceImpl final
mojo::PendingReceiver<blink::mojom::PictureInPictureService>); mojo::PendingReceiver<blink::mojom::PictureInPictureService>);
~PictureInPictureServiceImpl() override; ~PictureInPictureServiceImpl() override;
RenderFrameHost* render_frame_host_ = nullptr; PictureInPictureWindowControllerImpl& GetController();
std::unique_ptr<PictureInPictureSession> active_session_;
DISALLOW_COPY_AND_ASSIGN(PictureInPictureServiceImpl); DISALLOW_COPY_AND_ASSIGN(PictureInPictureServiceImpl);
}; };
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
#include "base/test/bind_test_util.h" #include "base/test/bind_test_util.h"
#include "build/build_config.h" #include "build/build_config.h"
#include "content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h"
#include "content/common/media/media_player_delegate_messages.h" #include "content/common/media/media_player_delegate_messages.h"
#include "content/public/browser/overlay_window.h" #include "content/public/browser/overlay_window.h"
#include "content/public/browser/web_contents_delegate.h" #include "content/public/browser/web_contents_delegate.h"
...@@ -22,6 +23,8 @@ ...@@ -22,6 +23,8 @@
#include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h" #include "testing/gmock/include/gmock/gmock.h"
using testing::_;
namespace content { namespace content {
class DummyPictureInPictureSessionObserver class DummyPictureInPictureSessionObserver
...@@ -137,6 +140,11 @@ class PictureInPictureServiceImplTest : public RenderViewHostImplTestHarness { ...@@ -137,6 +140,11 @@ class PictureInPictureServiceImplTest : public RenderViewHostImplTestHarness {
TEST_F(PictureInPictureServiceImplTest, MAYBE_EnterPictureInPicture) { TEST_F(PictureInPictureServiceImplTest, MAYBE_EnterPictureInPicture) {
const int kPlayerVideoOnlyId = 30; const int kPlayerVideoOnlyId = 30;
const PictureInPictureWindowControllerImpl* controller =
PictureInPictureWindowControllerImpl::GetOrCreateForWebContents(
contents());
ASSERT_TRUE(controller);
DummyPictureInPictureSessionObserver observer; DummyPictureInPictureSessionObserver observer;
mojo::Receiver<blink::mojom::PictureInPictureSessionObserver> mojo::Receiver<blink::mojom::PictureInPictureSessionObserver>
...@@ -146,7 +154,7 @@ TEST_F(PictureInPictureServiceImplTest, MAYBE_EnterPictureInPicture) { ...@@ -146,7 +154,7 @@ TEST_F(PictureInPictureServiceImplTest, MAYBE_EnterPictureInPicture) {
observer_receiver.Bind(observer_remote.InitWithNewPipeAndPassReceiver()); observer_receiver.Bind(observer_remote.InitWithNewPipeAndPassReceiver());
// If Picture-in-Picture there shouldn't be an active session. // If Picture-in-Picture there shouldn't be an active session.
EXPECT_FALSE(service().active_session_for_testing()); EXPECT_FALSE(controller->active_session_for_testing());
viz::SurfaceId surface_id = viz::SurfaceId surface_id =
viz::SurfaceId(viz::FrameSinkId(1, 1), viz::SurfaceId(viz::FrameSinkId(1, 1),
...@@ -171,7 +179,7 @@ TEST_F(PictureInPictureServiceImplTest, MAYBE_EnterPictureInPicture) { ...@@ -171,7 +179,7 @@ TEST_F(PictureInPictureServiceImplTest, MAYBE_EnterPictureInPicture) {
window_size = b; window_size = b;
})); }));
EXPECT_TRUE(service().active_session_for_testing()); EXPECT_TRUE(controller->active_session_for_testing());
EXPECT_TRUE(session_remote); EXPECT_TRUE(session_remote);
EXPECT_EQ(gfx::Size(42, 42), window_size); EXPECT_EQ(gfx::Size(42, 42), window_size);
...@@ -181,15 +189,20 @@ TEST_F(PictureInPictureServiceImplTest, MAYBE_EnterPictureInPicture) { ...@@ -181,15 +189,20 @@ TEST_F(PictureInPictureServiceImplTest, MAYBE_EnterPictureInPicture) {
contents()->GetMainFrame()->OnMessageReceived( contents()->GetMainFrame()->OnMessageReceived(
MediaPlayerDelegateHostMsg_OnMediaDestroyed( MediaPlayerDelegateHostMsg_OnMediaDestroyed(
contents()->GetMainFrame()->GetRoutingID(), kPlayerVideoOnlyId)); contents()->GetMainFrame()->GetRoutingID(), kPlayerVideoOnlyId));
EXPECT_TRUE(service().active_session_for_testing()); EXPECT_TRUE(controller->active_session_for_testing());
} }
TEST_F(PictureInPictureServiceImplTest, EnterPictureInPicture_NotSupported) { TEST_F(PictureInPictureServiceImplTest, EnterPictureInPicture_NotSupported) {
const int kPlayerVideoOnlyId = 30; const int kPlayerVideoOnlyId = 30;
const PictureInPictureWindowControllerImpl* controller =
PictureInPictureWindowControllerImpl::GetOrCreateForWebContents(
contents());
ASSERT_TRUE(controller);
EXPECT_FALSE(controller->active_session_for_testing());
mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver> mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver>
observer_remote; observer_remote;
EXPECT_FALSE(service().active_session_for_testing());
viz::SurfaceId surface_id = viz::SurfaceId surface_id =
viz::SurfaceId(viz::FrameSinkId(1, 1), viz::SurfaceId(viz::FrameSinkId(1, 1),
viz::LocalSurfaceId( viz::LocalSurfaceId(
...@@ -213,10 +226,52 @@ TEST_F(PictureInPictureServiceImplTest, EnterPictureInPicture_NotSupported) { ...@@ -213,10 +226,52 @@ TEST_F(PictureInPictureServiceImplTest, EnterPictureInPicture_NotSupported) {
window_size = b; window_size = b;
})); }));
EXPECT_FALSE(service().active_session_for_testing()); EXPECT_FALSE(controller->active_session_for_testing());
// The |session_remote| won't be bound because the |pending_remote| received
// in the StartSessionCallback will be invalid due to PictureInPictureSession // The |session_remote| won't be bound because the |remote| received in the
// not ever being created (meaning the the receiver won't be bound either). // StartSessionCallback will be invalid due to PictureInPictureSession not
// ever being created (meaning the the receiver won't be bound either).
EXPECT_FALSE(session_remote);
EXPECT_EQ(gfx::Size(), window_size);
}
// The |surface_id| is an optional parameter in the StartSession() call but
// needs to be non-null in order to create a session at the moment. The creation
// will early return if that condition isn't satisfied, failing to create the
// session.
TEST_F(PictureInPictureServiceImplTest, EnterPictureInPicture_NoSurfaceId) {
const int kPlayerVideoOnlyId = 30;
const PictureInPictureWindowControllerImpl* controller =
PictureInPictureWindowControllerImpl::GetOrCreateForWebContents(
contents());
ASSERT_TRUE(controller);
EXPECT_FALSE(controller->active_session_for_testing());
mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver>
observer_remote;
EXPECT_CALL(delegate(), EnterPictureInPicture(_, _, _)).Times(0);
mojo::Remote<blink::mojom::PictureInPictureSession> session_remote;
gfx::Size window_size;
service().StartSession(
kPlayerVideoOnlyId, base::nullopt, gfx::Size(42, 42),
true /* show_play_pause_button */, std::move(observer_remote),
base::BindLambdaForTesting(
[&](mojo::PendingRemote<blink::mojom::PictureInPictureSession> remote,
const gfx::Size& b) {
if (remote.is_valid())
session_remote.Bind(std::move(remote));
window_size = b;
}));
EXPECT_FALSE(controller->active_session_for_testing());
// The |session_remote| won't be bound because the |remote| received in the
// StartSessionCallback will be invalid due to PictureInPictureSession not
// ever being created (meaning the the receiver won't be bound either).
EXPECT_FALSE(session_remote); EXPECT_FALSE(session_remote);
EXPECT_EQ(gfx::Size(), window_size); EXPECT_EQ(gfx::Size(), window_size);
} }
......
...@@ -16,25 +16,14 @@ namespace content { ...@@ -16,25 +16,14 @@ namespace content {
PictureInPictureSession::PictureInPictureSession( PictureInPictureSession::PictureInPictureSession(
PictureInPictureServiceImpl* service, PictureInPictureServiceImpl* service,
const MediaPlayerId& player_id, const MediaPlayerId& player_id,
const base::Optional<viz::SurfaceId>& surface_id,
const gfx::Size& natural_size,
bool show_play_pause_button,
mojo::PendingReceiver<blink::mojom::PictureInPictureSession> receiver, mojo::PendingReceiver<blink::mojom::PictureInPictureSession> receiver,
mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver> observer, mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver> observer)
gfx::Size* window_size)
: service_(service), : service_(service),
receiver_(this, std::move(receiver)), receiver_(this, std::move(receiver)),
player_id_(player_id), player_id_(player_id),
observer_(std::move(observer)) { observer_(std::move(observer)) {
receiver_.set_disconnect_handler(base::BindOnce( receiver_.set_disconnect_handler(base::BindOnce(
&PictureInPictureSession::OnConnectionError, base::Unretained(this))); &PictureInPictureSession::OnConnectionError, base::Unretained(this)));
GetController().SetActiveSession(this);
GetController().EmbedSurface(surface_id.value(), natural_size);
GetController().SetAlwaysHidePlayPauseButton(show_play_pause_button);
GetController().Show();
*window_size = GetController().GetSize();
} }
PictureInPictureSession::~PictureInPictureSession() { PictureInPictureSession::~PictureInPictureSession() {
...@@ -50,17 +39,28 @@ void PictureInPictureSession::Update( ...@@ -50,17 +39,28 @@ void PictureInPictureSession::Update(
const base::Optional<viz::SurfaceId>& surface_id, const base::Optional<viz::SurfaceId>& surface_id,
const gfx::Size& natural_size, const gfx::Size& natural_size,
bool show_play_pause_button) { bool show_play_pause_button) {
player_id_ = MediaPlayerId(service_->render_frame_host_, player_id); player_id_ = MediaPlayerId(service_->render_frame_host(), player_id);
GetController().EmbedSurface(surface_id.value(), natural_size); GetController().EmbedSurface(surface_id.value(), natural_size);
GetController().SetAlwaysHidePlayPauseButton(show_play_pause_button); GetController().SetAlwaysHidePlayPauseButton(show_play_pause_button);
GetController().SetActiveSession(this);
} }
void PictureInPictureSession::NotifyWindowResized(const gfx::Size& size) { void PictureInPictureSession::NotifyWindowResized(const gfx::Size& size) {
observer_->OnWindowSizeChanged(size); observer_->OnWindowSizeChanged(size);
} }
void PictureInPictureSession::Disconnect() {
// |is_stopping_| shouldn't be true for the implementation in //chrome but if
// the WebContentsDelegate's Picture-in-Picture calls are empty, it's possible
// for `Disconnect()` to be called even after `StopInternal()` as the
// expectation of self-destruction will no longer be true.
if (is_stopping_)
return;
is_stopping_ = true;
observer_->OnStopped();
}
void PictureInPictureSession::Shutdown() { void PictureInPictureSession::Shutdown() {
if (is_stopping_) if (is_stopping_)
return; return;
...@@ -73,8 +73,6 @@ void PictureInPictureSession::StopInternal(StopCallback callback) { ...@@ -73,8 +73,6 @@ void PictureInPictureSession::StopInternal(StopCallback callback) {
is_stopping_ = true; is_stopping_ = true;
GetWebContentsImpl()->ExitPictureInPicture();
// `OnStopped()` should only be called if there is no callback to run, as a // `OnStopped()` should only be called if there is no callback to run, as a
// contract in the API. // contract in the API.
if (callback) if (callback)
...@@ -82,10 +80,8 @@ void PictureInPictureSession::StopInternal(StopCallback callback) { ...@@ -82,10 +80,8 @@ void PictureInPictureSession::StopInternal(StopCallback callback) {
else else
observer_->OnStopped(); observer_->OnStopped();
GetController().SetActiveSession(nullptr); // |this| will be deleted after this call.
GetWebContentsImpl()->ExitPictureInPicture();
// Reset must happen after everything is done as it will destroy |this|.
service_->active_session_.reset();
} }
void PictureInPictureSession::OnConnectionError() { void PictureInPictureSession::OnConnectionError() {
......
...@@ -20,9 +20,10 @@ class WebContentsImpl; ...@@ -20,9 +20,10 @@ class WebContentsImpl;
// The PicutreInPictureSession communicates with the // The PicutreInPictureSession communicates with the
// PictureInPictureWindowController and the WebContents. It is created by the // PictureInPictureWindowController and the WebContents. It is created by the
// PictureInPictureService but deletes itself. When created, the session will // PictureInPictureWindowControllerImpl which also deletes it. When created, the
// enter Picture-in-Picture and when deleted, it will automatically exit // session will be expected to be active (in Picture-in-Picture) and when
// Picture-in-Picture unless another session became active. // deleted, it will automatically exit Picture-in-Picture unless another session
// became active.
// The session MUST be stopped before its dtor runs to avoid unexpected // The session MUST be stopped before its dtor runs to avoid unexpected
// deletion. // deletion.
class PictureInPictureSession : public blink::mojom::PictureInPictureSession { class PictureInPictureSession : public blink::mojom::PictureInPictureSession {
...@@ -30,13 +31,9 @@ class PictureInPictureSession : public blink::mojom::PictureInPictureSession { ...@@ -30,13 +31,9 @@ class PictureInPictureSession : public blink::mojom::PictureInPictureSession {
PictureInPictureSession( PictureInPictureSession(
PictureInPictureServiceImpl* service, PictureInPictureServiceImpl* service,
const MediaPlayerId& player_id, const MediaPlayerId& player_id,
const base::Optional<viz::SurfaceId>& surface_id,
const gfx::Size& natural_size,
bool show_play_pause_button,
mojo::PendingReceiver<blink::mojom::PictureInPictureSession> receiver, mojo::PendingReceiver<blink::mojom::PictureInPictureSession> receiver,
mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver> mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver>
observer, observer);
gfx::Size* window_size);
~PictureInPictureSession() override; ~PictureInPictureSession() override;
// blink::mojom::PictureInPictureSession interface. // blink::mojom::PictureInPictureSession interface.
...@@ -52,10 +49,19 @@ class PictureInPictureSession : public blink::mojom::PictureInPictureSession { ...@@ -52,10 +49,19 @@ class PictureInPictureSession : public blink::mojom::PictureInPictureSession {
// if there are none. // if there are none.
const base::Optional<MediaPlayerId>& player_id() const { return player_id_; } const base::Optional<MediaPlayerId>& player_id() const { return player_id_; }
// Stops the session without closing the window. It will prevent the session
// to later trying to shutdown when the PictureInPictureWindowController is
// notified.
void Disconnect();
// Shuts down the session. Called by the window controller when the window is // Shuts down the session. Called by the window controller when the window is
// closed. // closed.
void Shutdown(); void Shutdown();
// Returns the PictureInPictureServiceImpl instance associated with this
// session. It cannot be null.
PictureInPictureServiceImpl* service() { return service_; }
private: private:
PictureInPictureSession() = delete; PictureInPictureSession() = delete;
...@@ -74,7 +80,9 @@ class PictureInPictureSession : public blink::mojom::PictureInPictureSession { ...@@ -74,7 +80,9 @@ class PictureInPictureSession : public blink::mojom::PictureInPictureSession {
// session. // session.
PictureInPictureWindowControllerImpl& GetController(); PictureInPictureWindowControllerImpl& GetController();
// Owns |this|. // Will notified The PictureInPictureWindowControllerImpl who owns |this| when
// it gets destroyed in order for |this| to be destroyed too. Indirectly owns
// |this|.
PictureInPictureServiceImpl* service_; PictureInPictureServiceImpl* service_;
mojo::Receiver<blink::mojom::PictureInPictureSession> receiver_; mojo::Receiver<blink::mojom::PictureInPictureSession> receiver_;
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#include "content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h" #include "content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h"
#include <set> #include <set>
#include <utility>
#include "components/viz/common/surfaces/surface_id.h" #include "components/viz/common/surfaces/surface_id.h"
#include "content/browser/media/media_web_contents_observer.h" #include "content/browser/media/media_web_contents_observer.h"
...@@ -15,6 +16,7 @@ ...@@ -15,6 +16,7 @@
#include "content/public/browser/content_browser_client.h" #include "content/public/browser/content_browser_client.h"
#include "content/public/browser/overlay_window.h" #include "content/public/browser/overlay_window.h"
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h" // for PictureInPictureResult
#include "content/public/browser/web_contents_observer.h" #include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_client.h" #include "content/public/common/content_client.h"
...@@ -207,15 +209,44 @@ void PictureInPictureWindowControllerImpl::UpdateMediaPlayerId() { ...@@ -207,15 +209,44 @@ void PictureInPictureWindowControllerImpl::UpdateMediaPlayerId() {
UpdatePlaybackState(IsPlayerActive(), !media_player_id_.has_value()); UpdatePlaybackState(IsPlayerActive(), !media_player_id_.has_value());
} }
void PictureInPictureWindowControllerImpl::SetActiveSession( PictureInPictureResult PictureInPictureWindowControllerImpl::StartSession(
PictureInPictureSession* session) { PictureInPictureServiceImpl* service,
if (active_session_ == session) const MediaPlayerId& player_id,
return; const viz::SurfaceId& surface_id,
const gfx::Size& natural_size,
bool show_play_pause_button,
mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver> observer,
mojo::PendingRemote<blink::mojom::PictureInPictureSession>* session_remote,
gfx::Size* window_size) {
auto result = initiator_->EnterPictureInPicture(surface_id, natural_size);
// Picture-in-Picture may not be supported by all embedders, so we should only
// create the session if the EnterPictureInPicture request was successful.
if (result != PictureInPictureResult::kSuccess)
return result;
if (active_session_) if (active_session_)
active_session_->Shutdown(); active_session_->Disconnect();
active_session_ = std::make_unique<PictureInPictureSession>(
service, player_id, session_remote->InitWithNewPipeAndPassReceiver(),
std::move(observer));
active_session_ = session; EmbedSurface(surface_id, natural_size);
SetAlwaysHidePlayPauseButton(show_play_pause_button);
Show();
*window_size = GetSize();
return result;
}
void PictureInPictureWindowControllerImpl::OnServiceDeleted(
PictureInPictureServiceImpl* service) {
if (!active_session_ || active_session_->service() != service)
return;
active_session_->Shutdown();
active_session_ = nullptr;
} }
void PictureInPictureWindowControllerImpl::SetAlwaysHidePlayPauseButton( void PictureInPictureWindowControllerImpl::SetAlwaysHidePlayPauseButton(
...@@ -314,10 +345,11 @@ void PictureInPictureWindowControllerImpl::OnLeavingPictureInPicture( ...@@ -314,10 +345,11 @@ void PictureInPictureWindowControllerImpl::OnLeavingPictureInPicture(
} }
if (media_player_id_.has_value()) { if (media_player_id_.has_value()) {
if (active_session_) if (active_session_) {
active_session_->Shutdown(); active_session_->Shutdown();
active_session_ = nullptr;
}
active_session_ = nullptr;
media_player_id_.reset(); media_player_id_.reset();
} }
} }
......
...@@ -10,26 +10,37 @@ ...@@ -10,26 +10,37 @@
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
#include "components/viz/common/surfaces/parent_local_surface_id_allocator.h" #include "components/viz/common/surfaces/parent_local_surface_id_allocator.h"
#include "content/common/content_export.h"
#include "content/public/browser/media_player_id.h" #include "content/public/browser/media_player_id.h"
#include "content/public/browser/picture_in_picture_window_controller.h" #include "content/public/browser/picture_in_picture_window_controller.h"
#include "content/public/browser/web_contents_observer.h" #include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_contents_user_data.h" #include "content/public/browser/web_contents_user_data.h"
#include "services/media_session/public/mojom/media_session.mojom.h" #include "services/media_session/public/mojom/media_session.mojom.h"
#include "third_party/blink/public/mojom/picture_in_picture/picture_in_picture.mojom.h"
namespace content { namespace content {
class MediaWebContentsObserver; class MediaWebContentsObserver;
class PictureInPictureServiceImpl;
class PictureInPictureSession; class PictureInPictureSession;
class WebContents; class WebContents;
class WebContentsImpl; class WebContentsImpl;
enum class PictureInPictureResult;
// TODO(thakis,mlamouri): PictureInPictureWindowControllerImpl isn't
// CONTENT_EXPORT'd because it creates complicated build issues with // PictureInPictureWindowControllerImpl is the corner stone of the
// WebContentsUserData being a non-exported template. As a result, the class // Picture-in-Picture feature in the //content layer. It handles the session
// uses CONTENT_EXPORT for methods that are being used from tests. // creation requests (sent by the PictureInPictureServiceImpl), owns the session
// CONTENT_EXPORT should be moved back to the class when the Windows build will // object and therefore handles its lifetime, and communicate with the rest of
// work with it. https://crbug.com/589840. // the browser. Requests to the WebContents are sent by the controller and it
class PictureInPictureWindowControllerImpl // gets notified when the browser needs it to update the Picture-in-Picture
// session.
// The PictureInPictureWindowControllerImpl is managing Picture-in-Picture at a
// WebContents level. If multiple calls request a Picture-in-Picture session
// either in the same frame or in different frames, the controller will handle
// creating the new session, stopping the current one and making sure the window
// is kept around when possible.
class CONTENT_EXPORT PictureInPictureWindowControllerImpl
: public PictureInPictureWindowController, : public PictureInPictureWindowController,
public WebContentsUserData<PictureInPictureWindowControllerImpl>, public WebContentsUserData<PictureInPictureWindowControllerImpl>,
public WebContentsObserver { public WebContentsObserver {
...@@ -37,31 +48,31 @@ class PictureInPictureWindowControllerImpl ...@@ -37,31 +48,31 @@ class PictureInPictureWindowControllerImpl
// Gets a reference to the controller associated with |initiator| and creates // Gets a reference to the controller associated with |initiator| and creates
// one if it does not exist. The returned pointer is guaranteed to be // one if it does not exist. The returned pointer is guaranteed to be
// non-null. // non-null.
CONTENT_EXPORT static PictureInPictureWindowControllerImpl* static PictureInPictureWindowControllerImpl* GetOrCreateForWebContents(
GetOrCreateForWebContents(WebContents* initiator); WebContents* initiator);
~PictureInPictureWindowControllerImpl() override; ~PictureInPictureWindowControllerImpl() override;
using PlayerSet = std::set<int>; using PlayerSet = std::set<int>;
// PictureInPictureWindowController: // PictureInPictureWindowController:
CONTENT_EXPORT void Show() override; void Show() override;
CONTENT_EXPORT void Close(bool should_pause_video) override; void Close(bool should_pause_video) override;
CONTENT_EXPORT void CloseAndFocusInitiator() override; void CloseAndFocusInitiator() override;
CONTENT_EXPORT void OnWindowDestroyed() override; void OnWindowDestroyed() override;
CONTENT_EXPORT OverlayWindow* GetWindowForTesting() override; OverlayWindow* GetWindowForTesting() override;
CONTENT_EXPORT void UpdateLayerBounds() override; void UpdateLayerBounds() override;
CONTENT_EXPORT bool IsPlayerActive() override; bool IsPlayerActive() override;
CONTENT_EXPORT WebContents* GetInitiatorWebContents() override; WebContents* GetInitiatorWebContents() override;
CONTENT_EXPORT bool TogglePlayPause() override; bool TogglePlayPause() override;
CONTENT_EXPORT void UpdatePlaybackState(bool is_playing, void UpdatePlaybackState(bool is_playing,
bool reached_end_of_stream) override; bool reached_end_of_stream) override;
CONTENT_EXPORT void SetAlwaysHidePlayPauseButton(bool is_visible) override; void SetAlwaysHidePlayPauseButton(bool is_visible) override;
CONTENT_EXPORT void SkipAd() override; void SkipAd() override;
CONTENT_EXPORT void NextTrack() override; void NextTrack() override;
CONTENT_EXPORT void PreviousTrack() override; void PreviousTrack() override;
CONTENT_EXPORT void MediaSessionActionsChanged( void MediaSessionActionsChanged(
const std::set<media_session::mojom::MediaSessionAction>& actions); const std::set<media_session::mojom::MediaSessionAction>& actions);
gfx::Size GetSize(); gfx::Size GetSize();
...@@ -82,19 +93,37 @@ class PictureInPictureWindowControllerImpl ...@@ -82,19 +93,37 @@ class PictureInPictureWindowControllerImpl
void EmbedSurface(const viz::SurfaceId& surface_id, void EmbedSurface(const viz::SurfaceId& surface_id,
const gfx::Size& natural_size); const gfx::Size& natural_size);
// Sets the active Picture-in-Picture session associated with the controller. // Called by PictureInPictureServiceImpl when a session request is received.
// This is different from the service's active session as there is one // The call should return the |session_remote| and |window_size| as out
// controller per WebContents and one service per RenderFrameHost. // params. A failure to create the session should be expressed with an empty
// The current session may be shut down as a side effect of this. // |window_size| and uninitialized |session_remote|.
void SetActiveSession(PictureInPictureSession* session); // Returns whether the session creation was successful.
PictureInPictureResult StartSession(
PictureInPictureServiceImpl* service,
const MediaPlayerId&,
const viz::SurfaceId& surface_id,
const gfx::Size& natural_size,
bool show_play_pause_button,
mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver>,
mojo::PendingRemote<blink::mojom::PictureInPictureSession>*
session_remote,
gfx::Size* window_size);
// Called by PictureInPictureServiceImpl when the service is about to be
// destroyed. It allows |this| to close the |active_session_| if it is
// associated with the service.
void OnServiceDeleted(PictureInPictureServiceImpl* service);
PictureInPictureSession* active_session_for_testing() const {
return active_session_.get();
}
private: private:
friend class WebContentsUserData<PictureInPictureWindowControllerImpl>; friend class WebContentsUserData<PictureInPictureWindowControllerImpl>;
// Use PictureInPictureWindowControllerImpl::GetOrCreateForWebContents() to // Use PictureInPictureWindowControllerImpl::GetOrCreateForWebContents() to
// create an instance. // create an instance.
CONTENT_EXPORT explicit PictureInPictureWindowControllerImpl( explicit PictureInPictureWindowControllerImpl(WebContents* initiator);
WebContents* initiator);
// Signal to the media player that |this| is leaving Picture-in-Picture mode. // Signal to the media player that |this| is leaving Picture-in-Picture mode.
void OnLeavingPictureInPicture(bool should_pause_video); void OnLeavingPictureInPicture(bool should_pause_video);
...@@ -141,7 +170,7 @@ class PictureInPictureWindowControllerImpl ...@@ -141,7 +170,7 @@ class PictureInPictureWindowControllerImpl
// session object makes the bridge with the renderer process by handling // session object makes the bridge with the renderer process by handling
// requests and holding states such as the active player id. // requests and holding states such as the active player id.
// The session will be nullptr when there is no active session. // The session will be nullptr when there is no active session.
PictureInPictureSession* active_session_ = nullptr; std::unique_ptr<PictureInPictureSession> active_session_;
WEB_CONTENTS_USER_DATA_KEY_DECL(); WEB_CONTENTS_USER_DATA_KEY_DECL();
......
...@@ -1019,6 +1019,7 @@ test("content_browsertests") { ...@@ -1019,6 +1019,7 @@ test("content_browsertests") {
"../browser/origin_trials/origin_trials_browsertest.cc", "../browser/origin_trials/origin_trials_browsertest.cc",
"../browser/payments/payment_app_browsertest.cc", "../browser/payments/payment_app_browsertest.cc",
"../browser/performance_memory_browsertest.cc", "../browser/performance_memory_browsertest.cc",
"../browser/picture_in_picture/picture_in_picture_content_browsertest.cc",
"../browser/pointer_lock_browsertest.cc", "../browser/pointer_lock_browsertest.cc",
"../browser/pointer_lock_browsertest.h", "../browser/pointer_lock_browsertest.h",
"../browser/pointer_lock_browsertest_mac.mm", "../browser/pointer_lock_browsertest_mac.mm",
......
<!DOCTYPE html>
<html>
<body>
<video src="../tulip2.webm"></video>
<video src="../tulip2.webm"></video>
<iframe src="data:text/html,<video src='../tulip2.webm'></video>"></iframe>
</body>
<script>
const videos = document.querySelectorAll('video');
videos[0].addEventListener('playing', e => {
document.title = 'videos[0] playing';
});
videos[0].addEventListener('enterpictureinpicture', e => {
document.title = 'videos[0] entered picture-in-picture';
});
videos[1].addEventListener('playing', e => {
document.title = 'videos[1] playing';
});
videos[1].addEventListener('enterpictureinpicture', e => {
document.title = 'videos[1] entered picture-in-picture';
});
let iframeVideos = [];
if (window.top == window.self) {
const iframe = document.createElement('iframe');
iframe.src = 'two-videos.html';
document.body.appendChild(iframe);
iframe.addEventListener('load', () => {
document.title = 'iframe loaded';
iframeVideos = iframe.contentDocument.querySelectorAll('video');
iframeVideos[0].addEventListener('playing', e => {
document.title = 'iframeVideos[0] playing';
});
iframeVideos[0].addEventListener('enterpictureinpicture', e => {
document.title = 'iframeVideos[0] entered picture-in-picture';
});
});
}
</script>
</html>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment