diff --git a/browser/base/content/browser-context.inc b/browser/base/content/browser-context.inc index 526f0f69a6..bbdb0139fd 100644 --- a/browser/base/content/browser-context.inc +++ b/browser/base/content/browser-context.inc @@ -159,6 +159,9 @@ label="&mediaHideControls.label;" accesskey="&mediaHideControls.accesskey;" oncommand="gContextMenu.mediaCommand('hidecontrols');"/> + { () => { let media = message.objects.element; switch (message.data.command) { + case "pictureinpicture": { + let {PictureInPicture} = Cu.import("resource:///modules/PictureInPicture.jsm", {}); + let data; + try { + data = PictureInPicture.register(media, message.data.data); + } catch (error) { + data = {id: message.data.data, error: error.message}; + } + sendAsyncMessage("PictureInPicture:Prepared", data); + break; + } case "play": media.play(); break; diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js index 5a47c1269f..61740a1599 100644 --- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -442,6 +442,11 @@ nsContextMenu.prototype = { this.showItem("context-media-loop", onMedia); this.showItem("context-media-showcontrols", onMedia && !this.target.controls); this.showItem("context-media-hidecontrols", this.target.controls && (this.onVideo || (this.onAudio && !this.inSyntheticDoc))); + this.showItem("context-video-pictureinpicture", this.onVideo); + this.setItemAttr("context-video-pictureinpicture", "disabled", + !this.onVideo || this.target.error || + !this.target.videoWidth || !this.target.videoHeight || + !!this.target.mediaKeys); this.showItem("context-video-fullscreen", this.onVideo && this.target.ownerDocument.fullscreenElement == null); this.showItem("context-media-eme-learnmore", this.onDRMMedia); this.showItem("context-media-eme-separator", this.onDRMMedia); @@ -1682,6 +1687,11 @@ nsContextMenu.prototype = { this.browser.messageManager.sendAsyncMessage("SwitchDocumentDirection"); }, + pictureInPicture: function() { + let {PictureInPicture} = Cu.import("resource:///modules/PictureInPicture.jsm", {}); + PictureInPicture.open(this.browser, this.target); + }, + mediaCommand : function(command, data) { let mm = this.browser.messageManager; let win = this.browser.ownerGlobal; diff --git a/browser/base/content/pictureInPicture.js b/browser/base/content/pictureInPicture.js new file mode 100644 index 0000000000..ce9cc5a894 --- /dev/null +++ b/browser/base/content/pictureInPicture.js @@ -0,0 +1,87 @@ +/* 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 Player = { + browser: null, + source: null, + sourceTab: null, + + init() { + let args = window.arguments[0]; + this.source = args.browser; + if (!this.source || !this.source.isConnected) { + window.close(); + return; + } + this.sourceTab = this.source.ownerGlobal.gBrowser.getTabForBrowser(this.source); + this.close = () => window.close(); + this.sourceTab.addEventListener("TabClose", this.close); + this.source.addEventListener("oop-browser-crashed", this.close); + + this.playLabel = document.getElementById("play").label; + this.muteLabel = document.getElementById("mute").label; + let browser = this.browser = document.createElement("browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("flex", "1"); + browser.setAttribute("disablehistory", "true"); + browser.setAttribute("remote", this.source.isRemoteBrowser ? "true" : "false"); + if (this.source.isRemoteBrowser) { + browser.setAttribute("remoteType", this.source.getAttribute("remoteType")); + } + // The process-local source reference requires this process affinity. + browser.relatedBrowser = this.source; + document.getElementById("player-container").appendChild(browser); + let mm = browser.messageManager; + mm.addMessageListener("PictureInPicture:Ready", () => { + mm.sendAsyncMessage("PictureInPicture:Init", {id: args.id}); + }); + mm.addMessageListener("PictureInPicture:Close", this.close); + mm.addMessageListener("PictureInPicture:State", message => { + document.getElementById("play").label = message.data.paused ? + this.playLabel : document.getElementById("pause-label").value; + document.getElementById("mute").label = message.data.muted ? + document.getElementById("unmute-label").value : this.muteLabel; + }); + browser.addEventListener("oop-browser-crashed", this.close); + mm.loadFrameScript("chrome://browser/content/pictureInPictureContent.js", false); + }, + + command(command) { + if (this.browser) { + this.browser.messageManager.sendAsyncMessage("PictureInPicture:Command", {command}); + } + }, + + returnToTab() { + if (this.source && this.source.isConnected) { + let win = this.source.ownerGlobal; + win.gBrowser.selectedTab = this.sourceTab; + win.focus(); + } + window.close(); + }, + + destroy() { + if (this.sourceTab) { + this.sourceTab.removeEventListener("TabClose", this.close); + this.source.removeEventListener("oop-browser-crashed", this.close); + } + if (this.browser) { + this.command("close"); + } + this.source = this.sourceTab = null; + }, +}; + +window.addEventListener("load", () => Player.init(), {once: true}); +window.addEventListener("unload", () => Player.destroy(), {once: true}); +window.addEventListener("keydown", event => { + if (event.key == "Escape") { + window.close(); + } else if (event.key == " " && event.target.localName != "button") { + event.preventDefault(); + Player.command("playpause"); + } +}); diff --git a/browser/base/content/pictureInPicture.xul b/browser/base/content/pictureInPicture.xul new file mode 100644 index 0000000000..c8d23854e9 --- /dev/null +++ b/browser/base/content/pictureInPicture.xul @@ -0,0 +1,21 @@ + + + + + +