Bug 1576915 Port of Picture in Picture to JSWindowActors r=mconley
authorTeja Bayya <bayyatej.dev@gmail.com>
Fri, 01 Nov 2019 14:15:22 +0000
changeset 500184 2012ad5d5af45bc7e7fec2a24b1e2472c7536d8d
parent 500183 2cad80ddf5c89ff844752e499313cdcea5b0f554
child 500185 240f3e17395faa6790dd9fc165693c0a1d7eb3f7
push id99450
push usermconley@mozilla.com
push dateFri, 01 Nov 2019 15:45:19 +0000
treeherderautoland@2012ad5d5af4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1576915
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1576915 Port of Picture in Picture to JSWindowActors r=mconley Differential Revision: https://phabricator.services.mozilla.com/D49797
browser/components/BrowserGlue.jsm
toolkit/actors/PictureInPictureChild.jsm
toolkit/components/pictureinpicture/PictureInPicture.jsm
toolkit/components/pictureinpicture/content/player.js
toolkit/modules/ActorManagerParent.jsm
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -547,17 +547,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
 });
 
 // eslint-disable-next-line no-unused-vars
 XPCOMUtils.defineLazyModuleGetters(this, {
   AboutLoginsParent: "resource:///modules/AboutLoginsParent.jsm",
   AsyncPrefs: "resource://gre/modules/AsyncPrefs.jsm",
   ContentClick: "resource:///modules/ContentClick.jsm",
   PluginManager: "resource:///actors/PluginParent.jsm",
-  PictureInPicture: "resource://gre/modules/PictureInPicture.jsm",
   ReaderParent: "resource:///modules/ReaderParent.jsm",
 });
 
 /**
  * IF YOU ADD OR REMOVE FROM THIS LIST, PLEASE UPDATE THE LIST ABOVE AS WELL.
  * XXX Bug 1325373 is for making eslint detect these automatically.
  */
 
@@ -647,21 +646,16 @@ const listeners = {
     "AboutLogins:OpenSite": ["AboutLoginsParent"],
     "AboutLogins:SortChanged": ["AboutLoginsParent"],
     "AboutLogins:Subscribe": ["AboutLoginsParent"],
     "AboutLogins:SyncEnable": ["AboutLoginsParent"],
     "AboutLogins:SyncOptions": ["AboutLoginsParent"],
     "AboutLogins:UpdateLogin": ["AboutLoginsParent"],
     "Content:Click": ["ContentClick"],
     ContentSearch: ["ContentSearch"],
-    "PictureInPicture:Request": ["PictureInPicture"],
-    "PictureInPicture:Close": ["PictureInPicture"],
-    "PictureInPicture:Playing": ["PictureInPicture"],
-    "PictureInPicture:Paused": ["PictureInPicture"],
-    "PictureInPicture:OpenToggleContextMenu": ["PictureInPicture"],
     "Reader:FaviconRequest": ["ReaderParent"],
     "Reader:UpdateReaderButton": ["ReaderParent"],
     "rtcpeer:CancelRequest": ["webrtcUI"],
     "rtcpeer:Request": ["webrtcUI"],
     "webrtc:CancelRequest": ["webrtcUI"],
     "webrtc:Request": ["webrtcUI"],
     "webrtc:StopRecording": ["webrtcUI"],
     "webrtc:UpdateBrowserIndicators": ["webrtcUI"],
--- a/toolkit/actors/PictureInPictureChild.jsm
+++ b/toolkit/actors/PictureInPictureChild.jsm
@@ -1,20 +1,16 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 var EXPORTED_SYMBOLS = ["PictureInPictureChild", "PictureInPictureToggleChild"];
 
-const { ActorChild } = ChromeUtils.import(
-  "resource://gre/modules/ActorChild.jsm"
-);
-
 ChromeUtils.defineModuleGetter(
   this,
   "DeferredTask",
   "resource://gre/modules/DeferredTask.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "Services",
@@ -39,60 +35,66 @@ var gWeakPlayerContent = null;
 // mouseover
 var gWeakIntersectingVideosForTesting = new WeakSet();
 
 /**
  * The PictureInPictureToggleChild is responsible for displaying the overlaid
  * Picture-in-Picture toggle over top of <video> elements that the mouse is
  * hovering.
  */
-class PictureInPictureToggleChild extends ActorChild {
+class PictureInPictureToggleChild extends JSWindowActorChild {
   constructor(dispatcher) {
     super(dispatcher);
     // We need to maintain some state about various things related to the
     // Picture-in-Picture toggles - however, for now, the same
     // PictureInPictureToggleChild might be re-used for different documents.
     // We keep the state stashed inside of this WeakMap, keyed on the document
     // itself.
     this.weakDocStates = new WeakMap();
     this.toggleEnabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF);
+    this.toggleTesting = Services.prefs.getBoolPref(TOGGLE_TESTING_PREF, false);
 
-    Services.prefs.addObserver(TOGGLE_ENABLED_PREF, this);
-    this.toggleTesting = Services.prefs.getBoolPref(TOGGLE_TESTING_PREF, false);
+    // Bug 1570744 - JSWindowActorChild's cannot be used as nsIObserver's
+    // directly, so we create a new function here instead to act as our
+    // nsIObserver, which forwards the notification to the observe method.
+    this.observerFunction = (subject, topic, data) => {
+      this.observe(subject, topic, data);
+    };
+    Services.prefs.addObserver(TOGGLE_ENABLED_PREF, this.observerFunction);
   }
 
-  cleanup() {
+  willDestroy() {
     this.removeMouseButtonListeners();
-    Services.prefs.removeObserver(TOGGLE_ENABLED_PREF, this);
+    Services.prefs.removeObserver(TOGGLE_ENABLED_PREF, this.observerFunction);
   }
 
   observe(subject, topic, data) {
     if (topic == "nsPref:changed" && data == TOGGLE_ENABLED_PREF) {
       this.toggleEnabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF);
 
       if (this.toggleEnabled) {
         // We have enabled the Picture-in-Picture toggle, so we need to make
         // sure we register all of the videos that might already be on the page.
-        this.content.requestIdleCallback(() => {
-          let videos = this.content.document.querySelectorAll("video");
+        this.contentWindow.requestIdleCallback(() => {
+          let videos = this.document.querySelectorAll("video");
           for (let video of videos) {
             this.registerVideo(video);
           }
         });
       }
     }
   }
 
   /**
    * Returns the state for the current document referred to via
-   * this.content.document. If no such state exists, creates it, stores it
+   * this.document. If no such state exists, creates it, stores it
    * and returns it.
    */
   get docState() {
-    let state = this.weakDocStates.get(this.content.document);
+    let state = this.weakDocStates.get(this.document);
     if (!state) {
       state = {
         // A reference to the IntersectionObserver that's monitoring for videos
         // to become visible.
         intersectionObserver: null,
         // A WeakSet of videos that are supposedly visible, according to the
         // IntersectionObserver.
         weakVisibleVideos: new WeakSet(),
@@ -114,35 +116,35 @@ class PictureInPictureToggleChild extend
         // This is a DeferredTask to hide the toggle after a period of mouse
         // inactivity.
         hideToggleDeferredTask: null,
         // If we reach a point where we're tracking videos for mouse movements,
         // then this will be true. If there are no videos worth tracking, then
         // this is false.
         isTrackingVideos: false,
       };
-      this.weakDocStates.set(this.content.document, state);
+      this.weakDocStates.set(this.document, state);
     }
 
     return state;
   }
 
   handleEvent(event) {
     if (!event.isTrusted) {
       // We don't care about synthesized events that might be coming from
       // content JS.
       return;
     }
 
     switch (event.type) {
       case "UAWidgetSetupOrChange": {
         if (
           this.toggleEnabled &&
-          event.target instanceof this.content.HTMLVideoElement &&
-          event.target.ownerDocument == this.content.document
+          event.target instanceof this.contentWindow.HTMLVideoElement &&
+          event.target.ownerDocument == this.document
         ) {
           this.registerVideo(event.target);
         }
         break;
       }
       case "contextmenu": {
         if (this.toggleEnabled) {
           this.checkContextMenu(event);
@@ -184,19 +186,22 @@ class PictureInPictureToggleChild extend
    * visible.
    *
    * @param {Element} video The <video> element to register.
    */
   registerVideo(video) {
     let state = this.docState;
     if (!state.intersectionObserver) {
       let fn = this.onIntersection.bind(this);
-      state.intersectionObserver = new this.content.IntersectionObserver(fn, {
-        threshold: [0.0, 0.5],
-      });
+      state.intersectionObserver = new this.contentWindow.IntersectionObserver(
+        fn,
+        {
+          threshold: [0.0, 0.5],
+        }
+      );
     }
 
     state.intersectionObserver.observe(video);
   }
 
   /**
    * Called by the IntersectionObserver callback once a video becomes visible.
    * This adds some fine-grained checking to ensure that a sufficient amount of
@@ -250,75 +255,77 @@ class PictureInPictureToggleChild extend
     // run this idle callback within an acceptable time. While we're
     // testing, we'll bypass the idle callback performance optimization
     // and run our callbacks as soon as possible during the next idle
     // period.
     if (!oldVisibleVideosCount && state.visibleVideosCount) {
       if (this.toggleTesting) {
         this.beginTrackingMouseOverVideos();
       } else {
-        this.content.requestIdleCallback(() => {
+        this.contentWindow.requestIdleCallback(() => {
           this.beginTrackingMouseOverVideos();
         });
       }
     } else if (oldVisibleVideosCount && !state.visibleVideosCount) {
       if (this.toggleTesting) {
         this.stopTrackingMouseOverVideos();
       } else {
-        this.content.requestIdleCallback(() => {
+        this.contentWindow.requestIdleCallback(() => {
           this.stopTrackingMouseOverVideos();
         });
       }
     }
   }
 
   addMouseButtonListeners() {
     // We want to try to cancel the mouse events from continuing
     // on into content if the user has clicked on the toggle, so
     // we don't use the mozSystemGroup here, and add the listener
     // to the parent target of the window, which in this case,
     // is the windowRoot. Since this event listener is attached to
     // part of the outer window, we need to also remove it in a
     // pagehide event listener in the event that the page unloads
     // before stopTrackingMouseOverVideos fires.
-    this.content.windowRoot.addEventListener("pointerdown", this, {
+    this.contentWindow.windowRoot.addEventListener("pointerdown", this, {
       capture: true,
     });
-    this.content.windowRoot.addEventListener("mousedown", this, {
+    this.contentWindow.windowRoot.addEventListener("mousedown", this, {
       capture: true,
     });
-    this.content.windowRoot.addEventListener("mouseup", this, {
+    this.contentWindow.windowRoot.addEventListener("mouseup", this, {
       capture: true,
     });
-    this.content.windowRoot.addEventListener("pointerup", this, {
+    this.contentWindow.windowRoot.addEventListener("pointerup", this, {
       capture: true,
     });
-    this.content.windowRoot.addEventListener("click", this, { capture: true });
-    this.content.windowRoot.addEventListener("mouseout", this, {
+    this.contentWindow.windowRoot.addEventListener("click", this, {
+      capture: true,
+    });
+    this.contentWindow.windowRoot.addEventListener("mouseout", this, {
       capture: true,
     });
   }
 
   removeMouseButtonListeners() {
-    this.content.windowRoot.removeEventListener("pointerdown", this, {
+    this.contentWindow.windowRoot.removeEventListener("pointerdown", this, {
       capture: true,
     });
-    this.content.windowRoot.removeEventListener("mousedown", this, {
+    this.contentWindow.windowRoot.removeEventListener("mousedown", this, {
       capture: true,
     });
-    this.content.windowRoot.removeEventListener("mouseup", this, {
+    this.contentWindow.windowRoot.removeEventListener("mouseup", this, {
       capture: true,
     });
-    this.content.windowRoot.removeEventListener("pointerup", this, {
+    this.contentWindow.windowRoot.removeEventListener("pointerup", this, {
       capture: true,
     });
-    this.content.windowRoot.removeEventListener("click", this, {
+    this.contentWindow.windowRoot.removeEventListener("click", this, {
       capture: true,
     });
-    this.content.windowRoot.removeEventListener("mouseout", this, {
+    this.contentWindow.windowRoot.removeEventListener("mouseout", this, {
       capture: true,
     });
   }
 
   /**
    * One of the challenges of displaying this toggle is that many sites put
    * things over top of <video> elements, like custom controls, or images, or
    * all manner of things that might intercept mouseevents that would normally
@@ -333,46 +340,46 @@ class PictureInPictureToggleChild extend
    */
   beginTrackingMouseOverVideos() {
     let state = this.docState;
     if (!state.mousemoveDeferredTask) {
       state.mousemoveDeferredTask = new DeferredTask(() => {
         this.checkLastMouseMove();
       }, MOUSEMOVE_PROCESSING_DELAY_MS);
     }
-    this.content.document.addEventListener("mousemove", this, {
+    this.document.addEventListener("mousemove", this, {
       mozSystemGroup: true,
       capture: true,
     });
-    this.content.addEventListener("pageshow", this, {
+    this.contentWindow.addEventListener("pageshow", this, {
       mozSystemGroup: true,
     });
-    this.content.addEventListener("pagehide", this, {
+    this.contentWindow.addEventListener("pagehide", this, {
       mozSystemGroup: true,
     });
     this.addMouseButtonListeners();
     state.isTrackingVideos = true;
   }
 
   /**
    * If we no longer have any interesting videos in the viewport, we deregister
    * the mousemove and click listeners, and also remove any toggles that might
    * be on the page still.
    */
   stopTrackingMouseOverVideos() {
     let state = this.docState;
     state.mousemoveDeferredTask.disarm();
-    this.content.document.removeEventListener("mousemove", this, {
+    this.document.removeEventListener("mousemove", this, {
       mozSystemGroup: true,
       capture: true,
     });
-    this.content.removeEventListener("pageshow", this, {
+    this.contentWindow.removeEventListener("pageshow", this, {
       mozSystemGroup: true,
     });
-    this.content.removeEventListener("pagehide", this, {
+    this.contentWindow.removeEventListener("pagehide", this, {
       mozSystemGroup: true,
     });
     this.removeMouseButtonListeners();
     let oldOverVideo = state.weakOverVideo && state.weakOverVideo.get();
     if (oldOverVideo) {
       this.onMouseLeaveVideo(oldOverVideo);
     }
     state.isTrackingVideos = false;
@@ -435,17 +442,17 @@ class PictureInPictureToggleChild extend
     }
 
     let shadowRoot = video.openOrClosedShadowRoot;
     if (!shadowRoot) {
       return;
     }
 
     let { clientX, clientY } = event;
-    let winUtils = this.content.windowUtils;
+    let winUtils = this.contentWindow.windowUtils;
     // We use winUtils.nodesFromRect instead of document.elementsFromPoint,
     // since document.elementsFromPoint always flushes layout. The 1's in that
     // function call are for the size of the rect that we want, which is 1x1.
     //
     // We pass the aOnlyVisible boolean argument to check that the video isn't
     // occluded by anything visible at the point of mousedown. If it is, we'll
     // ignore the mousedown.
     let elements = winUtils.nodesFromRect(
@@ -470,19 +477,22 @@ class PictureInPictureToggleChild extend
       event.stopImmediatePropagation();
 
       Services.telemetry.keyedScalarAdd(
         "pictureinpicture.opened_method",
         "toggle",
         1
       );
 
-      let pipEvent = new this.content.CustomEvent("MozTogglePictureInPicture", {
-        bubbles: true,
-      });
+      let pipEvent = new this.contentWindow.CustomEvent(
+        "MozTogglePictureInPicture",
+        {
+          bubbles: true,
+        }
+      );
       video.dispatchEvent(pipEvent);
 
       // Since we've initiated Picture-in-Picture, we can go ahead and
       // hide the toggle now.
       this.onMouseLeaveVideo(video);
     }
   }
 
@@ -571,17 +581,17 @@ class PictureInPictureToggleChild extend
    * milliseconds. Checked to see if that mousemove happens to be overtop of
    * any interesting <video> elements that we want to display the toggle
    * on. If so, puts the toggle on that video.
    */
   checkLastMouseMove() {
     let state = this.docState;
     let event = state.lastMouseMoveEvent;
     let { clientX, clientY } = event;
-    let winUtils = this.content.windowUtils;
+    let winUtils = this.contentWindow.windowUtils;
     // We use winUtils.nodesFromRect instead of document.elementsFromPoint,
     // since document.elementsFromPoint always flushes layout. The 1's in that
     // function call are for the size of the rect that we want, which is 1x1.
     let elements = winUtils.nodesFromRect(
       clientX,
       clientY,
       1,
       1,
@@ -758,34 +768,34 @@ class PictureInPictureToggleChild extend
       return;
     }
 
     let toggle = shadowRoot.getElementById("pictureInPictureToggleButton");
     if (this.isMouseOverToggle(toggle, event)) {
       event.stopImmediatePropagation();
       event.preventDefault();
 
-      this.mm.sendAsyncMessage("PictureInPicture:OpenToggleContextMenu", {
+      this.sendAsyncMessage("PictureInPicture:OpenToggleContextMenu", {
         screenX: event.screenX,
         screenY: event.screenY,
         mozInputSource: event.mozInputSource,
       });
     }
   }
 
   /**
    * This is a test-only function that returns true if a video is being tracked
    * for mouseover events after having intersected the viewport.
    */
   static isTracking(video) {
     return gWeakIntersectingVideosForTesting.has(video);
   }
 }
 
-class PictureInPictureChild extends ActorChild {
+class PictureInPictureChild extends JSWindowActorChild {
   static videoIsPlaying(video) {
     return !!(
       video.currentTime > 0 &&
       !video.paused &&
       !video.ended &&
       video.readyState > 2
     );
   }
@@ -806,21 +816,21 @@ class PictureInPictureChild extends Acto
       }
       case "pagehide": {
         // The originating video's content document has unloaded,
         // so close Picture-in-Picture.
         this.closePictureInPicture({ reason: "pagehide" });
         break;
       }
       case "play": {
-        this.mm.sendAsyncMessage("PictureInPicture:Playing");
+        this.sendAsyncMessage("PictureInPicture:Playing");
         break;
       }
       case "pause": {
-        this.mm.sendAsyncMessage("PictureInPicture:Paused");
+        this.sendAsyncMessage("PictureInPicture:Paused");
         break;
       }
     }
   }
 
   get weakVideo() {
     if (gWeakVideo) {
       return gWeakVideo.get();
@@ -865,17 +875,17 @@ class PictureInPictureChild extends Acto
       if (this.weakVideo) {
         // There's a pre-existing Picture-in-Picture window for a video
         // in this content process. Send a message to the parent to close
         // the Picture-in-Picture window.
         await this.closePictureInPicture({ reason: "new-pip" });
       }
 
       gWeakVideo = Cu.getWeakReference(video);
-      this.mm.sendAsyncMessage("PictureInPicture:Request", {
+      this.sendAsyncMessage("PictureInPicture:Request", {
         playing: PictureInPictureChild.videoIsPlaying(video),
         videoHeight: video.videoHeight,
         videoWidth: video.videoWidth,
       });
     }
   }
 
   /**
@@ -898,18 +908,17 @@ class PictureInPictureChild extends Acto
    *
    * @resolves {undefined} Once the pre-existing Picture-in-Picture
    * window has unloaded.
    */
   async closePictureInPicture({ reason }) {
     if (this.weakVideo) {
       this.untrackOriginatingVideo(this.weakVideo);
     }
-
-    this.mm.sendAsyncMessage("PictureInPicture:Close", {
+    this.sendAsyncMessage("PictureInPicture:Close", {
       browingContextId: this.docShell.browsingContext.id,
       reason,
     });
 
     if (this.weakPlayerContent) {
       if (!this.weakPlayerContent.closed) {
         await new Promise(resolve => {
           this.weakPlayerContent.addEventListener("unload", resolve, {
@@ -992,30 +1001,31 @@ class PictureInPictureChild extends Acto
     if (!originatingVideo) {
       // If the video element has gone away before we've had a chance to set up
       // Picture-in-Picture for it, tell the parent to close the Picture-in-Picture
       // window.
       await this.closePictureInPicture({ reason: "setup-failure" });
       return;
     }
 
-    let webProgress = this.mm.docShell
+    this.contentWindow.location.reload();
+    let webProgress = this.docShell
       .QueryInterface(Ci.nsIInterfaceRequestor)
       .getInterface(Ci.nsIWebProgress);
     if (webProgress.isLoadingDocument) {
       await new Promise(resolve => {
-        this.mm.addEventListener("load", resolve, {
+        this.contentWindow.addEventListener("load", resolve, {
           once: true,
           mozSystemGroup: true,
           capture: true,
         });
       });
     }
 
-    let doc = this.content.document;
+    let doc = this.document;
     let playerVideo = doc.createElement("video");
 
     doc.body.style.overflow = "hidden";
     doc.body.style.margin = "0";
 
     // Force the player video to assume maximum height and width of the
     // containing window
     playerVideo.style.height = "100vh";
@@ -1024,29 +1034,29 @@ class PictureInPictureChild extends Acto
       'url("chrome://global/skin/media/imagedoc-darknoise.png")';
 
     doc.body.appendChild(playerVideo);
 
     originatingVideo.cloneElementVisually(playerVideo);
 
     this.trackOriginatingVideo(originatingVideo);
 
-    this.content.addEventListener(
+    this.contentWindow.addEventListener(
       "unload",
       () => {
         if (this.weakVideo) {
           this.untrackOriginatingVideo(this.weakVideo);
           this.weakVideo.stopCloningElementVisually();
         }
         gWeakVideo = null;
       },
       { once: true }
     );
 
-    gWeakPlayerContent = Cu.getWeakReference(this.content);
+    gWeakPlayerContent = Cu.getWeakReference(this.contentWindow);
   }
 
   play() {
     let video = this.weakVideo;
     if (video) {
       video.play();
     }
   }
--- a/toolkit/components/pictureinpicture/PictureInPicture.jsm
+++ b/toolkit/components/pictureinpicture/PictureInPicture.jsm
@@ -1,15 +1,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-var EXPORTED_SYMBOLS = ["PictureInPicture"];
+var EXPORTED_SYMBOLS = [
+  "PictureInPicture",
+  "PictureInPictureParent",
+  "PictureInPictureToggleParent",
+];
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const { AppConstants } = ChromeUtils.import(
   "resource://gre/modules/AppConstants.jsm"
 );
 
 const PLAYER_URI = "chrome://global/content/pictureinpicture/player.xhtml";
 var PLAYER_FEATURES =
@@ -31,62 +35,82 @@ const TOGGLE_ENABLED_PREF =
 let gCloseReasons = new WeakMap();
 
 /**
  * To differentiate windows in the Telemetry Event Log, each Picture-in-Picture
  * player window is given a unique ID.
  */
 let gNextWindowID = 0;
 
+class PictureInPictureToggleParent extends JSWindowActorParent {
+  receiveMessage(aMessage) {
+    let browsingContext = aMessage.target.browsingContext;
+    let browser = browsingContext.top.embedderElement;
+    switch (aMessage.name) {
+      case "PictureInPicture:OpenToggleContextMenu": {
+        let win = browser.ownerGlobal;
+        PictureInPicture.openToggleContextMenu(win, aMessage.data);
+        break;
+      }
+    }
+  }
+}
+
 /**
  * This module is responsible for creating a Picture in Picture window to host
  * a clone of a video element running in web content.
  */
 
-var PictureInPicture = {
-  // Listeners are added in nsBrowserGlue.js lazily
+class PictureInPictureParent extends JSWindowActorParent {
   receiveMessage(aMessage) {
-    let browser = aMessage.target;
+    let browsingContext = aMessage.target.browsingContext;
+    let browser = browsingContext.top.embedderElement;
 
     switch (aMessage.name) {
       case "PictureInPicture:Request": {
         let videoData = aMessage.data;
-        this.handlePictureInPictureRequest(browser, videoData);
+        PictureInPicture.handlePictureInPictureRequest(browser, videoData);
         break;
       }
       case "PictureInPicture:Close": {
         /**
          * Content has requested that its Picture in Picture window go away.
          */
         let reason = aMessage.data.reason;
-        this.closePipWindow({ reason });
+        PictureInPicture.closePipWindow({ reason });
         break;
       }
       case "PictureInPicture:Playing": {
-        let player = this.weakPipPlayer && this.weakPipPlayer.get();
+        let player =
+          PictureInPicture.weakPipPlayer &&
+          PictureInPicture.weakPipPlayer.get();
         if (player) {
           player.setIsPlayingState(true);
         }
         break;
       }
       case "PictureInPicture:Paused": {
-        let player = this.weakPipPlayer && this.weakPipPlayer.get();
+        let player =
+          PictureInPicture.weakPipPlayer &&
+          PictureInPicture.weakPipPlayer.get();
         if (player) {
           player.setIsPlayingState(false);
         }
         break;
       }
-      case "PictureInPicture:OpenToggleContextMenu": {
-        let win = browser.ownerGlobal;
-        this.openToggleContextMenu(win, aMessage.data);
-        break;
-      }
     }
-  },
+  }
+}
 
+/**
+ * This module is responsible for creating a Picture in Picture window to host
+ * a clone of a video element running in web content.
+ */
+
+var PictureInPicture = {
   /**
    * Called when the browser UI handles the View:PictureInPicture command via
    * the keyboard.
    */
   onCommand(event) {
     let win = event.target.ownerGlobal;
     let browser = win.gBrowser.selectedBrowser;
     browser.messageManager.sendAsyncMessage("PictureInPicture:KeyToggle");
--- a/toolkit/components/pictureinpicture/content/player.js
+++ b/toolkit/components/pictureinpicture/content/player.js
@@ -46,17 +46,17 @@ let Player = {
   WINDOW_EVENTS: [
     "click",
     "contextmenu",
     "keydown",
     "mouseout",
     "resize",
     "unload",
   ],
-  mm: null,
+  actor: null,
   /**
    * Used for resizing Telemetry to avoid recording an event for every resize
    * event. Instead, we wait until RESIZE_DEBOUNCE_RATE_MS has passed since the
    * last resize event before recording.
    */
   resizeDebouncer: null,
   /**
    * Used for window movement Telemetry to determine if the player window has
@@ -86,22 +86,20 @@ let Player = {
     let holder = document.querySelector(".player-holder");
     let browser = document.getElementById("browser");
     browser.remove();
 
     browser.setAttribute("nodefaultsrc", "true");
     browser.sameProcessAsFrameLoader = originatingBrowser.frameLoader;
     holder.appendChild(browser);
 
-    browser.loadURI("about:blank", {
-      triggeringPrincipal: originatingBrowser.contentPrincipal,
-    });
-
-    this.mm = browser.frameLoader.messageManager;
-    this.mm.sendAsyncMessage("PictureInPicture:SetupPlayer");
+    this.actor = browser.browsingContext.currentWindowGlobal.getActor(
+      "PictureInPicture"
+    );
+    this.actor.sendAsyncMessage("PictureInPicture:SetupPlayer");
 
     for (let eventType of this.WINDOW_EVENTS) {
       addEventListener(eventType, this);
     }
 
     // If the content process hosting the video crashes, let's
     // just close the window for now.
     browser.addEventListener("oop-browser-crashed", this);
@@ -181,20 +179,20 @@ let Player = {
     switch (event.target.id) {
       case "close": {
         PictureInPicture.closePipWindow({ reason: "close-button" });
         break;
       }
 
       case "playpause": {
         if (!this.isPlaying) {
-          this.mm.sendAsyncMessage("PictureInPicture:Play");
+          this.actor.sendAsyncMessage("PictureInPicture:Play");
           this.revealControls(false);
         } else {
-          this.mm.sendAsyncMessage("PictureInPicture:Pause");
+          this.actor.sendAsyncMessage("PictureInPicture:Pause");
           this.revealControls(true);
         }
 
         break;
       }
 
       case "unpip": {
         PictureInPicture.focusTabAndClosePip();
--- a/toolkit/modules/ActorManagerParent.jsm
+++ b/toolkit/modules/ActorManagerParent.jsm
@@ -315,16 +315,45 @@ let ACTORS = {
         "PasswordManager:fillGeneratedPassword",
         "FormAutoComplete:PopupOpened",
         "FormAutoComplete:PopupClosed",
       ],
     },
 
     allFrames: true,
   },
+  PictureInPicture: {
+    parent: {
+      moduleURI: "resource://gre/modules/PictureInPicture.jsm",
+    },
+    child: {
+      moduleURI: "resource://gre/actors/PictureInPictureChild.jsm",
+      events: {
+        MozTogglePictureInPicture: { capture: true },
+        MozStopPictureInPicture: { capture: true },
+      },
+    },
+
+    allFrames: true,
+  },
+
+  PictureInPictureToggle: {
+    parent: {
+      moduleURI: "resource://gre/modules/PictureInPicture.jsm",
+    },
+    child: {
+      moduleURI: "resource://gre/actors/PictureInPictureChild.jsm",
+      events: {
+        UAWidgetSetupOrChange: {},
+        contextmenu: { capture: true },
+      },
+    },
+
+    allFrames: true,
+  },
 
   Select: {
     parent: {
       moduleURI: "resource://gre/actors/SelectParent.jsm",
     },
 
     child: {
       moduleURI: "resource://gre/actors/SelectChild.jsm",
@@ -397,44 +426,16 @@ let LEGACY_ACTORS = {
         "DOM:Manifest:FireAppInstalledEvent",
         "DOM:ManifestObtainer:Obtain",
         "DOM:WebManifest:fetchIcon",
         "DOM:WebManifest:hasManifestLink",
       ],
     },
   },
 
-  PictureInPicture: {
-    child: {
-      module: "resource://gre/actors/PictureInPictureChild.jsm",
-      events: {
-        MozTogglePictureInPicture: { capture: true },
-        MozStopPictureInPicture: { capture: true },
-      },
-
-      messages: [
-        "PictureInPicture:SetupPlayer",
-        "PictureInPicture:Play",
-        "PictureInPicture:Pause",
-        "PictureInPicture:KeyToggle",
-      ],
-    },
-  },
-
-  PictureInPictureToggle: {
-    child: {
-      allFrames: true,
-      module: "resource://gre/actors/PictureInPictureChild.jsm",
-      events: {
-        UAWidgetSetupOrChange: {},
-        contextmenu: { capture: true },
-      },
-    },
-  },
-
   PopupBlocking: {
     child: {
       module: "resource://gre/actors/PopupBlockingChild.jsm",
       events: {
         DOMPopupBlocked: { capture: true },
       },
     },
   },