diff --git a/browser/base/content/tabbrowser.js b/browser/base/content/tabbrowser.js index 75513e7d2263..96928a9cf879 100644 --- a/browser/base/content/tabbrowser.js +++ b/browser/base/content/tabbrowser.js @@ -63,6 +63,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { E10SUtils: "resource://gre/modules/E10SUtils.jsm", + PictureInPicture: "resource://gre/modules/PictureInPicture.jsm", }); XPCOMUtils.defineLazyServiceGetters(this, { MacSharingService: [ @@ -5290,7 +5291,8 @@ (aBrowser == this.selectedBrowser && window.windowState != window.STATE_MINIMIZED && !window.isFullyOccluded) || - this._printPreviewBrowsers.has(aBrowser) + this._printPreviewBrowsers.has(aBrowser) || + this.PictureInPicture.isOriginatingBrowser(aBrowser) ); }, diff --git a/browser/modules/AsyncTabSwitcher.jsm b/browser/modules/AsyncTabSwitcher.jsm index 4cffcd5f812a..50a1c400384e 100644 --- a/browser/modules/AsyncTabSwitcher.jsm +++ b/browser/modules/AsyncTabSwitcher.jsm @@ -12,6 +12,7 @@ const { XPCOMUtils } = ChromeUtils.import( ); XPCOMUtils.defineLazyModuleGetters(this, { AppConstants: "resource://gre/modules/AppConstants.jsm", + PictureInPicture: "resource://gre/modules/PictureInPicture.jsm", Services: "resource://gre/modules/Services.jsm", }); @@ -649,8 +650,7 @@ class AsyncTabSwitcher { let numPending = 0; let numWarming = 0; for (let [tab, state] of this.tabState) { - // Skip print preview browsers since they shouldn't affect tab switching. - if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) { + if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) { continue; } @@ -726,7 +726,7 @@ class AsyncTabSwitcher { // Unload any tabs that can be unloaded. for (let [tab, state] of this.tabState) { - if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) { + if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) { continue; } @@ -852,8 +852,7 @@ class AsyncTabSwitcher { onSizeModeOrOcclusionStateChange() { if (this.minimizedOrFullyOccluded) { for (let [tab, state] of this.tabState) { - // Skip print preview browsers since they shouldn't affect tab switching. - if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) { + if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) { continue; } @@ -915,6 +914,19 @@ class AsyncTabSwitcher { } } + /** + * Check if the browser should be deactivated. If the browser is a print preivew or + * PiP browser then we won't deactive it. + * @param browser The browser to check if it should be deactivated + * @returns false if a print preview or PiP browser else true + */ + shouldDeactivateDocShell(browser) { + return !( + this.tabbrowser._printPreviewBrowsers.has(browser) || + PictureInPicture.isOriginatingBrowser(browser) + ); + } + shouldActivateDocShell(browser) { let tab = this.tabbrowser.getTabForBrowser(browser); let state = this.getTabState(tab); @@ -1220,6 +1232,8 @@ class AsyncTabSwitcher { let linkedBrowser = tab.linkedBrowser; let isActive = linkedBrowser && linkedBrowser.docShellIsActive; let isRendered = linkedBrowser && linkedBrowser.renderLayers; + let isPiP = + linkedBrowser && PictureInPicture.isOriginatingBrowser(linkedBrowser); if (tab === this.lastVisibleTab) { tabString += "V"; @@ -1253,6 +1267,9 @@ class AsyncTabSwitcher { if (isRendered) { extraStates += "R"; } + if (isPiP) { + extraStates += "P"; + } if (extraStates != "") { tabString += `(${extraStates})`; } diff --git a/toolkit/components/pictureinpicture/PictureInPicture.jsm b/toolkit/components/pictureinpicture/PictureInPicture.jsm index e2fc5d1a1610..d2fef3fab91c 100644 --- a/toolkit/components/pictureinpicture/PictureInPicture.jsm +++ b/toolkit/components/pictureinpicture/PictureInPicture.jsm @@ -148,6 +148,9 @@ var PictureInPicture = { // Maps PiP player windows to their originating content's browser weakWinToBrowser: new WeakMap(), + // Maps a browser to the number of PiP windows it has + browserWeakMap: new WeakMap(), + /** * Returns the player window if one exists and if it hasn't yet been closed. * @@ -173,12 +176,39 @@ var PictureInPicture = { } }, + /** + * Increase the count of PiP windows for a given browser + * @param browser The browser to increase PiP count in browserWeakMap + */ + addPiPBrowserToWeakMap(browser) { + let count = this.browserWeakMap.has(browser) + ? this.browserWeakMap.get(browser) + : 0; + this.browserWeakMap.set(browser, count + 1); + }, + + /** + * Decrease the count of PiP windows for a given browser. + * If the count becomes 0, we will remove the browser from the WeakMap + * @param browser The browser to decrease PiP count in browserWeakMap + */ + removePiPBrowserFromWeakMap(browser) { + let count = this.browserWeakMap.get(browser); + if (count <= 1) { + this.browserWeakMap.delete(browser); + } else { + this.browserWeakMap.set(browser, count - 1); + } + }, + onPipSwappedBrowsers(event) { let otherTab = event.detail; if (otherTab) { for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) { if (this.weakWinToBrowser.get(win) === event.target.linkedBrowser) { this.weakWinToBrowser.set(win, otherTab.linkedBrowser); + this.removePiPBrowserFromWeakMap(event.target.linkedBrowser); + this.addPiPBrowserToWeakMap(otherTab.linkedBrowser); } } otherTab.addEventListener("TabSwapPictureInPicture", this); @@ -285,6 +315,8 @@ var PictureInPicture = { if (!win) { return; } + this.removePiPBrowserFromWeakMap(this.weakWinToBrowser.get(win)); + await this.closePipWindow(win); gCloseReasons.set(win, reason); }, @@ -335,6 +367,7 @@ var PictureInPicture = { gNextWindowID++; this.weakWinToBrowser.set(win, browser); + this.addPiPBrowserToWeakMap(browser); Services.prefs.setBoolPref( "media.videocontrols.picture-in-picture.video-toggle.has-used", @@ -719,6 +752,18 @@ var PictureInPicture = { Services.prefs.setBoolPref(TOGGLE_ENABLED_PREF, false); }, + /** + * This is used in AsyncTabSwitcher.jsm and tabbrowser.js to check if the browser + * currently has a PiP window. + * If the browser has a PiP window we want to keep the browser in an active state because + * the browser is still partially visible. + * @param browser The browser to check if it has a PiP window + * @returns true if browser has PiP window else false + */ + isOriginatingBrowser(browser) { + return this.browserWeakMap.has(browser); + }, + moveToggle() { // Get the current position let position = Services.prefs.getStringPref( diff --git a/toolkit/components/pictureinpicture/tests/browser.ini b/toolkit/components/pictureinpicture/tests/browser.ini index 042bad08c7a2..05e83f1d2172 100644 --- a/toolkit/components/pictureinpicture/tests/browser.ini +++ b/toolkit/components/pictureinpicture/tests/browser.ini @@ -37,6 +37,7 @@ prefs = media.videocontrols.picture-in-picture.video-toggle.position="right" [browser_aaa_run_first_firstTimePiPToggleEvents.js] +[browser_backgroundTab.js] [browser_cannotTriggerFromContent.js] [browser_close_unpip_focus.js] [browser_closePipPause.js] diff --git a/toolkit/components/pictureinpicture/tests/browser_backgroundTab.js b/toolkit/components/pictureinpicture/tests/browser_backgroundTab.js new file mode 100644 index 000000000000..b04f3f3414b2 --- /dev/null +++ b/toolkit/components/pictureinpicture/tests/browser_backgroundTab.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +/** + * This test creates a PiP window, then switches to another tab and confirms + * that the PiP tab is still active. + */ +add_task(async () => { + let videoID = "no-controls"; + let firstTab = gBrowser.selectedTab; + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let originatingTab = gBrowser.getTabForBrowser(browser); + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + await BrowserTestUtils.switchTab(gBrowser, firstTab); + + let switcher = gBrowser._getSwitcher(); + + Assert.equal( + switcher.getTabState(originatingTab), + switcher.STATE_LOADED, + "The originating browser tab should be in STATE_LOADED." + ); + + await BrowserTestUtils.closeWindow(pipWin); + } + ); +}); + +/** + * This test creates a PiP window, then minimizes the browser and confirms + * that the PiP tab is still active. + */ +add_task(async () => { + let videoID = "no-controls"; + await BrowserTestUtils.withNewTab( + { + url: TEST_PAGE, + gBrowser, + }, + async browser => { + let originatingTab = gBrowser.getTabForBrowser(browser); + let pipWin = await triggerPictureInPicture(browser, videoID); + ok(pipWin, "Got Picture-in-Picture window."); + + let promiseSizeModeChange = BrowserTestUtils.waitForEvent( + window, + "sizemodechange" + ); + window.minimize(); + await promiseSizeModeChange; + + let switcher = gBrowser._getSwitcher(); + + Assert.equal( + switcher.getTabState(originatingTab), + switcher.STATE_LOADED, + "The originating browser tab should be in STATE_LOADED." + ); + + await BrowserTestUtils.closeWindow(pipWin); + } + ); +});