Bug 1678390: Prevent Picture-in-Picture windows from opening on top of one another r=mconley

Differential Revision: https://phabricator.services.mozilla.com/D97847
This commit is contained in:
Hunter Jones 2022-04-21 21:58:39 +00:00
Родитель 2e75f81b2e
Коммит 738d90142b
3 изменённых файлов: 218 добавлений и 4 удалений

Просмотреть файл

@ -23,6 +23,10 @@ XPCOMUtils.defineLazyServiceGetters(this, {
WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"],
});
const { Rect, Point } = ChromeUtils.import(
"resource://gre/modules/Geometry.jsm"
);
const PLAYER_URI = "chrome://global/content/pictureinpicture/player.xhtml";
var PLAYER_FEATURES =
"chrome,titlebar=yes,alwaysontop,lockaspectratio,resizable";
@ -147,7 +151,7 @@ var PictureInPicture = {
* Returns the player window if one exists and if it hasn't yet been closed.
*
* @param {PictureInPictureParent} pipActorRef
* Reference to the calling PictureInPictureParent actor
* Reference to the calling PictureInPictureParent actor
*
* @return {DOM Window} the player window if it exists and is not in the
* process of being closed. Returns null otherwise.
@ -428,7 +432,7 @@ var PictureInPicture = {
* The preferred width of the video.
*
* @param {PictureInPictureParent} actorReference
* Reference to the calling PictureInPictureParent
* Reference to the calling PictureInPictureParent
*
* @returns {Promise}
* Resolves once the window has opened and loaded the player component.
@ -436,9 +440,21 @@ var PictureInPicture = {
async openPipWindow(parentWin, videoData) {
let { top, left, width, height } = this.fitToScreen(parentWin, videoData);
let { left: resolvedLeft, top: resolvedTop } = this.resolveOverlapConflicts(
left,
top,
width,
height
);
top = Math.round(resolvedTop);
left = Math.round(resolvedLeft);
width = Math.round(width);
height = Math.round(height);
let features =
`${PLAYER_FEATURES},top=${Math.round(top)},left=${Math.round(left)},` +
`outerWidth=${Math.round(width)},outerHeight=${Math.round(height)}`;
`${PLAYER_FEATURES},top=${top},left=${left},outerWidth=${width},` +
`outerHeight=${height}`;
let pipWindow = Services.ww.openWindow(
parentWin,
@ -691,6 +707,157 @@ var PictureInPicture = {
return { top, left, width, height };
},
/**
* This function will take the size and potential location of a new
* Picture-in-Picture player window, and try to return the location
* coordinates that will best ensure that the player window will not overlap
* with other pre-existing player windows.
*
* @param {int} left
* x position of left edge for Picture-in-Picture window that is being
* opened
* @param {int} top
* y position of top edge for Picture-in-Picture window that is being
* opened
* @param {int} width
* Width of Picture-in-Picture window that is being opened
* @param {int} height
* Height of Picture-in-Picture window that is being opened
*
* @returns {object}
* An object with the following properties:
*
* top (int):
* The recommended top position for the player window.
*
* left (int):
* The recommended left position for the player window.
*/
resolveOverlapConflicts(left, top, width, height) {
// This algorithm works by first identifying the possible candidate
// locations that the new player window could be placed without overlapping
// other player windows (assuming that a confict is discovered at all of
// course). The optimal candidate is then selected by its distance to the
// original conflict, shorter distances are better.
//
// Candidates are discovered by iterating over each of the sides of every
// pre-existing player window. One candidate is collected for each side.
// This is done to ensure that the new player window will be opened to
// tightly fit along the edge of another player window.
//
// These candidates are then pruned for candidates that will introduce
// further conflicts. Finally the ideal candidate is selected from this
// pool of remaining candidates, optimized for minimizing distance to
// the original conflict.
let playerRects = [];
for (let playerWin of Services.wm.getEnumerator(WINDOW_TYPE)) {
playerRects.push(
new Rect(
playerWin.screenX,
playerWin.screenY,
playerWin.outerWidth,
playerWin.outerHeight
)
);
}
const newPlayerRect = new Rect(left, top, width, height);
let conflictingPipRect = playerRects.find(rect =>
rect.intersects(newPlayerRect)
);
if (!conflictingPipRect) {
// no conflicts found
return { left, top };
}
const conflictLoc = conflictingPipRect.center();
// Will try to resolve a better placement only on the screen where
// the conflict occurred
const conflictScreen = this.getWorkingScreen(conflictLoc.x, conflictLoc.y);
const [
screenTop,
screenLeft,
screenWidth,
screenHeight,
] = this.getAvailScreenSize(conflictScreen);
const screenRect = new Rect(
screenTop,
screenLeft,
screenWidth,
screenHeight
);
const getEdgeCandidates = rect => {
return [
// left edge's candidate
new Point(rect.left - newPlayerRect.width, rect.top),
// top edge's candidate
new Point(rect.left, rect.top - newPlayerRect.height),
// right edge's candidate
new Point(rect.right + newPlayerRect.width, rect.top),
// bottom edge's candidate
new Point(rect.left, rect.bottom),
];
};
let candidateLocations = [];
for (const playerRect of playerRects) {
for (let candidateLoc of getEdgeCandidates(playerRect)) {
const candidateRect = new Rect(
candidateLoc.x,
candidateLoc.y,
width,
height
);
if (!screenRect.contains(candidateRect)) {
continue;
}
// test that no PiPs conflict with this candidate box
if (playerRects.some(rect => rect.intersects(candidateRect))) {
continue;
}
const candidateCenter = candidateRect.center();
const candidateDistanceToConflict =
Math.abs(conflictLoc.x - candidateCenter.x) +
Math.abs(conflictLoc.y - candidateCenter.y);
candidateLocations.push({
distanceToConflict: candidateDistanceToConflict,
location: candidateLoc,
});
}
}
if (!candidateLocations.length) {
// if no suitable candidates can be found, return the original location
return { left, top };
}
// sort candidates by distance to the conflict, select the closest
const closestCandidate = candidateLocations.sort(
(firstCand, secondCand) =>
firstCand.distanceToConflict - secondCand.distanceToConflict
)[0];
if (!closestCandidate) {
// can occur if there were no valid candidates, return original location
return { left, top };
}
const resolvedX = closestCandidate.location.x;
const resolvedY = closestCandidate.location.y;
return { left: resolvedX, top: resolvedY };
},
/**
* Resizes the the PictureInPicture player window.
*

Просмотреть файл

@ -45,6 +45,7 @@ prefs =
[browser_closePipPause.js]
[browser_contextMenu.js]
skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1569205
[browser_conflictingPips.js]
[browser_cornerSnapping.js]
run-if = os == "mac"
[browser_closePlayer.js]

Просмотреть файл

@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* If multiple PiPs try to open in the same place, they should not overlap.
*/
add_task(async () => {
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE,
gBrowser,
},
async browser => {
let firstPip = await triggerPictureInPicture(browser, "with-controls");
ok(firstPip, "Got first PiP window");
await ensureMessageAndClosePiP(browser, "with-controls", firstPip, false);
info("Closed first PiP to save location");
let secondPip = await triggerPictureInPicture(browser, "with-controls");
ok(secondPip, "Got second PiP window");
let thirdPip = await triggerPictureInPicture(browser, "no-controls");
ok(thirdPip, "Got third PiP window");
Assert.ok(
secondPip.screenX != thirdPip.screenX ||
secondPip.screenY != thirdPip.screenY,
"Conflicting PiPs were successfully opened in different locations"
);
await ensureMessageAndClosePiP(
browser,
"with-controls",
secondPip,
false
);
info("Second PiP was still open and is now closed");
await ensureMessageAndClosePiP(browser, "no-controls", thirdPip, false);
info("Third PiP was still open and is now closed");
}
);
});