summaryrefslogtreecommitdiffstats
path: root/toolkit/components/pictureinpicture/PictureInPicture.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/pictureinpicture/PictureInPicture.jsm')
-rw-r--r--toolkit/components/pictureinpicture/PictureInPicture.jsm827
1 files changed, 827 insertions, 0 deletions
diff --git a/toolkit/components/pictureinpicture/PictureInPicture.jsm b/toolkit/components/pictureinpicture/PictureInPicture.jsm
new file mode 100644
index 0000000000..f1f17fd3f0
--- /dev/null
+++ b/toolkit/components/pictureinpicture/PictureInPicture.jsm
@@ -0,0 +1,827 @@
+/* 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",
+ "PictureInPictureParent",
+ "PictureInPictureToggleParent",
+ "PictureInPictureLauncherParent",
+];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const PLAYER_URI = "chrome://global/content/pictureinpicture/player.xhtml";
+var PLAYER_FEATURES =
+ "chrome,titlebar=yes,alwaysontop,lockaspectratio,resizable";
+/* Don't use dialog on Gtk as it adds extra border and titlebar to PIP window */
+if (!AppConstants.MOZ_WIDGET_GTK) {
+ PLAYER_FEATURES += ",dialog";
+}
+const WINDOW_TYPE = "Toolkit:PictureInPicture";
+const PIP_ENABLED_PREF = "media.videocontrols.picture-in-picture.enabled";
+const MULTI_PIP_ENABLED_PREF =
+ "media.videocontrols.picture-in-picture.allow-multiple";
+const TOGGLE_ENABLED_PREF =
+ "media.videocontrols.picture-in-picture.video-toggle.enabled";
+
+/**
+ * If closing the Picture-in-Picture player window occurred for a reason that
+ * we can easily detect (user clicked on the close button, originating tab unloaded,
+ * user clicked on the unpip button), that will be stashed in gCloseReasons so that
+ * we can note it in Telemetry when the window finally unloads.
+ */
+let gCloseReasons = new WeakMap();
+
+/**
+ * Tracks the number of currently open player windows for Telemetry tracking
+ */
+let gCurrentPlayerCount = 0;
+
+/**
+ * To differentiate windows in the Telemetry Event Log, each Picture-in-Picture
+ * player window is given a unique ID.
+ */
+let gNextWindowID = 0;
+
+class PictureInPictureLauncherParent extends JSWindowActorParent {
+ receiveMessage(aMessage) {
+ switch (aMessage.name) {
+ case "PictureInPicture:Request": {
+ let videoData = aMessage.data;
+ PictureInPicture.handlePictureInPictureRequest(this.manager, videoData);
+ break;
+ }
+ }
+ }
+}
+
+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.
+ */
+class PictureInPictureParent extends JSWindowActorParent {
+ receiveMessage(aMessage) {
+ switch (aMessage.name) {
+ case "PictureInPicture:Resize": {
+ let videoData = aMessage.data;
+ PictureInPicture.resizePictureInPictureWindow(videoData, this);
+ break;
+ }
+ case "PictureInPicture:Close": {
+ /**
+ * Content has requested that its Picture in Picture window go away.
+ */
+ let reason = aMessage.data.reason;
+
+ if (PictureInPicture.isMultiPipEnabled) {
+ PictureInPicture.closeSinglePipWindow({ reason, actorRef: this });
+ } else {
+ PictureInPicture.closeAllPipWindows({ reason });
+ }
+ break;
+ }
+ case "PictureInPicture:Playing": {
+ let player = PictureInPicture.getWeakPipPlayer(this);
+ if (player) {
+ player.setIsPlayingState(true);
+ }
+ break;
+ }
+ case "PictureInPicture:Paused": {
+ let player = PictureInPicture.getWeakPipPlayer(this);
+ if (player) {
+ player.setIsPlayingState(false);
+ }
+ break;
+ }
+ case "PictureInPicture:Muting": {
+ let player = PictureInPicture.getWeakPipPlayer(this);
+ if (player) {
+ player.setIsMutedState(true);
+ }
+ break;
+ }
+ case "PictureInPicture:Unmuting": {
+ let player = PictureInPicture.getWeakPipPlayer(this);
+ if (player) {
+ player.setIsMutedState(false);
+ }
+ 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 = {
+ // Maps PictureInPictureParent actors to their corresponding PiP player windows
+ weakPipToWin: new WeakMap(),
+
+ // Maps PiP player windows to their originating content's browser
+ weakWinToBrowser: new WeakMap(),
+
+ /**
+ * Returns the player window if one exists and if it hasn't yet been closed.
+ *
+ * @param pipActorRef (PictureInPictureParent)
+ * Reference to the calling PictureInPictureParent actor
+ *
+ * @return {DOM Window} the player window if it exists and is not in the
+ * process of being closed. Returns null otherwise.
+ */
+ getWeakPipPlayer(pipActorRef) {
+ let playerWin = this.weakPipToWin.get(pipActorRef);
+ if (!playerWin || playerWin.closed) {
+ return null;
+ }
+ return playerWin;
+ },
+
+ /**
+ * Called when the browser UI handles the View:PictureInPicture command via
+ * the keyboard.
+ */
+ onCommand(event) {
+ if (!Services.prefs.getBoolPref(PIP_ENABLED_PREF, false)) {
+ return;
+ }
+
+ let win = event.target.ownerGlobal;
+ let browser = win.gBrowser.selectedBrowser;
+ let actor = browser.browsingContext.currentWindowGlobal.getActor(
+ "PictureInPictureLauncher"
+ );
+ actor.sendAsyncMessage("PictureInPicture:KeyToggle");
+ },
+
+ async focusTabAndClosePip(window, pipActor) {
+ let browser = this.weakWinToBrowser.get(window);
+ if (!browser) {
+ return;
+ }
+
+ let gBrowser = browser.ownerGlobal.gBrowser;
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ gBrowser.selectedTab = tab;
+ await this.closeSinglePipWindow({ reason: "unpip", actorRef: pipActor });
+ },
+
+ /**
+ * Remove attribute which enables pip icon in tab
+ *
+ * @param window {Window}
+ * A PictureInPicture player's window, used to resolve the player's
+ * associated originating content browser
+ */
+ clearPipTabIcon(window) {
+ const browser = this.weakWinToBrowser.get(window);
+ if (!browser) {
+ return;
+ }
+
+ // see if no other pip windows are open for this content browser
+ for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) {
+ if (
+ win !== window &&
+ this.weakWinToBrowser.has(win) &&
+ this.weakWinToBrowser.get(win) === browser
+ ) {
+ return;
+ }
+ }
+
+ let gBrowser = browser.ownerGlobal.gBrowser;
+ let tab = gBrowser.getTabForBrowser(browser);
+ if (tab) {
+ tab.removeAttribute("pictureinpicture");
+ }
+ },
+
+ /**
+ * Closes and waits for passed PiP player window to finish closing.
+ *
+ * @param pipWin {Window}
+ * Player window to close
+ */
+ async closePipWindow(pipWin) {
+ if (pipWin.closed) {
+ return;
+ }
+ let closedPromise = new Promise(resolve => {
+ pipWin.addEventListener("unload", resolve, { once: true });
+ });
+ pipWin.close();
+ await closedPromise;
+ },
+
+ /**
+ * Closes a single PiP window. Used exclusively in conjunction with support
+ * for multiple PiP windows
+ *
+ * @param {Object} closeData
+ * Additional data required to complete a close operation on a PiP window
+ * @param {PictureInPictureParent} closeData.actorRef
+ * The PictureInPictureParent actor associated with the PiP window being closed
+ * @param {string} closeData.reason
+ * The reason for closing this PiP window
+ */
+ async closeSinglePipWindow(closeData) {
+ const { reason, actorRef } = closeData;
+ const win = this.getWeakPipPlayer(actorRef);
+ if (!win) {
+ return;
+ }
+
+ await this.closePipWindow(win);
+ gCloseReasons.set(win, reason);
+ },
+
+ /**
+ * Find and close any pre-existing Picture in Picture windows. Used exclusively
+ * when multiple PiP window support is turned off. All windows can be closed because it
+ * is assumed that only 1 window is open when it is called.
+ *
+ * @param {Object} closeData
+ * Additional data required to complete a close operation on a PiP window
+ * @param {string} closeData.reason
+ * The reason why this PiP is being closed
+ */
+ async closeAllPipWindows(closeData) {
+ const { reason } = closeData;
+
+ // This uses an enumerator, but there really should only be one of
+ // these things.
+ for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) {
+ if (win.closed) {
+ continue;
+ }
+ let closedPromise = new Promise(resolve => {
+ win.addEventListener("unload", resolve, { once: true });
+ });
+ gCloseReasons.set(win, reason);
+ win.close();
+ await closedPromise;
+ }
+ },
+
+ /**
+ * A request has come up from content to open a Picture in Picture
+ * window.
+ *
+ * @param wgp (WindowGlobalParent)
+ * The WindowGlobalParent that is requesting the Picture in Picture
+ * window.
+ *
+ * @param videoData (object)
+ * An object containing the following properties:
+ *
+ * videoHeight (int):
+ * The preferred height of the video.
+ *
+ * videoWidth (int):
+ * The preferred width of the video.
+ *
+ * @returns Promise
+ * Resolves once the Picture in Picture window has been created, and
+ * the player component inside it has finished loading.
+ */
+ async handlePictureInPictureRequest(wgp, videoData) {
+ if (!this.isMultiPipEnabled) {
+ // If there's a pre-existing PiP window, close it first if multiple
+ // pips are disabled
+ await this.closeAllPipWindows({ reason: "new-pip" });
+
+ gCurrentPlayerCount = 1;
+ } else {
+ // track specific number of open pip players if multi pip is
+ // enabled
+
+ gCurrentPlayerCount += 1;
+ }
+
+ Services.telemetry.scalarSetMaximum(
+ "pictureinpicture.most_concurrent_players",
+ gCurrentPlayerCount
+ );
+
+ let browser = wgp.browsingContext.top.embedderElement;
+ let parentWin = browser.ownerGlobal;
+
+ let win = await this.openPipWindow(parentWin, videoData);
+ win.setIsPlayingState(videoData.playing);
+ win.setIsMutedState(videoData.isMuted);
+
+ // set attribute which shows pip icon in tab
+ let tab = parentWin.gBrowser.getTabForBrowser(browser);
+ tab.setAttribute("pictureinpicture", true);
+
+ win.setupPlayer(gNextWindowID.toString(), wgp, videoData.videoRef);
+ gNextWindowID++;
+
+ this.weakWinToBrowser.set(win, browser);
+
+ Services.prefs.setBoolPref(
+ "media.videocontrols.picture-in-picture.video-toggle.has-used",
+ true
+ );
+ },
+
+ /**
+ * unload event has been called in player.js, cleanup our preserved
+ * browser object.
+ */
+ unload(window) {
+ TelemetryStopwatch.finish(
+ "FX_PICTURE_IN_PICTURE_WINDOW_OPEN_DURATION",
+ window
+ );
+
+ let reason = gCloseReasons.get(window) || "other";
+ Services.telemetry.keyedScalarAdd(
+ "pictureinpicture.closed_method",
+ reason,
+ 1
+ );
+ gCurrentPlayerCount -= 1;
+ // Saves the location of the Picture in Picture window
+ this.savePosition(window);
+ this.clearPipTabIcon(window);
+ },
+
+ /**
+ * Open a Picture in Picture window on the same screen as parentWin,
+ * sized based on the information in videoData.
+ *
+ * @param parentWin (chrome window)
+ * The window hosting the browser that requested the Picture in
+ * Picture window.
+ *
+ * @param videoData (object)
+ * An object containing the following properties:
+ *
+ * videoHeight (int):
+ * The preferred height of the video.
+ *
+ * videoWidth (int):
+ * The preferred width of the video.
+ *
+ * @param actorReference (PictureInPictureParent)
+ * Reference to the calling PictureInPictureParent
+ *
+ * @returns Promise
+ * Resolves once the window has opened and loaded the player component.
+ */
+ async openPipWindow(parentWin, videoData) {
+ let { top, left, width, height } = this.fitToScreen(parentWin, videoData);
+
+ let features =
+ `${PLAYER_FEATURES},top=${top},left=${left},` +
+ `outerWidth=${width},outerHeight=${height}`;
+
+ let pipWindow = Services.ww.openWindow(
+ parentWin,
+ PLAYER_URI,
+ null,
+ features,
+ null
+ );
+
+ TelemetryStopwatch.start(
+ "FX_PICTURE_IN_PICTURE_WINDOW_OPEN_DURATION",
+ pipWindow,
+ {
+ inSeconds: true,
+ }
+ );
+
+ return new Promise(resolve => {
+ pipWindow.addEventListener(
+ "load",
+ () => {
+ resolve(pipWindow);
+ },
+ { once: true }
+ );
+ });
+ },
+
+ /**
+ * This function tries to restore the last known Picture-in-Picture location
+ * and size. If those values are unknown or offscreen, then a default
+ * location and size is used.
+ *
+ * @param requestingWin (chrome window|player window)
+ * The window hosting the browser that requested the Picture in
+ * Picture window. If this is an existing player window then the returned
+ * player size and position will be determined based on the existing
+ * player window's size and position.
+ *
+ * @param videoData (object)
+ * An object containing the following properties:
+ *
+ * videoHeight (int):
+ * The preferred height of the video.
+ *
+ * videoWidth (int):
+ * The preferred width of the video.
+ *
+ * @returns (object)
+ * The size and position for the player window.
+ *
+ * top (int):
+ * The top position for the player window.
+ *
+ * left (int):
+ * The left position for the player window.
+ *
+ * width (int):
+ * The width of the player window.
+ *
+ * height (int):
+ * The height of the player window.
+ */
+ fitToScreen(requestingWin, videoData) {
+ let { videoHeight, videoWidth } = videoData;
+
+ const isPlayer = requestingWin.document.location.href == PLAYER_URI;
+
+ let top, left, width, height;
+ if (isPlayer) {
+ // requestingWin is a PiP player, conserve its dimensions in this case
+ left = requestingWin.screenX;
+ top = requestingWin.screenY;
+ width = requestingWin.innerWidth;
+ height = requestingWin.innerHeight;
+ } else {
+ // requestingWin is a content window, load last PiP's dimensions
+ ({ top, left, width, height } = this.loadPosition());
+ }
+
+ // Check that previous location and size were loaded
+ if (!isNaN(top) && !isNaN(left) && !isNaN(width) && !isNaN(height)) {
+ // Center position of PiP window
+ let centerX = left + width / 2;
+ let centerY = top + height / 2;
+
+ // Get the screen of the last PiP using the center of the PiP
+ // window to check.
+ // PiP screen will be the default screen if the center was
+ // not on a screen.
+ let PiPScreen = this.getWorkingScreen(centerX, centerY);
+
+ // We have the screen, now we will get the dimensions of the screen
+ let [
+ PiPScreenLeft,
+ PiPScreenTop,
+ PiPScreenWidth,
+ PiPScreenHeight,
+ ] = this.getAvailScreenSize(PiPScreen);
+
+ // Check that the center of the last PiP location is within the screen limits
+ // If it's not, then we will use the default size and position
+ if (
+ PiPScreenLeft <= centerX &&
+ centerX <= PiPScreenLeft + PiPScreenWidth &&
+ PiPScreenTop <= centerY &&
+ centerY <= PiPScreenTop + PiPScreenHeight
+ ) {
+ let oldWidth = width;
+
+ // The new PiP window will keep the height of the old
+ // PiP window and adjust the width to the correct ratio
+ width = Math.round((height * videoWidth) / videoHeight);
+
+ // Minimum window size on Windows is 136
+ if (AppConstants.platform == "win") {
+ width = 136 > width ? 136 : width;
+ }
+
+ // WIGGLE_ROOM allows the PiP window to be within 5 pixels of the right
+ // side of the screen to stay snapped to the right side
+ const WIGGLE_ROOM = 5;
+ // If the PiP window was right next to the right side of the screen
+ // then move the PiP window to the right the same distance that
+ // the width changes from previous width to current width
+ let rightScreen = PiPScreenLeft + PiPScreenWidth;
+ let distFromRight = rightScreen - (left + width);
+ if (
+ 0 < distFromRight &&
+ distFromRight <= WIGGLE_ROOM + (oldWidth - width)
+ ) {
+ left += distFromRight;
+ }
+
+ // Checks if some of the PiP window is off screen and
+ // if so it will adjust to move everything on screen
+ if (left < PiPScreenLeft) {
+ // off the left of the screen
+ // slide right
+ left += PiPScreenLeft - left;
+ }
+ if (top < PiPScreenTop) {
+ // off the top of the screen
+ // slide down
+ top += PiPScreenTop - top;
+ }
+ if (left + width > PiPScreenLeft + PiPScreenWidth) {
+ // off the right of the screen
+ // slide left
+ left += PiPScreenLeft + PiPScreenWidth - left - width;
+ }
+ if (top + height > PiPScreenTop + PiPScreenHeight) {
+ // off the bottom of the screen
+ // slide up
+ top += PiPScreenTop + PiPScreenHeight - top - height;
+ }
+ return { top, left, width, height };
+ }
+ }
+
+ // We don't have the size or position of the last PiP window, so fall
+ // back to calculating the default location.
+ let screen = this.getWorkingScreen(
+ requestingWin.screenX,
+ requestingWin.screenY,
+ requestingWin.innerWidth,
+ requestingWin.innerHeight
+ );
+ let [
+ screenLeft,
+ screenTop,
+ screenWidth,
+ screenHeight,
+ ] = this.getAvailScreenSize(screen);
+
+ // The Picture in Picture window will be a maximum of a quarter of
+ // the screen height, and a third of the screen width.
+ const MAX_HEIGHT = screenHeight / 4;
+ const MAX_WIDTH = screenWidth / 3;
+
+ width = videoWidth;
+ height = videoHeight;
+ let aspectRatio = videoWidth / videoHeight;
+
+ if (videoHeight > MAX_HEIGHT || videoWidth > MAX_WIDTH) {
+ // We're bigger than the max.
+ // Take the largest dimension and clamp it to the associated max.
+ // Recalculate the other dimension to maintain aspect ratio.
+ if (videoWidth >= videoHeight) {
+ // We're clamping the width, so the height must be adjusted to match
+ // the original aspect ratio. Since aspect ratio is width over height,
+ // that means we need to _divide_ the MAX_WIDTH by the aspect ratio to
+ // calculate the appropriate height.
+ width = MAX_WIDTH;
+ height = Math.round(MAX_WIDTH / aspectRatio);
+ } else {
+ // We're clamping the height, so the width must be adjusted to match
+ // the original aspect ratio. Since aspect ratio is width over height,
+ // this means we need to _multiply_ the MAX_HEIGHT by the aspect ratio
+ // to calculate the appropriate width.
+ height = MAX_HEIGHT;
+ width = Math.round(MAX_HEIGHT * aspectRatio);
+ }
+ }
+
+ // Now that we have the dimensions of the video, we need to figure out how
+ // to position it in the bottom right corner. Since we know the width of the
+ // available rect, we need to subtract the dimensions of the window we're
+ // opening to get the top left coordinates that openWindow expects.
+ //
+ // In event that the user has multiple displays connected, we have to
+ // calculate the top-left coordinate of the new window in absolute
+ // coordinates that span the entire display space, since this is what the
+ // openWindow expects for its top and left feature values.
+ //
+ // The screenWidth and screenHeight values only tell us the available
+ // dimensions on the screen that the parent window is on. We add these to
+ // the screenLeft and screenTop values, which tell us where this screen is
+ // located relative to the "origin" in absolute coordinates.
+ let isRTL = Services.locale.isAppLocaleRTL;
+ left = isRTL ? screenLeft : screenLeft + screenWidth - width;
+ top = screenTop + screenHeight - height;
+
+ return { top, left, width, height };
+ },
+
+ resizePictureInPictureWindow(videoData, actorRef) {
+ let win = this.getWeakPipPlayer(actorRef);
+
+ if (!win) {
+ return;
+ }
+
+ let { top, left, width, height } = this.fitToScreen(win, videoData);
+ win.resizeTo(width, height);
+ win.moveTo(left, top);
+ },
+
+ openToggleContextMenu(window, data) {
+ let document = window.document;
+ let popup = document.getElementById("pictureInPictureToggleContextMenu");
+
+ // We synthesize a new MouseEvent to propagate the inputSource to the
+ // subsequently triggered popupshowing event.
+ let newEvent = document.createEvent("MouseEvent");
+ newEvent.initNSMouseEvent(
+ "contextmenu",
+ true,
+ true,
+ null,
+ 0,
+ data.screenX,
+ data.screenY,
+ 0,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ null,
+ 0,
+ data.mozInputSource
+ );
+ popup.openPopupAtScreen(newEvent.screenX, newEvent.screenY, true, newEvent);
+ },
+
+ hideToggle() {
+ Services.prefs.setBoolPref(TOGGLE_ENABLED_PREF, false);
+ },
+
+ /**
+ * This function takes a screen and will return the left, top, width and
+ * height of the screen
+ * @param screen
+ * The screen we need to get the sizec and coordinates of
+ *
+ * @returns array
+ * Size and location of screen
+ *
+ * screenLeft.value (int):
+ * The left position for the screen.
+ *
+ * screenTop.value (int):
+ * The top position for the screen.
+ *
+ * screenWidth.value (int):
+ * The width of the screen.
+ *
+ * screenHeight.value (int):
+ * The height of the screen.
+ */
+ getAvailScreenSize(screen) {
+ let screenLeft = {},
+ screenTop = {},
+ screenWidth = {},
+ screenHeight = {};
+ screen.GetAvailRectDisplayPix(
+ screenLeft,
+ screenTop,
+ screenWidth,
+ screenHeight
+ );
+ let fullLeft = {},
+ fullTop = {},
+ fullWidth = {},
+ fullHeight = {};
+ screen.GetRectDisplayPix(fullLeft, fullTop, fullWidth, fullHeight);
+
+ // We have to divide these dimensions by the CSS scale factor for the
+ // display in order for the video to be positioned correctly on displays
+ // that are not at a 1.0 scaling.
+ let scaleFactor = screen.contentsScaleFactor / screen.defaultCSSScaleFactor;
+ screenWidth.value *= scaleFactor;
+ screenHeight.value *= scaleFactor;
+ screenLeft.value =
+ (screenLeft.value - fullLeft.value) * scaleFactor + fullLeft.value;
+ screenTop.value =
+ (screenTop.value - fullTop.value) * scaleFactor + fullTop.value;
+
+ return [
+ screenLeft.value,
+ screenTop.value,
+ screenWidth.value,
+ screenHeight.value,
+ ];
+ },
+
+ /**
+ * This function takes in a left and top value and returns the screen they
+ * are located on.
+ * If the left and top are not on any screen, it will return the
+ * default screen
+ * @param left
+ * left or x coordinate
+ * @param top
+ * top or y coordinate
+ *
+ * @returns screen
+ * the screen the left and top are on otherwise, default screen
+ */
+ getWorkingScreen(left, top, width = 1, height = 1) {
+ // Get the screen manager
+ let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(
+ Ci.nsIScreenManager
+ );
+ // use screenForRect to get screen
+ // this returns the default screen if left and top are not
+ // on any screen
+ let screen = screenManager.screenForRect(left, top, width, height);
+
+ return screen;
+ },
+
+ /**
+ * Saves position and size of Picture-in-Picture window
+ * @param win The Picture-in-Picture window
+ */
+ savePosition(win) {
+ let xulStore = Services.xulStore;
+
+ let left = win.screenX;
+ let top = win.screenY;
+ let width = win.innerWidth;
+ let height = win.innerHeight;
+
+ xulStore.setValue(PLAYER_URI, "picture-in-picture", "left", left);
+ xulStore.setValue(PLAYER_URI, "picture-in-picture", "top", top);
+ xulStore.setValue(PLAYER_URI, "picture-in-picture", "width", width);
+ xulStore.setValue(PLAYER_URI, "picture-in-picture", "height", height);
+ },
+
+ /**
+ * Load last Picture in Picture location and size
+ * @returns object
+ * The size and position of the last Picture in Picture window.
+ *
+ * top (int):
+ * The top position for the last player window.
+ * Otherwise NaN
+ *
+ * left (int):
+ * The left position for the last player window.
+ * Otherwise NaN
+ *
+ * width (int):
+ * The width of the player last window.
+ * Otherwise NaN
+ *
+ * height (int):
+ * The height of the player last window.
+ * Otherwise NaN
+ */
+ loadPosition() {
+ let xulStore = Services.xulStore;
+
+ let left = parseInt(
+ xulStore.getValue(PLAYER_URI, "picture-in-picture", "left")
+ );
+ let top = parseInt(
+ xulStore.getValue(PLAYER_URI, "picture-in-picture", "top")
+ );
+ let width = parseInt(
+ xulStore.getValue(PLAYER_URI, "picture-in-picture", "width")
+ );
+ let height = parseInt(
+ xulStore.getValue(PLAYER_URI, "picture-in-picture", "height")
+ );
+
+ return { top, left, width, height };
+ },
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ PictureInPicture,
+ "isMultiPipEnabled",
+ MULTI_PIP_ENABLED_PREF,
+ false
+);