toolkit/actors/PictureInPictureChild.jsm
author Mike Conley <mconley@mozilla.com>
Fri, 01 Mar 2019 22:37:26 +0000
changeset 519925 d26b9b95a47e9eb0c879ba645e6dc89f4efda1f4
parent 516516 519b52f60bb95a1568641ccd4fcf9b409dcd45e8
child 521447 10e9bbf2d1df3b261a86356d3e9e15da962a9568
permissions -rw-r--r--
Bug 1521964 - Make PictureInPictureChild use the cloneElementVisually API. r=Felipe Differential Revision: https://phabricator.services.mozilla.com/D21402

/* -*- 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"];

const {ActorChild} = ChromeUtils.import("resource://gre/modules/ActorChild.jsm");

var gWeakVideo = null;

class PictureInPictureChild extends ActorChild {
  handleEvent(event) {
    switch (event.type) {
      case "MozTogglePictureInPicture": {
        this.togglePictureInPicture(event.target);
        break;
      }
    }
  }

  togglePictureInPicture(video) {
    if (this.inPictureInPicture(video)) {
      this.closePictureInPicture(video);
    } else {
      this.requestPictureInPicture(video);
    }
  }

  inPictureInPicture(video) {
    return gWeakVideo && gWeakVideo.get() === video;
  }

  closePictureInPicture() {
    this.mm.sendAsyncMessage("PictureInPicture:Close", {
      browingContextId: this.docShell.browsingContext.id,
    });
  }

  requestPictureInPicture(video) {
    gWeakVideo = Cu.getWeakReference(video);
    this.mm.sendAsyncMessage("PictureInPicture:Request", {
      videoHeight: video.videoHeight,
      videoWidth: video.videoWidth,
    });
  }

  receiveMessage(message) {
    switch (message.name) {
      case "PictureInPicture:SetupPlayer": {
        this.setupPlayer();
        break;
      }
    }
  }

  async setupPlayer() {
    if (!gWeakVideo) {
      this.closePictureInPicture();
    }

    let originatingVideo = gWeakVideo.get();
    if (!originatingVideo) {
      this.closePictureInPicture();
    }

    let webProgress = this.mm
                          .docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIWebProgress);
    if (webProgress.isLoadingDocument) {
      await new Promise(resolve => {
        this.mm.addEventListener("load", resolve, {
          once: true,
          mozSystemGroup: true,
          capture: true,
        });
      });
    }

    let doc = this.content.document;
    let playerVideo = originatingVideo.cloneNode();
    playerVideo.removeAttribute("controls");

    // Mute the video and rely on the originating video's audio playback.
    // This way, we sidestep the AutoplayPolicy blocking stuff.
    playerVideo.muted = true;

    // Force the player video to assume maximum height and width of the
    // containing window
    playerVideo.style.height = "100vh";
    playerVideo.style.width = "100vw";

    // And now try to get rid of as much surrounding whitespace as possible.
    playerVideo.style.margin = "0";
    doc.body.style.overflow = "hidden";
    doc.body.style.margin = "0";

    doc.body.appendChild(playerVideo);

    originatingVideo.cloneElementVisually(playerVideo);

    let originatingWindow = originatingVideo.ownerGlobal;
    originatingWindow.addEventListener("unload", (e) => {
      this.closePictureInPicture(originatingVideo);
    }, { once: true });

    this.content.addEventListener("unload", () => {
      gWeakVideo = null;
    }, { once: true });
  }
}