/* 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/. */ import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs"; /** * A request is pending from the moment the launch event is sent until either * its player is set up or a newer request supersedes it, and active from then * until the window closes. The embedder can only show one window, so a request * supersedes whatever came before it, whichever session it came from. * * @typedef {object} PiP * @property {"pending"|"active"} state * @property {object} sourceDispatcher * EventDispatcher of the source session. * @property {object} videoRef * ContentDOMReference to the video element, handed to the player actor to * clone. Absent for a placement no request has claimed. * @property {Element} playerBrowser * Active only. Browser of the player session showing the cloned video. * @property {number} bcGroupId * Pending only. Browsing context group of the source, which the player * session is opened in. * @property {string} remoteType * Pending only. Content process the player session has to be opened in, for * the same reason. * @property {?string} token * Pending only. Placement token handed to the embedder, until a session is * opened against it. Null once consumed: a token places a single session. * @property {Function} resolve * Pending only. Settles the promise returned by request() once the player is * set up. * @property {Function} reject * Pending only. Settles that promise if setting the player up failed. */ /** @type {?PiP} */ let gPiP = null; /** * Process-global state coordinating a picture-in-picture window with the session * whose video it clones. */ export const GeckoViewPictureInPictureController = { // `sourceDispatcher` must be the EventDispatcher of the session the video lives in: the embedder // identifies the tab to return to when the picture-in-picture window is expanded by which // session the launch event arrives on. request(sourceWGP, videoRef, sourceDispatcher) { const bc = sourceWGP.browsingContext; const bcGroupId = bc.group.id; const remoteType = sourceWGP.domProcess.remoteType; if (!sourceDispatcher) { return Promise.reject( new Error("No EventDispatcher for the requesting session") ); } // The window already clones this video. A second player would not help, since a video is // cloned into a single element and the old player stops the clone when it unloads. if ( gPiP?.state === "active" && gPiP.videoRef.browsingContextId === videoRef.browsingContextId && gPiP.videoRef.id === videoRef.id ) { return Promise.resolve(); } // A request that supersedes an unanswered one takes its placement with it. this._takePending()?.reject?.( new Error("Superseded by a newer picture-in-picture request") ); const token = this.mintPlacementToken(bcGroupId, remoteType); const { promise, resolve, reject } = Promise.withResolvers(); Object.assign(gPiP, { videoRef, sourceDispatcher, resolve, reject, }); sourceDispatcher.sendRequest("GeckoView:PictureInPicture:Launch", { placementToken: token, }); return promise; }, // Mints a token standing for the placement a session cloning the video has to be opened in, to // be handed to the embedder. Good for a single session, until it is consumed. mintPlacementToken(bcGroupId, remoteType) { const token = Services.uuid.generateUUID().toString(); gPiP = { state: "pending", bcGroupId, remoteType, token }; return token; }, // Returns the browsing context group and remote type `token` stands for, and invalidates it so // that it places a single session. Returns null for a token that was never handed out, was // already used, or belongs to a request that has since been superseded. consumePlacement(token) { if (gPiP?.token !== token) { warn`No placement for token ${token}, opening the session unplaced`; return null; } gPiP.token = null; const { bcGroupId, remoteType } = gPiP; return { browsingContextGroupId: bcGroupId, remoteType }; }, // Requests picture-in-picture for the video the focused frame is playing. async requestForCurrentMedia(browser, sourceDispatcher) { // The session's own browsing context only reaches the top document, whose