Reland "Picture-in-Picture: move key methods into a service."

This reverts commit 9a3eafe448d45259b15e2c8bfa4916da2b39ee75.

Reason for revert: the flake source was found and the test will be disabled until then.

Original change's description:
> Revert "Picture-in-Picture: move key methods into a service."
>
> This reverts commit 45675d0dfab5de54253d40cb2bd52ef034c3a200.
>
> Reason for revert: Speculative revert for crbug.com/930338
>
> Original change's description:
> > Picture-in-Picture: move key methods into a service.
> >
> > This is moving most of the Picture-in-Picture feature into a service.
> > Follow-up CLs will move the remaining methods and some Android auto-pip
> > code. It will also include further cleanups.
> >
> > This CL was meant to be the smallest CL that could move the core feature
> > set without breaking compilation or tests. It's larger than expected.
> >
> > Bug: 919860, 908400
> > Change-Id: I72c5faad9a14de6645a7cd1565b29d6a353e24a3
> > Binary-Size: cause unknown, landing approved by agrieve@
> > Reviewed-on: https://chromium-review.googlesource.com/c/1379049
> > Commit-Queue: Mounir Lamouri <mlamouri@chromium.org>
> > Reviewed-by: Daniel Cheng <dcheng@chromium.org>
> > Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
> > Reviewed-by: Jochen Eisinger <jochen@chromium.org>
> > Cr-Commit-Position: refs/heads/master@{#630361}
>
> TBR=dcheng@chromium.org,dalecurtis@chromium.org,beaufort.francois@gmail.com,mlamouri@chromium.org,agrieve@chromium.org,jochen@chromium.org
>
> Change-Id: I88efa31a23ab1c55b256e5813fa20fd2d8776a21
> No-Presubmit: true
> No-Tree-Checks: true
> No-Try: true
> Bug: 919860, 908400, 930338
> Reviewed-on: https://chromium-review.googlesource.com/c/1461546
> Reviewed-by: Karan Bhatia <karandeepb@chromium.org>
> Commit-Queue: Karan Bhatia <karandeepb@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#630550}

TBR=dcheng@chromium.org,dalecurtis@chromium.org,beaufort.francois@gmail.com,mlamouri@chromium.org,agrieve@chromium.org,karandeepb@chromium.org,jochen@chromium.org

# Not skipping CQ checks because original CL landed > 1 day ago.

Bug: 919860, 908400, 930338
Change-Id: I1bd2ab7ade56569a83951ec942685d9f77dc4af5
Binary-Size: cause unknown, landing approved by agrieve@
No-Presubmit: true
Reviewed-on: https://chromium-review.googlesource.com/c/1465063
Commit-Queue: Mounir Lamouri <mlamouri@chromium.org>
Reviewed-by: Mounir Lamouri <mlamouri@chromium.org>
Reviewed-by: Kentaro Hara <haraken@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#631030}
diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_controller_browsertest.cc b/chrome/browser/picture_in_picture/picture_in_picture_window_controller_browsertest.cc
index 1e0834a..dc73d30 100644
--- a/chrome/browser/picture_in_picture/picture_in_picture_window_controller_browsertest.cc
+++ b/chrome/browser/picture_in_picture/picture_in_picture_window_controller_browsertest.cc
@@ -1829,6 +1829,7 @@
 
   content::WebContents* active_web_contents =
       browser()->tab_strip_model()->GetActiveWebContents();
+
   ASSERT_NE(nullptr, active_web_contents);
 
   ASSERT_TRUE(content::ExecuteScript(active_web_contents, "video.play();"));
@@ -2570,3 +2571,21 @@
             content::TitleWatcher(active_web_contents, expected_title)
                 .WaitAndGetTitle());
 }
+
+// Tests that exiting Picture-in-Picture when the video has no source fires the
+// event and resolves the callback.
+IN_PROC_BROWSER_TEST_F(PictureInPictureWindowControllerBrowserTest,
+                       ExitFireEventAndCallbackWhenNoSource) {
+  LoadTabAndEnterPictureInPicture(browser());
+
+  content::WebContents* active_web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(content::ExecuteScript(active_web_contents,
+                                     "video.src=''; exitPictureInPicture();"));
+
+  // 'left' is sent when the first video leaves Picture-in-Picture.
+  base::string16 expected_title = base::ASCIIToUTF16("left");
+  EXPECT_EQ(expected_title,
+            content::TitleWatcher(active_web_contents, expected_title)
+                .WaitAndGetTitle());
+}
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index 0bdcde6c..078e20d 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -1291,6 +1291,8 @@
     "permissions/permission_service_impl.h",
     "picture_in_picture/overlay_surface_embedder.cc",
     "picture_in_picture/overlay_surface_embedder.h",
+    "picture_in_picture/picture_in_picture_service_impl.cc",
+    "picture_in_picture/picture_in_picture_service_impl.h",
     "picture_in_picture/picture_in_picture_window_controller_impl.cc",
     "picture_in_picture/picture_in_picture_window_controller_impl.h",
     "portal/portal.cc",
diff --git a/content/browser/frame_host/render_frame_host_impl.cc b/content/browser/frame_host/render_frame_host_impl.cc
index fb5cebf..66ce76d 100644
--- a/content/browser/frame_host/render_frame_host_impl.cc
+++ b/content/browser/frame_host/render_frame_host_impl.cc
@@ -73,6 +73,7 @@
 #include "content/browser/permissions/permission_controller_impl.h"
 #include "content/browser/permissions/permission_service_context.h"
 #include "content/browser/permissions/permission_service_impl.h"
+#include "content/browser/picture_in_picture/picture_in_picture_service_impl.h"
 #include "content/browser/portal/portal.h"
 #include "content/browser/presentation/presentation_service_impl.h"
 #include "content/browser/quota_dispatcher_host.h"
@@ -4087,6 +4088,9 @@
 
   registry_->AddInterface(base::BindRepeating(&WakeLockServiceImpl::Create,
                                               base::Unretained(this)));
+
+  registry_->AddInterface(base::BindRepeating(
+      &PictureInPictureServiceImpl::Create, base::Unretained(this)));
 }
 
 void RenderFrameHostImpl::ResetWaitingState() {
diff --git a/content/browser/media/media_web_contents_observer.cc b/content/browser/media/media_web_contents_observer.cc
index 6e29f3c..740bdc6 100644
--- a/content/browser/media/media_web_contents_observer.cc
+++ b/content/browser/media/media_web_contents_observer.cc
@@ -77,11 +77,6 @@
     picture_in_picture_allowed_in_fullscreen_.reset();
     fullscreen_player_.reset();
   }
-
-  // Usually the frame will exit PIP before it is deleted but for OOPIF, it
-  // seems that the player never notifies the browser process.
-  if (pip_player_ && pip_player_->render_frame_host == render_frame_host)
-    ExitPictureInPictureInternal();
 }
 
 void MediaWebContentsObserver::MaybeUpdateAudibleState() {
@@ -120,15 +115,6 @@
   return fullscreen_player_;
 }
 
-const base::Optional<WebContentsObserver::MediaPlayerId>&
-MediaWebContentsObserver::GetPictureInPictureVideoMediaPlayerId() const {
-  return pip_player_;
-}
-
-void MediaWebContentsObserver::ResetPictureInPictureVideoMediaPlayerId() {
-  pip_player_.reset();
-}
-
 bool MediaWebContentsObserver::OnMessageReceived(
     const IPC::Message& msg,
     RenderFrameHost* render_frame_host) {
@@ -148,16 +134,8 @@
     IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnMediaSizeChanged,
                         OnMediaSizeChanged)
     IPC_MESSAGE_HANDLER(
-        MediaPlayerDelegateHostMsg_OnPictureInPictureModeStarted,
-        OnPictureInPictureModeStarted)
-    IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnPictureInPictureModeEnded,
-                        OnPictureInPictureModeEnded)
-    IPC_MESSAGE_HANDLER(
         MediaPlayerDelegateHostMsg_OnSetPictureInPictureCustomControls,
         OnSetPictureInPictureCustomControls)
-    IPC_MESSAGE_HANDLER(
-        MediaPlayerDelegateHostMsg_OnPictureInPictureSurfaceChanged,
-        OnPictureInPictureSurfaceChanged)
     IPC_MESSAGE_UNHANDLED(handled = false)
   IPC_END_MESSAGE_MAP()
   return handled;
@@ -187,16 +165,6 @@
   return MediaPlayerEntryExists(player_id, active_audio_players_);
 }
 
-void MediaWebContentsObserver::OnPictureInPictureWindowResize(
-    const gfx::Size& window_size) {
-  DCHECK(pip_player_.has_value());
-
-  pip_player_->render_frame_host->Send(
-      new MediaPlayerDelegateMsg_OnPictureInPictureWindowResize(
-          pip_player_->render_frame_host->GetRoutingID(),
-          pip_player_->delegate_id, window_size));
-}
-
 void MediaWebContentsObserver::OnMediaDestroyed(
     RenderFrameHost* render_frame_host,
     int delegate_id) {
@@ -212,16 +180,6 @@
   const bool removed_video =
       RemoveMediaPlayerEntry(player_id, &active_video_players_);
 
-  if (!web_contents()->IsBeingDestroyed() && pip_player_ == player_id) {
-    PictureInPictureWindowControllerImpl* pip_controller =
-        PictureInPictureWindowControllerImpl::FromWebContents(
-            web_contents_impl());
-    if (pip_controller) {
-      pip_controller->UpdatePlaybackState(false /* is not playing */,
-                                          reached_end_of_stream);
-    }
-  }
-
   if (removed_audio || removed_video) {
     // Notify observers the player has been "paused".
     web_contents_impl()->MediaStoppedPlaying(
@@ -263,16 +221,6 @@
     return;
   }
 
-  if (!web_contents()->IsBeingDestroyed() && pip_player_ == id) {
-    PictureInPictureWindowControllerImpl* pip_controller =
-        PictureInPictureWindowControllerImpl::FromWebContents(
-            web_contents_impl());
-    if (pip_controller) {
-      pip_controller->UpdatePlaybackState(true /* is not playing */,
-                                          false /* reached_end_of_stream */);
-    }
-  }
-
   // Notify observers of the new player.
   web_contents_impl()->MediaStartedPlaying(
       WebContentsObserver::MediaPlayerInfo(has_video, has_audio), id);
@@ -317,41 +265,6 @@
   web_contents_impl()->MediaResized(size, id);
 }
 
-void MediaWebContentsObserver::OnPictureInPictureModeStarted(
-    RenderFrameHost* render_frame_host,
-    int delegate_id,
-    const viz::SurfaceId& surface_id,
-    const gfx::Size& natural_size,
-    int request_id,
-    bool show_play_pause_button) {
-  DCHECK(surface_id.is_valid());
-  pip_player_ = MediaPlayerId(render_frame_host, delegate_id);
-
-  gfx::Size window_size =
-      web_contents_impl()->EnterPictureInPicture(surface_id, natural_size);
-
-  if (auto* pip_controller =
-          PictureInPictureWindowControllerImpl::FromWebContents(
-              web_contents_impl()))
-    pip_controller->SetAlwaysHidePlayPauseButton(show_play_pause_button);
-
-  render_frame_host->Send(
-      new MediaPlayerDelegateMsg_OnPictureInPictureModeStarted_ACK(
-          render_frame_host->GetRoutingID(), delegate_id, request_id,
-          window_size));
-}
-
-void MediaWebContentsObserver::OnPictureInPictureModeEnded(
-    RenderFrameHost* render_frame_host,
-    int delegate_id,
-    int request_id) {
-  ExitPictureInPictureInternal();
-
-  render_frame_host->Send(
-      new MediaPlayerDelegateMsg_OnPictureInPictureModeEnded_ACK(
-          render_frame_host->GetRoutingID(), delegate_id, request_id));
-}
-
 void MediaWebContentsObserver::OnSetPictureInPictureCustomControls(
     RenderFrameHost* render_frame_host,
     int delegate_id,
@@ -363,26 +276,6 @@
     pip_controller->SetPictureInPictureCustomControls(controls);
 }
 
-void MediaWebContentsObserver::OnPictureInPictureSurfaceChanged(
-    RenderFrameHost* render_frame_host,
-    int delegate_id,
-    const viz::SurfaceId& surface_id,
-    const gfx::Size& natural_size,
-    bool show_play_pause_button) {
-  DCHECK(surface_id.is_valid());
-
-  pip_player_ = MediaPlayerId(render_frame_host, delegate_id);
-
-  // The PictureInPictureWindowController instance may not have been created by
-  // the embedder.
-  if (auto* pip_controller =
-          PictureInPictureWindowControllerImpl::FromWebContents(
-              web_contents_impl())) {
-    pip_controller->EmbedSurface(surface_id, natural_size);
-    pip_controller->SetAlwaysHidePlayPauseButton(show_play_pause_button);
-  }
-}
-
 void MediaWebContentsObserver::ClearWakeLocks(
     RenderFrameHost* render_frame_host) {
   std::set<MediaPlayerId> video_players;
@@ -482,16 +375,6 @@
   player_map->erase(it);
 }
 
-void MediaWebContentsObserver::ExitPictureInPictureInternal() {
-  DCHECK(pip_player_);
-
-  web_contents_impl()->ExitPictureInPicture();
-
-  // Reset must happen after notifying the WebContents because it may interact
-  // with it.
-  ResetPictureInPictureVideoMediaPlayerId();
-}
-
 WebContentsImpl* MediaWebContentsObserver::web_contents_impl() const {
   return static_cast<WebContentsImpl*>(web_contents());
 }
diff --git a/content/browser/media/media_web_contents_observer.h b/content/browser/media/media_web_contents_observer.h
index d1eb2af..788c018 100644
--- a/content/browser/media/media_web_contents_observer.h
+++ b/content/browser/media/media_web_contents_observer.h
@@ -34,10 +34,6 @@
 class Size;
 }  // namespace size
 
-namespace viz {
-class SurfaceId;
-}  // namespace viz
-
 namespace content {
 
 class AudibleMetrics;
@@ -72,14 +68,6 @@
   // Gets the MediaPlayerId of the fullscreen video if it exists.
   const base::Optional<MediaPlayerId>& GetFullscreenVideoMediaPlayerId() const;
 
-  // Gets the MediaPlayerId of the picture in picture video if it exists.
-  const base::Optional<MediaPlayerId>& GetPictureInPictureVideoMediaPlayerId()
-      const;
-
-  // Reset the MediaPlayerId of the picture in picture video when user closes
-  // Picture-in-Picture window manually.
-  void ResetPictureInPictureVideoMediaPlayerId();
-
   // WebContentsObserver implementation.
   void WebContentsDestroyed() override;
   void RenderFrameDeleted(RenderFrameHost* render_frame_host) override;
@@ -96,11 +84,6 @@
   // Returns whether or not the given player id is active.
   bool IsPlayerActive(const MediaPlayerId& player_id) const;
 
-  // Called by the Picture-in-Picture controller when the associated window is
-  // resized. |window_size| represents the new size of the window. It MUST be
-  // called when there is a player in Picture-in-Picture.
-  void OnPictureInPictureWindowResize(const gfx::Size& window_size);
-
   bool has_audio_wake_lock_for_testing() const {
     return has_audio_wake_lock_for_testing_;
   }
@@ -135,24 +118,10 @@
   void OnMediaMutedStatusChanged(RenderFrameHost* render_frame_host,
                                  int delegate_id,
                                  bool muted);
-  void OnPictureInPictureModeStarted(RenderFrameHost* render_frame_host,
-                                     int delegate_id,
-                                     const viz::SurfaceId&,
-                                     const gfx::Size& natural_size,
-                                     int request_id,
-                                     bool show_play_pause_button);
-  void OnPictureInPictureModeEnded(RenderFrameHost* render_frame_host,
-                                   int delegate_id,
-                                   int request_id);
   void OnSetPictureInPictureCustomControls(
       RenderFrameHost* render_frame_host,
       int delegate_id,
       const std::vector<blink::PictureInPictureControlInfo>& controls);
-  void OnPictureInPictureSurfaceChanged(RenderFrameHost*,
-                                        int delegate_id,
-                                        const viz::SurfaceId&,
-                                        const gfx::Size&,
-                                        bool show_play_pause_button);
 
   // Clear |render_frame_host|'s tracking entry for its WakeLocks.
   void ClearWakeLocks(RenderFrameHost* render_frame_host);
@@ -176,10 +145,6 @@
                                    ActiveMediaPlayerMap* player_map,
                                    std::set<MediaPlayerId>* removed_players);
 
-  // Internal method to exit Picture-in-Picture from an event received from the
-  // renderer process.
-  void ExitPictureInPictureInternal();
-
   // Convenience method that casts web_contents() to a WebContentsImpl*.
   WebContentsImpl* web_contents_impl() const;
 
@@ -191,7 +156,6 @@
   ActiveMediaPlayerMap active_video_players_;
   device::mojom::WakeLockPtr audio_wake_lock_;
   base::Optional<MediaPlayerId> fullscreen_player_;
-  base::Optional<MediaPlayerId> pip_player_;
   base::Optional<bool> picture_in_picture_allowed_in_fullscreen_;
   bool has_audio_wake_lock_for_testing_ = false;
 
diff --git a/content/browser/picture_in_picture/picture_in_picture_service_impl.cc b/content/browser/picture_in_picture/picture_in_picture_service_impl.cc
new file mode 100644
index 0000000..4d3bc41b
--- /dev/null
+++ b/content/browser/picture_in_picture/picture_in_picture_service_impl.cc
@@ -0,0 +1,130 @@
+// Copyright 2019 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 <utility>
+
+#include "content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h"
+#include "content/browser/web_contents/web_contents_impl.h"
+
+namespace content {
+
+// static
+void PictureInPictureServiceImpl::Create(
+    RenderFrameHost* render_frame_host,
+    blink::mojom::PictureInPictureServiceRequest request) {
+  DCHECK(render_frame_host);
+  new PictureInPictureServiceImpl(render_frame_host, std::move(request));
+}
+
+// static
+PictureInPictureServiceImpl* PictureInPictureServiceImpl::CreateForTesting(
+    RenderFrameHost* render_frame_host,
+    blink::mojom::PictureInPictureServiceRequest request) {
+  return new PictureInPictureServiceImpl(render_frame_host, std::move(request));
+}
+
+void PictureInPictureServiceImpl::StartSession(
+    uint32_t player_id,
+    const base::Optional<viz::SurfaceId>& surface_id,
+    const gfx::Size& natural_size,
+    bool show_play_pause_button,
+    StartSessionCallback callback) {
+  player_id_ = MediaPlayerId(render_frame_host_, player_id);
+
+  auto* pip_controller = GetController();
+  if (pip_controller)
+    pip_controller->set_service(this);
+
+  gfx::Size window_size = web_contents_impl()->EnterPictureInPicture(
+      surface_id.value(), natural_size);
+
+  if (pip_controller)
+    pip_controller->SetAlwaysHidePlayPauseButton(show_play_pause_button);
+
+  std::move(callback).Run(window_size);
+}
+
+void PictureInPictureServiceImpl::EndSession(EndSessionCallback callback) {
+  DCHECK(player_id_);
+
+  ExitPictureInPictureInternal();
+
+  std::move(callback).Run();
+}
+
+void PictureInPictureServiceImpl::UpdateSession(
+    uint32_t player_id,
+    const base::Optional<viz::SurfaceId>& surface_id,
+    const gfx::Size& natural_size,
+    bool show_play_pause_button) {
+  player_id_ = MediaPlayerId(render_frame_host_, player_id);
+
+  // The PictureInPictureWindowController instance may not have been created by
+  // the embedder.
+  if (auto* pip_controller = GetController()) {
+    pip_controller->EmbedSurface(surface_id.value(), natural_size);
+    pip_controller->SetAlwaysHidePlayPauseButton(show_play_pause_button);
+    pip_controller->set_service(this);
+  }
+}
+
+void PictureInPictureServiceImpl::SetDelegate(
+    blink::mojom::PictureInPictureDelegatePtr delegate) {
+  delegate.set_connection_error_handler(
+      base::BindOnce(&PictureInPictureServiceImpl::OnDelegateDisconnected,
+                     // delegate is held by |this|.
+                     base::Unretained(this)));
+
+  if (delegate_)
+    mojo::ReportBadMessage("SetDelegate() should only be called once.");
+
+  delegate_ = std::move(delegate);
+}
+
+void PictureInPictureServiceImpl::NotifyWindowResized(const gfx::Size& size) {
+  if (delegate_)
+    delegate_->PictureInPictureWindowSizeChanged(size);
+}
+
+PictureInPictureServiceImpl::PictureInPictureServiceImpl(
+    RenderFrameHost* render_frame_host,
+    blink::mojom::PictureInPictureServiceRequest request)
+    : FrameServiceBase(render_frame_host, std::move(request)),
+      render_frame_host_(render_frame_host) {}
+
+PictureInPictureServiceImpl::~PictureInPictureServiceImpl() {
+  if (player_id_)
+    ExitPictureInPictureInternal();
+  if (GetController())
+    GetController()->set_service(nullptr);
+}
+
+PictureInPictureWindowControllerImpl*
+PictureInPictureServiceImpl::GetController() {
+  return PictureInPictureWindowControllerImpl::GetOrCreateForWebContents(
+      web_contents_impl());
+}
+
+void PictureInPictureServiceImpl::OnDelegateDisconnected() {
+  delegate_ = nullptr;
+}
+
+void PictureInPictureServiceImpl::ExitPictureInPictureInternal() {
+  web_contents_impl()->ExitPictureInPicture();
+
+  // Reset must happen after notifying the WebContents because it may interact
+  // with it.
+  player_id_.reset();
+
+  if (auto* controller = GetController())
+    controller->set_service(nullptr);
+}
+
+WebContentsImpl* PictureInPictureServiceImpl::web_contents_impl() {
+  return static_cast<WebContentsImpl*>(web_contents());
+}
+
+}  // namespace content
diff --git a/content/browser/picture_in_picture/picture_in_picture_service_impl.h b/content/browser/picture_in_picture/picture_in_picture_service_impl.h
new file mode 100644
index 0000000..28710e3
--- /dev/null
+++ b/content/browser/picture_in_picture/picture_in_picture_service_impl.h
@@ -0,0 +1,78 @@
+// Copyright 2019 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.
+
+#ifndef CONTENT_BROWSER_PICTURE_IN_PICTURE_PICTURE_IN_PICTURE_SERVICE_IMPL_H_
+#define CONTENT_BROWSER_PICTURE_IN_PICTURE_PICTURE_IN_PICTURE_SERVICE_IMPL_H_
+
+#include "base/containers/unique_ptr_adapters.h"
+#include "content/common/content_export.h"
+#include "content/public/browser/frame_service_base.h"
+#include "third_party/blink/public/mojom/picture_in_picture/picture_in_picture.mojom.h"
+
+namespace content {
+
+class PictureInPictureWindowControllerImpl;
+
+// Receives Picture-in-Picture messages from a given RenderFrame. There is one
+// PictureInPictureServiceImpl per RenderFrameHost. It talks directly to the
+// PictureInPictureWindowControllerImpl. Only one service interacts with the
+// window at a given time.
+class CONTENT_EXPORT PictureInPictureServiceImpl final
+    : public content::FrameServiceBase<blink::mojom::PictureInPictureService> {
+ public:
+  static void Create(RenderFrameHost*,
+                     blink::mojom::PictureInPictureServiceRequest);
+  static PictureInPictureServiceImpl* CreateForTesting(
+      RenderFrameHost*,
+      blink::mojom::PictureInPictureServiceRequest);
+
+  // PictureInPictureService implementation.
+  void StartSession(uint32_t player_id,
+                    const base::Optional<viz::SurfaceId>& surface_id,
+                    const gfx::Size& natural_size,
+                    bool show_play_pause_button,
+                    StartSessionCallback) final;
+  void EndSession(EndSessionCallback) final;
+  void UpdateSession(uint32_t player_id,
+                     const base::Optional<viz::SurfaceId>& surface_id,
+                     const gfx::Size& natural_size,
+                     bool show_play_pause_button) final;
+  void SetDelegate(blink::mojom::PictureInPictureDelegatePtr) final;
+
+  void NotifyWindowResized(const gfx::Size&);
+
+  // Returns the player that is currently in Picture-in-Picture in the context
+  // of the frame associated with the service. Returns nullopt if there are
+  // none.
+  const base::Optional<MediaPlayerId>& player_id() const { return player_id_; }
+  void ResetPlayerId() { player_id_.reset(); }
+
+ private:
+  PictureInPictureServiceImpl(RenderFrameHost*,
+                              blink::mojom::PictureInPictureServiceRequest);
+  ~PictureInPictureServiceImpl() override;
+
+  // Returns the PictureInPictureWindowControllerImpl associated with the
+  // WebContents. Can be null.
+  PictureInPictureWindowControllerImpl* GetController();
+
+  // Callack run when the delegate is disconnected. Only one delegate should be
+  // set at any given time.
+  void OnDelegateDisconnected();
+
+  // Implementation of ExitPictureInPicture without callback handling.
+  void ExitPictureInPictureInternal();
+
+  WebContentsImpl* web_contents_impl();
+
+  blink::mojom::PictureInPictureDelegatePtr delegate_ = nullptr;
+  RenderFrameHost* render_frame_host_ = nullptr;
+  base::Optional<MediaPlayerId> player_id_;
+
+  DISALLOW_COPY_AND_ASSIGN(PictureInPictureServiceImpl);
+};
+
+}  // namespace content
+
+#endif  // CONTENT_BROWSER_PICTURE_IN_PICTURE_PICTURE_IN_PICTURE_SERVICE_IMPL_H_
diff --git a/content/browser/picture_in_picture/picture_in_picture_service_impl_unittest.cc b/content/browser/picture_in_picture/picture_in_picture_service_impl_unittest.cc
new file mode 100644
index 0000000..abefe2a56
--- /dev/null
+++ b/content/browser/picture_in_picture/picture_in_picture_service_impl_unittest.cc
@@ -0,0 +1,145 @@
+// Copyright 2019 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 <memory>
+#include <utility>
+#include <vector>
+
+#include "content/common/media/media_player_delegate_messages.h"
+#include "content/public/browser/overlay_window.h"
+#include "content/public/browser/web_contents_delegate.h"
+#include "content/test/test_content_browser_client.h"
+#include "content/test/test_render_frame_host.h"
+#include "content/test/test_render_view_host.h"
+#include "content/test/test_web_contents.h"
+#include "mojo/public/cpp/bindings/interface_ptr.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace content {
+
+class PictureInPictureDelegate : public WebContentsDelegate {
+ public:
+  PictureInPictureDelegate() = default;
+
+  MOCK_METHOD3(EnterPictureInPicture,
+               gfx::Size(WebContents*,
+                         const viz::SurfaceId&,
+                         const gfx::Size&));
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(PictureInPictureDelegate);
+};
+
+class TestOverlayWindow : public OverlayWindow {
+ public:
+  TestOverlayWindow() = default;
+  ~TestOverlayWindow() override{};
+
+  static std::unique_ptr<OverlayWindow> Create(
+      PictureInPictureWindowController* controller) {
+    return std::unique_ptr<OverlayWindow>(new TestOverlayWindow());
+  }
+
+  bool IsActive() const override { return false; }
+  void Close() override {}
+  void ShowInactive() override {}
+  void Hide() override {}
+  void SetPictureInPictureCustomControls(
+      const std::vector<blink::PictureInPictureControlInfo>& controls)
+      override {}
+  bool IsVisible() const override { return false; }
+  bool IsAlwaysOnTop() const override { return false; }
+  ui::Layer* GetLayer() override { return nullptr; }
+  gfx::Rect GetBounds() const override { return gfx::Rect(); }
+  void UpdateVideoSize(const gfx::Size& natural_size) override {}
+  void SetPlaybackState(PlaybackState playback_state) override {}
+  void SetAlwaysHidePlayPauseButton(bool is_visible) override {}
+  ui::Layer* GetWindowBackgroundLayer() override { return nullptr; }
+  ui::Layer* GetVideoLayer() override { return nullptr; }
+  gfx::Rect GetVideoBounds() override { return gfx::Rect(); }
+  void SetSkipAdButtonVisibility(bool is_visible) override {}
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(TestOverlayWindow);
+};
+
+class PictureInPictureTestBrowserClient : public TestContentBrowserClient {
+ public:
+  PictureInPictureTestBrowserClient() = default;
+  ~PictureInPictureTestBrowserClient() override = default;
+
+  std::unique_ptr<OverlayWindow> CreateWindowForPictureInPicture(
+      PictureInPictureWindowController* controller) override {
+    return TestOverlayWindow::Create(controller);
+  }
+};
+
+class PictureInPictureServiceImplTest : public RenderViewHostImplTestHarness {
+ public:
+  void SetUp() override {
+    RenderViewHostImplTestHarness::SetUp();
+    // WebUIControllerFactory::RegisterFactory(
+    //     ContentWebUIControllerFactory::GetInstance());
+
+    SetBrowserClientForTesting(&browser_client_);
+
+    TestRenderFrameHost* render_frame_host = contents()->GetMainFrame();
+    render_frame_host->InitializeRenderFrameIfNeeded();
+
+    contents()->SetDelegate(&delegate_);
+
+    blink::mojom::PictureInPictureServiceRequest request;
+    service_impl_ = PictureInPictureServiceImpl::CreateForTesting(
+        render_frame_host, std::move(request));
+  }
+
+  void TearDown() override {
+    // WebUIControllerFactory::UnregisterFactoryForTesting(
+    //     ContentWebUIControllerFactory::GetInstance());
+    RenderViewHostImplTestHarness::TearDown();
+  }
+
+  PictureInPictureServiceImpl& service() { return *service_impl_; }
+
+  PictureInPictureDelegate& delegate() { return delegate_; }
+
+ private:
+  PictureInPictureTestBrowserClient browser_client_;
+  PictureInPictureDelegate delegate_;
+  // Will be deleted when the frame is destroyed.
+  PictureInPictureServiceImpl* service_impl_;
+};
+
+TEST_F(PictureInPictureServiceImplTest, EnterPictureInPicture) {
+  const int kPlayerVideoOnlyId = 30;
+
+  // If Picture-in-Picture was never triggered, the media player id would not be
+  // set.
+  EXPECT_FALSE(service().player_id().has_value());
+
+  viz::SurfaceId surface_id =
+      viz::SurfaceId(viz::FrameSinkId(1, 1),
+                     viz::LocalSurfaceId(
+                         11, base::UnguessableToken::Deserialize(0x111111, 0)));
+
+  EXPECT_CALL(delegate(),
+              EnterPictureInPicture(contents(), surface_id, gfx::Size(42, 42)));
+
+  service().StartSession(kPlayerVideoOnlyId, surface_id, gfx::Size(42, 42),
+                         true /* show_play_pause_button */, base::DoNothing());
+  EXPECT_TRUE(service().player_id().has_value());
+  EXPECT_EQ(kPlayerVideoOnlyId, service().player_id()->delegate_id);
+
+  // Picture-in-Picture media player id should not be reset when the media is
+  // destroyed (e.g. video stops playing). This allows the Picture-in-Picture
+  // window to continue to control the media.
+  contents()->GetMainFrame()->OnMessageReceived(
+      MediaPlayerDelegateHostMsg_OnMediaDestroyed(
+          contents()->GetMainFrame()->GetRoutingID(), kPlayerVideoOnlyId));
+  EXPECT_TRUE(service().player_id().has_value());
+}
+
+}  // namespace content
diff --git a/content/browser/picture_in_picture/picture_in_picture_window_controller_impl.cc b/content/browser/picture_in_picture/picture_in_picture_window_controller_impl.cc
index 44534b1..b26f9d0 100644
--- a/content/browser/picture_in_picture/picture_in_picture_window_controller_impl.cc
+++ b/content/browser/picture_in_picture/picture_in_picture_window_controller_impl.cc
@@ -10,6 +10,7 @@
 #include "content/browser/media/media_web_contents_observer.h"
 #include "content/browser/media/session/media_session_impl.h"
 #include "content/browser/picture_in_picture/overlay_surface_embedder.h"
+#include "content/browser/picture_in_picture/picture_in_picture_service_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/common/media/media_player_delegate_messages.h"
 #include "content/public/browser/content_browser_client.h"
@@ -57,7 +58,8 @@
 
 PictureInPictureWindowControllerImpl::PictureInPictureWindowControllerImpl(
     WebContents* initiator)
-    : initiator_(static_cast<WebContentsImpl* const>(initiator)) {
+    : WebContentsObserver(initiator),
+      initiator_(static_cast<WebContentsImpl* const>(initiator)) {
   DCHECK(initiator_);
 
   media_web_contents_observer_ = initiator_->media_web_contents_observer();
@@ -126,9 +128,7 @@
   // surface id was updated for the same video, this is a no-op. This could
   // be updated for a different video if another media player on the same
   // |initiator_| enters Picture-in-Picture mode.
-  media_player_id_ =
-      media_web_contents_observer_->GetPictureInPictureVideoMediaPlayerId();
-  UpdatePlaybackState(IsPlayerActive(), !media_player_id_.has_value());
+  UpdateMediaPlayerId();
 
   window_->UpdateVideoSize(natural_size);
 
@@ -142,9 +142,9 @@
 }
 
 void PictureInPictureWindowControllerImpl::UpdateLayerBounds() {
-  if (media_player_id_.has_value() && window_ && window_->IsVisible()) {
-    media_web_contents_observer_->OnPictureInPictureWindowResize(
-        window_->GetBounds().size());
+  if (media_player_id_.has_value() && service_ && window_ &&
+      window_->IsVisible()) {
+    service_->NotifyWindowResized(window_->GetBounds().size());
   }
 
   if (embedder_)
@@ -152,10 +152,12 @@
 }
 
 bool PictureInPictureWindowControllerImpl::IsPlayerActive() {
-  if (!media_player_id_.has_value()) {
-    media_player_id_ =
-        media_web_contents_observer_->GetPictureInPictureVideoMediaPlayerId();
-  }
+  if (!media_player_id_.has_value())
+    media_player_id_ = service_ ? service_->player_id() : base::nullopt;
+
+  // At creation time, the player id may not be set.
+  if (!media_player_id_.has_value())
+    return false;
 
   return media_player_id_.has_value() &&
          media_web_contents_observer_->IsPlayerActive(*media_player_id_);
@@ -219,6 +221,11 @@
           media_player_id_->delegate_id, control_id));
 }
 
+void PictureInPictureWindowControllerImpl::UpdateMediaPlayerId() {
+  media_player_id_ = service_ ? service_->player_id() : base::nullopt;
+  UpdatePlaybackState(IsPlayerActive(), !media_player_id_.has_value());
+}
+
 void PictureInPictureWindowControllerImpl::SetAlwaysHidePlayPauseButton(
     bool is_visible) {
   always_hide_play_pause_button_ = is_visible;
@@ -254,6 +261,33 @@
   window_->SetSkipAdButtonVisibility(media_session_action_skip_ad_handled_);
 }
 
+void PictureInPictureWindowControllerImpl::MediaStartedPlaying(
+    const MediaPlayerInfo&,
+    const MediaPlayerId& media_player_id) {
+  if (initiator_->IsBeingDestroyed())
+    return;
+
+  if (media_player_id_ != media_player_id)
+    return;
+
+  UpdatePlaybackState(true /* is_playing */, false /* reached_end_of_stream */);
+}
+
+void PictureInPictureWindowControllerImpl::MediaStoppedPlaying(
+    const MediaPlayerInfo&,
+    const MediaPlayerId& media_player_id,
+    WebContentsObserver::MediaStoppedReason reason) {
+  if (initiator_->IsBeingDestroyed())
+    return;
+
+  if (media_player_id_ != media_player_id)
+    return;
+
+  UpdatePlaybackState(
+      false /* is_playing */,
+      reason == WebContentsObserver::MediaStoppedReason::kReachedEndOfStream);
+}
+
 void PictureInPictureWindowControllerImpl::OnLeavingPictureInPicture(
     bool should_pause_video,
     bool should_reset_pip_player) {
@@ -269,8 +303,12 @@
         new MediaPlayerDelegateMsg_EndPictureInPictureMode(
             media_player_id_->render_frame_host->GetRoutingID(),
             media_player_id_->delegate_id));
-    if (should_reset_pip_player)
-      media_web_contents_observer_->ResetPictureInPictureVideoMediaPlayerId();
+
+    if (should_reset_pip_player) {
+      DCHECK(service_);
+      service_->ResetPlayerId();
+      media_player_id_.reset();
+    }
   }
 }
 
diff --git a/content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h b/content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h
index f44a7ca..91701f7 100644
--- a/content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h
+++ b/content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h
@@ -14,10 +14,11 @@
 
 namespace content {
 
+class MediaWebContentsObserver;
 class OverlaySurfaceEmbedder;
+class PictureInPictureServiceImpl;
 class WebContents;
 class WebContentsImpl;
-class MediaWebContentsObserver;
 
 // TODO(thakis,mlamouri): PictureInPictureWindowControllerImpl isn't
 // CONTENT_EXPORT'd because it creates complicated build issues with
@@ -27,7 +28,8 @@
 // work with it. https://crbug.com/589840.
 class PictureInPictureWindowControllerImpl
     : public PictureInPictureWindowController,
-      public WebContentsUserData<PictureInPictureWindowControllerImpl> {
+      public WebContentsUserData<PictureInPictureWindowControllerImpl>,
+      public WebContentsObserver {
  public:
   // Gets a reference to the controller associated with |initiator| and creates
   // one if it does not exist. The returned pointer is guaranteed to be
@@ -62,6 +64,22 @@
   CONTENT_EXPORT void MediaSessionActionsChanged(
       const std::set<media_session::mojom::MediaSessionAction>& actions);
 
+  // WebContentsObserver:
+  void MediaStartedPlaying(const MediaPlayerInfo&,
+                           const MediaPlayerId&) override;
+  void MediaStoppedPlaying(const MediaPlayerInfo&,
+                           const MediaPlayerId&,
+                           WebContentsObserver::MediaStoppedReason) override;
+
+  // TODO(mlamouri): temporary method used because of the media player id is
+  // stored in a different location from the one that is used to update the
+  // state of this object.
+  void UpdateMediaPlayerId();
+
+  void set_service(PictureInPictureServiceImpl* service) {
+    service_ = service;
+  };
+
  private:
   friend class WebContentsUserData<PictureInPictureWindowControllerImpl>;
 
@@ -89,6 +107,7 @@
 
   std::unique_ptr<OverlayWindow> window_;
   std::unique_ptr<OverlaySurfaceEmbedder> embedder_;
+  // TODO(929156): remove this as it should be accessible via `web_contents()`.
   WebContentsImpl* const initiator_;
 
   // Used to determine the state of the media player and route messages to
@@ -109,6 +128,11 @@
   // Session API in UpdatePlayPauseButtonVisibility().
   bool always_hide_play_pause_button_ = false;
 
+  // Service currently associated with the Picture-in-Picture window. The
+  // service makes the bridge with the renderer process by sending enter/exit
+  // requests. It is also holding the Picture-in-Picture MediaPlayerId.
+  PictureInPictureServiceImpl* service_ = nullptr;
+
   WEB_CONTENTS_USER_DATA_KEY_DECL();
 
   DISALLOW_COPY_AND_ASSIGN(PictureInPictureWindowControllerImpl);
diff --git a/content/browser/web_contents/web_contents_impl_unittest.cc b/content/browser/web_contents/web_contents_impl_unittest.cc
index 5d505aa..74e086c0 100644
--- a/content/browser/web_contents/web_contents_impl_unittest.cc
+++ b/content/browser/web_contents/web_contents_impl_unittest.cc
@@ -26,7 +26,6 @@
 #include "content/browser/webui/web_ui_controller_factory_registry.h"
 #include "content/common/frame_messages.h"
 #include "content/common/input/synthetic_web_input_event_builders.h"
-#include "content/common/media/media_player_delegate_messages.h"
 #include "content/common/view_messages.h"
 #include "content/public/browser/content_browser_client.h"
 #include "content/public/browser/global_request_id.h"
@@ -35,7 +34,6 @@
 #include "content/public/browser/navigation_details.h"
 #include "content/public/browser/notification_details.h"
 #include "content/public/browser/notification_source.h"
-#include "content/public/browser/overlay_window.h"
 #include "content/public/browser/render_widget_host_view.h"
 #include "content/public/browser/ssl_host_state_delegate.h"
 #include "content/public/browser/storage_partition.h"
@@ -3242,113 +3240,6 @@
   EXPECT_EQ(SK_ColorGREEN, observer.last_theme_color());
 }
 
-class PictureInPictureDelegate : public WebContentsDelegate {
- public:
-  PictureInPictureDelegate() = default;
-
-  MOCK_METHOD3(EnterPictureInPicture,
-               gfx::Size(content::WebContents* web_contents,
-                         const viz::SurfaceId&,
-                         const gfx::Size&));
-
- private:
-  DISALLOW_COPY_AND_ASSIGN(PictureInPictureDelegate);
-};
-
-class TestOverlayWindow : public OverlayWindow {
- public:
-  TestOverlayWindow() = default;
-  ~TestOverlayWindow() override{};
-
-  static std::unique_ptr<OverlayWindow> Create(
-      PictureInPictureWindowController* controller) {
-    return std::unique_ptr<OverlayWindow>(new TestOverlayWindow());
-  }
-
-  bool IsActive() const override { return false; }
-  void Close() override {}
-  void ShowInactive() override {}
-  void Hide() override {}
-  void SetPictureInPictureCustomControls(
-      const std::vector<blink::PictureInPictureControlInfo>& controls)
-      override {}
-  bool IsVisible() const override { return false; }
-  bool IsAlwaysOnTop() const override { return false; }
-  ui::Layer* GetLayer() override { return nullptr; }
-  gfx::Rect GetBounds() const override { return gfx::Rect(); }
-  void UpdateVideoSize(const gfx::Size& natural_size) override {}
-  void SetPlaybackState(PlaybackState playback_state) override {}
-  void SetAlwaysHidePlayPauseButton(bool is_visible) override {}
-  void SetSkipAdButtonVisibility(bool is_visible) override {}
-  ui::Layer* GetWindowBackgroundLayer() override { return nullptr; }
-  ui::Layer* GetVideoLayer() override { return nullptr; }
-  gfx::Rect GetVideoBounds() override { return gfx::Rect(); }
-
- private:
-  DISALLOW_COPY_AND_ASSIGN(TestOverlayWindow);
-};
-
-class PictureInPictureTestBrowserClient : public TestContentBrowserClient {
- public:
-  PictureInPictureTestBrowserClient()
-      : original_browser_client_(SetBrowserClientForTesting(this)) {}
-
-  ~PictureInPictureTestBrowserClient() override {
-    SetBrowserClientForTesting(original_browser_client_);
-  }
-
-  std::unique_ptr<OverlayWindow> CreateWindowForPictureInPicture(
-      PictureInPictureWindowController* controller) override {
-    return TestOverlayWindow::Create(controller);
-  }
-
- private:
-  ContentBrowserClient* original_browser_client_;
-};
-
-TEST_F(WebContentsImplTest, EnterPictureInPicture) {
-  PictureInPictureTestBrowserClient browser_client;
-  SetBrowserClientForTesting(&browser_client);
-
-  const int kPlayerVideoOnlyId = 30; /* arbitrary and used for tests */
-
-  PictureInPictureDelegate delegate;
-  contents()->SetDelegate(&delegate);
-
-  MediaWebContentsObserver* observer =
-      contents()->media_web_contents_observer();
-  TestRenderFrameHost* rfh = main_test_rfh();
-  rfh->InitializeRenderFrameIfNeeded();
-
-  // If Picture-in-Picture was never triggered, the media player id would not be
-  // set.
-  EXPECT_FALSE(observer->GetPictureInPictureVideoMediaPlayerId().has_value());
-
-  viz::SurfaceId surface_id =
-      viz::SurfaceId(viz::FrameSinkId(1, 1),
-                     viz::LocalSurfaceId(
-                         11, base::UnguessableToken::Deserialize(0x111111, 0)));
-
-  EXPECT_CALL(delegate,
-              EnterPictureInPicture(contents(), surface_id, gfx::Size(42, 42)));
-
-  rfh->OnMessageReceived(
-      MediaPlayerDelegateHostMsg_OnPictureInPictureModeStarted(
-          rfh->GetRoutingID(), kPlayerVideoOnlyId, surface_id /* surface_id */,
-          gfx::Size(42, 42) /* natural_size */, 1 /* request_id */,
-          true /* show_play_pause_button */));
-  EXPECT_TRUE(observer->GetPictureInPictureVideoMediaPlayerId().has_value());
-  EXPECT_EQ(kPlayerVideoOnlyId,
-            observer->GetPictureInPictureVideoMediaPlayerId()->delegate_id);
-
-  // Picture-in-Picture media player id should not be reset when the media is
-  // destroyed (e.g. video stops playing). This allows the Picture-in-Picture
-  // window to continue to control the media.
-  rfh->OnMessageReceived(MediaPlayerDelegateHostMsg_OnMediaDestroyed(
-      rfh->GetRoutingID(), kPlayerVideoOnlyId));
-  EXPECT_TRUE(observer->GetPictureInPictureVideoMediaPlayerId().has_value());
-}
-
 TEST_F(WebContentsImplTest, ParseDownloadHeaders) {
   download::DownloadUrlParameters::RequestHeadersType request_headers =
       WebContentsImpl::ParseDownloadHeaders("A: 1\r\nB: 2\r\nC: 3\r\n\r\n");
diff --git a/content/common/media/media_player_delegate_messages.h b/content/common/media/media_player_delegate_messages.h
index b482679e..4870d5b2 100644
--- a/content/common/media/media_player_delegate_messages.h
+++ b/content/common/media/media_player_delegate_messages.h
@@ -76,23 +76,6 @@
                     int /* delegate_id, distinguishes instances */,
                     std::string /* control_id */)
 
-IPC_MESSAGE_ROUTED2(MediaPlayerDelegateMsg_OnPictureInPictureWindowResize,
-                    int /* delegate_id, distinguishes instances */,
-                    gfx::Size /* window_size */)
-
-// ----------------------------------------------------------------------------
-// Messages from the browser to the renderer acknowledging changes happened.
-// ----------------------------------------------------------------------------
-
-IPC_MESSAGE_ROUTED3(MediaPlayerDelegateMsg_OnPictureInPictureModeStarted_ACK,
-                    int /* delegate id */,
-                    int /* request_id */,
-                    gfx::Size /* window_size */)
-
-IPC_MESSAGE_ROUTED2(MediaPlayerDelegateMsg_OnPictureInPictureModeEnded_ACK,
-                    int /* delegate id */,
-                    int /* request_id */)
-
 // ----------------------------------------------------------------------------
 // Messages from the renderer notifying the browser of playback state changes.
 // ----------------------------------------------------------------------------
@@ -124,23 +107,6 @@
                     int /* delegate_id, distinguishes instances */,
                     gfx::Size /* new size of video */)
 
-IPC_MESSAGE_ROUTED5(MediaPlayerDelegateHostMsg_OnPictureInPictureModeStarted,
-                    int /* delegate id */,
-                    viz::SurfaceId /* surface_id */,
-                    gfx::Size /* natural_size */,
-                    int /* request_id */,
-                    bool /* show_play_pause_button */)
-
-IPC_MESSAGE_ROUTED2(MediaPlayerDelegateHostMsg_OnPictureInPictureModeEnded,
-                    int /* delegate id */,
-                    int /* request_id */)
-
-IPC_MESSAGE_ROUTED4(MediaPlayerDelegateHostMsg_OnPictureInPictureSurfaceChanged,
-                    int /* delegate id */,
-                    viz::SurfaceId /* surface_id */,
-                    gfx::Size /* natural_size */,
-                    bool /* show_play_pause_button */)
-
 IPC_MESSAGE_ROUTED2(
     MediaPlayerDelegateHostMsg_OnSetPictureInPictureCustomControls,
     int /* delegate id */,
diff --git a/content/public/app/content_browser_manifest.cc b/content/public/app/content_browser_manifest.cc
index cf6cf19..54bccd3 100644
--- a/content/public/app/content_browser_manifest.cc
+++ b/content/public/app/content_browser_manifest.cc
@@ -227,6 +227,7 @@
                   "blink.mojom.MediaSessionService",
                   "blink.mojom.NotificationService",
                   "blink.mojom.PermissionService",
+                  "blink.mojom.PictureInPictureService",
                   "blink.mojom.Portal",
                   "blink.mojom.PrefetchURLLoaderService",
                   "blink.mojom.PresentationService",
diff --git a/content/renderer/media/renderer_webmediaplayer_delegate.cc b/content/renderer/media/renderer_webmediaplayer_delegate.cc
index f6dbb40..d7d9255 100644
--- a/content/renderer/media/renderer_webmediaplayer_delegate.cc
+++ b/content/renderer/media/renderer_webmediaplayer_delegate.cc
@@ -120,30 +120,6 @@
                                                            delegate_id, muted));
 }
 
-void RendererWebMediaPlayerDelegate::DidPictureInPictureModeStart(
-    int delegate_id,
-    const viz::SurfaceId& surface_id,
-    const gfx::Size& natural_size,
-    blink::WebMediaPlayer::PipWindowOpenedCallback callback,
-    bool show_play_pause_button) {
-  int request_id = next_picture_in_picture_callback_id_++;
-  enter_picture_in_picture_callback_map_.insert(
-      std::make_pair(request_id, std::move(callback)));
-  Send(new MediaPlayerDelegateHostMsg_OnPictureInPictureModeStarted(
-      routing_id(), delegate_id, surface_id, natural_size, request_id,
-      show_play_pause_button));
-}
-
-void RendererWebMediaPlayerDelegate::DidPictureInPictureModeEnd(
-    int delegate_id,
-    base::OnceClosure callback) {
-  int request_id = next_picture_in_picture_callback_id_++;
-  exit_picture_in_picture_callback_map_.insert(
-      std::make_pair(request_id, std::move(callback)));
-  Send(new MediaPlayerDelegateHostMsg_OnPictureInPictureModeEnded(
-      routing_id(), delegate_id, request_id));
-}
-
 void RendererWebMediaPlayerDelegate::DidSetPictureInPictureCustomControls(
     int delegate_id,
     const std::vector<blink::PictureInPictureControlInfo>& controls) {
@@ -151,24 +127,6 @@
       routing_id(), delegate_id, controls));
 }
 
-void RendererWebMediaPlayerDelegate::DidPictureInPictureSurfaceChange(
-    int delegate_id,
-    const viz::SurfaceId& surface_id,
-    const gfx::Size& natural_size,
-    bool show_play_pause_button) {
-  Send(new MediaPlayerDelegateHostMsg_OnPictureInPictureSurfaceChanged(
-      routing_id(), delegate_id, surface_id, natural_size,
-      show_play_pause_button));
-}
-
-void RendererWebMediaPlayerDelegate::
-    RegisterPictureInPictureWindowResizeCallback(
-        int player_id,
-        blink::WebMediaPlayer::PipWindowResizedCallback callback) {
-  picture_in_picture_window_resize_observer_ =
-      std::make_pair(player_id, std::move(callback));
-}
-
 void RendererWebMediaPlayerDelegate::DidPause(int player_id) {
   DVLOG(2) << __func__ << "(" << player_id << ")";
   DCHECK(id_map_.Lookup(player_id));
@@ -291,13 +249,6 @@
                         OnPictureInPictureModeEnded)
     IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_ClickPictureInPictureControl,
                         OnPictureInPictureControlClicked)
-    IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_OnPictureInPictureModeEnded_ACK,
-                        OnPictureInPictureModeEndedAck)
-    IPC_MESSAGE_HANDLER(
-        MediaPlayerDelegateMsg_OnPictureInPictureModeStarted_ACK,
-        OnPictureInPictureModeStartedAck)
-    IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_OnPictureInPictureWindowResize,
-                        OnPictureInPictureWindowResize)
     IPC_MESSAGE_UNHANDLED(return false)
   IPC_END_MESSAGE_MAP()
   return true;
@@ -417,39 +368,6 @@
     observer->OnPictureInPictureControlClicked(control_id);
 }
 
-void RendererWebMediaPlayerDelegate::OnPictureInPictureModeEndedAck(
-    int player_id,
-    int request_id) {
-  auto iter = exit_picture_in_picture_callback_map_.find(request_id);
-  DCHECK(iter != exit_picture_in_picture_callback_map_.end());
-
-  std::move(iter->second).Run();
-  exit_picture_in_picture_callback_map_.erase(iter);
-}
-
-void RendererWebMediaPlayerDelegate::OnPictureInPictureModeStartedAck(
-    int player_id,
-    int request_id,
-    const gfx::Size& window_size) {
-  auto iter = enter_picture_in_picture_callback_map_.find(request_id);
-  DCHECK(iter != enter_picture_in_picture_callback_map_.end());
-
-  std::move(iter->second).Run(blink::WebSize(window_size));
-  enter_picture_in_picture_callback_map_.erase(iter);
-}
-
-void RendererWebMediaPlayerDelegate::OnPictureInPictureWindowResize(
-    int player_id,
-    const gfx::Size& window_size) {
-  if (!picture_in_picture_window_resize_observer_ ||
-      picture_in_picture_window_resize_observer_->first != player_id) {
-    return;
-  }
-
-  picture_in_picture_window_resize_observer_->second.Run(
-      blink::WebSize(window_size));
-}
-
 void RendererWebMediaPlayerDelegate::ScheduleUpdateTask() {
   if (!pending_update_task_) {
     base::ThreadTaskRunnerHandle::Get()->PostTask(
diff --git a/content/renderer/media/renderer_webmediaplayer_delegate.h b/content/renderer/media/renderer_webmediaplayer_delegate.h
index ad78d316..1f7242c3 100644
--- a/content/renderer/media/renderer_webmediaplayer_delegate.h
+++ b/content/renderer/media/renderer_webmediaplayer_delegate.h
@@ -65,23 +65,9 @@
       blink::WebFullscreenVideoStatus fullscreen_video_status) override;
   void DidPlayerSizeChange(int delegate_id, const gfx::Size& size) override;
   void DidPlayerMutedStatusChange(int delegate_id, bool muted) override;
-  void DidPictureInPictureModeStart(
-      int delegate_id,
-      const viz::SurfaceId&,
-      const gfx::Size&,
-      blink::WebMediaPlayer::PipWindowOpenedCallback,
-      bool show_play_pause_button) override;
-  void DidPictureInPictureModeEnd(int delegate_id, base::OnceClosure) override;
   void DidSetPictureInPictureCustomControls(
       int delegate_id,
       const std::vector<blink::PictureInPictureControlInfo>& controls) override;
-  void DidPictureInPictureSurfaceChange(int delegate_id,
-                                        const viz::SurfaceId&,
-                                        const gfx::Size&,
-                                        bool show_play_pause_button) override;
-  void RegisterPictureInPictureWindowResizeCallback(
-      int player_id,
-      blink::WebMediaPlayer::PipWindowResizedCallback) override;
 
   // content::RenderFrameObserver overrides.
   void WasHidden() override;
@@ -114,11 +100,6 @@
   void OnPictureInPictureModeEnded(int player_id);
   void OnPictureInPictureControlClicked(int player_id,
                                         const std::string& control_id);
-  void OnPictureInPictureModeEndedAck(int player_id, int request_id);
-  void OnPictureInPictureModeStartedAck(int player_id,
-                                        int request_id,
-                                        const gfx::Size&);
-  void OnPictureInPictureWindowResize(int player_id, const gfx::Size&);
 
   // Schedules UpdateTask() to run soon.
   void ScheduleUpdateTask();
@@ -185,35 +166,6 @@
   // when the idle cleanup timer should be fired more aggressively.
   bool is_jelly_bean_;
 
-  // Map associating a callback with a request sent to the browser process. The
-  // index is used as a unique request id that is passed to the browser process
-  // and will then ACK with the same id which will be used to run the right
-  // callback.
-  using ExitPictureInPictureCallbackMap =
-      base::flat_map<int, base::OnceClosure>;
-  ExitPictureInPictureCallbackMap exit_picture_in_picture_callback_map_;
-
-  // Map associating a callback with a request sent to the browser process. The
-  // index is used as a unique request id that is passed to the browser process
-  // and will then ACK with the same id which will be used to run the right
-  // callback.
-  using EnterPictureInPictureCallbackMap =
-      base::flat_map<int, blink::WebMediaPlayer::PipWindowOpenedCallback>;
-  EnterPictureInPictureCallbackMap enter_picture_in_picture_callback_map_;
-
-  // Counter that is used to use unique request id associated with
-  // picture-in-picture callbacks. It is incremented every time it is used.
-  int next_picture_in_picture_callback_id_ = 0;
-
-  // Associating a player id and a Picture-in-Picture window resize callback.
-  // It holds the callback alive and guarantees that the notification sent from
-  // the browser proccess matches the player currently in Picture-in-Picture in
-  // the renderer.
-  using PictureInPictureWindowResizeObserver =
-      std::pair<int, blink::WebMediaPlayer::PipWindowResizedCallback>;
-  base::Optional<PictureInPictureWindowResizeObserver>
-      picture_in_picture_window_resize_observer_;
-
   DISALLOW_COPY_AND_ASSIGN(RendererWebMediaPlayerDelegate);
 };
 
diff --git a/content/renderer/media/stream/webmediaplayer_ms.cc b/content/renderer/media/stream/webmediaplayer_ms.cc
index 5f58e9e..3b11c6d 100644
--- a/content/renderer/media/stream/webmediaplayer_ms.cc
+++ b/content/renderer/media/stream/webmediaplayer_ms.cc
@@ -425,11 +425,8 @@
   // disabled.
   // The viz::SurfaceId may be updated when the video begins playback or when
   // the size of the video changes.
-  if (client_ && IsInPictureInPicture() && !client_->IsInAutoPIP()) {
-    delegate_->DidPictureInPictureSurfaceChange(
-        delegate_id_, surface_id, NaturalSize(),
-        false /* show_play_pause_button */);
-  }
+  if (client_)
+    client_->OnPictureInPictureStateChange();
 }
 
 void WebMediaPlayerMS::TrackAdded(const blink::WebMediaStreamTrack& track) {
@@ -460,6 +457,16 @@
     audio_renderer_->Stop();
 }
 
+int WebMediaPlayerMS::GetDelegateId() {
+  return delegate_id_;
+}
+
+base::Optional<viz::SurfaceId> WebMediaPlayerMS::GetSurfaceId() {
+  if (bridge_)
+    return bridge_->GetSurfaceId();
+  return base::nullopt;
+}
+
 void WebMediaPlayerMS::Reload() {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   if (web_stream_.IsNull())
@@ -640,30 +647,17 @@
   delegate_->DidPlayerMutedStatusChange(delegate_id_, volume == 0.0);
 }
 
-void WebMediaPlayerMS::EnterPictureInPicture(
-    blink::WebMediaPlayer::PipWindowOpenedCallback callback) {
+void WebMediaPlayerMS::EnterPictureInPicture() {
   if (!bridge_)
     ActivateSurfaceLayerForVideo();
 
   DCHECK(bridge_);
-
-  const viz::SurfaceId& surface_id = bridge_->GetSurfaceId();
-  DCHECK(surface_id.is_valid());
-
-  // Notifies the browser process that the player should now be in
-  // Picture-in-Picture mode.
-  delegate_->DidPictureInPictureModeStart(delegate_id_, surface_id,
-                                          NaturalSize(), std::move(callback),
-                                          false /* show_play_pause_button */);
+  DCHECK(bridge_->GetSurfaceId().is_valid());
 }
 
-void WebMediaPlayerMS::ExitPictureInPicture(
-    blink::WebMediaPlayer::PipWindowClosedCallback callback) {
-  // Notifies the browser process that Picture-in-Picture has ended. It will
-  // clear out the states and close the window.
-  delegate_->DidPictureInPictureModeEnd(delegate_id_, std::move(callback));
-
+void WebMediaPlayerMS::ExitPictureInPicture() {
   // Internal cleanups.
+  // TODO(mlamouri): remove the need for this.
   OnPictureInPictureModeEnded();
 }
 
@@ -672,14 +666,6 @@
   delegate_->DidSetPictureInPictureCustomControls(delegate_id_, controls);
 }
 
-void WebMediaPlayerMS::RegisterPictureInPictureWindowResizeCallback(
-    blink::WebMediaPlayer::PipWindowResizedCallback callback) {
-  DCHECK(IsInPictureInPicture() && !client_->IsInAutoPIP());
-
-  delegate_->RegisterPictureInPictureWindowResizeCallback(delegate_id_,
-                                                          std::move(callback));
-}
-
 void WebMediaPlayerMS::SetSinkId(
     const blink::WebString& sink_id,
     std::unique_ptr<blink::WebSetSinkIdCallbacks> web_callback) {
diff --git a/content/renderer/media/stream/webmediaplayer_ms.h b/content/renderer/media/stream/webmediaplayer_ms.h
index 1dde82e..3fcc581 100644
--- a/content/renderer/media/stream/webmediaplayer_ms.h
+++ b/content/renderer/media/stream/webmediaplayer_ms.h
@@ -116,14 +116,10 @@
   void Seek(double seconds) override;
   void SetRate(double rate) override;
   void SetVolume(double volume) override;
-  void EnterPictureInPicture(
-      blink::WebMediaPlayer::PipWindowOpenedCallback callback) override;
-  void ExitPictureInPicture(
-      blink::WebMediaPlayer::PipWindowClosedCallback callback) override;
+  void EnterPictureInPicture() override;
+  void ExitPictureInPicture() override;
   void SetPictureInPictureCustomControls(
       const std::vector<blink::PictureInPictureControlInfo>&) override;
-  void RegisterPictureInPictureWindowResizeCallback(
-      blink::WebMediaPlayer::PipWindowResizedCallback) override;
   void SetSinkId(
       const blink::WebString& sink_id,
       std::unique_ptr<blink::WebSetSinkIdCallbacks> web_callback) override;
@@ -240,6 +236,8 @@
   void TrackAdded(const blink::WebMediaStreamTrack& track) override;
   void TrackRemoved(const blink::WebMediaStreamTrack& track) override;
   void ActiveStateChanged(bool is_active) override;
+  int GetDelegateId() override;
+  base::Optional<viz::SurfaceId> GetSurfaceId() override;
 
   void OnDisplayTypeChanged(WebMediaPlayer::DisplayType) override;
 
diff --git a/content/renderer/media/stream/webmediaplayer_ms_unittest.cc b/content/renderer/media/stream/webmediaplayer_ms_unittest.cc
index ccf92dd..c649211 100644
--- a/content/renderer/media/stream/webmediaplayer_ms_unittest.cc
+++ b/content/renderer/media/stream/webmediaplayer_ms_unittest.cc
@@ -113,21 +113,9 @@
     EXPECT_EQ(delegate_id_, delegate_id);
   }
 
-  MOCK_METHOD5(DidPictureInPictureModeStart,
-               void(int,
-                    const viz::SurfaceId&,
-                    const gfx::Size&,
-                    blink::WebMediaPlayer::PipWindowOpenedCallback,
-                    bool));
-  MOCK_METHOD2(DidPictureInPictureModeEnd,
-               void(int, blink::WebMediaPlayer::PipWindowClosedCallback));
   MOCK_METHOD2(DidSetPictureInPictureCustomControls,
                void(int,
                     const std::vector<blink::PictureInPictureControlInfo>&));
-  MOCK_METHOD4(DidPictureInPictureSurfaceChange,
-               void(int, const viz::SurfaceId&, const gfx::Size&, bool));
-  MOCK_METHOD2(RegisterPictureInPictureWindowResizeCallback,
-               void(int, blink::WebMediaPlayer::PipWindowResizedCallback));
 
   void DidPause(int delegate_id) override {
     EXPECT_EQ(delegate_id_, delegate_id);
@@ -618,6 +606,7 @@
   void StopRendering() override;
   void DidReceiveFrame() override;
   bool IsDrivingFrameUpdates() const override { return true; }
+  void OnPictureInPictureStateChange() override {}
 
   // For test use
   void SetBackgroundRendering(bool background_rendering) {
@@ -1407,68 +1396,9 @@
 }
 #endif
 
-// Tests delegate methods are called when Picture-in-Picture is triggered.
-TEST_P(WebMediaPlayerMSTest, PictureInPictureTriggerCallback) {
-  InitializeWebMediaPlayerMS();
-
-  // It works only a surface layer is used instead of a video layer.
-  if (!enable_surface_layer_for_video_) {
-    EXPECT_CALL(*this, DoSetCcLayer(false));
-    return;
-  }
-
-  MockMediaStreamVideoRenderer* provider = LoadAndGetFrameProvider(true);
-
-  int tokens[] = {0,   33,  66,  100, 133, 166, 200, 233, 266, 300,
-                  333, 366, 400, 433, 466, 500, 533, 566, 600};
-  std::vector<int> timestamps(tokens, tokens + sizeof(tokens) / sizeof(int));
-  provider->QueueFrames(timestamps);
-
-  EXPECT_CALL(*submitter_ptr_, StartRendering());
-  EXPECT_CALL(*this, DisplayType()).Times(2);
-  EXPECT_CALL(*this, DoReadyStateChanged(
-                         blink::WebMediaPlayer::kReadyStateHaveMetadata));
-  EXPECT_CALL(*this, DoReadyStateChanged(
-                         blink::WebMediaPlayer::kReadyStateHaveEnoughData));
-  EXPECT_CALL(*this,
-              CheckSizeChanged(gfx::Size(kStandardWidth, kStandardHeight)));
-  message_loop_controller_.RunAndWaitForStatus(
-      media::PipelineStatus::PIPELINE_OK);
-  testing::Mock::VerifyAndClearExpectations(this);
-
-  EXPECT_CALL(*this, DisplayType())
-      .WillRepeatedly(
-          Return(blink::WebMediaPlayer::DisplayType::kPictureInPicture));
-
-  const gfx::Size natural_size = player_->NaturalSize();
-  EXPECT_CALL(delegate_, DidPictureInPictureSurfaceChange(
-                             delegate_.delegate_id(),
-                             surface_layer_bridge_ptr_->GetSurfaceId(),
-                             natural_size, false))
-      .Times(2);
-
-  player_->OnSurfaceIdUpdated(surface_layer_bridge_ptr_->GetSurfaceId());
-
-  EXPECT_CALL(delegate_, DidPictureInPictureModeStart(
-                             delegate_.delegate_id(),
-                             surface_layer_bridge_ptr_->GetSurfaceId(),
-                             natural_size, _, false));
-
-  player_->EnterPictureInPicture(base::DoNothing());
-  player_->OnSurfaceIdUpdated(surface_layer_bridge_ptr_->GetSurfaceId());
-
-  // Updating SurfaceId should NOT exit Picture-in-Picture.
-  EXPECT_CALL(delegate_, DidPictureInPictureModeEnd(delegate_.delegate_id(), _))
-      .Times(0);
-
-  testing::Mock::VerifyAndClearExpectations(this);
-  EXPECT_CALL(*this, DoSetCcLayer(false));
-  EXPECT_CALL(*submitter_ptr_, StopUsingProvider());
-}
-
-INSTANTIATE_TEST_SUITE_P(,
-                         WebMediaPlayerMSTest,
-                         ::testing::Combine(::testing::Bool(),
-                                            ::testing::Bool(),
-                                            ::testing::Bool()));
+INSTANTIATE_TEST_CASE_P(,
+                        WebMediaPlayerMSTest,
+                        ::testing::Combine(::testing::Bool(),
+                                           ::testing::Bool(),
+                                           ::testing::Bool()));
 }  // namespace content
diff --git a/content/renderer/media_capture_from_element/html_video_element_capturer_source_unittest.cc b/content/renderer/media_capture_from_element/html_video_element_capturer_source_unittest.cc
index 18325df..510dff25 100644
--- a/content/renderer/media_capture_from_element/html_video_element_capturer_source_unittest.cc
+++ b/content/renderer/media_capture_from_element/html_video_element_capturer_source_unittest.cc
@@ -50,12 +50,10 @@
   void Seek(double seconds) override {}
   void SetRate(double) override {}
   void SetVolume(double) override {}
-  void EnterPictureInPicture(PipWindowOpenedCallback) override {}
-  void ExitPictureInPicture(PipWindowClosedCallback) override {}
+  void EnterPictureInPicture() override {}
+  void ExitPictureInPicture() override {}
   void SetPictureInPictureCustomControls(
       const std::vector<blink::PictureInPictureControlInfo>&) override {}
-  void RegisterPictureInPictureWindowResizeCallback(
-      PipWindowResizedCallback) override {}
   blink::WebTimeRanges Buffered() const override {
     return blink::WebTimeRanges();
   }
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 8b6db3d0..245c3e9 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -1542,6 +1542,7 @@
     "../browser/payments/payment_app_content_unittest_base.h",
     "../browser/payments/payment_app_provider_impl_unittest.cc",
     "../browser/payments/payment_manager_unittest.cc",
+    "../browser/picture_in_picture/picture_in_picture_service_impl_unittest.cc",
     "../browser/plugin_list_unittest.cc",
     "../browser/presentation/presentation_service_impl_unittest.cc",
     "../browser/renderer_host/clipboard_host_impl_unittest.cc",
diff --git a/media/blink/webmediaplayer_delegate.h b/media/blink/webmediaplayer_delegate.h
index af33ac9..c3f9c0d7 100644
--- a/media/blink/webmediaplayer_delegate.h
+++ b/media/blink/webmediaplayer_delegate.h
@@ -16,10 +16,6 @@
 class Size;
 }  // namespace gfx
 
-namespace viz {
-class SurfaceId;
-}  // namespace viz
-
 namespace media {
 
 enum class MediaContentType;
@@ -118,38 +114,12 @@
   // Notify that the muted status of the media player has changed.
   virtual void DidPlayerMutedStatusChange(int delegate_id, bool muted) = 0;
 
-  // Notify that the source media player has entered Picture-in-Picture mode.
-  virtual void DidPictureInPictureModeStart(
-      int delegate_id,
-      const viz::SurfaceId&,
-      const gfx::Size&,
-      blink::WebMediaPlayer::PipWindowOpenedCallback,
-      bool show_play_pause_button) = 0;
-
-  // Notify that the source media player has exited Picture-in-Picture mode.
-  virtual void DidPictureInPictureModeEnd(int delegate_id,
-                                          base::OnceClosure) = 0;
-
   // Notify that custom controls have been sent to be assigned to the
   // Picture-in-Picture window.
   virtual void DidSetPictureInPictureCustomControls(
       int delegate_id,
       const std::vector<blink::PictureInPictureControlInfo>&) = 0;
 
-  // Notify that the media player in Picture-in-Picture had a change of surface.
-  virtual void DidPictureInPictureSurfaceChange(
-      int delegate_id,
-      const viz::SurfaceId&,
-      const gfx::Size&,
-      bool show_play_pause_button) = 0;
-
-  // Registers a callback associated with a player that will be called when
-  // receiving a notification from the browser process that the
-  // Picture-in-Picture associated to this player has been resized.
-  virtual void RegisterPictureInPictureWindowResizeCallback(
-      int player_id,
-      blink::WebMediaPlayer::PipWindowResizedCallback) = 0;
-
   // Notify that playback is stopped. This will drop wake locks and remove any
   // external controls.
   //
diff --git a/media/blink/webmediaplayer_impl.cc b/media/blink/webmediaplayer_impl.cc
index 6e9d472..7dcdd0b 100644
--- a/media/blink/webmediaplayer_impl.cc
+++ b/media/blink/webmediaplayer_impl.cc
@@ -517,11 +517,8 @@
   // disabled.
   // The viz::SurfaceId may be updated when the video begins playback or when
   // the size of the video changes.
-  if (client_ && IsInPictureInPicture() && !client_->IsInAutoPIP()) {
-    delegate_->DidPictureInPictureSurfaceChange(
-        delegate_id_, surface_id, pipeline_metadata_.natural_size,
-        ShouldShowPlayPauseButtonInPictureInPictureWindow());
-  }
+  if (client_)
+    client_->OnPictureInPictureStateChange();
 }
 
 bool WebMediaPlayerImpl::SupportsOverlayFullscreenVideo() {
@@ -905,30 +902,17 @@
   UpdatePlayState();
 }
 
-void WebMediaPlayerImpl::EnterPictureInPicture(
-    blink::WebMediaPlayer::PipWindowOpenedCallback callback) {
+void WebMediaPlayerImpl::EnterPictureInPicture() {
   if (!surface_layer_for_video_enabled_)
     ActivateSurfaceLayerForVideo();
 
   DCHECK(bridge_);
-
-  const viz::SurfaceId& surface_id = bridge_->GetSurfaceId();
-  DCHECK(surface_id.is_valid());
-
-  // Notifies the browser process that the player should now be in
-  // Picture-in-Picture mode.
-  delegate_->DidPictureInPictureModeStart(
-      delegate_id_, surface_id, pipeline_metadata_.natural_size,
-      std::move(callback), ShouldShowPlayPauseButtonInPictureInPictureWindow());
+  DCHECK(bridge_->GetSurfaceId().is_valid());
 }
 
-void WebMediaPlayerImpl::ExitPictureInPicture(
-    blink::WebMediaPlayer::PipWindowClosedCallback callback) {
-  // Notifies the browser process that Picture-in-Picture has ended. It will
-  // clear out the states and close the window.
-  delegate_->DidPictureInPictureModeEnd(delegate_id_, std::move(callback));
-
+void WebMediaPlayerImpl::ExitPictureInPicture() {
   // Internal cleanups.
+  // TODO(mlamouri): remove the need for this.
   OnPictureInPictureModeEnded();
 }
 
@@ -937,14 +921,6 @@
   delegate_->DidSetPictureInPictureCustomControls(delegate_id_, controls);
 }
 
-void WebMediaPlayerImpl::RegisterPictureInPictureWindowResizeCallback(
-    blink::WebMediaPlayer::PipWindowResizedCallback callback) {
-  DCHECK(IsInPictureInPicture() && !client_->IsInAutoPIP());
-
-  delegate_->RegisterPictureInPictureWindowResizeCallback(delegate_id_,
-                                                          std::move(callback));
-}
-
 void WebMediaPlayerImpl::SetSinkId(
     const blink::WebString& sink_id,
     std::unique_ptr<blink::WebSetSinkIdCallbacks> web_callback) {
@@ -3145,6 +3121,16 @@
   return opaque_;
 }
 
+int WebMediaPlayerImpl::GetDelegateId() {
+  return delegate_id_;
+}
+
+base::Optional<viz::SurfaceId> WebMediaPlayerImpl::GetSurfaceId() {
+  if (!surface_layer_for_video_enabled_)
+    return base::nullopt;
+  return bridge_->GetSurfaceId();
+}
+
 bool WebMediaPlayerImpl::ShouldPauseVideoWhenHidden() const {
   if (!is_background_video_playback_enabled_)
     return true;
@@ -3449,11 +3435,6 @@
          WebMediaPlayer::DisplayType::kPictureInPicture;
 }
 
-bool WebMediaPlayerImpl::ShouldShowPlayPauseButtonInPictureInPictureWindow()
-    const {
-  return Duration() != std::numeric_limits<double>::infinity();
-}
-
 void WebMediaPlayerImpl::MaybeSetContainerName() {
   // MSE nor MediaPlayerRenderer provide container information.
   if (chunk_demuxer_ || using_media_player_renderer_)
diff --git a/media/blink/webmediaplayer_impl.h b/media/blink/webmediaplayer_impl.h
index 2030aee..7183daa 100644
--- a/media/blink/webmediaplayer_impl.h
+++ b/media/blink/webmediaplayer_impl.h
@@ -120,14 +120,10 @@
   void Seek(double seconds) override;
   void SetRate(double rate) override;
   void SetVolume(double volume) override;
-  void EnterPictureInPicture(
-      blink::WebMediaPlayer::PipWindowOpenedCallback callback) override;
-  void ExitPictureInPicture(
-      blink::WebMediaPlayer::PipWindowClosedCallback callback) override;
+  void EnterPictureInPicture() override;
+  void ExitPictureInPicture() override;
   void SetPictureInPictureCustomControls(
       const std::vector<blink::PictureInPictureControlInfo>&) override;
-  void RegisterPictureInPictureWindowResizeCallback(
-      blink::WebMediaPlayer::PipWindowResizedCallback callback) override;
   void SetSinkId(
       const blink::WebString& sink_id,
       std::unique_ptr<blink::WebSetSinkIdCallbacks> web_callback) override;
@@ -264,10 +260,11 @@
   // paused; see UpdatePlayState_ComputePlayState() for the exact details.
   void ForceStaleStateForTesting(ReadyState target_state) override;
   bool IsSuspendedForTesting() override;
-
   bool DidLazyLoad() const override;
   void OnBecameVisible() override;
   bool IsOpaque() const override;
+  int GetDelegateId() override;
+  base::Optional<viz::SurfaceId> GetSurfaceId() override;
 
   bool IsBackgroundMediaSuspendEnabled() const {
     return is_background_suspend_enabled_;
@@ -578,11 +575,6 @@
 
   void SendBytesReceivedUpdate();
 
-  // Returns whether the Picture-in-Picture window should contain a play/pause
-  // button. It will return false if video is "live", in other words if duration
-  // is equals to Infinity.
-  bool ShouldShowPlayPauseButtonInPictureInPictureWindow() const;
-
   blink::WebLocalFrame* const frame_;
 
   // The playback state last reported to |delegate_|, to avoid setting duplicate
diff --git a/media/blink/webmediaplayer_impl_unittest.cc b/media/blink/webmediaplayer_impl_unittest.cc
index ddc23ba..33f932eb 100644
--- a/media/blink/webmediaplayer_impl_unittest.cc
+++ b/media/blink/webmediaplayer_impl_unittest.cc
@@ -148,9 +148,9 @@
   MOCK_METHOD1(ActivateViewportIntersectionMonitoring, void(bool));
   MOCK_METHOD1(MediaRemotingStarted, void(const blink::WebString&));
   MOCK_METHOD1(MediaRemotingStopped, void(blink::WebLocalizedString::Name));
-  MOCK_METHOD0(PictureInPictureStarted, void());
   MOCK_METHOD0(PictureInPictureStopped, void());
   MOCK_METHOD1(PictureInPictureControlClicked, void(const blink::WebString&));
+  MOCK_METHOD0(OnPictureInPictureStateChange, void());
   MOCK_CONST_METHOD0(CouldPlayIfEnoughData, bool());
   MOCK_METHOD0(RequestPlay, void());
   MOCK_METHOD0(RequestPause, void());
@@ -215,21 +215,9 @@
     DCHECK_EQ(player_id_, delegate_id);
   }
 
-  MOCK_METHOD5(DidPictureInPictureModeStart,
-               void(int,
-                    const viz::SurfaceId&,
-                    const gfx::Size&,
-                    blink::WebMediaPlayer::PipWindowOpenedCallback,
-                    bool));
-  MOCK_METHOD2(DidPictureInPictureModeEnd,
-               void(int, blink::WebMediaPlayer::PipWindowClosedCallback));
   MOCK_METHOD2(DidSetPictureInPictureCustomControls,
                void(int,
                     const std::vector<blink::PictureInPictureControlInfo>&));
-  MOCK_METHOD4(DidPictureInPictureSurfaceChange,
-               void(int, const viz::SurfaceId&, const gfx::Size&, bool));
-  MOCK_METHOD2(RegisterPictureInPictureWindowResizeCallback,
-               void(int, blink::WebMediaPlayer::PipWindowResizedCallback));
 
   void ClearStaleFlag(int player_id) override {
     DCHECK_EQ(player_id_, player_id);
@@ -1566,8 +1554,8 @@
   }
 }
 
-// Tests delegate methods are called when Picture-in-Picture is triggered.
-TEST_F(WebMediaPlayerImplTest, PictureInPictureTriggerCallback) {
+// Tests that updating the surface id calls OnPictureInPictureStateChange.
+TEST_F(WebMediaPlayerImplTest, PictureInPictureStateChange) {
   base::test::ScopedFeatureList feature_list;
   feature_list.InitFromCommandLine(kUseSurfaceLayerForVideo.name, "");
 
@@ -1588,68 +1576,10 @@
   EXPECT_CALL(client_, DisplayType())
       .WillRepeatedly(
           Return(blink::WebMediaPlayer::DisplayType::kPictureInPicture));
-  EXPECT_CALL(delegate_,
-              DidPictureInPictureSurfaceChange(
-                  delegate_.player_id(), surface_id_, GetNaturalSize(), true))
-      .Times(2);
+  EXPECT_CALL(client_, OnPictureInPictureStateChange()).Times(1);
 
   wmpi_->OnSurfaceIdUpdated(surface_id_);
 
-  EXPECT_CALL(delegate_,
-              DidPictureInPictureModeStart(delegate_.player_id(), surface_id_,
-                                           GetNaturalSize(), _, true));
-
-  wmpi_->EnterPictureInPicture(base::DoNothing());
-  wmpi_->OnSurfaceIdUpdated(surface_id_);
-
-  // Updating SurfaceId should NOT exit Picture-in-Picture.
-  EXPECT_CALL(delegate_, DidPictureInPictureModeEnd(delegate_.player_id(), _))
-      .Times(0);
-  EXPECT_CALL(*surface_layer_bridge_ptr_, ClearObserver());
-}
-
-// Tests delegate methods are called with the appropriate play/pause button
-// state when Picture-in-Picture is triggered and video duration is infinity.
-TEST_F(WebMediaPlayerImplTest,
-       PictureInPictureTriggerWithInfiniteDurationCallback) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitFromCommandLine(kUseSurfaceLayerForVideo.name, "");
-
-  InitializeWebMediaPlayerImpl();
-  SetDuration(kInfiniteDuration);
-
-  EXPECT_CALL(*surface_layer_bridge_ptr_, CreateSurfaceLayer());
-  EXPECT_CALL(*surface_layer_bridge_ptr_, GetSurfaceId())
-      .WillRepeatedly(ReturnRef(surface_id_));
-  EXPECT_CALL(*surface_layer_bridge_ptr_, GetLocalSurfaceIdAllocationTime())
-      .WillRepeatedly(Return(base::TimeTicks()));
-  EXPECT_CALL(*compositor_, EnableSubmission(_, _, _, _));
-  EXPECT_CALL(*surface_layer_bridge_ptr_, SetContentsOpaque(false));
-
-  PipelineMetadata metadata;
-  metadata.has_video = true;
-  OnMetadata(metadata);
-
-  EXPECT_CALL(client_, DisplayType())
-      .WillRepeatedly(
-          Return(blink::WebMediaPlayer::DisplayType::kPictureInPicture));
-  EXPECT_CALL(delegate_,
-              DidPictureInPictureSurfaceChange(
-                  delegate_.player_id(), surface_id_, GetNaturalSize(), false))
-      .Times(2);
-
-  wmpi_->OnSurfaceIdUpdated(surface_id_);
-
-  EXPECT_CALL(delegate_,
-              DidPictureInPictureModeStart(delegate_.player_id(), surface_id_,
-                                           GetNaturalSize(), _, false));
-
-  wmpi_->EnterPictureInPicture(base::DoNothing());
-  wmpi_->OnSurfaceIdUpdated(surface_id_);
-
-  // Updating SurfaceId should NOT exit Picture-in-Picture.
-  EXPECT_CALL(delegate_, DidPictureInPictureModeEnd(delegate_.player_id(), _))
-      .Times(0);
   EXPECT_CALL(*surface_layer_bridge_ptr_, ClearObserver());
 }
 
diff --git a/third_party/blink/public/mojom/BUILD.gn b/third_party/blink/public/mojom/BUILD.gn
index d588cf8..52434a2 100644
--- a/third_party/blink/public/mojom/BUILD.gn
+++ b/third_party/blink/public/mojom/BUILD.gn
@@ -59,6 +59,7 @@
     "notifications/notification.mojom",
     "page/display_cutout.mojom",
     "payments/payment_app.mojom",
+    "picture_in_picture/picture_in_picture.mojom",
     "plugins/plugin_registry.mojom",
     "presentation/presentation.mojom",
     "quota/quota_dispatcher_host.mojom",
@@ -110,6 +111,7 @@
     "//services/device/public/mojom",
     "//services/network/public/mojom",
     "//services/service_manager/public/mojom",
+    "//services/viz/public/interfaces",
     "//skia/public/interfaces",
     "//third_party/blink/public:web_feature_mojo_bindings",
     "//third_party/blink/public/mojom/usb",
diff --git a/third_party/blink/public/mojom/picture_in_picture/OWNERS b/third_party/blink/public/mojom/picture_in_picture/OWNERS
new file mode 100644
index 0000000..08850f4
--- /dev/null
+++ b/third_party/blink/public/mojom/picture_in_picture/OWNERS
@@ -0,0 +1,2 @@
+per-file *.mojom=set noparent
+per-file *.mojom=file://ipc/SECURITY_OWNERS
diff --git a/third_party/blink/public/mojom/picture_in_picture/picture_in_picture.mojom b/third_party/blink/public/mojom/picture_in_picture/picture_in_picture.mojom
new file mode 100644
index 0000000..9146179e
--- /dev/null
+++ b/third_party/blink/public/mojom/picture_in_picture/picture_in_picture.mojom
@@ -0,0 +1,58 @@
+// Copyright 2019 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.
+
+module blink.mojom;
+
+import "services/viz/public/interfaces/compositing/surface_id.mojom";
+import "ui/gfx/geometry/mojo/geometry.mojom";
+
+// PictureInPictureDelegate is associated to a PictureInPictureService. It will
+// be notified by running its callbacks when a change happened to the running
+// Picture-in-Picture session.
+interface PictureInPictureDelegate {
+  PictureInPictureWindowSizeChanged(gfx.mojom.Size size);
+};
+
+// PictureInPictureService is a Document-associated interface that lives in
+// the browser process. It is responsible for implementing the browser-side
+// support for https://wicg.github.io/picture-in-picture and manages all
+// picture-in-picture interactions for Document. Picture-in-picture allows
+// a Document to display a video in an always-on-top floating window with
+// basic playback functionality (e.g. play and pause).
+interface PictureInPictureService {
+  // Notify the service that it should start a Picture-in-Picture session.
+  // player_id: WebMediaPlayer id used to control playback via the
+  //            Picture-in-Picture window.
+  // surface_id: SurfaceId to be shown in the Picture-in-Picture window.
+  // natural_size: Size of the video frames.
+  // show_play_pause_button: Whether a play/pause control should be offered in
+  //                         the Picture-in-Picture window.
+  // NOTE: this method shouldn't be called twice in a raw. An interleaved call
+  // to EndSession() would be expected. In case of some parameters have changed,
+  // UpdateSession() should be called.
+  StartSession(
+      uint32 player_id,
+      viz.mojom.SurfaceId? surface_id,
+      gfx.mojom.Size natural_size,
+      bool show_play_pause_button) => (gfx.mojom.Size size);
+
+  // Notify the service to end the Picture-in-Picture session. It will close
+  // the Picture-in-Picture window.
+  EndSession() => ();
+
+  // Notify the service that some information about the client have change. All
+  // information associated with the Picture-in-Picture session are sent again.
+  // A Picture-in-Picture session must already be active from a previous call to
+  // StartSession().
+  UpdateSession(
+      uint32 player_id,
+      viz.mojom.SurfaceId? surface_id,
+      gfx.mojom.Size natural_size,
+      bool show_play_pause_button);
+
+  // Associate a PictureInPictureDelegate with the service. All changes in
+  // Picture-in-Picture states will be sent to the delegate. Only one delegate
+  // can be set at a time.
+  SetDelegate(PictureInPictureDelegate delegate);
+};
diff --git a/third_party/blink/public/platform/web_media_player.h b/third_party/blink/public/platform/web_media_player.h
index edfea4c..55115e8 100644
--- a/third_party/blink/public/platform/web_media_player.h
+++ b/third_party/blink/public/platform/web_media_player.h
@@ -31,7 +31,9 @@
 #ifndef THIRD_PARTY_BLINK_PUBLIC_PLATFORM_WEB_MEDIA_PLAYER_H_
 #define THIRD_PARTY_BLINK_PUBLIC_PLATFORM_WEB_MEDIA_PLAYER_H_
 
+#include "base/optional.h"
 #include "base/time/time.h"
+#include "components/viz/common/surfaces/surface_id.h"
 #include "third_party/blink/public/platform/web_callbacks.h"
 #include "third_party/blink/public/platform/web_content_decryption_module.h"
 #include "third_party/blink/public/platform/web_media_source.h"
@@ -137,14 +139,6 @@
     kAlways,
   };
 
-  // Callback to get notified when the Picture-in-Picture window is opened.
-  using PipWindowOpenedCallback = base::OnceCallback<void(const WebSize&)>;
-  // Callback to get notified when Picture-in-Picture window is closed.
-  using PipWindowClosedCallback = base::OnceClosure;
-  // Callback to get notified when the Picture-in-Picture window is resized.
-  using PipWindowResizedCallback =
-      base::RepeatingCallback<void(const WebSize&)>;
-
   virtual ~WebMediaPlayer() = default;
 
   virtual LoadTiming Load(LoadType, const WebMediaPlayerSource&, CorsMode) = 0;
@@ -158,16 +152,13 @@
 
   // Enter Picture-in-Picture and notifies Blink with window size
   // when video successfully enters Picture-in-Picture.
-  virtual void EnterPictureInPicture(PipWindowOpenedCallback) = 0;
+  // TODO(mlamouri): rename to "OnRequestPictureInPicture".
+  virtual void EnterPictureInPicture() = 0;
   // Exit Picture-in-Picture and notifies Blink when it's done.
-  virtual void ExitPictureInPicture(PipWindowClosedCallback) = 0;
+  virtual void ExitPictureInPicture() = 0;
   // Assign custom controls to the Picture-in-Picture window.
   virtual void SetPictureInPictureCustomControls(
       const std::vector<PictureInPictureControlInfo>&) = 0;
-  // Register a callback that will be run when the Picture-in-Picture window
-  // is resized.
-  virtual void RegisterPictureInPictureWindowResizeCallback(
-      PipWindowResizedCallback) = 0;
 
   virtual void RequestRemotePlayback() {}
   virtual void RequestRemotePlaybackControl() {}
@@ -411,6 +402,18 @@
   virtual void OnBecameVisible() {}
 
   virtual bool IsOpaque() const { return false; }
+
+  // Returns the id given by the WebMediaPlayerDelegate. This is used by the
+  // Blink code to pass a player id to mojo services.
+  // TODO(mlamouri): remove this and move the id handling to Blink.
+  virtual int GetDelegateId() { return -1; };
+
+  // Returns the SurfaceId the video element is currently using.
+  // Returns base::nullopt if the element isn't a video or doesn't have a
+  // SurfaceId associated to it.
+  virtual base::Optional<viz::SurfaceId> GetSurfaceId() {
+    return base::nullopt;
+  }
 };
 
 }  // namespace blink
diff --git a/third_party/blink/public/platform/web_media_player_client.h b/third_party/blink/public/platform/web_media_player_client.h
index 49594467..e4cab565 100644
--- a/third_party/blink/public/platform/web_media_player_client.h
+++ b/third_party/blink/public/platform/web_media_player_client.h
@@ -179,6 +179,15 @@
   // Request the player to pause playback.
   virtual void RequestPause() = 0;
 
+  // Notify the client that one of the state used by Picture-in-Picture has
+  // changed. The client will then have to poll the states from the associated
+  // WebMediaPlayer.
+  // The states are:
+  //  - Delegate ID;
+  //  - Surface ID;
+  //  - Natural Size.
+  virtual void OnPictureInPictureStateChange() = 0;
+
  protected:
   ~WebMediaPlayerClient() = default;
 };
diff --git a/third_party/blink/renderer/core/frame/picture_in_picture_controller.h b/third_party/blink/renderer/core/frame/picture_in_picture_controller.h
index 98b35b8..87d82d73 100644
--- a/third_party/blink/renderer/core/frame/picture_in_picture_controller.h
+++ b/third_party/blink/renderer/core/frame/picture_in_picture_controller.h
@@ -83,6 +83,8 @@
       HTMLVideoElement*,
       const std::vector<PictureInPictureControlInfo>&) = 0;
 
+  // Notifies that one of the states used by Picture-in-Picture has changed.
+  virtual void OnPictureInPictureStateChange() = 0;
 
   void Trace(blink::Visitor*) override;
 
diff --git a/third_party/blink/renderer/core/html/media/html_audio_element.h b/third_party/blink/renderer/core/html/media/html_audio_element.h
index 2463517..b6076b7 100644
--- a/third_party/blink/renderer/core/html/media/html_audio_element.h
+++ b/third_party/blink/renderer/core/html/media/html_audio_element.h
@@ -55,6 +55,7 @@
   void PictureInPictureControlClicked(const WebString& control_id) override {
     NOTREACHED();
   }
+  void OnPictureInPictureStateChange() final { NOTREACHED(); }
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/html/media/html_media_element.h b/third_party/blink/renderer/core/html/media/html_media_element.h
index 674f448..0d0e45c 100644
--- a/third_party/blink/renderer/core/html/media/html_media_element.h
+++ b/third_party/blink/renderer/core/html/media/html_media_element.h
@@ -333,6 +333,8 @@
   // becomes visible again.
   bool PausedWhenVisible() const;
 
+  void SetCcLayerForTesting(cc::Layer* layer) { SetCcLayer(layer); }
+
  protected:
   HTMLMediaElement(const QualifiedName&, Document&);
   ~HTMLMediaElement() override;
diff --git a/third_party/blink/renderer/core/html/media/html_video_element.cc b/third_party/blink/renderer/core/html/media/html_video_element.cc
index dad6245..43565f1 100644
--- a/third_party/blink/renderer/core/html/media/html_video_element.cc
+++ b/third_party/blink/renderer/core/html/media/html_video_element.cc
@@ -502,8 +502,10 @@
 void HTMLVideoElement::DidEnterFullscreen() {
   UpdateControlsVisibility();
 
-  if (DisplayType() == WebMediaPlayer::DisplayType::kPictureInPicture)
-    exitPictureInPicture(base::DoNothing());
+  if (DisplayType() == WebMediaPlayer::DisplayType::kPictureInPicture) {
+    PictureInPictureController::From(GetDocument())
+        .ExitPictureInPicture(this, nullptr);
+  }
 
   if (GetWebMediaPlayer()) {
     // FIXME: There is no embedder-side handling in web test mode.
@@ -661,19 +663,12 @@
          PictureInPictureController::Status::kEnabled;
 }
 
-void HTMLVideoElement::enterPictureInPicture(
-    WebMediaPlayer::PipWindowOpenedCallback callback) {
+void HTMLVideoElement::enterPictureInPicture() {
   if (DisplayType() == WebMediaPlayer::DisplayType::kFullscreen)
     Fullscreen::ExitFullscreen(GetDocument());
 
   if (GetWebMediaPlayer())
-    GetWebMediaPlayer()->EnterPictureInPicture(std::move(callback));
-}
-
-void HTMLVideoElement::exitPictureInPicture(
-    WebMediaPlayer::PipWindowClosedCallback callback) {
-  if (GetWebMediaPlayer())
-    GetWebMediaPlayer()->ExitPictureInPicture(std::move(callback));
+    GetWebMediaPlayer()->EnterPictureInPicture();
 }
 
 void HTMLVideoElement::SendCustomControlsToPipWindow() {
@@ -712,6 +707,16 @@
   return is_auto_picture_in_picture_;
 }
 
+void HTMLVideoElement::OnPictureInPictureStateChange() {
+  if (DisplayType() != WebMediaPlayer::DisplayType::kPictureInPicture ||
+      IsInAutoPIP()) {
+    return;
+  }
+
+  PictureInPictureController::From(GetDocument())
+      .OnPictureInPictureStateChange();
+}
+
 void HTMLVideoElement::OnEnteredPictureInPicture() {
   if (!picture_in_picture_interstitial_) {
     picture_in_picture_interstitial_ =
diff --git a/third_party/blink/renderer/core/html/media/html_video_element.h b/third_party/blink/renderer/core/html/media/html_video_element.h
index 61759f9b..b8313c26 100644
--- a/third_party/blink/renderer/core/html/media/html_video_element.h
+++ b/third_party/blink/renderer/core/html/media/html_video_element.h
@@ -175,14 +175,14 @@
 
   void MediaRemotingStarted(const WebString& remote_device_friendly_name) final;
   bool SupportsPictureInPicture() const final;
-  void enterPictureInPicture(WebMediaPlayer::PipWindowOpenedCallback callback);
-  void exitPictureInPicture(WebMediaPlayer::PipWindowClosedCallback callback);
+  void enterPictureInPicture();
   void SendCustomControlsToPipWindow();
   void PictureInPictureStopped() final;
   void PictureInPictureControlClicked(const WebString& control_id) final;
   void MediaRemotingStopped(WebLocalizedString::Name error_msg) final;
   WebMediaPlayer::DisplayType DisplayType() const final;
   bool IsInAutoPIP() const final;
+  void OnPictureInPictureStateChange() final;
 
   // Used by the PictureInPictureController as callback when the video element
   // enters or exits Picture-in-Picture state.
diff --git a/third_party/blink/renderer/core/html/media/video_wake_lock_test.cc b/third_party/blink/renderer/core/html/media/video_wake_lock_test.cc
index 8fd93c8e..bc01a5ee 100644
--- a/third_party/blink/renderer/core/html/media/video_wake_lock_test.cc
+++ b/third_party/blink/renderer/core/html/media/video_wake_lock_test.cc
@@ -5,6 +5,7 @@
 #include "third_party/blink/renderer/core/html/media/video_wake_lock.h"
 
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/picture_in_picture/picture_in_picture.mojom-blink.h"
 #include "third_party/blink/renderer/core/dom/events/event.h"
 #include "third_party/blink/renderer/core/frame/picture_in_picture_controller.h"
 #include "third_party/blink/renderer/core/html/media/html_media_test_helper.h"
@@ -15,23 +16,107 @@
 
 namespace blink {
 
-class VideoWakeLockMediaPlayer : public EmptyWebMediaPlayer {
+// The VideoWakeLockPictureInPictureService implements the PictureInPicture
+// service in the same process as the test and guarantees that the callbacks are
+// called in order for the events to be fired. set_run_loop() MUST be called
+// before each attempt to enter/leave Picture-in-Picture.
+class VideoWakeLockPictureInPictureService
+    : public mojom::blink::PictureInPictureService {
  public:
-  void EnterPictureInPicture(PipWindowOpenedCallback callback) final {
+  VideoWakeLockPictureInPictureService() : binding_(this) {}
+  ~VideoWakeLockPictureInPictureService() override = default;
+
+  void Bind(mojo::ScopedMessagePipeHandle handle) {
+    binding_.Bind(
+        mojom::blink::PictureInPictureServiceRequest(std::move(handle)));
+  }
+
+  void StartSession(uint32_t,
+                    const base::Optional<viz::SurfaceId>&,
+                    const blink::WebSize&,
+                    bool,
+                    StartSessionCallback callback) final {
     std::move(callback).Run(WebSize());
   }
 
-  void ExitPictureInPicture(PipWindowClosedCallback callback) final {
+  void EndSession(EndSessionCallback callback) final {
     std::move(callback).Run();
   }
+
+  void UpdateSession(uint32_t,
+                     const base::Optional<viz::SurfaceId>&,
+                     const blink::WebSize&,
+                     bool) final {}
+  void SetDelegate(mojom::blink::PictureInPictureDelegatePtr) final {}
+
+ private:
+  mojo::Binding<mojom::blink::PictureInPictureService> binding_;
+};
+
+class VideoWakeLockFrameClient : public test::MediaStubLocalFrameClient {
+ public:
+  static VideoWakeLockFrameClient* Create(
+      std::unique_ptr<WebMediaPlayer> player) {
+    return MakeGarbageCollected<VideoWakeLockFrameClient>(std::move(player));
+  }
+
+  explicit VideoWakeLockFrameClient(std::unique_ptr<WebMediaPlayer> player)
+      : test::MediaStubLocalFrameClient(std::move(player)),
+        interface_provider_(new service_manager::InterfaceProvider()) {}
+
+  service_manager::InterfaceProvider* GetInterfaceProvider() override {
+    return interface_provider_.get();
+  }
+
+ private:
+  std::unique_ptr<service_manager::InterfaceProvider> interface_provider_;
+
+  DISALLOW_COPY_AND_ASSIGN(VideoWakeLockFrameClient);
 };
 
 class VideoWakeLockTest : public PageTestBase {
  public:
+  // Helper class that will block running the test until the given event is
+  // fired on the given element.
+  class WaitForEvent : public NativeEventListener {
+   public:
+    static WaitForEvent* Create(Element* element, const AtomicString& name) {
+      return MakeGarbageCollected<WaitForEvent>(element, name);
+    }
+
+    WaitForEvent(Element* element, const AtomicString& name)
+        : element_(element), name_(name) {
+      element_->addEventListener(name_, this);
+      run_loop_.Run();
+    }
+
+    void Invoke(ExecutionContext*, Event*) final {
+      run_loop_.Quit();
+      element_->removeEventListener(name_, this);
+    }
+
+    void Trace(Visitor* visitor) final {
+      NativeEventListener::Trace(visitor);
+      visitor->Trace(element_);
+    }
+
+   private:
+    base::RunLoop run_loop_;
+    Member<Element> element_;
+    AtomicString name_;
+  };
+
   void SetUp() override {
     PageTestBase::SetupPageWithClients(
-        nullptr, test::MediaStubLocalFrameClient::Create(
-                     std::make_unique<VideoWakeLockMediaPlayer>()));
+        nullptr, VideoWakeLockFrameClient::Create(
+                     std::make_unique<EmptyWebMediaPlayer>()));
+
+    service_manager::InterfaceProvider::TestApi test_api(
+        GetFrame().Client()->GetInterfaceProvider());
+    test_api.SetBinderForName(
+        mojom::blink::PictureInPictureService::Name_,
+        WTF::BindRepeating(&VideoWakeLockPictureInPictureService::Bind,
+                           WTF::Unretained(&pip_service_)));
 
     video_ = HTMLVideoElement::Create(GetDocument());
     video_wake_lock_ = MakeGarbageCollected<VideoWakeLock>(*video_.Get());
@@ -57,16 +142,24 @@
   void SimulateEnterPictureInPicture() {
     PictureInPictureController::From(GetDocument())
         .EnterPictureInPicture(Video(), nullptr);
+
+    WaitForEvent::Create(video_.Get(),
+                         event_type_names::kEnterpictureinpicture);
   }
 
   void SimulateLeavePictureInPicture() {
     PictureInPictureController::From(GetDocument())
         .ExitPictureInPicture(Video(), nullptr);
+
+    WaitForEvent::Create(video_.Get(),
+                         event_type_names::kLeavepictureinpicture);
   }
 
  private:
   Persistent<HTMLVideoElement> video_;
   Persistent<VideoWakeLock> video_wake_lock_;
+
+  VideoWakeLockPictureInPictureService pip_service_;
 };
 
 TEST_F(VideoWakeLockTest, NoLockByDefault) {
diff --git a/third_party/blink/renderer/modules/BUILD.gn b/third_party/blink/renderer/modules/BUILD.gn
index db4b2a3..da0819b 100644
--- a/third_party/blink/renderer/modules/BUILD.gn
+++ b/third_party/blink/renderer/modules/BUILD.gn
@@ -321,6 +321,7 @@
     "peerconnection/rtc_quic_transport_test.cc",
     "peerconnection/rtc_quic_transport_test.h",
     "picture_in_picture/html_video_element_picture_in_picture_test.cc",
+    "picture_in_picture/picture_in_picture_controller_test.cc",
     "presentation/mock_presentation_service.h",
     "presentation/presentation_availability_state_test.cc",
     "presentation/presentation_availability_test.cc",
diff --git a/third_party/blink/renderer/modules/picture_in_picture/DEPS b/third_party/blink/renderer/modules/picture_in_picture/DEPS
index 03fe8e3..c3389f6 100644
--- a/third_party/blink/renderer/modules/picture_in_picture/DEPS
+++ b/third_party/blink/renderer/modules/picture_in_picture/DEPS
@@ -2,5 +2,6 @@
     "-third_party/blink/renderer/modules",
     "+third_party/blink/renderer/modules/event_modules.h",
     "+third_party/blink/renderer/modules/event_target_modules.h",
+    "+third_party/blink/renderer/modules/modules_export.h",
     "+third_party/blink/renderer/modules/picture_in_picture",
 ]
\ No newline at end of file
diff --git a/third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_impl.cc b/third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_impl.cc
index 3039b1c..3b96446 100644
--- a/third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_impl.cc
+++ b/third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_impl.cc
@@ -4,7 +4,11 @@
 
 #include "third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_impl.h"
 
+#include <limits>
+#include <utility>
+
 #include "base/bind_helpers.h"
+#include "services/service_manager/public/cpp/interface_provider.h"
 #include "third_party/blink/public/common/manifest/web_display_mode.h"
 #include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h"
 #include "third_party/blink/renderer/core/dom/document.h"
@@ -20,7 +24,14 @@
 
 namespace blink {
 
-PictureInPictureControllerImpl::~PictureInPictureControllerImpl() = default;
+namespace {
+
+bool ShouldShowPlayPauseButton(const HTMLVideoElement& element) {
+  return element.GetLoadType() != WebMediaPlayer::kLoadTypeMediaStream &&
+         element.duration() != std::numeric_limits<double>::infinity();
+}
+
+}  // namespace
 
 // static
 PictureInPictureControllerImpl* PictureInPictureControllerImpl::Create(
@@ -88,19 +99,32 @@
 void PictureInPictureControllerImpl::EnterPictureInPicture(
     HTMLVideoElement* element,
     ScriptPromiseResolver* resolver) {
-  if (picture_in_picture_element_ != element) {
-    element->enterPictureInPicture(
-        WTF::Bind(&PictureInPictureControllerImpl::OnEnteredPictureInPicture,
-                  WrapPersistent(this), WrapPersistent(element),
-                  WrapPersistent(resolver)));
-    // If the media element has already been given custom controls, this will
-    // ensure that they get set. Otherwise, this will do nothing.
-    element->SendCustomControlsToPipWindow();
+  if (picture_in_picture_element_ == element) {
+    if (resolver)
+      resolver->Resolve(picture_in_picture_window_);
+
     return;
   }
 
-  if (resolver)
-    resolver->Resolve(picture_in_picture_window_);
+  element->enterPictureInPicture();
+
+  DCHECK(element->GetWebMediaPlayer());
+
+  if (!EnsureService())
+    return;
+
+  picture_in_picture_service_->StartSession(
+      element->GetWebMediaPlayer()->GetDelegateId(),
+      element->GetWebMediaPlayer()->GetSurfaceId(),
+      element->GetWebMediaPlayer()->NaturalSize(),
+      ShouldShowPlayPauseButton(*element),
+      WTF::Bind(&PictureInPictureControllerImpl::OnEnteredPictureInPicture,
+                WrapPersistent(this), WrapPersistent(element),
+                WrapPersistent(resolver)));
+
+  // If the media element has already been given custom controls, this will
+  // ensure that they get set. Otherwise, this will do nothing.
+  element->SendCustomControlsToPipWindow();
 }
 
 void PictureInPictureControllerImpl::OnEnteredPictureInPicture(
@@ -112,7 +136,7 @@
       resolver->Reject(
           DOMException::Create(DOMExceptionCode::kInvalidStateError, ""));
     }
-    element->exitPictureInPicture(base::DoNothing());
+    ExitPictureInPicture(element, nullptr);
     return;
   }
 
@@ -132,11 +156,15 @@
           event_type_names::kEnterpictureinpicture,
           WrapPersistent(picture_in_picture_window_.Get())));
 
-  if (element->GetWebMediaPlayer()) {
-    element->GetWebMediaPlayer()->RegisterPictureInPictureWindowResizeCallback(
-        WTF::BindRepeating(&PictureInPictureWindow::OnResize,
-                           WrapPersistent(picture_in_picture_window_.Get())));
-  }
+  if (!EnsureService())
+    return;
+
+  if (delegate_binding_.is_bound())
+    delegate_binding_.Close();
+
+  mojom::blink::PictureInPictureDelegatePtr delegate;
+  delegate_binding_.Bind(mojo::MakeRequest(&delegate));
+  picture_in_picture_service_->SetDelegate(std::move(delegate));
 
   if (resolver)
     resolver->Resolve(picture_in_picture_window_);
@@ -145,9 +173,16 @@
 void PictureInPictureControllerImpl::ExitPictureInPicture(
     HTMLVideoElement* element,
     ScriptPromiseResolver* resolver) {
-  element->exitPictureInPicture(
+  if (element->GetWebMediaPlayer())
+    element->GetWebMediaPlayer()->ExitPictureInPicture();
+
+  if (!EnsureService())
+    return;
+
+  picture_in_picture_service_->EndSession(
       WTF::Bind(&PictureInPictureControllerImpl::OnExitedPictureInPicture,
                 WrapPersistent(this), WrapPersistent(resolver)));
+  delegate_binding_.Close();
 }
 
 void PictureInPictureControllerImpl::SetPictureInPictureCustomControls(
@@ -243,7 +278,6 @@
 
   // Auto Picture-in-Picture is allowed only in a PWA window.
   if (!GetSupplementable()->GetFrame() ||
-      !GetSupplementable()->GetFrame()->View() ||
       GetSupplementable()->GetFrame()->View()->DisplayMode() ==
           WebDisplayMode::kWebDisplayModeBrowser) {
     return;
@@ -274,17 +308,53 @@
   }
 }
 
+void PictureInPictureControllerImpl::ContextDestroyed(Document*) {
+  picture_in_picture_service_.reset();
+  delegate_binding_.Close();
+}
+
+void PictureInPictureControllerImpl::OnPictureInPictureStateChange() {
+  DCHECK(picture_in_picture_element_);
+  DCHECK(picture_in_picture_element_->GetWebMediaPlayer());
+
+  picture_in_picture_service_->UpdateSession(
+      picture_in_picture_element_->GetWebMediaPlayer()->GetDelegateId(),
+      picture_in_picture_element_->GetWebMediaPlayer()->GetSurfaceId(),
+      picture_in_picture_element_->GetWebMediaPlayer()->NaturalSize(),
+      ShouldShowPlayPauseButton(*picture_in_picture_element_));
+}
+
+void PictureInPictureControllerImpl::PictureInPictureWindowSizeChanged(
+    const blink::WebSize& size) {
+  if (picture_in_picture_window_)
+    picture_in_picture_window_->OnResize(size);
+}
+
 void PictureInPictureControllerImpl::Trace(blink::Visitor* visitor) {
   visitor->Trace(picture_in_picture_element_);
   visitor->Trace(auto_picture_in_picture_elements_);
   visitor->Trace(picture_in_picture_window_);
   PictureInPictureController::Trace(visitor);
   PageVisibilityObserver::Trace(visitor);
+  DocumentShutdownObserver::Trace(visitor);
 }
 
 PictureInPictureControllerImpl::PictureInPictureControllerImpl(
     Document& document)
     : PictureInPictureController(document),
-      PageVisibilityObserver(document.GetPage()) {}
+      PageVisibilityObserver(document.GetPage()),
+      delegate_binding_(this) {}
+
+bool PictureInPictureControllerImpl::EnsureService() {
+  if (picture_in_picture_service_)
+    return true;
+
+  if (!GetSupplementable()->GetFrame())
+    return false;
+
+  GetSupplementable()->GetFrame()->GetInterfaceProvider().GetInterface(
+      mojo::MakeRequest(&picture_in_picture_service_));
+  return true;
+}
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_impl.h b/third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_impl.h
index af8a3f9..f0e16f1f 100644
--- a/third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_impl.h
+++ b/third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_impl.h
@@ -5,8 +5,11 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_PICTURE_IN_PICTURE_PICTURE_IN_PICTURE_CONTROLLER_IMPL_H_
 #define THIRD_PARTY_BLINK_RENDERER_MODULES_PICTURE_IN_PICTURE_PICTURE_IN_PICTURE_CONTROLLER_IMPL_H_
 
+#include "third_party/blink/public/mojom/picture_in_picture/picture_in_picture.mojom-blink.h"
+#include "third_party/blink/renderer/core/dom/document_shutdown_observer.h"
 #include "third_party/blink/renderer/core/frame/picture_in_picture_controller.h"
 #include "third_party/blink/renderer/core/page/page_visibility_observer.h"
+#include "third_party/blink/renderer/modules/modules_export.h"
 
 namespace blink {
 
@@ -23,14 +26,17 @@
 // PictureInPictureControllerImpl instance is associated to a Document. It is
 // supplement and therefore can be lazy-initiated. Callers should consider
 // whether they want to instantiate an object when they make a call.
-class PictureInPictureControllerImpl : public PictureInPictureController,
-                                       public PageVisibilityObserver {
+class MODULES_EXPORT PictureInPictureControllerImpl
+    : public PictureInPictureController,
+      public PageVisibilityObserver,
+      public DocumentShutdownObserver,
+      public blink::mojom::blink::PictureInPictureDelegate {
   USING_GARBAGE_COLLECTED_MIXIN(PictureInPictureControllerImpl);
   WTF_MAKE_NONCOPYABLE(PictureInPictureControllerImpl);
 
  public:
   explicit PictureInPictureControllerImpl(Document&);
-  ~PictureInPictureControllerImpl() override;
+  ~PictureInPictureControllerImpl() override = default;
 
   // Meant to be called internally by PictureInPictureController::From()
   // through ModulesInitializer.
@@ -67,12 +73,24 @@
       const std::vector<PictureInPictureControlInfo>&) override;
   Status IsElementAllowed(const HTMLVideoElement&) const override;
   bool IsPictureInPictureElement(const Element*) const override;
+  void OnPictureInPictureStateChange() override;
 
-  // PageVisibilityObserver implementation.
+  // Implementation of PictureInPictureDelegate.
+  void PictureInPictureWindowSizeChanged(const blink::WebSize&) override;
+
+  // Implementation of PageVisibilityObserver.
   void PageVisibilityChanged() override;
 
+  // Implementation of DocumentShutdownObserver.
+  void ContextDestroyed(Document*) override;
+
   void Trace(blink::Visitor*) override;
 
+  mojo::Binding<mojom::blink::PictureInPictureDelegate>&
+  GetDelegateBindingForTesting() {
+    return delegate_binding_;
+  }
+
  private:
   void OnEnteredPictureInPicture(HTMLVideoElement*,
                                  ScriptPromiseResolver*,
@@ -80,6 +98,10 @@
   void OnExitedPictureInPicture(ScriptPromiseResolver*) override;
   void OnPictureInPictureControlClicked(const WebString& control_id) override;
 
+  // Makes sure the `picture_in_picture_service_` is set. Returns whether it was
+  // initialized successfully.
+  bool EnsureService();
+
   // The Picture-in-Picture element for the associated document.
   Member<HTMLVideoElement> picture_in_picture_element_;
 
@@ -89,6 +111,12 @@
 
   // The Picture-in-Picture window for the associated document.
   Member<PictureInPictureWindow> picture_in_picture_window_;
+
+  // Mojo bindings for the delegate interface implemented by |this|.
+  mojo::Binding<mojom::blink::PictureInPictureDelegate> delegate_binding_;
+
+  // Picture-in-Picture service living in the browser process.
+  mojom::blink::PictureInPictureServicePtr picture_in_picture_service_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_test.cc b/third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_test.cc
new file mode 100644
index 0000000..05239c5
--- /dev/null
+++ b/third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_test.cc
@@ -0,0 +1,321 @@
+// Copyright 2019 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 "third_party/blink/renderer/modules/picture_in_picture/picture_in_picture_controller_impl.h"
+
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/picture_in_picture/picture_in_picture.mojom-blink.h"
+#include "third_party/blink/public/platform/web_media_stream.h"
+#include "third_party/blink/public/platform/web_media_stream_track.h"
+#include "third_party/blink/renderer/core/dom/events/event.h"
+#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
+#include "third_party/blink/renderer/core/html/media/html_media_test_helper.h"
+#include "third_party/blink/renderer/core/html/media/html_video_element.h"
+#include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/platform/testing/empty_web_media_player.h"
+#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
+
+using ::testing::_;
+
+namespace blink {
+
+// The MockPictureInPictureService implements the PictureInPicture service in
+// the same process as the test and guarantees that the callbacks are called in
+// order for the events to be fired.
+class MockPictureInPictureService
+    : public mojom::blink::PictureInPictureService {
+ public:
+  MockPictureInPictureService() : binding_(this) {
+    // Setup default implementations.
+    ON_CALL(*this, StartSession(_, _, _, _, _))
+        .WillByDefault([](uint32_t, const base::Optional<viz::SurfaceId>&,
+                          const blink::WebSize&, bool,
+                          StartSessionCallback callback) {
+          std::move(callback).Run(WebSize());
+        });
+    ON_CALL(*this, EndSession(_))
+        .WillByDefault(
+            [](EndSessionCallback callback) { std::move(callback).Run(); });
+  }
+  ~MockPictureInPictureService() override = default;
+
+  void Bind(mojo::ScopedMessagePipeHandle handle) {
+    binding_.Bind(
+        mojom::blink::PictureInPictureServiceRequest(std::move(handle)));
+  }
+
+  MOCK_METHOD5(StartSession,
+               void(uint32_t,
+                    const base::Optional<viz::SurfaceId>&,
+                    const blink::WebSize&,
+                    bool,
+                    StartSessionCallback));
+  MOCK_METHOD1(EndSession, void(EndSessionCallback));
+  MOCK_METHOD4(UpdateSession,
+               void(uint32_t,
+                    const base::Optional<viz::SurfaceId>&,
+                    const blink::WebSize&,
+                    bool));
+  MOCK_METHOD1(SetDelegate, void(mojom::blink::PictureInPictureDelegatePtr));
+
+ private:
+  mojo::Binding<mojom::blink::PictureInPictureService> binding_;
+
+  DISALLOW_COPY_AND_ASSIGN(MockPictureInPictureService);
+};
+
+// Helper class that will block running the test until the given event is fired
+// on the given element.
+class WaitForEvent : public NativeEventListener {
+ public:
+  static WaitForEvent* Create(Element* element, const AtomicString& name) {
+    return MakeGarbageCollected<WaitForEvent>(element, name);
+  }
+
+  WaitForEvent(Element* element, const AtomicString& name)
+      : element_(element), name_(name) {
+    element_->addEventListener(name_, this);
+    run_loop_.Run();
+  }
+
+  void Invoke(ExecutionContext*, Event*) final {
+    run_loop_.Quit();
+    element_->removeEventListener(name_, this);
+  }
+
+  void Trace(Visitor* visitor) final {
+    NativeEventListener::Trace(visitor);
+    visitor->Trace(element_);
+  }
+
+ private:
+  base::RunLoop run_loop_;
+  Member<Element> element_;
+  AtomicString name_;
+};
+
+class PictureInPictureControllerFrameClient
+    : public test::MediaStubLocalFrameClient {
+ public:
+  static PictureInPictureControllerFrameClient* Create(
+      std::unique_ptr<WebMediaPlayer> player) {
+    return MakeGarbageCollected<PictureInPictureControllerFrameClient>(
+        std::move(player));
+  }
+
+  explicit PictureInPictureControllerFrameClient(
+      std::unique_ptr<WebMediaPlayer> player)
+      : test::MediaStubLocalFrameClient(std::move(player)),
+        interface_provider_(new service_manager::InterfaceProvider()) {}
+
+  service_manager::InterfaceProvider* GetInterfaceProvider() override {
+    return interface_provider_.get();
+  }
+
+ private:
+  std::unique_ptr<service_manager::InterfaceProvider> interface_provider_;
+
+  DISALLOW_COPY_AND_ASSIGN(PictureInPictureControllerFrameClient);
+};
+
+// TODO: can probably be removed.
+class PictureInPictureControllerPlayer : public EmptyWebMediaPlayer {
+ public:
+  PictureInPictureControllerPlayer() = default;
+  ~PictureInPictureControllerPlayer() final = default;
+
+  double Duration() const final {
+    if (infinity_duration_)
+      return std::numeric_limits<double>::infinity();
+    return EmptyWebMediaPlayer::Duration();
+  }
+
+  void set_infinity_duration(bool value) { infinity_duration_ = value; }
+
+ private:
+  bool infinity_duration_ = false;
+
+  DISALLOW_COPY_AND_ASSIGN(PictureInPictureControllerPlayer);
+};
+
+class PictureInPictureControllerTest : public PageTestBase {
+ public:
+  void SetUp() override {
+    PageTestBase::SetupPageWithClients(
+        nullptr, PictureInPictureControllerFrameClient::Create(
+                     std::make_unique<PictureInPictureControllerPlayer>()));
+
+    service_manager::InterfaceProvider::TestApi test_api(
+        GetFrame().Client()->GetInterfaceProvider());
+    test_api.SetBinderForName(
+        mojom::blink::PictureInPictureService::Name_,
+        WTF::BindRepeating(&MockPictureInPictureService::Bind,
+                           WTF::Unretained(&mock_service_)));
+
+    video_ = HTMLVideoElement::Create(GetDocument());
+    layer_ = cc::Layer::Create();
+    video_->SetCcLayerForTesting(layer_.get());
+
+    std::string test_name =
+        testing::UnitTest::GetInstance()->current_test_info()->name();
+    if (test_name.find("MediaSource") != std::string::npos) {
+      blink::WebMediaStream web_media_stream;
+      blink::WebVector<blink::WebMediaStreamTrack> dummy_tracks;
+      web_media_stream.Initialize(dummy_tracks, dummy_tracks);
+      Video()->SetSrcObject(web_media_stream);
+    } else {
+      video_->SetSrc("http://example.com/foo.mp4");
+    }
+
+    test::RunPendingTasks();
+  }
+
+  HTMLVideoElement* Video() const { return video_.Get(); }
+  MockPictureInPictureService& Service() { return mock_service_; }
+
+ private:
+  Persistent<HTMLVideoElement> video_;
+  MockPictureInPictureService mock_service_;
+  scoped_refptr<cc::Layer> layer_;
+};
+
+TEST_F(PictureInPictureControllerTest, EnterPictureInPictureFiresEvent) {
+  EXPECT_EQ(nullptr, PictureInPictureControllerImpl::From(GetDocument())
+                         .PictureInPictureElement());
+
+  WebMediaPlayer* player = Video()->GetWebMediaPlayer();
+  EXPECT_CALL(Service(),
+              StartSession(player->GetDelegateId(), player->GetSurfaceId(),
+                           player->NaturalSize(), true, _));
+  EXPECT_CALL(Service(), SetDelegate(_));
+
+  PictureInPictureControllerImpl::From(GetDocument())
+      .EnterPictureInPicture(Video(), nullptr);
+
+  WaitForEvent::Create(Video(), event_type_names::kEnterpictureinpicture);
+
+  EXPECT_NE(nullptr, PictureInPictureControllerImpl::From(GetDocument())
+                         .PictureInPictureElement());
+
+  // `SetDelegate()` may or may not have been called yet. Waiting a bit for it.
+  test::RunPendingTasks();
+}
+
+TEST_F(PictureInPictureControllerTest, ExitPictureInPictureFiresEvent) {
+  EXPECT_EQ(nullptr, PictureInPictureControllerImpl::From(GetDocument())
+                         .PictureInPictureElement());
+
+  WebMediaPlayer* player = Video()->GetWebMediaPlayer();
+  EXPECT_CALL(Service(),
+              StartSession(player->GetDelegateId(), player->GetSurfaceId(),
+                           player->NaturalSize(), true, _));
+  EXPECT_CALL(Service(), EndSession(_));
+  EXPECT_CALL(Service(), SetDelegate(_));
+
+  PictureInPictureControllerImpl::From(GetDocument())
+      .EnterPictureInPicture(Video(), nullptr);
+  WaitForEvent::Create(Video(), event_type_names::kEnterpictureinpicture);
+
+  PictureInPictureControllerImpl::From(GetDocument())
+      .ExitPictureInPicture(Video(), nullptr);
+  WaitForEvent::Create(Video(), event_type_names::kLeavepictureinpicture);
+
+  EXPECT_EQ(nullptr, PictureInPictureControllerImpl::From(GetDocument())
+                         .PictureInPictureElement());
+}
+
+TEST_F(PictureInPictureControllerTest, StartObserving) {
+  EXPECT_FALSE(PictureInPictureControllerImpl::From(GetDocument())
+                   .GetDelegateBindingForTesting()
+                   .is_bound());
+
+  WebMediaPlayer* player = Video()->GetWebMediaPlayer();
+  EXPECT_CALL(Service(),
+              StartSession(player->GetDelegateId(), player->GetSurfaceId(),
+                           player->NaturalSize(), true, _));
+  EXPECT_CALL(Service(), SetDelegate(_));
+
+  PictureInPictureControllerImpl::From(GetDocument())
+      .EnterPictureInPicture(Video(), nullptr);
+
+  WaitForEvent::Create(Video(), event_type_names::kEnterpictureinpicture);
+
+  EXPECT_TRUE(PictureInPictureControllerImpl::From(GetDocument())
+                  .GetDelegateBindingForTesting()
+                  .is_bound());
+
+  // `SetDelegate()` may or may not have been called yet. Waiting a bit for it.
+  test::RunPendingTasks();
+}
+
+TEST_F(PictureInPictureControllerTest, StopObserving) {
+  EXPECT_FALSE(PictureInPictureControllerImpl::From(GetDocument())
+                   .GetDelegateBindingForTesting()
+                   .is_bound());
+
+  WebMediaPlayer* player = Video()->GetWebMediaPlayer();
+  EXPECT_CALL(Service(),
+              StartSession(player->GetDelegateId(), player->GetSurfaceId(),
+                           player->NaturalSize(), true, _));
+  EXPECT_CALL(Service(), EndSession(_));
+  EXPECT_CALL(Service(), SetDelegate(_));
+
+  PictureInPictureControllerImpl::From(GetDocument())
+      .EnterPictureInPicture(Video(), nullptr);
+  WaitForEvent::Create(Video(), event_type_names::kEnterpictureinpicture);
+
+  PictureInPictureControllerImpl::From(GetDocument())
+      .ExitPictureInPicture(Video(), nullptr);
+  WaitForEvent::Create(Video(), event_type_names::kLeavepictureinpicture);
+
+  EXPECT_FALSE(PictureInPictureControllerImpl::From(GetDocument())
+                   .GetDelegateBindingForTesting()
+                   .is_bound());
+}
+
+TEST_F(PictureInPictureControllerTest, PlayPauseButton_InfiniteDuration) {
+  EXPECT_EQ(nullptr, PictureInPictureControllerImpl::From(GetDocument())
+                         .PictureInPictureElement());
+
+  Video()->DurationChanged(std::numeric_limits<double>::infinity(), false);
+
+  WebMediaPlayer* player = Video()->GetWebMediaPlayer();
+  EXPECT_CALL(Service(),
+              StartSession(player->GetDelegateId(), player->GetSurfaceId(),
+                           player->NaturalSize(), false, _));
+  EXPECT_CALL(Service(), SetDelegate(_));
+
+  PictureInPictureControllerImpl::From(GetDocument())
+      .EnterPictureInPicture(Video(), nullptr);
+
+  WaitForEvent::Create(Video(), event_type_names::kEnterpictureinpicture);
+
+  // `SetDelegate()` may or may not have been called yet. Waiting a bit for it.
+  test::RunPendingTasks();
+}
+
+TEST_F(PictureInPictureControllerTest, PlayPauseButton_MediaSource) {
+  EXPECT_EQ(nullptr, PictureInPictureControllerImpl::From(GetDocument())
+                         .PictureInPictureElement());
+
+  // The test automatically setup the WebMediaPlayer with a MediaSource based on
+  // the test name.
+
+  WebMediaPlayer* player = Video()->GetWebMediaPlayer();
+  EXPECT_CALL(Service(),
+              StartSession(player->GetDelegateId(), player->GetSurfaceId(),
+                           player->NaturalSize(), false, _));
+  EXPECT_CALL(Service(), SetDelegate(_));
+
+  PictureInPictureControllerImpl::From(GetDocument())
+      .EnterPictureInPicture(Video(), nullptr);
+
+  WaitForEvent::Create(Video(), event_type_names::kEnterpictureinpicture);
+
+  // `SetDelegate()` may or may not have been called yet. Waiting a bit for it.
+  test::RunPendingTasks();
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/platform/testing/empty_web_media_player.h b/third_party/blink/renderer/platform/testing/empty_web_media_player.h
index 205fde9..fea2a40 100644
--- a/third_party/blink/renderer/platform/testing/empty_web_media_player.h
+++ b/third_party/blink/renderer/platform/testing/empty_web_media_player.h
@@ -28,12 +28,10 @@
   void Seek(double seconds) override {}
   void SetRate(double) override {}
   void SetVolume(double) override {}
-  void EnterPictureInPicture(PipWindowOpenedCallback) override {}
-  void ExitPictureInPicture(PipWindowClosedCallback) override {}
+  void EnterPictureInPicture() override {}
+  void ExitPictureInPicture() override {}
   void SetPictureInPictureCustomControls(
       const std::vector<PictureInPictureControlInfo>&) override {}
-  void RegisterPictureInPictureWindowResizeCallback(
-      PipWindowResizedCallback) override {}
   SurfaceLayerMode GetVideoSurfaceLayerMode() const override {
     return SurfaceLayerMode::kNever;
   }