mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-20 23:37:33 +09:00
PiP initial commit
This commit is contained in:
parent
5ec3ae4a4c
commit
9a1d4edaa9
14 changed files with 377 additions and 1 deletions
|
|
@ -159,6 +159,9 @@
|
|||
label="&mediaHideControls.label;"
|
||||
accesskey="&mediaHideControls.accesskey;"
|
||||
oncommand="gContextMenu.mediaCommand('hidecontrols');"/>
|
||||
<menuitem id="context-video-pictureinpicture"
|
||||
label="&pictureInPicture.title;"
|
||||
oncommand="gContextMenu.pictureInPicture();"/>
|
||||
<menuitem id="context-video-fullscreen"
|
||||
accesskey="&videoFullScreen.accesskey;"
|
||||
label="&videoFullScreen.label;"
|
||||
|
|
|
|||
|
|
@ -681,6 +681,17 @@ addMessageListener("ContextMenu:MediaCommand", (message) => {
|
|||
() => {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
87
browser/base/content/pictureInPicture.js
Normal file
87
browser/base/content/pictureInPicture.js
Normal file
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
21
browser/base/content/pictureInPicture.xul
Normal file
21
browser/base/content/pictureInPicture.xul
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- 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/. -->
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<!DOCTYPE window SYSTEM "chrome://browser/locale/browser.dtd">
|
||||
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
id="picture-in-picture" windowtype="Browser:PictureInPicture"
|
||||
title="&pictureInPicture.title;" width="480" height="318"
|
||||
persist="screenX screenY width height" style="min-width: 240px; min-height: 160px;">
|
||||
<script type="application/javascript" src="chrome://browser/content/pictureInPicture.js"/>
|
||||
<vbox id="player-container" flex="1" style="background: black;"/>
|
||||
<hbox align="center" pack="center">
|
||||
<button id="play" label="&mediaPlay.label;" oncommand="Player.command('playpause');"/>
|
||||
<button id="mute" label="&mediaMute.label;" oncommand="Player.command('mute');"/>
|
||||
<button id="return" label="&pictureInPicture.return;" oncommand="Player.returnToTab();"/>
|
||||
<button id="close" label="&pictureInPicture.close;" oncommand="window.close();"/>
|
||||
</hbox>
|
||||
<label id="pause-label" value="&mediaPause.label;" hidden="true"/>
|
||||
<label id="unmute-label" value="&mediaUnmute.label;" hidden="true"/>
|
||||
</window>
|
||||
129
browser/base/content/pictureInPictureContent.js
Normal file
129
browser/base/content/pictureInPictureContent.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/* 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 {PictureInPicture} = Components.utils.import(
|
||||
"resource:///modules/PictureInPicture.jsm", {});
|
||||
var source = null;
|
||||
var stream = null;
|
||||
var output = null;
|
||||
var still = null;
|
||||
var observer = null;
|
||||
var sourceWindow = null;
|
||||
var events = ["play", "pause", "volumechange", "ended", "seeked", "resize"];
|
||||
|
||||
function update() {
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
// Capture streams do not produce frames while their source is paused.
|
||||
// Keep a snapshot for that case, without changing the source's play state.
|
||||
still.hidden = !source.paused && !source.ended;
|
||||
if (!still.hidden && source.readyState >= 2) {
|
||||
still.width = source.videoWidth;
|
||||
still.height = source.videoHeight;
|
||||
still.getContext("2d").drawImage(source, 0, 0, still.width, still.height);
|
||||
}
|
||||
sendAsyncMessage("PictureInPicture:State",
|
||||
{paused: source.paused || source.ended, muted: source.muted});
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
if (source) {
|
||||
for (let name of events) {
|
||||
source.removeEventListener(name, update);
|
||||
}
|
||||
source.removeEventListener("emptied", closePlayer);
|
||||
source.removeEventListener("error", closePlayer);
|
||||
sourceWindow.removeEventListener("pagehide", closePlayer);
|
||||
source.mozPictureInPicture = false;
|
||||
source = sourceWindow = null;
|
||||
}
|
||||
if (output) {
|
||||
output.srcObject = null;
|
||||
output = null;
|
||||
}
|
||||
if (stream) {
|
||||
for (let track of stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
stream = null;
|
||||
}
|
||||
still = null;
|
||||
}
|
||||
|
||||
function closePlayer() {
|
||||
cleanup();
|
||||
sendAsyncMessage("PictureInPicture:Close");
|
||||
}
|
||||
|
||||
addMessageListener("PictureInPicture:Init", message => {
|
||||
try {
|
||||
source = PictureInPicture.takeSource(message.data.id);
|
||||
sourceWindow = source.ownerGlobal;
|
||||
let doc = content.document;
|
||||
doc.documentElement.style.cssText = "height:100%;background:black";
|
||||
doc.body.style.cssText = "margin:0;height:100%;overflow:hidden";
|
||||
output = doc.createElement("video");
|
||||
still = doc.createElement("canvas");
|
||||
for (let element of [output, still]) {
|
||||
element.style.cssText = "position:absolute;width:100%;height:100%;object-fit:contain";
|
||||
doc.body.appendChild(element);
|
||||
}
|
||||
output.muted = true; // Audio continues to come from the originating video.
|
||||
source.mozPictureInPicture = true;
|
||||
stream = source.mozCaptureStream();
|
||||
output.srcObject = stream;
|
||||
output.play().catch(error => {
|
||||
Components.utils.reportError(error);
|
||||
closePlayer();
|
||||
});
|
||||
for (let name of events) {
|
||||
source.addEventListener(name, update);
|
||||
}
|
||||
source.addEventListener("emptied", closePlayer);
|
||||
source.addEventListener("error", closePlayer);
|
||||
sourceWindow.addEventListener("pagehide", closePlayer);
|
||||
observer = new sourceWindow.MutationObserver(() => {
|
||||
if (source && !source.isConnected) {
|
||||
closePlayer();
|
||||
}
|
||||
});
|
||||
observer.observe(source.ownerDocument, {childList: true, subtree: true});
|
||||
update();
|
||||
} catch (error) {
|
||||
Components.utils.reportError(error);
|
||||
closePlayer();
|
||||
}
|
||||
});
|
||||
|
||||
addMessageListener("PictureInPicture:Command", message => {
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
switch (message.data.command) {
|
||||
case "playpause":
|
||||
if (source.paused || source.ended) {
|
||||
source.play().catch(Components.utils.reportError);
|
||||
} else {
|
||||
source.pause();
|
||||
}
|
||||
break;
|
||||
case "mute":
|
||||
source.muted = !source.muted;
|
||||
break;
|
||||
case "close":
|
||||
cleanup();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Frame-script unload also covers a parent window closing before an async
|
||||
// close command can be delivered.
|
||||
addEventListener("unload", cleanup);
|
||||
sendAsyncMessage("PictureInPicture:Ready");
|
||||
|
|
@ -2,6 +2,9 @@
|
|||
# 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/.
|
||||
browser.jar:
|
||||
content/browser/pictureInPicture.xul (content/pictureInPicture.xul)
|
||||
content/browser/pictureInPicture.js (content/pictureInPicture.js)
|
||||
content/browser/pictureInPictureContent.js (content/pictureInPictureContent.js)
|
||||
% content browser %content/browser/ contentaccessible=yes
|
||||
#ifdef XP_MACOSX
|
||||
% overlay chrome://mozapps/content/downloads/downloads.xul chrome://browser/content/downloadManagerOverlay.xul
|
||||
|
|
|
|||
|
|
@ -777,3 +777,7 @@ you can use these alternative items. Otherwise, their values should be empty. -
|
|||
|
||||
<!ENTITY emeLearnMoreContextMenu.label "Learn more about DRM…">
|
||||
<!ENTITY emeLearnMoreContextMenu.accesskey "D">
|
||||
|
||||
<!ENTITY pictureInPicture.title "Picture-in-Picture">
|
||||
<!ENTITY pictureInPicture.return "Return to tab">
|
||||
<!ENTITY pictureInPicture.close "Close">
|
||||
|
|
|
|||
84
browser/modules/PictureInPicture.jsm
Normal file
84
browser/modules/PictureInPicture.jsm
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/* 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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PictureInPicture"];
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
|
||||
// Each content process has its own module instance. Only a weak reference to
|
||||
// the source crosses between frame scripts; DOM objects never cross processes.
|
||||
this.PictureInPicture = {
|
||||
source: null,
|
||||
window: null,
|
||||
request: 0,
|
||||
|
||||
register(video, id) {
|
||||
if (!video || video.localName != "video" || video.error ||
|
||||
!video.videoWidth || !video.videoHeight || video.mediaKeys) {
|
||||
throw new Error("This video is not available for Picture-in-Picture");
|
||||
}
|
||||
this.source = {id, video: Cu.getWeakReference(video)};
|
||||
return {id, width: video.videoWidth, height: video.videoHeight};
|
||||
},
|
||||
|
||||
takeSource(id) {
|
||||
if (!this.source || this.source.id != id) {
|
||||
throw new Error("Picture-in-Picture source process is unavailable");
|
||||
}
|
||||
let video = this.source.video.get();
|
||||
this.source = null;
|
||||
if (!video || !video.isConnected) {
|
||||
throw new Error("Picture-in-Picture source has been removed");
|
||||
}
|
||||
return video;
|
||||
},
|
||||
|
||||
open(browser, video) {
|
||||
let id = Services.uuid.generateUUID().toString();
|
||||
let request = ++this.request;
|
||||
let mm = browser.messageManager;
|
||||
let timer;
|
||||
let ready = message => {
|
||||
if (message.data.id != id) {
|
||||
return;
|
||||
}
|
||||
mm.removeMessageListener("PictureInPicture:Prepared", ready);
|
||||
clearTimeout(timer);
|
||||
if (request != this.request || !browser.isConnected) {
|
||||
return;
|
||||
}
|
||||
if (message.data.error) {
|
||||
Cu.reportError(message.data.error);
|
||||
return;
|
||||
}
|
||||
this.openWindow(browser, message.data);
|
||||
};
|
||||
mm.addMessageListener("PictureInPicture:Prepared", ready);
|
||||
timer = setTimeout(() => {
|
||||
mm.removeMessageListener("PictureInPicture:Prepared", ready);
|
||||
}, 10000);
|
||||
mm.sendAsyncMessage("ContextMenu:MediaCommand",
|
||||
{command: "pictureinpicture", data: id},
|
||||
{element: video});
|
||||
},
|
||||
|
||||
openWindow(browser, data) {
|
||||
if (this.window && !this.window.closed) {
|
||||
this.window.close();
|
||||
}
|
||||
let screen = browser.ownerGlobal.screen;
|
||||
let width = Math.min(480, screen.availWidth);
|
||||
let height = Math.min(Math.round(width * data.height / data.width) + 48,
|
||||
Math.round(screen.availHeight * 0.8));
|
||||
this.window = browser.ownerGlobal.openDialog(
|
||||
"chrome://browser/content/pictureInPicture.xul", "",
|
||||
"chrome,dialog=no,resizable,alwaysRaised,width=" + width +
|
||||
",height=" + height,
|
||||
{browser, id: data.id});
|
||||
return this.window;
|
||||
},
|
||||
};
|
||||
|
|
@ -22,6 +22,7 @@ EXTRA_JS_MODULES += [
|
|||
'NetworkPrioritizer.jsm',
|
||||
'offlineAppCache.jsm',
|
||||
'PermissionUI.jsm',
|
||||
'PictureInPicture.jsm',
|
||||
'PluginContent.jsm',
|
||||
'ProcessHangMonitor.jsm',
|
||||
'ReaderParent.jsm',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue