From 55a53a25762f0391ff9c73691d7dee6ff494481f Mon Sep 17 00:00:00 2001 From: wuggy Date: Sun, 13 Sep 2026 12:18:07 -0700 Subject: [PATCH] Finish off PiP --- browser/base/content/pictureInPicture.css | 97 +++++++++++++++++++ browser/base/content/pictureInPicture.js | 77 +++++++++++++-- browser/base/content/pictureInPicture.xul | 36 +++++-- .../base/content/pictureInPictureContent.js | 48 ++++++++- .../locales/en-US/chrome/browser/browser.dtd | 2 + browser/modules/PictureInPicture.jsm | 2 +- widget/cocoa/nsCocoaWindow.h | 1 + widget/cocoa/nsCocoaWindow.mm | 15 +++ widget/gtk/nsWindow.cpp | 9 ++ widget/gtk/nsWindow.h | 1 + widget/nsIWidget.h | 7 ++ widget/windows/nsWindow.cpp | 15 +++ widget/windows/nsWindow.h | 1 + xpfe/appshell/nsXULWindow.cpp | 3 + 14 files changed, 293 insertions(+), 21 deletions(-) diff --git a/browser/base/content/pictureInPicture.css b/browser/base/content/pictureInPicture.css index 2055477436..6403392694 100644 --- a/browser/base/content/pictureInPicture.css +++ b/browser/base/content/pictureInPicture.css @@ -5,3 +5,100 @@ browser[remote="true"] { -moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser"); } + +#picture-in-picture { + min-width: 300px; + min-height: 170px; + background: black; + color: white; +} + +#player-container { + background: black; +} + +#player-overlay { + -moz-window-dragging: drag; +} + +.pip-controls { + opacity: 0; + pointer-events: none; + transition: opacity 150ms ease; +} + +#player-overlay:hover .pip-controls, +#player-overlay[keyboard] .pip-controls, +#player-overlay[seeking] .pip-controls { + opacity: 1; + pointer-events: auto; +} + +#top-controls { + padding: 8px; + background: linear-gradient(rgba(0, 0, 0, .55), transparent); +} + +#playback-controls { + padding: 12px 10px 8px; + background: linear-gradient(transparent, rgba(0, 0, 0, .85)); + -moz-window-dragging: no-drag; +} + +.pip-controls button { + -moz-appearance: none; + -moz-window-dragging: no-drag; + min-width: 0; + margin: 2px; + padding: 6px 8px; + border: 1px solid transparent; + border-radius: 4px; + background: rgba(30, 30, 30, .8); + color: white; + text-shadow: none; +} + +.pip-controls button:hover, +.pip-controls button:focus { + border-color: white; + background: #444; +} + +#close { + font-size: 22px; + width: 32px; + height: 32px; + padding: 0; +} + +#seek { + display: block; + width: 100%; + margin: 0 0 6px; + cursor: pointer; + -moz-window-dragging: no-drag; +} + +#seek:disabled { + opacity: .45; + cursor: default; +} + +#time { + margin: 0 6px; + font-variant-numeric: tabular-nums; +} + +#resize { + justify-self: end; + align-self: end; + width: 14px; + height: 14px; + cursor: se-resize; + -moz-window-dragging: no-drag; + opacity: 0; +} + +#resize:hover { + opacity: 1; +} diff --git a/browser/base/content/pictureInPicture.js b/browser/base/content/pictureInPicture.js index 99715d2a28..7f3df38927 100644 --- a/browser/base/content/pictureInPicture.js +++ b/browser/base/content/pictureInPicture.js @@ -11,6 +11,8 @@ var Player = { browser: null, source: null, sourceTab: null, + seeking: false, + state: null, init() { if (this.browser) { @@ -53,21 +55,70 @@ var Player = { mm.addMessageListener("PictureInPicture:Ready", ready); 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; + this.updateState(message.data); }); browser.addEventListener("oop-browser-crashed", this.close); mm.loadFrameScript("chrome://browser/content/pictureInPictureContent.js", false); }, - command(command) { + command(command, data = {}) { if (this.browser) { - this.browser.messageManager.sendAsyncMessage("PictureInPicture:Command", {command}); + this.browser.messageManager.sendAsyncMessage("PictureInPicture:Command", + Object.assign({command}, data)); } }, + formatTime(seconds) { + if (!Number.isFinite(seconds) || seconds < 0) { + return "0:00"; + } + seconds = Math.floor(seconds); + let hours = Math.floor(seconds / 3600); + let minutes = Math.floor(seconds / 60) % 60; + let pad = value => value < 10 ? "0" + value : String(value); + return (hours ? hours + ":" + pad(minutes) : String(minutes)) + + ":" + pad(seconds % 60); + }, + + updateTime(position) { + let duration = this.state && this.state.duration; + let total = Number.isFinite(duration) ? this.formatTime(duration) : + document.getElementById("live-label").value; + document.getElementById("time").value = this.formatTime(position) + " / " + total; + }, + + updateState(state) { + this.state = state; + document.getElementById("play").label = state.paused ? + this.playLabel : document.getElementById("pause-label").value; + document.getElementById("mute").label = state.muted ? + document.getElementById("unmute-label").value : this.muteLabel; + if (!this.seeking) { + let seek = document.getElementById("seek"); + seek.disabled = !(state.seekableEnd > state.seekableStart); + seek.min = state.seekableStart; + seek.max = state.seekableEnd || 1; + seek.value = state.currentTime; + this.updateTime(state.currentTime); + } + }, + + seek(value) { + let position = Number(value); + if (!Number.isFinite(position) || document.getElementById("seek").disabled) { + return; + } + this.seeking = true; + document.getElementById("player-overlay").setAttribute("seeking", "true"); + this.updateTime(position); + this.command("seek", {time: position}); + }, + + finishSeeking() { + this.seeking = false; + document.getElementById("player-overlay").removeAttribute("seeking"); + }, + returnToTab() { if (this.source && this.source.isConnected) { let win = this.source.ownerGlobal; @@ -104,10 +155,22 @@ window.addEventListener("unload", function onUnload(event) { } }); window.addEventListener("keydown", event => { + document.getElementById("player-overlay").setAttribute("keyboard", "true"); if (event.key == "Escape") { window.close(); - } else if (event.key == " " && event.target.localName != "button") { + } else if (event.target.localName == "input" || event.target.localName == "button") { + return; + } else if (event.key == " ") { event.preventDefault(); Player.command("playpause"); + } else if (event.key == "ArrowLeft" || event.key == "ArrowRight") { + event.preventDefault(); + if (Player.state) { + Player.command("seek", {time: Player.state.currentTime + + (event.key == "ArrowLeft" ? -5 : 5)}); + } } }); +window.addEventListener("mousemove", () => { + document.getElementById("player-overlay").removeAttribute("keyboard"); +}); diff --git a/browser/base/content/pictureInPicture.xul b/browser/base/content/pictureInPicture.xul index 865f05e645..9c37362607 100644 --- a/browser/base/content/pictureInPicture.xul +++ b/browser/base/content/pictureInPicture.xul @@ -6,17 +6,35 @@ + title="&pictureInPicture.title;" width="480" height="270" + hidechrome="true" + persist="screenX screenY width height">