author | Mike Conley <mconley@mozilla.com> |
Wed, 22 Jul 2020 16:02:13 +0000 | |
changeset 609681 | c2260bc5bbcffb9aaf56f858bdc98e551c607ea2 |
parent 609680 | 6355e6901d9896d8e85e45fa95b37397283e50c4 |
child 609682 | 02d2ee1875b0510e89e63109ef869ee1aa8f5404 |
push id | 2389 |
push user | ffxbld-merge |
push date | Mon, 17 Aug 2020 10:06:13 +0000 |
treeherder | mozilla-release@7ca9da01bf57 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | mstriemer, flod |
bugs | 1653663 |
milestone | 80.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -410,16 +410,19 @@ pref("media.decoder-doctor.decode-warnin pref("media.decoder-doctor.verbose", false); // URL to report decode issues pref("media.decoder-doctor.new-issue-endpoint", "https://webcompat.com/issues/new"); pref("media.videocontrols.picture-in-picture.enabled", false); pref("media.videocontrols.picture-in-picture.video-toggle.enabled", false); pref("media.videocontrols.picture-in-picture.video-toggle.always-show", false); pref("media.videocontrols.picture-in-picture.video-toggle.min-video-secs", 45); +pref("media.videocontrols.picture-in-picture.video-toggle.mode", -1); +pref("media.videocontrols.picture-in-picture.video-toggle.position", "right"); +pref("media.videocontrols.picture-in-picture.video-toggle.has-used", false); #ifdef MOZ_WEBRTC pref("media.navigator.video.enabled", true); pref("media.navigator.video.default_fps",30); pref("media.navigator.video.use_remb", true); pref("media.navigator.video.use_transport_cc", true); pref("media.peerconnection.video.use_rtx", true); pref("media.peerconnection.video.use_rtx.blocklist", "");
--- a/toolkit/actors/PictureInPictureChild.jsm +++ b/toolkit/actors/PictureInPictureChild.jsm @@ -21,25 +21,32 @@ ChromeUtils.defineModuleGetter( "TOGGLE_POLICIES", "resource://gre/modules/PictureInPictureTogglePolicy.jsm" ); ChromeUtils.defineModuleGetter( this, "TOGGLE_POLICY_STRINGS", "resource://gre/modules/PictureInPictureTogglePolicy.jsm" ); +ChromeUtils.defineModuleGetter( + this, + "Rect", + "resource://gre/modules/Geometry.jsm" +); const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); const TOGGLE_ENABLED_PREF = "media.videocontrols.picture-in-picture.video-toggle.enabled"; const TOGGLE_TESTING_PREF = "media.videocontrols.picture-in-picture.video-toggle.testing"; +const TOGGLE_EXPERIMENTAL_MODE_PREF = + "media.videocontrols.picture-in-picture.video-toggle.mode"; const MOUSEMOVE_PROCESSING_DELAY_MS = 50; const TOGGLE_HIDING_TIMEOUT_MS = 2000; // A weak reference to the most recent <video> in this content // process that is being viewed in Picture-in-Picture. var gWeakVideo = null; // A weak reference to the content window of the most recent // Picture-in-Picture window for this content process. @@ -68,44 +75,66 @@ class PictureInPictureToggleChild extend // We need to maintain some state about various things related to the // Picture-in-Picture toggles - however, for now, the same // PictureInPictureToggleChild might be re-used for different documents. // We keep the state stashed inside of this WeakMap, keyed on the document // itself. this.weakDocStates = new WeakMap(); this.toggleEnabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF); this.toggleTesting = Services.prefs.getBoolPref(TOGGLE_TESTING_PREF, false); + this.experimentalToggle = + Services.prefs.getIntPref(TOGGLE_EXPERIMENTAL_MODE_PREF, -1) != -1; // Bug 1570744 - JSWindowActorChild's cannot be used as nsIObserver's // directly, so we create a new function here instead to act as our // nsIObserver, which forwards the notification to the observe method. this.observerFunction = (subject, topic, data) => { this.observe(subject, topic, data); }; Services.prefs.addObserver(TOGGLE_ENABLED_PREF, this.observerFunction); + Services.prefs.addObserver( + TOGGLE_EXPERIMENTAL_MODE_PREF, + this.observerFunction + ); } willDestroy() { this.stopTrackingMouseOverVideos(); Services.prefs.removeObserver(TOGGLE_ENABLED_PREF, this.observerFunction); + Services.prefs.removeObserver( + TOGGLE_EXPERIMENTAL_MODE_PREF, + this.observerFunction + ); } observe(subject, topic, data) { - if (topic == "nsPref:changed" && data == TOGGLE_ENABLED_PREF) { - this.toggleEnabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF); + if (topic != "nsPref:changed") { + return; + } + + switch (data) { + case TOGGLE_ENABLED_PREF: { + this.toggleEnabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF); - if (this.toggleEnabled) { - // We have enabled the Picture-in-Picture toggle, so we need to make - // sure we register all of the videos that might already be on the page. - this.contentWindow.requestIdleCallback(() => { - let videos = this.document.querySelectorAll("video"); - for (let video of videos) { - this.registerVideo(video); - } - }); + if (this.toggleEnabled) { + // We have enabled the Picture-in-Picture toggle, so we need to make + // sure we register all of the videos that might already be on the page. + this.contentWindow.requestIdleCallback(() => { + let videos = this.document.querySelectorAll("video"); + for (let video of videos) { + this.registerVideo(video); + } + }); + } + break; + } + case TOGGLE_EXPERIMENTAL_MODE_PREF: { + this.experimentalToggle = + Services.prefs.getIntPref(TOGGLE_EXPERIMENTAL_MODE_PREF, -1) != -1; + break; } } } /** * Returns the state for the current document referred to via * this.document. If no such state exists, creates it, stores it * and returns it. @@ -526,17 +555,17 @@ class PictureInPictureToggleChild extend true, false, true /* aOnlyVisible */ ); if (!Array.from(elements).includes(video)) { return; } - let toggle = shadowRoot.getElementById("pictureInPictureToggleButton"); + let toggle = this.getToggleElement(shadowRoot); if (this.isMouseOverToggle(toggle, event)) { let state = this.docState; state.isClickingToggle = true; state.clickedElement = Cu.getWeakReference(event.originalTarget); event.stopImmediatePropagation(); Services.telemetry.keyedScalarAdd( "pictureinpicture.opened_method", @@ -697,17 +726,17 @@ class PictureInPictureToggleChild extend // if there was one. this.onMouseLeaveVideo(oldOverVideo); } return; } let state = this.docState; - let toggle = shadowRoot.getElementById("pictureInPictureToggleButton"); + let toggle = this.getToggleElement(shadowRoot); let controlsOverlay = shadowRoot.querySelector(".controlsOverlay"); if (!state.hasCheckedPolicy) { // We cache the matchers process-wide. We'll skip this while running tests to make that // easier. let toggleOverrides = this.toggleTesting ? PictureInPictureToggleChild.getToggleOverrides() : gToggleOverrides; @@ -788,17 +817,17 @@ class PictureInPictureToggleChild extend * @param {Element} video The video that the mouse left. */ onMouseLeaveVideo(video) { let state = this.docState; let shadowRoot = video.openOrClosedShadowRoot; if (shadowRoot) { let controlsOverlay = shadowRoot.querySelector(".controlsOverlay"); - let toggle = shadowRoot.getElementById("pictureInPictureToggleButton"); + let toggle = this.getToggleElement(shadowRoot); controlsOverlay.classList.remove("hovering"); toggle.classList.remove("hovering"); } state.weakOverVideo = null; if (!this.toggleTesting) { state.hideToggleDeferredTask.disarm(); @@ -817,16 +846,34 @@ class PictureInPictureToggleChild extend * * @return {Boolean} */ isMouseOverToggle(toggle, event) { let toggleRect = toggle.ownerGlobal.windowUtils.getBoundsWithoutFlushing( toggle ); + if (this.experimentalToggle) { + // The way the experimental toggles are currently implemented with + // absolute positioning, the root toggle element bounds don't actually + // contain all of the toggle child element bounds. Until we find a way to + // sort that out, we workaround the issue by having each clickable child + // elements of the toggle have a clicklable class, and then compute the + // smallest rect that contains all of their bounding rects and use that + // as the hitbox. + toggleRect = Rect.fromRect(toggleRect); + let clickableChildren = toggle.querySelectorAll(".clickable"); + for (let child of clickableChildren) { + let childRect = Rect.fromRect( + child.ownerGlobal.windowUtils.getBoundsWithoutFlushing(child) + ); + toggleRect.expandToContain(childRect); + } + } + // If the toggle has no dimensions, we're definitely not over it. if (!toggleRect.width || !toggleRect.height) { return false; } let { clientX, clientY } = event; return ( @@ -850,30 +897,44 @@ class PictureInPictureToggleChild extend return; } let shadowRoot = video.openOrClosedShadowRoot; if (!shadowRoot) { return; } - let toggle = shadowRoot.getElementById("pictureInPictureToggleButton"); + let toggle = this.getToggleElement(shadowRoot); if (this.isMouseOverToggle(toggle, event)) { event.stopImmediatePropagation(); event.preventDefault(); this.sendAsyncMessage("PictureInPicture:OpenToggleContextMenu", { screenX: event.screenX, screenY: event.screenY, mozInputSource: event.mozInputSource, }); } } /** + * Returns the appropriate root element for the Picture-in-Picture toggle, + * depending on whether or not we're using the experimental toggle preference. + * + * @param {Element} shadowRoot The shadowRoot of the video element. + * @returns {Element} The toggle element. + */ + getToggleElement(shadowRoot) { + if (!this.experimentalToggle) { + return shadowRoot.getElementById("pictureInPictureToggleButton"); + } + return shadowRoot.getElementById("pictureInPictureToggleExperiment"); + } + + /** * This is a test-only function that returns true if a video is being tracked * for mouseover events after having intersected the viewport. */ static isTracking(video) { return gWeakIntersectingVideosForTesting.has(video); } /**
--- a/toolkit/actors/UAWidgetsChild.jsm +++ b/toolkit/actors/UAWidgetsChild.jsm @@ -72,16 +72,19 @@ class UAWidgetsChild extends JSWindowAct case "video": case "audio": uri = "chrome://global/content/elements/videocontrols.js"; widgetName = "VideoControlsWidget"; prefKeys = [ "media.videocontrols.picture-in-picture.video-toggle.enabled", "media.videocontrols.picture-in-picture.video-toggle.always-show", "media.videocontrols.picture-in-picture.video-toggle.min-video-secs", + "media.videocontrols.picture-in-picture.video-toggle.mode", + "media.videocontrols.picture-in-picture.video-toggle.position", + "media.videocontrols.picture-in-picture.video-toggle.has-used", ]; break; case "input": uri = "chrome://global/content/elements/datetimebox.js"; widgetName = "DateTimeBoxWidget"; break; case "embed": case "object":
--- a/toolkit/components/pictureinpicture/PictureInPicture.jsm +++ b/toolkit/components/pictureinpicture/PictureInPicture.jsm @@ -236,16 +236,21 @@ var PictureInPicture = { win.setIsMutedState(videoData.isMuted); // set attribute which shows pip icon in tab let tab = parentWin.gBrowser.getTabForBrowser(browser); tab.setAttribute("pictureinpicture", true); win.setupPlayer(gNextWindowID.toString(), browser); gNextWindowID++; + + Services.prefs.setBoolPref( + "media.videocontrols.picture-in-picture.video-toggle.has-used", + true + ); }, /** * unload event has been called in player.js, cleanup our preserved * browser object. */ unload(window) { TelemetryStopwatch.finish(
--- a/toolkit/content/widgets/videocontrols.js +++ b/toolkit/content/widgets/videocontrols.js @@ -173,16 +173,85 @@ this.VideoControlsWidget = class { // Picture-in-Picture capability on <video> elements that have // srcObject != null. if (someVideo.srcObject) { return false; } return true; } + + /** + * Some variations on the Picture-in-Picture toggle are being experimented with. + * These variations have slightly different setup parameters from the currently + * shipping toggle, so this method sets up the experimental toggles in the event + * that they're being used. It also will enable the appropriate stylesheet for + * the preferred toggle experiment. + * + * @param {Object} prefs + * The preferences set that was passed to the UAWidget. + * @param {ShadowRoot} shadowRoot + * The shadowRoot of the <video> element where the video controls are. + * @param {Element} toggle + * The toggle element. + * @param {Object} reflowedDimensions + * An object representing the reflowed dimensions of the <video>. Properties + * are: + * + * videoWidth (Number): + * The width of the video in pixels. + * + * videoHeight (Number): + * The height of the video in pixels. + */ + static setupToggleExperiment(prefs, shadowRoot, toggle, reflowedDimensions) { + let mode = String( + prefs["media.videocontrols.picture-in-picture.video-toggle.mode"] + ); + let videocontrols = shadowRoot.firstChild; + let sheets = videocontrols.querySelectorAll("link[rel='stylesheet'][mode]"); + for (let sheet of sheets) { + sheet.disabled = sheet.getAttribute("mode") != mode; + } + + // These thresholds are all in pixels + const SMALL_VIDEO_WIDTH_MAX = 320; + const MEDIUM_VIDEO_WIDTH_MAX = 720; + + let isSmall = reflowedDimensions.videoWidth <= SMALL_VIDEO_WIDTH_MAX; + toggle.toggleAttribute("small-video", isSmall); + toggle.toggleAttribute( + "medium-video", + !isSmall && reflowedDimensions.videoWidth <= MEDIUM_VIDEO_WIDTH_MAX + ); + + toggle.setAttribute( + "position", + prefs["media.videocontrols.picture-in-picture.video-toggle.position"] + ); + toggle.toggleAttribute( + "has-used", + prefs["media.videocontrols.picture-in-picture.video-toggle.has-used"] + ); + } + + /** + * Disables any lingering stylesheets that might still be active after + * we've determined that a toggle experiment should be removed. + * + * @param {ShadowRoot} shadowRoot + * The shadowRoot of the <video> element where the video controls are. + */ + static cleanupToggleExperiment(shadowRoot) { + let videocontrols = shadowRoot.firstChild; + let sheets = videocontrols.querySelectorAll("link[rel='stylesheet'][mode]"); + for (let sheet of sheets) { + sheet.disabled = true; + } + } }; this.VideoControlsImplWidget = class { constructor(shadowRoot, prefs) { this.shadowRoot = shadowRoot; this.prefs = prefs; this.element = shadowRoot.host; this.document = this.element.ownerDocument; @@ -543,19 +612,38 @@ this.VideoControlsImplWidget = class { this.pipToggleEnabled && !this.isShowingPictureInPictureMessage && VideoControlsWidget.shouldShowPictureInPictureToggle( this.prefs, this.video, this.reflowedDimensions ) ) { - this.pictureInPictureToggleButton.removeAttribute("hidden"); + if ( + this.prefs[ + "media.videocontrols.picture-in-picture.video-toggle.mode" + ] == -1 + ) { + VideoControlsWidget.cleanupToggleExperiment(this.shadowRoot); + this.pictureInPictureToggleButton.removeAttribute("hidden"); + this.pictureInPictureToggleExperiment.setAttribute("hidden", true); + } else { + this.pictureInPictureToggleButton.setAttribute("hidden", true); + this.pictureInPictureToggleExperiment.removeAttribute("hidden"); + VideoControlsWidget.setupToggleExperiment( + this.prefs, + this.shadowRoot, + this.pictureInPictureToggleExperiment, + this.reflowedDimensions + ); + } } else { + VideoControlsWidget.cleanupToggleExperiment(this.shadowRoot); this.pictureInPictureToggleButton.setAttribute("hidden", true); + this.pictureInPictureToggleExperiment.setAttribute("hidden", true); } }, setupNewLoadState() { // For videos with |autoplay| set, we'll leave the controls initially hidden, // so that they don't get in the way of the playing video. Otherwise we'll // go ahead and reveal the controls now, so they're an obvious user cue. var shouldShow = @@ -2308,16 +2396,19 @@ this.VideoControlsImplWidget = class { ); this.textTrackList = this.shadowRoot.getElementById("textTrackList"); this.textTrackListContainer = this.shadowRoot.getElementById( "textTrackListContainer" ); this.pictureInPictureToggleButton = this.shadowRoot.getElementById( "pictureInPictureToggleButton" ); + this.pictureInPictureToggleExperiment = this.shadowRoot.getElementById( + "pictureInPictureToggleExperiment" + ); if (this.positionDurationBox) { this.durationSpan = this.positionDurationBox.getElementsByTagName( "span" )[0]; } let isMobile = this.window.navigator.appVersion.includes("Android"); @@ -2589,16 +2680,18 @@ this.VideoControlsImplWidget = class { parser.forceEnableDTD(); let parserDoc = parser.parseFromString( `<!DOCTYPE bindings [ <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd"> %videocontrolsDTD; ]> <div class="videocontrols" xmlns="http://www.w3.org/1999/xhtml" role="none"> <link rel="stylesheet" href="chrome://global/skin/media/videocontrols.css" /> + <link rel="stylesheet" href="chrome://global/skin/media/pictureinpicture-mode-1.css" mode="1" disabled="true" /> + <link rel="stylesheet" href="chrome://global/skin/media/pictureinpicture-mode-2.css" mode="2" disabled="true" /> <div id="controlsContainer" class="controlsContainer" role="none"> <div id="statusOverlay" class="statusOverlay stackItem" hidden="true"> <div id="statusIcon" class="statusIcon"></div> <bdi class="statusLabel" id="errorAborted">&error.aborted;</bdi> <bdi class="statusLabel" id="errorNetwork">&error.network;</bdi> <bdi class="statusLabel" id="errorDecode">&error.decode;</bdi> <bdi class="statusLabel" id="errorSrcNotSupported">&error.srcNotSupported;</bdi> <bdi class="statusLabel" id="errorNoSource">&error.noSource2;</bdi> @@ -2616,16 +2709,30 @@ this.VideoControlsImplWidget = class { <div id="clickToPlay" class="clickToPlay" hidden="true"></div> </div> <button id="pictureInPictureToggleButton" class="pictureInPictureToggleButton"> <div id="pictureInPictureToggleIcon" class="pictureInPictureToggleIcon"></div> <span class="pictureInPictureToggleLabel">&pictureInPicture.label;</span> </button> + <button id="pictureInPictureToggleExperiment" class="pip-wrapper" position="left" hidden="true"> + <div class="pip-small clickable"></div> + <div class="pip-expanded clickable"> + <span class="pip-icon-label clickable"> + <span class="pip-icon"></span> + <span class="pip-label">&pictureInPictureToggle.label;</span> + </span> + <div class="pip-explainer clickable"> + &pictureInPictureExplainer; + </div> + </div> + <div class="pip-icon clickable"></div> + </button> + <div id="controlBar" class="controlBar" role="none" hidden="true"> <button id="playButton" class="button playButton" playlabel="&playButton.playLabel;" pauselabel="&playButton.pauseLabel;" tabindex="-1"/> <div id="scrubberStack" class="scrubberStack progressContainer" role="none"> <div class="progressBackgroundBar stackItem" role="none"> @@ -2967,35 +3074,58 @@ this.NoControlsDesktopImplWidget = class if ( this.pipToggleEnabled && VideoControlsWidget.shouldShowPictureInPictureToggle( this.prefs, this.video, this.reflowedDimensions ) ) { - this.pictureInPictureToggleButton.removeAttribute("hidden"); + if ( + this.prefs[ + "media.videocontrols.picture-in-picture.video-toggle.mode" + ] == -1 + ) { + VideoControlsWidget.cleanupToggleExperiment(this.shadowRoot); + this.pictureInPictureToggleButton.removeAttribute("hidden"); + this.pictureInPictureToggleExperiment.setAttribute("hidden", true); + } else { + this.pictureInPictureToggleButton.setAttribute("hidden", true); + this.pictureInPictureToggleExperiment.removeAttribute("hidden"); + VideoControlsWidget.setupToggleExperiment( + this.prefs, + this.shadowRoot, + this.pictureInPictureToggleExperiment, + this.reflowedDimensions + ); + } } else { + VideoControlsWidget.cleanupToggleExperiment(this.shadowRoot); this.pictureInPictureToggleButton.setAttribute("hidden", true); + this.pictureInPictureToggleExperiment.setAttribute("hidden", true); } }, init(shadowRoot, prefs) { this.shadowRoot = shadowRoot; this.prefs = prefs; this.video = shadowRoot.host; this.videocontrols = shadowRoot.firstChild; this.document = this.videocontrols.ownerDocument; this.window = this.document.defaultView; this.shadowRoot = shadowRoot; this.pictureInPictureToggleButton = this.shadowRoot.getElementById( "pictureInPictureToggleButton" ); + this.pictureInPictureToggleExperiment = this.shadowRoot.getElementById( + "pictureInPictureToggleExperiment" + ); + if (this.document.fullscreenElement) { this.videocontrols.setAttribute("inDOMFullscreen", true); } // Default the Picture-in-Picture toggle button to being hidden. We might unhide it // later if we determine that this video is qualified to show it. this.pictureInPictureToggleButton.setAttribute("hidden", true); @@ -3072,22 +3202,37 @@ this.NoControlsDesktopImplWidget = class parser.forceEnableDTD(); let parserDoc = parser.parseFromString( `<!DOCTYPE bindings [ <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd"> %videocontrolsDTD; ]> <div class="videocontrols" xmlns="http://www.w3.org/1999/xhtml" role="none"> <link rel="stylesheet" href="chrome://global/skin/media/videocontrols.css" /> + <link rel="stylesheet" href="chrome://global/skin/media/pictureinpicture-mode-1.css" mode="1" disabled="true" /> + <link rel="stylesheet" href="chrome://global/skin/media/pictureinpicture-mode-2.css" mode="2" disabled="true" /> <div id="controlsContainer" class="controlsContainer" role="none"> <div class="controlsOverlay stackItem"> <button id="pictureInPictureToggleButton" class="pictureInPictureToggleButton"> <div id="pictureInPictureToggleIcon" class="pictureInPictureToggleIcon"></div> <span class="pictureInPictureToggleLabel">&pictureInPicture.label;</span> </button> + <button id="pictureInPictureToggleExperiment" class="pip-wrapper" position="left" hidden="true"> + <div class="pip-small clickable"></div> + <div class="pip-expanded clickable"> + <span class="pip-icon-label clickable"> + <span class="pip-icon"></span> + <span class="pip-label">&pictureInPictureToggle.label;</span> + </span> + <div class="pip-explainer clickable"> + &pictureInPictureExplainer; + </div> + </div> + <div class="pip-icon"></div> + </button> </div> </div> </div>`, "application/xml" ); this.shadowRoot.importNodeAndAppendChildAt( this.shadowRoot, parserDoc.documentElement,
--- a/toolkit/locales/en-US/chrome/global/videocontrols.dtd +++ b/toolkit/locales/en-US/chrome/global/videocontrols.dtd @@ -1,28 +1,41 @@ <!-- 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/. --> +<!ENTITY % brandDTD + SYSTEM "chrome://branding/locale/brand.dtd"> + %brandDTD; + <!ENTITY playButton.playLabel "Play"> <!ENTITY playButton.pauseLabel "Pause"> <!ENTITY muteButton.muteLabel "Mute"> <!ENTITY muteButton.unmuteLabel "Unmute"> <!ENTITY fullscreenButton.enterfullscreenlabel "Full Screen"> <!ENTITY fullscreenButton.exitfullscreenlabel "Exit Full Screen"> <!ENTITY castingButton.castingLabel "Cast to Screen"> <!ENTITY closedCaption.off "Off"> <!-- LOCALIZATION NOTE (volumeScrubber.label): This label is exposed to accessibility software to clarify what the slider is for. --> <!ENTITY volumeScrubber.label "Volume slider"> <!-- LOCALIZATION NOTE (pictureInPicture.label): This string is used as part of the Picture-in-Picture video toggle button when the mouse is hovering it. --> <!ENTITY pictureInPicture.label "Picture-in-Picture"> +<!-- LOCALIZATION NOTE (pictureInPictureToggle.label): This string is used as the +label for a variation of the Picture-in-Picture video toggle button when the mouse is +hovering over the video. --> +<!ENTITY pictureInPictureToggle.label "Watch in Picture-in-Picture"> +<!-- LOCALIZATION NOTE (pictureInPictureExplainer): This string is used as part of +a variation of the Picture-in-Picture video toggle button. When using this variation, +this string appears below the toggle when the mouse hovers the toggle. --> +<!ENTITY pictureInPictureExplainer "Play videos in the foreground while you do other things in &brandShortName;"> + <!ENTITY error.aborted "Video loading stopped."> <!ENTITY error.network "Video playback aborted due to a network error."> <!ENTITY error.decode "Video can’t be played because the file is corrupt."> <!ENTITY error.srcNotSupported "Video format or MIME type is not supported."> <!ENTITY error.noSource2 "No video with supported format and MIME type found."> <!ENTITY error.generic "Video playback aborted due to an unknown error."> <!ENTITY status.pictureInPicture "This video is playing in Picture-in-Picture mode.">
--- a/toolkit/themes/shared/jar.inc.mn +++ b/toolkit/themes/shared/jar.inc.mn @@ -104,16 +104,18 @@ toolkit.jar: skin/classic/global/media/closedCaptionButton-cc-on.svg (../../shared/media/closedCaptionButton-cc-on.svg) skin/classic/global/media/fullscreenEnterButton.svg (../../shared/media/fullscreenEnterButton.svg) skin/classic/global/media/fullscreenExitButton.svg (../../shared/media/fullscreenExitButton.svg) skin/classic/global/media/TopLevelImageDocument.css (../../shared/media/TopLevelImageDocument.css) skin/classic/global/media/TopLevelVideoDocument.css (../../shared/media/TopLevelVideoDocument.css) skin/classic/global/media/imagedoc-lightnoise.png (../../shared/media/imagedoc-lightnoise.png) skin/classic/global/media/imagedoc-darknoise.png (../../shared/media/imagedoc-darknoise.png) * skin/classic/global/media/videocontrols.css (../../shared/media/videocontrols.css) + skin/classic/global/media/pictureinpicture-mode-1.css (../../shared/media/pictureinpicture-mode-1.css) + skin/classic/global/media/pictureinpicture-mode-2.css (../../shared/media/pictureinpicture-mode-2.css) skin/classic/global/media/pauseButton.svg (../../shared/media/pauseButton.svg) skin/classic/global/media/playButton.svg (../../shared/media/playButton.svg) skin/classic/global/media/error.png (../../shared/media/error.png) skin/classic/global/media/throbber.png (../../shared/media/throbber.png) skin/classic/global/media/stalled.png (../../shared/media/stalled.png) #ifdef MOZ_PLACES skin/classic/mozapps/places/defaultFavicon.svg (../../shared/places/defaultFavicon.svg) #endif
new file mode 100644 --- /dev/null +++ b/toolkit/themes/shared/media/pictureinpicture-mode-1.css @@ -0,0 +1,56 @@ +/* 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/. */ + +@namespace url("http://www.w3.org/1999/xhtml"); + +.pip-expanded, +.pip-small { + border: 1px solid rgba(255,255,255,0.1); + box-sizing: border-box; +} + +.pip-expanded { + translate: -33%; + transition: opacity 250ms, scale 200ms, translate 190ms; + transition-timing-function: cubic-bezier(.07,.95,0,1); + justify-content: center; + pointer-events: none; +} + +.pip-wrapper[position="right"] > .pip-expanded > .pip-icon-label > .pip-label { + margin-right: var(--pip-icon-width-with-margins); + margin-left: var(--pip-toggle-margin); +} + +.pip-wrapper[position="right"] > .pip-expanded { + translate: calc(-100% + 48px); + transform-origin: right; +} + +.pip-wrapper.hovering:not([small-video]) > .pip-small + .pip-expanded, +.pip-wrapper.hovering:not([small-video]) > .pip-expanded { + opacity: 1; + scale: 1; + pointer-events: auto; +} + +.pip-wrapper[position="left"].hovering:not([small-video]) > .pip-expanded { + translate: 0; +} + +.pip-wrapper[position="right"].hovering:not([small-video]) > .pip-small + .pip-expanded, +.pip-wrapper[position="right"].hovering:not([small-video]) > .pip-expanded { + translate: calc(-100% + var(--pip-icon-width-with-margins)); +} + +.pip-wrapper.hovering:not([small-video]) > .pip-small { + opacity: 0; + transition: opacity 200ms; +} + +.pip-wrapper:is([small-video]) > .pip-expanded, +.pip-explainer, +.pip-icon-label > .pip-icon { + display: none; +}
new file mode 100644 --- /dev/null +++ b/toolkit/themes/shared/media/pictureinpicture-mode-2.css @@ -0,0 +1,126 @@ +/* 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/. */ + +@namespace url("http://www.w3.org/1999/xhtml"); + +.pip-wrapper { + top: calc(70% - 40px); + --pip-highlight-width: 2px; +} + +.pip-wrapper[position="left"] > .pip-expanded > .pip-icon-label > .pip-icon { + display: none; +} + +.pip-expanded, +.pip-small { + border: 1px solid rgba(255,255,255,0.1); + box-sizing: border-box; +} + +.pip-expanded { + border: var(--pip-highlight-width) solid transparent; + border-bottom: 1px solid transparent; + transition: opacity 250ms, scale 200ms, translate 190ms; + justify-content: left; +} + +.pip-wrapper[position="right"] > .pip-expanded { + translate: calc(-100% + 48px); + transform-origin: right; + justify-content: right; +} + +.pip-wrapper:is([small-video],[has-used]) > .pip-expanded, +.pip-wrapper[position="right"]:not(:is([small-video],[has-used])) > .pip-icon { + display: none; +} + +.pip-wrapper[position="right"] > .pip-expanded > .pip-icon-label > .pip-icon { + position: relative; + top: 0; + left: 0; + display: inline-block; +} + +.pip-wrapper[position="right"] > .pip-expanded > .pip-icon-label { + display: flex; + flex-direction: row; + align-content: center; +} + +.pip-wrapper[position="right"] > .pip-expanded > .pip-icon-label > .pip-icon, +.pip-wrapper[position="right"] > .pip-expanded > .pip-icon-label > .pip-label { + margin-top: auto; + margin-bottom: auto; +} + +.pip-wrapper[position="right"] > .pip-expanded > .pip-icon-label > .pip-icon { + margin-left: var(--pip-toggle-margin); + margin-right: var(--pip-toggle-margin); +} + +.pip-wrapper[position="right"] > .pip-expanded > .pip-icon-label > .pip-label { + margin-right: var(--pip-toggle-margin); +} + +.pip-wrapper.hovering > .pip-expanded { + box-shadow: none; + border: var(--pip-highlight-width) solid rgba(0, 254, 255, 1); + /* Remove bottom border but keep text centred with padding. */ + border-bottom: none; + padding-bottom: 1px; + pointer-events: none; +} + +.pip-wrapper:not(:is([small-video],[has-used])) > .pip-expanded { + opacity: 1; + scale: 1; + pointer-events: auto; +} + +.pip-wrapper:not(:is([small-video],[has-used])).hovering > .pip-expanded { + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; +} + +.pip-wrapper:not(:is([small-video],[has-used])) > .pip-small { + opacity: 0; + transition: opacity 200ms; +} + +.pip-explainer { + opacity: 0; +} + +.pip-explainer { + padding: 6px 16px 8px 8px; + translate: 0; + transition: opacity 250ms, translate 190ms; + transition-timing-function: cubic-bezier(.07,.95,0,1); + background: rgba(12,12,13,0.65); + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; + border: 2px solid rgba(0, 254, 255, 1); + border-top: 0; + box-shadow: 0px 4px 4px rgba(12,12,13,0.25); + opacity: 0; + margin-left: -2px; + margin-right: -2px; + width: calc(100% - 24px); + word-break: break-word; + pointer-events: none; +} + +.pip-wrapper.hovering > .pip-expanded > .pip-explainer { + pointer-events: auto; +} + +.pip-wrapper.hovering > .pip-expanded > .pip-explainer { + opacity: 1; +} + +.pip-wrapper.hovering > .pip-expanded > .pip-explainer { + translate: 0 calc(40px - var(--pip-highlight-width)); +}
--- a/toolkit/themes/shared/media/videocontrols.css +++ b/toolkit/themes/shared/media/videocontrols.css @@ -613,8 +613,118 @@ %ifdef XP_WIN @media (-moz-windows-default-theme: 0) { .controlsSpacer, .clickToPlay { background-color: transparent; } } %endif + +.pip-wrapper { + position: absolute; + cursor: pointer; + -moz-appearance: none; + background: none; + border: none; + text-align: unset; + top: calc(75% - 40px); + opacity: 0; + transition: opacity 200ms; + --pip-icon-size: 24px; + --pip-toggle-margin: 8px; + --pip-icon-width-with-margins: calc(2 * var(--pip-toggle-margin) + var(--pip-icon-size)); +} + +.pip-wrapper[policy="hidden"] { + display: none; +} + +.pip-label { + font-size: 16px; + line-height: 1.2; +} + +.pip-expanded { + font-size: 14px; +} + +.pip-wrapper[medium-video] > .pip-expanded > .pip-icon-label > .pip-label { + font-size: 13px; +} + +.pip-wrapper[medium-video] > .pip-expanded { + font-size: 11px; +} + +.controlsOverlay.hovering > .pip-wrapper { + opacity: 0.8; +} + +.controlsOverlay[hidetoggle="true"].hovering > .pip-wrapper:not(.hovering) { + opacity: 0; +} + +.controlsOverlay.hovering > .pip-wrapper.hovering { + opacity: 1; +} + +.pip-wrapper[position="right"] { + right: 40px; +} + +.pip-wrapper[position="left"] { + left: 12px; +} + +.pip-expanded, +.pip-small, +.pip-icon, +.pip-explainer { + position: absolute; + left: 0; + top: 0; +} + +.pip-icon { + top: 8px; + left: 8px; + pointer-events: none; + background-image: url("chrome://global/skin/media/pictureinpicture.svg"); + background-size: var(--pip-icon-size) var(--pip-icon-size); + -moz-context-properties: fill; + fill: #fff; + height: var(--pip-icon-size); + width: var(--pip-icon-size); + background-repeat: no-repeat; + background-position: center, center; +} + +.pip-wrapper[position="left"] > .pip-expanded > .pip-icon-label > .pip-label { + margin-left: var(--pip-icon-width-with-margins); + margin-right: var(--pip-toggle-margin); +} + +.pip-expanded, +.pip-explainer { + user-select: none; +} + +.pip-small { + background-color: rgba(12,12,13,0.65); + box-shadow: 0px 4px 4px rgba(12,12,13,0.25); + width: 40px; + height: 40px; + border-radius: 25px; +} + +.pip-expanded { + display: flex; + height: 40px; + background-color: rgba(12,12,13,0.9); + box-shadow: 0px 4px 4px rgba(12,12,13,0.25); + width: max-content; + border-radius: 8px; + opacity: 0; + color: #fff; + align-items: center; + scale: 0.33 1; +}