зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1520329 - Add messaging infrastructure for opening videos in a Picture in Picture window. r=Felipe
Differential Revision: https://phabricator.services.mozilla.com/D16903 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
1677d55556
Коммит
52278792ca
|
@ -94,7 +94,12 @@ const messageListeners = {
|
|||
if (this.content.document.fullscreenEnabled) {
|
||||
media.requestFullscreen();
|
||||
}
|
||||
|
||||
break;
|
||||
case "pictureinpicture":
|
||||
let event = new this.content.CustomEvent("MozTogglePictureInPicture", {
|
||||
bubbles: true,
|
||||
}, this.content);
|
||||
media.dispatchEvent(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1486,6 +1486,9 @@ pref("media.autoplay.block-webaudio", true);
|
|||
pref("media.autoplay.block-webaudio", false);
|
||||
#endif
|
||||
|
||||
#ifdef NIGHTLY_BUILD
|
||||
pref("media.videocontrols.picture-in-picture.enabled", false);
|
||||
#endif
|
||||
|
||||
// Play with different values of the decay time and get telemetry,
|
||||
// 0 means to randomize (and persist) the experiment value in users' profiles,
|
||||
|
|
|
@ -175,6 +175,13 @@
|
|||
accesskey="&leaveDOMFullScreen.accesskey;"
|
||||
label="&leaveDOMFullScreen.label;"
|
||||
oncommand="gContextMenu.leaveDOMFullScreen();"/>
|
||||
#ifdef NIGHTLY_BUILD
|
||||
<!-- Don't forget to add a properly localized label and access key
|
||||
before letting this ride up to beta. -->
|
||||
<menuitem id="context-video-pictureinpicture"
|
||||
label="Picture in Picture"
|
||||
oncommand="gContextMenu.mediaCommand('pictureinpicture');"/>
|
||||
#endif
|
||||
<menuseparator id="context-media-sep-commands"/>
|
||||
<menuitem id="context-reloadimage"
|
||||
label="&reloadImageCmd.label;"
|
||||
|
|
|
@ -670,6 +670,12 @@ nsContextMenu.prototype = {
|
|||
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-fullscreen", this.onVideo && !this.target.ownerDocument.fullscreen);
|
||||
if (AppConstants.NIGHTLY_BUILD) {
|
||||
let shouldDisplay = Services.prefs.getBoolPref("media.videocontrols.picture-in-picture.enabled") &&
|
||||
this.onVideo &&
|
||||
!this.target.ownerDocument.fullscreen;
|
||||
this.showItem("context-video-pictureinpicture", shouldDisplay);
|
||||
}
|
||||
this.showItem("context-media-eme-learnmore", this.onDRMMedia);
|
||||
this.showItem("context-media-eme-separator", this.onDRMMedia);
|
||||
|
||||
|
|
|
@ -415,6 +415,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
|||
PageThumbs: "resource://gre/modules/PageThumbs.jsm",
|
||||
PdfJs: "resource://pdf.js/PdfJs.jsm",
|
||||
PermissionUI: "resource:///modules/PermissionUI.jsm",
|
||||
PictureInPicture: "resource://gre/modules/PictureInPicture.jsm",
|
||||
PingCentre: "resource:///modules/PingCentre.jsm",
|
||||
PlacesBackups: "resource://gre/modules/PlacesBackups.jsm",
|
||||
PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
|
||||
|
@ -512,6 +513,8 @@ const listeners = {
|
|||
"ContentSearch": ["ContentSearch"],
|
||||
"FormValidation:ShowPopup": ["FormValidationHandler"],
|
||||
"FormValidation:HidePopup": ["FormValidationHandler"],
|
||||
"PictureInPicture:Request": ["PictureInPicture"],
|
||||
"PictureInPicture:Close": ["PictureInPicture"],
|
||||
"Prompt:Open": ["RemotePrompt"],
|
||||
"Reader:FaviconRequest": ["ReaderParent"],
|
||||
"Reader:UpdateReaderButton": ["ReaderParent"],
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
/* -*- 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 stream = originatingVideo.mozCaptureStream();
|
||||
|
||||
let doc = this.content.document;
|
||||
let playerVideo = doc.createElement("video");
|
||||
playerVideo.srcObject = stream;
|
||||
playerVideo.removeAttribute("controls");
|
||||
playerVideo.setAttribute("autoplay", "true");
|
||||
|
||||
// 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";
|
||||
|
||||
playerVideo.play();
|
||||
|
||||
// A little hack to make the current frame show up in the player
|
||||
if (originatingVideo.paused) {
|
||||
await originatingVideo.play();
|
||||
await originatingVideo.pause();
|
||||
}
|
||||
|
||||
doc.body.appendChild(playerVideo);
|
||||
|
||||
let originatingWindow = originatingVideo.ownerGlobal;
|
||||
originatingWindow.addEventListener("unload", (e) => {
|
||||
this.closePictureInPicture(originatingVideo);
|
||||
}, { once: true });
|
||||
|
||||
this.content.addEventListener("unload", () => {
|
||||
gWeakVideo = null;
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
|
@ -40,3 +40,8 @@ FINAL_TARGET_FILES.actors += [
|
|||
'WebNavigationChild.jsm',
|
||||
'ZoomChild.jsm',
|
||||
]
|
||||
|
||||
if CONFIG['NIGHTLY_BUILD']:
|
||||
FINAL_TARGET_FILES.actors += [
|
||||
'PictureInPictureChild.jsm',
|
||||
]
|
||||
|
|
|
@ -87,6 +87,9 @@ if CONFIG['MOZ_BUILD_APP'] != 'mobile/android':
|
|||
if CONFIG['NS_PRINTING']:
|
||||
DIRS += ['printing']
|
||||
|
||||
if CONFIG['NIGHTLY_BUILD']:
|
||||
DIRS += ['pictureinpicture']
|
||||
|
||||
if CONFIG['BUILD_CTYPES']:
|
||||
DIRS += ['ctypes']
|
||||
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
/* 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"];
|
||||
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const PLAYER_URI = "chrome://global/content/pictureinpicture/player.xhtml";
|
||||
const PLAYER_FEATURES = `chrome,titlebar=no,alwaysontop,resizable`;
|
||||
const WINDOW_TYPE = "Toolkit:PictureInPicture";
|
||||
|
||||
/**
|
||||
* 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
|
||||
receiveMessage(aMessage) {
|
||||
let browser = aMessage.target;
|
||||
|
||||
switch (aMessage.name) {
|
||||
case "PictureInPicture:Request": {
|
||||
let videoData = aMessage.data;
|
||||
this.handlePictureInPictureRequest(browser, videoData);
|
||||
break;
|
||||
}
|
||||
case "PictureInPicture:Close": {
|
||||
/**
|
||||
* Content has requested that its Picture in Picture window go away.
|
||||
*/
|
||||
this.closePipWindow();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find and close any pre-existing Picture in Picture windows.
|
||||
*/
|
||||
closePipWindow() {
|
||||
// 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;
|
||||
}
|
||||
|
||||
win.close();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A request has come up from content to open a Picture in Picture
|
||||
* window.
|
||||
*
|
||||
* @param browser (xul:browser)
|
||||
* The browser 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(browser, videoData) {
|
||||
let parentWin = browser.ownerGlobal;
|
||||
this.closePipWindow();
|
||||
let win = await this.openPipWindow(parentWin, videoData);
|
||||
win.setupPlayer(browser, videoData);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @returns Promise
|
||||
* Resolves once the window has opened and loaded the player component.
|
||||
*/
|
||||
async openPipWindow(parentWin, videoData) {
|
||||
let { videoHeight, videoWidth } = videoData;
|
||||
|
||||
// The Picture in Picture window will open on the same display as the
|
||||
// originating window, and anchor to the bottom right.
|
||||
let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
|
||||
.getService(Ci.nsIScreenManager);
|
||||
let screen = screenManager.screenForRect(parentWin.screenX,
|
||||
parentWin.screenY, 1, 1);
|
||||
|
||||
// Now that we have the right screen, let's see how much available
|
||||
// real-estate there is for us to work with.
|
||||
let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {};
|
||||
screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth,
|
||||
screenHeight);
|
||||
|
||||
// For now, 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.value / 4;
|
||||
const MAX_WIDTH = screenWidth.value / 3;
|
||||
|
||||
let resultWidth = videoWidth;
|
||||
let resultHeight = videoHeight;
|
||||
|
||||
if (videoHeight > MAX_HEIGHT || videoWidth > MAX_WIDTH) {
|
||||
let aspectRatio = videoWidth / videoHeight;
|
||||
// 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.
|
||||
resultWidth = MAX_WIDTH;
|
||||
resultHeight = Math.floor(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.
|
||||
resultHeight = MAX_HEIGHT;
|
||||
resultWidth = Math.floor(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.
|
||||
let pipLeft = screenWidth.value - resultWidth;
|
||||
let pipTop = screenHeight.value - resultHeight;
|
||||
let features = `${PLAYER_FEATURES},top=${pipTop},left=${pipLeft},` +
|
||||
`width=${resultWidth},height=${resultHeight}`;
|
||||
|
||||
let pipWindow =
|
||||
Services.ww.openWindow(parentWin, PLAYER_URI, null, features, null);
|
||||
|
||||
return new Promise(resolve => {
|
||||
pipWindow.addEventListener("load", () => {
|
||||
resolve(pipWindow);
|
||||
}, { once: true });
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/* 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/. */
|
||||
|
||||
async function setupPlayer(originatingBrowser, videoData) {
|
||||
window.windowUtils.setChromeMargin(0, 0, 0, 0);
|
||||
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,
|
||||
});
|
||||
|
||||
let mm = browser.frameLoader.messageManager;
|
||||
mm.sendAsyncMessage("PictureInPicture:SetupPlayer");
|
||||
|
||||
// If the content process hosting the video crashes, let's
|
||||
// just close the window for now.
|
||||
browser.addEventListener("oop-browser-crashed", () => {
|
||||
window.close();
|
||||
});
|
||||
|
||||
await window.promiseDocumentFlushed(() => {});
|
||||
browser.style.MozWindowDragging = "drag";
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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/. -->
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
|
||||
%htmlDTD;
|
||||
]>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
windowtype="Toolkit:PictureInPicture">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="chrome://global/skin/pictureinpicture/player.css"/>
|
||||
<script type="application/javascript"
|
||||
src="chrome://global/content/pictureinpicture/player.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="player-holder">
|
||||
<xul:browser type="content" primary="true" remote="true" remoteType="web" id="browser"></xul:browser>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
#filter substitution
|
||||
# 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/.
|
||||
|
||||
toolkit.jar:
|
||||
content/global/pictureinpicture/player.xhtml (content/player.xhtml)
|
||||
content/global/pictureinpicture/player.js (content/player.js)
|
|
@ -0,0 +1,11 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'PictureInPicture.jsm',
|
||||
]
|
|
@ -97,6 +97,7 @@ var EXPORTED_SYMBOLS = ["ActorManagerParent"];
|
|||
|
||||
const {ExtensionUtils} = ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
|
||||
|
||||
const {DefaultMap} = ExtensionUtils;
|
||||
|
||||
|
@ -337,6 +338,21 @@ let ACTORS = {
|
|||
},
|
||||
};
|
||||
|
||||
if (AppConstants.NIGHTLY_BUILD) {
|
||||
ACTORS.PictureInPicture = {
|
||||
child: {
|
||||
module: "resource://gre/actors/PictureInPictureChild.jsm",
|
||||
events: {
|
||||
"MozTogglePictureInPicture": {capture: true, wantUntrusted: true},
|
||||
},
|
||||
|
||||
messages: [
|
||||
"PictureInPicture:SetupPlayer",
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class ActorSet {
|
||||
constructor(group, actorSide) {
|
||||
this.group = group;
|
||||
|
|
|
@ -105,3 +105,4 @@ toolkit.jar:
|
|||
skin/classic/global/plugins/contentPluginBlocked.png (../../shared/plugins/contentPluginBlocked.png)
|
||||
skin/classic/global/plugins/contentPluginCrashed.png (../../shared/plugins/contentPluginCrashed.png)
|
||||
skin/classic/global/plugins/contentPluginStripe.png (../../shared/plugins/contentPluginStripe.png)
|
||||
skin/classic/global/pictureinpicture/player.css (../../shared/pictureinpicture/player.css)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/* 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/. */
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.player-holder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
browser {
|
||||
flex: 1;
|
||||
}
|
Загрузка…
Ссылка в новой задаче