Commit a5495227 authored by Mark Striemer's avatar Mark Striemer
Browse files

Bug 1535437 - Part 2: Resize PiP window when video source resizes r=mconley

Differential Revision: https://phabricator.services.mozilla.com/D50139

--HG--
extra : moz-landing-system : lando
parent d97254d2
...@@ -868,6 +868,16 @@ class PictureInPictureChild extends JSWindowActorChild { ...@@ -868,6 +868,16 @@ class PictureInPictureChild extends JSWindowActorChild {
} }
break; break;
} }
case "resize": {
let video = event.target;
if (this.inPictureInPicture(video)) {
this.sendAsyncMessage("PictureInPicture:Resize", {
videoHeight: video.videoHeight,
videoWidth: video.videoWidth,
});
}
break;
}
} }
} }
...@@ -1043,6 +1053,7 @@ class PictureInPictureChild extends JSWindowActorChild { ...@@ -1043,6 +1053,7 @@ class PictureInPictureChild extends JSWindowActorChild {
originatingVideo.addEventListener("play", this); originatingVideo.addEventListener("play", this);
originatingVideo.addEventListener("pause", this); originatingVideo.addEventListener("pause", this);
originatingVideo.addEventListener("volumechange", this); originatingVideo.addEventListener("volumechange", this);
originatingVideo.addEventListener("resize", this);
} }
} }
...@@ -1059,6 +1070,7 @@ class PictureInPictureChild extends JSWindowActorChild { ...@@ -1059,6 +1070,7 @@ class PictureInPictureChild extends JSWindowActorChild {
originatingVideo.removeEventListener("play", this); originatingVideo.removeEventListener("play", this);
originatingVideo.removeEventListener("pause", this); originatingVideo.removeEventListener("pause", this);
originatingVideo.removeEventListener("volumechange", this); originatingVideo.removeEventListener("volumechange", this);
originatingVideo.removeEventListener("resize", this);
} }
} }
......
...@@ -70,6 +70,11 @@ class PictureInPictureParent extends JSWindowActorParent { ...@@ -70,6 +70,11 @@ class PictureInPictureParent extends JSWindowActorParent {
PictureInPicture.handlePictureInPictureRequest(browser, videoData); PictureInPicture.handlePictureInPictureRequest(browser, videoData);
break; break;
} }
case "PictureInPicture:Resize": {
let videoData = aMessage.data;
PictureInPicture.resizePictureInPictureWindow(videoData);
break;
}
case "PictureInPicture:Close": { case "PictureInPicture:Close": {
/** /**
* Content has requested that its Picture in Picture window go away. * Content has requested that its Picture in Picture window go away.
...@@ -274,7 +279,76 @@ var PictureInPicture = { ...@@ -274,7 +279,76 @@ var PictureInPicture = {
* Resolves once the window has opened and loaded the player component. * Resolves once the window has opened and loaded the player component.
*/ */
async openPipWindow(parentWin, videoData) { async openPipWindow(parentWin, videoData) {
let { top, left, width, height } = this.fitToScreen(parentWin, videoData);
let features =
`${PLAYER_FEATURES},top=${top},left=${left},` +
`outerWidth=${width},outerHeight=${height}`;
let pipWindow = Services.ww.openWindow(
parentWin,
PLAYER_URI,
null,
features,
null
);
TelemetryStopwatch.start(
"FX_PICTURE_IN_PICTURE_WINDOW_OPEN_DURATION",
pipWindow,
{
inSeconds: true,
}
);
return new Promise(resolve => {
pipWindow.addEventListener(
"load",
() => {
resolve(pipWindow);
},
{ once: true }
);
});
},
/**
* Calculate the desired size and position for a Picture in Picture window
* for the provided window and videoData.
*
* @param windowOrPlayer (chrome window|player window)
* The window hosting the browser that requested the Picture in
* Picture window. If this is an existing player window then the returned
* player size and position will be determined based on the existing
* player window's size and position.
*
* @param videoData (object)
* An object containing the following properties:
*
* videoHeight (int):
* The preferred height of the video.
*
* videoWidth (int):
* The preferred width of the video.
*
* @returns object
* The size and position for the player window.
*
* top (int):
* The top position for the player window.
*
* left (int):
* The left position for the player window.
*
* width (int):
* The width of the player window.
*
* height (int):
* The height of the player window.
*/
fitToScreen(windowOrPlayer, videoData) {
let { videoHeight, videoWidth } = videoData; let { videoHeight, videoWidth } = videoData;
let isPlayerWindow = windowOrPlayer == this.getWeakPipPlayer();
// The Picture in Picture window will open on the same display as the // The Picture in Picture window will open on the same display as the
// originating window, and anchor to the bottom right. // originating window, and anchor to the bottom right.
...@@ -282,8 +356,8 @@ var PictureInPicture = { ...@@ -282,8 +356,8 @@ var PictureInPicture = {
Ci.nsIScreenManager Ci.nsIScreenManager
); );
let screen = screenManager.screenForRect( let screen = screenManager.screenForRect(
parentWin.screenX, windowOrPlayer.screenX,
parentWin.screenY, windowOrPlayer.screenY,
1, 1,
1 1
); );
...@@ -317,33 +391,47 @@ var PictureInPicture = { ...@@ -317,33 +391,47 @@ var PictureInPicture = {
screenTop.value = screenTop.value =
(screenTop.value - fullTop.value) * scaleFactor + fullTop.value; (screenTop.value - fullTop.value) * scaleFactor + fullTop.value;
// For now, the Picture in Picture window will be a maximum of a quarter // If we have a player window, maintain the previous player window's size by
// of the screen height, and a third of the screen width. // clamping the new video's largest dimension to the player window's
const MAX_HEIGHT = screenHeight.value / 4; // largest dimension.
const MAX_WIDTH = screenWidth.value / 3; //
// Otherwise the Picture in Picture window will be a maximum of a quarter of
let resultWidth = videoWidth; // the screen height, and a third of the screen width.
let resultHeight = videoHeight; let preferredSize;
if (isPlayerWindow) {
let prevWidth = windowOrPlayer.innerWidth;
let prevHeight = windowOrPlayer.innerHeight;
preferredSize = prevWidth >= prevHeight ? prevWidth : prevHeight;
}
const MAX_HEIGHT = preferredSize || screenHeight.value / 4;
const MAX_WIDTH = preferredSize || screenWidth.value / 3;
if (videoHeight > MAX_HEIGHT || videoWidth > MAX_WIDTH) { let width = videoWidth;
let height = videoHeight;
let aspectRatio = videoWidth / videoHeight; let aspectRatio = videoWidth / videoHeight;
// We're bigger than the max - take the largest dimension and clamp
// it to the associated max. Recalculate the other dimension to maintain if (
// aspect ratio. videoHeight > MAX_HEIGHT ||
videoWidth > MAX_WIDTH ||
(isPlayerWindow && videoHeight < MAX_HEIGHT && videoWidth < MAX_WIDTH)
) {
// We're bigger than the max, or smaller than the previous player window.
// Take the largest dimension and clamp it to the associated max.
// Recalculate the other dimension to maintain aspect ratio.
if (videoWidth >= videoHeight) { if (videoWidth >= videoHeight) {
// We're clamping the width, so the height must be adjusted to match // We're clamping the width, so the height must be adjusted to match
// the original aspect ratio. Since aspect ratio is width over height, // the original aspect ratio. Since aspect ratio is width over height,
// that means we need to _divide_ the MAX_WIDTH by the aspect ratio to // that means we need to _divide_ the MAX_WIDTH by the aspect ratio to
// calculate the appropriate height. // calculate the appropriate height.
resultWidth = MAX_WIDTH; width = MAX_WIDTH;
resultHeight = Math.round(MAX_WIDTH / aspectRatio); height = Math.round(MAX_WIDTH / aspectRatio);
} else { } else {
// We're clamping the height, so the width must be adjusted to match // We're clamping the height, so the width must be adjusted to match
// the original aspect ratio. Since aspect ratio is width over height, // the original aspect ratio. Since aspect ratio is width over height,
// this means we need to _multiply_ the MAX_HEIGHT by the aspect ratio // this means we need to _multiply_ the MAX_HEIGHT by the aspect ratio
// to calculate the appropriate width. // to calculate the appropriate width.
resultHeight = MAX_HEIGHT; height = MAX_HEIGHT;
resultWidth = Math.round(MAX_HEIGHT * aspectRatio); width = Math.round(MAX_HEIGHT * aspectRatio);
} }
} }
...@@ -362,39 +450,23 @@ var PictureInPicture = { ...@@ -362,39 +450,23 @@ var PictureInPicture = {
// the screenLeft and screenTop values, which tell us where this screen is // the screenLeft and screenTop values, which tell us where this screen is
// located relative to the "origin" in absolute coordinates. // located relative to the "origin" in absolute coordinates.
let isRTL = Services.locale.isAppLocaleRTL; let isRTL = Services.locale.isAppLocaleRTL;
let pipLeft = isRTL let left = isRTL
? screenLeft.value ? screenLeft.value
: screenLeft.value + screenWidth.value - resultWidth; : screenLeft.value + screenWidth.value - width;
let pipTop = screenTop.value + screenHeight.value - resultHeight; let top = screenTop.value + screenHeight.value - height;
let features =
`${PLAYER_FEATURES},top=${pipTop},left=${pipLeft},` +
`outerWidth=${resultWidth},outerHeight=${resultHeight}`;
let pipWindow = Services.ww.openWindow( return { top, left, width, height };
parentWin, },
PLAYER_URI,
null,
features,
null
);
TelemetryStopwatch.start( resizePictureInPictureWindow(videoData) {
"FX_PICTURE_IN_PICTURE_WINDOW_OPEN_DURATION", let win = this.getWeakPipPlayer();
pipWindow,
{ if (!win) {
inSeconds: true, return;
} }
);
return new Promise(resolve => { let { width, height } = this.fitToScreen(win, videoData);
pipWindow.addEventListener( win.resizeTo(width, height);
"load",
() => {
resolve(pipWindow);
},
{ once: true }
);
});
}, },
openToggleContextMenu(window, data) { openToggleContextMenu(window, data) {
......
...@@ -10,6 +10,8 @@ support-files = ...@@ -10,6 +10,8 @@ support-files =
test-transparent-overlay-1.html test-transparent-overlay-1.html
test-transparent-overlay-2.html test-transparent-overlay-2.html
test-video.mp4 test-video.mp4
test-video-cropped.mp4
test-video-vertical.mp4
prefs = prefs =
media.videocontrols.picture-in-picture.enabled=true media.videocontrols.picture-in-picture.enabled=true
...@@ -29,6 +31,8 @@ skip-if = os == "mac" # Bug 1599376 ...@@ -29,6 +31,8 @@ skip-if = os == "mac" # Bug 1599376
skip-if = debug skip-if = debug
[browser_removeVideoElement.js] [browser_removeVideoElement.js]
[browser_rerequestPiP.js] [browser_rerequestPiP.js]
[browser_resizeVideo.js]
skip-if = os == 'linux' # Bug 1594223
[browser_showMessage.js] [browser_showMessage.js]
[browser_stripVideoStyles.js] [browser_stripVideoStyles.js]
[browser_thirdPartyIframe.js] [browser_thirdPartyIframe.js]
......
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that if a <video> element is resized the Picture-in-Picture window
* will be resized to match the new dimensions.
*/
add_task(async () => {
for (let videoID of ["with-controls", "no-controls"]) {
info(`Testing ${videoID} case.`);
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE,
gBrowser,
},
async browser => {
let pipWin = await triggerPictureInPicture(browser, videoID);
async function switchVideoSource(src) {
let videoResized = BrowserTestUtils.waitForEvent(pipWin, "resize");
await ContentTask.spawn(
browser,
{ src, videoID },
async ({ src, videoID }) => {
let doc = content.document;
let video = doc.getElementById(videoID);
video.src = src;
}
);
await videoResized;
}
Assert.ok(pipWin, "Got PiP window.");
let initialWidth = pipWin.innerWidth;
let initialHeight = pipWin.innerHeight;
let initialAspectRatio = initialWidth / initialHeight;
Assert.equal(
Math.floor(initialAspectRatio * 100),
177, // 16 / 9 = 1.777777777
"Original aspect ratio is 16:9"
);
await switchVideoSource("test-video-cropped.mp4");
let resizedWidth = pipWin.innerWidth;
let resizedHeight = pipWin.innerHeight;
let resizedAspectRatio = resizedWidth / resizedHeight;
Assert.equal(
Math.floor(resizedAspectRatio * 100),
133, // 4 / 3 = 1.333333333
"Resized aspect ratio is 4:3"
);
Assert.equal(
initialWidth,
resizedWidth,
"Resized video has the same width"
);
Assert.greater(
resizedHeight,
initialHeight,
"Resized video grew vertically"
);
await switchVideoSource("test-video-vertical.mp4");
let verticalAspectRatio = pipWin.innerWidth / pipWin.innerHeight;
Assert.equal(
Math.floor(verticalAspectRatio * 100),
50, // 1 / 2 = 0.5
"Vertical aspect ratio is 1:2"
);
Assert.less(
pipWin.innerWidth,
resizedWidth,
"Vertical video width shrunk"
);
Assert.equal(
resizedWidth,
pipWin.innerHeight,
"Vertical video height matches previous width"
);
await switchVideoSource("test-video.mp4");
let restoredAspectRatio = pipWin.innerWidth / pipWin.innerHeight;
Assert.equal(
Math.floor(restoredAspectRatio * 100),
177,
"Restored aspect ratio is still 16:9"
);
Assert.equal(
initialWidth,
pipWin.innerWidth,
"Restored video has its original width"
);
Assert.equal(
initialHeight,
pipWin.innerHeight,
"Restored video has its original height"
);
await BrowserTestUtils.closeWindow(pipWin);
}
);
}
});
...@@ -46,6 +46,7 @@ async function triggerPictureInPicture(browser, videoID) { ...@@ -46,6 +46,7 @@ async function triggerPictureInPicture(browser, videoID) {
}); });
let win = await domWindowOpened; let win = await domWindowOpened;
await BrowserTestUtils.waitForEvent(win, "load"); await BrowserTestUtils.waitForEvent(win, "load");
await win.promiseDocumentFlushed(() => {});
await videoReady; await videoReady;
return win; return win;
} }
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment