Bug 1592286 - Add URL-mapped policy support to the Picture-in-Picture toggle. r=mstriemer

I went with "policy" rather than "position" since "hidden" isn't really a position.

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mike Conley 2019-12-21 00:24:06 +00:00
Родитель 016cd664f6
Коммит 8416f879eb
8 изменённых файлов: 357 добавлений и 45 удалений

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

@ -16,6 +16,20 @@ ChromeUtils.defineModuleGetter(
"Services",
"resource://gre/modules/Services.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"TOGGLE_POLICIES",
"resource://gre/modules/PictureInPictureTogglePolicy.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"TOGGLE_POLICY_STRINGS",
"resource://gre/modules/PictureInPictureTogglePolicy.jsm"
);
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const TOGGLE_ENABLED_PREF =
"media.videocontrols.picture-in-picture.video-toggle.enabled";
@ -35,6 +49,14 @@ var gWeakPlayerContent = null;
// mouseover
var gWeakIntersectingVideosForTesting = new WeakSet();
// Overrides are expected to stay constant for the lifetime of a
// content process, so we set this as a lazy process global.
// See PictureInPictureToggleChild.getToggleOverrides for a sense
// of what the return type is.
XPCOMUtils.defineLazyGetter(this, "gToggleOverrides", () => {
return PictureInPictureToggleChild.getToggleOverrides();
});
/**
* The PictureInPictureToggleChild is responsible for displaying the overlaid
* Picture-in-Picture toggle over top of <video> elements that the mouse is
@ -120,6 +142,8 @@ class PictureInPictureToggleChild extends JSWindowActorChild {
// then this will be true. If there are no videos worth tracking, then
// this is false.
isTrackingVideos: false,
togglePolicy: TOGGLE_POLICIES.DEFAULT,
hasCheckedPolicy: false,
};
this.weakDocStates.set(this.document, state);
}
@ -659,8 +683,38 @@ class PictureInPictureToggleChild extends JSWindowActorChild {
return;
}
let state = this.docState;
let toggle = shadowRoot.getElementById("pictureInPictureToggleButton");
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;
// Do we have any toggle overrides? If so, try to apply them.
for (let [override, policy] of toggleOverrides) {
if (override.matches(this.document.documentURI)) {
state.togglePolicy = policy;
break;
}
}
state.hasCheckedPolicy = true;
}
// The built-in <video> controls are along the bottom, which would overlap the
// toggle if the override is set to BOTTOM, so we ignore overrides that set
// a policy of BOTTOM for <video> elements with controls.
if (
state.togglePolicy != TOGGLE_POLICIES.DEFAULT &&
!(state.togglePolicy == TOGGLE_POLICIES.BOTTOM && video.controls)
) {
toggle.setAttribute("policy", TOGGLE_POLICY_STRINGS[state.togglePolicy]);
}
controlsOverlay.removeAttribute("hidetoggle");
// The hideToggleDeferredTask we create here is for automatically hiding
@ -670,7 +724,6 @@ class PictureInPictureToggleChild extends JSWindowActorChild {
//
// We disable the toggle hiding timeout during testing to reduce
// non-determinism from timers when testing the toggle.
let state = this.docState;
if (!state.hideToggleDeferredTask && !this.toggleTesting) {
state.hideToggleDeferredTask = new DeferredTask(() => {
controlsOverlay.setAttribute("hidetoggle", true);
@ -804,6 +857,27 @@ class PictureInPictureToggleChild extends JSWindowActorChild {
static isTracking(video) {
return gWeakIntersectingVideosForTesting.has(video);
}
/**
* Gets any Picture-in-Picture toggle overrides stored in the sharedData
* struct, and returns them as an Array of two-element Arrays, where the first
* element is a MatchPattern and the second element is a policy.
*
* @returns {Array<Array<2>>} Array of 2-element Arrays where the first element
* is a MatchPattern and the second element is a Number representing a toggle
* policy.
*/
static getToggleOverrides() {
let result = [];
let patterns = Services.cpmm.sharedData.get(
"PictureInPicture:ToggleOverrides"
);
for (let pattern in patterns) {
let matcher = new MatchPattern(pattern);
result.push([matcher, patterns[pattern]]);
}
return result;
}
}
class PictureInPictureChild extends JSWindowActorChild {

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

@ -0,0 +1,31 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["TOGGLE_POLICIES", "TOGGLE_POLICY_STRINGS"];
// These are the possible toggle positions along the right side of
// a qualified video element.
this.TOGGLE_POLICIES = {
DEFAULT: 1,
HIDDEN: 2,
TOP: 3,
ONE_QUARTER: 4,
THREE_QUARTERS: 5,
BOTTOM: 6,
};
// These strings are used in the videocontrols.css stylesheet as
// toggle policy attribute values for setting rules on the position
// of the toggle.
this.TOGGLE_POLICY_STRINGS = {
[TOGGLE_POLICIES.DEFAULT]: "default",
[TOGGLE_POLICIES.HIDDEN]: "hidden",
[TOGGLE_POLICIES.TOP]: "top",
[TOGGLE_POLICIES.ONE_QUARTER]: "one-quarter",
[TOGGLE_POLICIES.THREE_QUARTERS]: "three-quarters",
[TOGGLE_POLICIES.BOTTOM]: "bottom",
};

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

@ -8,6 +8,7 @@ JAR_MANIFESTS += ['jar.mn']
EXTRA_JS_MODULES += [
'PictureInPicture.jsm',
'PictureInPictureTogglePolicy.jsm',
]
BROWSER_CHROME_MANIFESTS += [

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

@ -44,6 +44,7 @@ skip-if = true # Bug 1546455
[browser_toggleOpaqueOverlay.js]
skip-if = true # Bug 1546455
[browser_togglePointerEventsNone.js]
[browser_togglePolicies.js]
[browser_toggleSimple.js]
skip-if = os == 'linux' # Bug 1546455
[browser_toggleTransparentOverlay-1.js]

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

@ -21,7 +21,7 @@ add_task(async () => {
await ensureVideosReady(browser);
let videoID = "no-controls";
let { toggleClientRect } = await prepareForToggleClick(browser, videoID);
await prepareForToggleClick(browser, videoID);
// Hover the mouse over the video to reveal the toggle, which is necessary
// if we want to click on the toggle.
@ -47,6 +47,8 @@ add_task(async () => {
HOVER_VIDEO_OPACITY
);
let toggleClientRect = await getToggleClientRect(browser, videoID);
// The toggle center, because of how it slides out, is actually outside
// of the bounds of a click event. For now, we move the mouse in by a
// hard-coded 2 pixels along the x and y axis to achieve the hover.

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

@ -0,0 +1,79 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const SHARED_DATA_KEY = "PictureInPicture:ToggleOverrides";
const { TOGGLE_POLICIES } = ChromeUtils.import(
"resource://gre/modules/PictureInPictureTogglePolicy.jsm"
);
/**
* Tests that by setting a Picture-in-Picture toggle position policy
* in the sharedData structure, that the toggle position can be
* change for a particular URI.
*/
add_task(async () => {
let positionPolicies = [
TOGGLE_POLICIES.TOP,
TOGGLE_POLICIES.ONE_QUARTER,
TOGGLE_POLICIES.THREE_QUARTERS,
TOGGLE_POLICIES.BOTTOM,
];
for (let policy of positionPolicies) {
Services.ppmm.sharedData.set(SHARED_DATA_KEY, {
"*://example.com/*": policy,
});
Services.ppmm.sharedData.flush();
let expectations = {
"with-controls": { canToggle: true, policy },
"no-controls": { canToggle: true, policy },
};
// For <video> elements with controls, the video controls overlap the
// toggle when its on the bottom and can't be clicked, so we'll ignore
// that case.
if (policy == TOGGLE_POLICIES.BOTTOM) {
expectations["with-controls"] = { canToggle: true };
}
await testToggle(TEST_PAGE, expectations);
// And ensure that other pages aren't affected by this override.
await testToggle(TEST_PAGE_2, {
"with-controls": { canToggle: true },
"no-controls": { canToggle: true },
});
}
Services.ppmm.sharedData.set(SHARED_DATA_KEY, {});
Services.ppmm.sharedData.flush();
});
/**
* Tests that by setting a Picture-in-Picture toggle hidden policy
* in the sharedData structure, that the toggle can be suppressed.
*/
add_task(async () => {
Services.ppmm.sharedData.set(SHARED_DATA_KEY, {
"*://example.com/*": TOGGLE_POLICIES.HIDDEN,
});
Services.ppmm.sharedData.flush();
await testToggle(TEST_PAGE, {
"with-controls": { canToggle: false, policy: TOGGLE_POLICIES.HIDDEN },
"no-controls": { canToggle: false, policy: TOGGLE_POLICIES.HIDDEN },
});
// And ensure that other pages aren't affected by this override.
await testToggle(TEST_PAGE_2, {
"with-controls": { canToggle: true },
"no-controls": { canToggle: true },
});
Services.ppmm.sharedData.set(SHARED_DATA_KEY, {});
Services.ppmm.sharedData.flush();
});

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

@ -12,6 +12,7 @@ const TEST_ROOT_2 = getRootDirectory(gTestPath).replace(
"http://example.org"
);
const TEST_PAGE = TEST_ROOT + "test-page.html";
const TEST_PAGE_2 = TEST_ROOT_2 + "test-page.html";
const TEST_PAGE_WITH_IFRAME = TEST_ROOT_2 + "test-page-with-iframe.html";
const WINDOW_TYPE = "Toolkit:PictureInPicture";
const TOGGLE_ID = "pictureInPictureToggleButton";
@ -129,6 +130,7 @@ async function toggleOpacityReachesThreshold(
let args = { videoID, TOGGLE_ID, opacityThreshold };
await SpecialPowers.spawn(browser, [args], async args => {
let { videoID, TOGGLE_ID, opacityThreshold } = args;
let video = content.document.getElementById(videoID);
let shadowRoot = video.openOrClosedShadowRoot;
let toggle = shadowRoot.getElementById(TOGGLE_ID);
@ -147,6 +149,54 @@ async function toggleOpacityReachesThreshold(
});
}
/**
* Tests that the toggle has the correct policy attribute set. This should be called
* either when the toggle is visible, or events have been queued such that the toggle
* will soon be visible.
*
* @param {Element} browser The <xul:browser> that has the <video> in it.
* @param {String} videoID The ID of the video element that we expect the toggle
* to appear on.
* @param {Number} policy Optional argument. If policy is defined, then it should
* be one of the values in the TOGGLE_POLICIES from PictureInPictureTogglePolicy.jsm.
* If undefined, this function will ensure no policy attribute is set.
*
* @return Promise
* @resolves When the check has completed.
*/
async function assertTogglePolicy(browser, videoID, policy) {
let args = { videoID, TOGGLE_ID, policy };
await SpecialPowers.spawn(browser, [args], async args => {
let { videoID, TOGGLE_ID, policy } = args;
let video = content.document.getElementById(videoID);
let shadowRoot = video.openOrClosedShadowRoot;
let controlsOverlay = shadowRoot.querySelector(".controlsOverlay");
let toggle = shadowRoot.getElementById(TOGGLE_ID);
await ContentTaskUtils.waitForCondition(() => {
return controlsOverlay.classList.contains("hovering");
}, "Waiting for the hovering state to be set on the video.");
if (policy) {
const { TOGGLE_POLICY_STRINGS } = ChromeUtils.import(
"resource://gre/modules/PictureInPictureTogglePolicy.jsm"
);
let policyAttr = toggle.getAttribute("policy");
Assert.equal(
policyAttr,
TOGGLE_POLICY_STRINGS[policy],
"The correct toggle policy is set."
);
} else {
Assert.ok(
!toggle.hasAttribute("policy"),
"No toggle policy should be set."
);
}
});
}
/**
* Tests that either all or none of the expected mousebutton events
* fire in web content when clicking on the page.
@ -202,29 +252,21 @@ async function assertSawMouseEvents(
* @return Promise
* @resolves With the following Object structure:
* {
* toggleClientRect: {
* top: <Number>,
* right: <Number>,
* left: <Number>,
* bottom: <Number>,
* },
* controls: <Boolean>,
* }
*
* Where toggleClientRect represents the client rectangle that the toggle is
* positioned in, and controls represents whether or not the video has the
* default control set displayed.
* Where controls represents whether or not the video has the default control set
* displayed.
*/
async function prepareForToggleClick(browser, videoID) {
// For each video, make sure it's scrolled into view, and get the rect for
// the toggle while we're at it.
let args = { videoID, TOGGLE_ID };
let args = { videoID };
return SpecialPowers.spawn(browser, [args], async args => {
let { videoID, TOGGLE_ID } = args;
let { videoID } = args;
let video = content.document.getElementById(videoID);
video.scrollIntoView({ behaviour: "instant" });
let shadowRoot = video.openOrClosedShadowRoot;
let toggle = shadowRoot.getElementById(TOGGLE_ID);
if (!video.controls) {
// For no-controls <video> elements, an IntersectionObserver is used
@ -244,19 +286,52 @@ async function prepareForToggleClick(browser, videoID) {
100
);
}
let rect = toggle.getBoundingClientRect();
return {
toggleClientRect: {
top: rect.top,
right: rect.right,
left: rect.left,
bottom: rect.bottom,
},
controls: video.controls,
};
});
}
/**
* Returns the client rect for the toggle if it's supposed to be visible
* on hover. Otherwise, returns the client rect for the video with the
* associated ID.
*
* @param {Element} browser The <xul:browser> that has the <video> loaded in it.
* @param {String} videoID The ID of the video that has the toggle.
*
* @return Promise
* @resolves With the following Object structure:
* {
* top: <Number>,
* right: <Number>,
* left: <Number>,
* bottom: <Number>,
* }
*/
async function getToggleClientRect(browser, videoID) {
let args = { videoID, TOGGLE_ID };
return ContentTask.spawn(browser, args, async args => {
let { videoID, TOGGLE_ID } = args;
let video = content.document.getElementById(videoID);
let shadowRoot = video.openOrClosedShadowRoot;
let toggle = shadowRoot.getElementById(TOGGLE_ID);
let rect = toggle.getBoundingClientRect();
if (!rect.width && !rect.height) {
rect = video.getBoundingClientRect();
}
return {
top: rect.top,
right: rect.right,
left: rect.left,
bottom: rect.bottom,
};
});
}
/**
* Test helper for the Picture-in-Picture toggle. Loads a page, and then
* tests the provided video elements for the toggle both appearing and
@ -266,11 +341,16 @@ async function prepareForToggleClick(browser, videoID) {
* @param {Object} expectations An object with the following schema:
* <video-element-id>: {
* canToggle: Boolean
* policy: Number (optional)
* }
* If canToggle is true, then it's expected that moving the mouse over the
* video and then clicking in the toggle region should open a
* Picture-in-Picture window. If canToggle is false, we expect that a click
* in this region will not result in the window opening.
*
* If policy is defined, then it should be one of the values in the
* TOGGLE_POLICIES from PictureInPictureTogglePolicy.jsm.
*
* @param {async Function} prepFn An optional asynchronous function to run
* before running the toggle test. The function is passed the opened
* <xul:browser> as its only argument once the testURL has finished loading.
@ -289,11 +369,13 @@ async function testToggle(testURL, expectations, prepFn = async () => {}) {
await prepFn(browser);
await ensureVideosReady(browser);
for (let [videoID, { canToggle }] of Object.entries(expectations)) {
for (let [videoID, { canToggle, policy }] of Object.entries(
expectations
)) {
await SimpleTest.promiseFocus(browser);
info(`Testing video with id: ${videoID}`);
await testToggleHelper(browser, videoID, canToggle);
await testToggleHelper(browser, videoID, canToggle, policy);
}
}
);
@ -308,15 +390,14 @@ async function testToggle(testURL, expectations, prepFn = async () => {}) {
* @param {String} videoID The ID of the video that has the toggle.
* @param {Boolean} canToggle True if we expect the toggle to be visible and
* clickable by the mouse for the associated video.
* @param {Number} policy Optional argument. If policy is defined, then it should
* be one of the values in the TOGGLE_POLICIES from PictureInPictureTogglePolicy.jsm.
*
* @return Promise
* @resolves When the check for the toggle is complete.
*/
async function testToggleHelper(browser, videoID, canToggle) {
let { toggleClientRect, controls } = await prepareForToggleClick(
browser,
videoID
);
async function testToggleHelper(browser, videoID, canToggle, policy) {
let { controls } = await prepareForToggleClick(browser, videoID);
// Hover the mouse over the video to reveal the toggle.
await BrowserTestUtils.synthesizeMouseAtCenter(
@ -334,11 +415,21 @@ async function testToggleHelper(browser, videoID, canToggle) {
browser
);
info("Checking toggle policy");
await assertTogglePolicy(browser, videoID, policy);
if (canToggle) {
info("Waiting for toggle to become visible");
await toggleOpacityReachesThreshold(browser, videoID, HOVER_VIDEO_OPACITY);
await toggleOpacityReachesThreshold(
browser,
videoID,
HOVER_VIDEO_OPACITY,
policy
);
}
let toggleClientRect = await getToggleClientRect(browser, videoID);
info("Hovering the toggle rect now.");
// The toggle center, because of how it slides out, is actually outside
// of the bounds of a click event. For now, we move the mouse in by a
@ -364,7 +455,12 @@ async function testToggleHelper(browser, videoID, canToggle) {
if (canToggle) {
info("Waiting for toggle to reach full opacity");
await toggleOpacityReachesThreshold(browser, videoID, HOVER_TOGGLE_OPACITY);
await toggleOpacityReachesThreshold(
browser,
videoID,
HOVER_TOGGLE_OPACITY,
policy
);
}
// First, ensure that a non-primary mouse click is ignored.
@ -377,16 +473,13 @@ async function testToggleHelper(browser, videoID, canToggle) {
browser
);
if (canToggle) {
// For videos without the built-in controls, we expect that all mouse events
// should have fired - otherwise, the events are all suppressed.
// Note that the right-click does not result in a "click" event firing.
await assertSawMouseEvents(browser, !controls, false);
} else {
// If we aren't showing the toggle, we expect all mouse events to be seen.
// Note that the right-click does not result in a "click" event firing.
await assertSawMouseEvents(browser, true, false);
}
// For videos without the built-in controls, we expect that all mouse events
// should have fired - otherwise, the events are all suppressed. For videos
// with controls, none of the events should be fired, as the controls overlay
// absorbs them all.
//
// Note that the right-click does not result in a "click" event firing.
await assertSawMouseEvents(browser, !controls, false);
// The message to open the Picture-in-Picture window would normally be sent
// immediately before this Promise resolved, so the window should have opened
@ -432,7 +525,7 @@ async function testToggleHelper(browser, videoID, canToggle) {
);
// If we aren't showing the toggle, we expect all mouse events to be seen.
await assertSawMouseEvents(browser, true);
await assertSawMouseEvents(browser, !controls);
// The message to open the Picture-in-Picture window would normally be sent
// immediately before this Promise resolved, so the window should have opened

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

@ -32,6 +32,7 @@
--pip-toggle-text-and-icon-color: rgb(255, 255, 255);
--pip-toggle-padding: 5px;
--pip-toggle-icon-width-height: 16px;
--pip-toggle-translate-x: calc(100% - var(--pip-toggle-icon-width-height) - 2 * var(--pip-toggle-padding));
}
.controlsContainer.touch {
--clickToPlay-size: 64px;
@ -449,13 +450,35 @@
padding: var(--pip-toggle-padding);
right: 0;
top: 50%;
transform: translateY(-50%) translateX(calc(100% - var(--pip-toggle-icon-width-height) - 2 * var(--pip-toggle-padding)));
transition: opacity 160ms linear, transform 160ms linear;
translate: var(--pip-toggle-translate-x) -50%;
transition: opacity 160ms linear, translate 160ms linear;
min-width: max-content;
pointer-events: auto;
opacity: 0;
}
.pictureInPictureToggleButton[policy="hidden"] {
display: none;
}
.pictureInPictureToggleButton[policy="top"] {
top: 0%;
translate: var(--pip-toggle-translate-x);
}
.pictureInPictureToggleButton[policy="one-quarter"] {
top: 25%;
}
.pictureInPictureToggleButton[policy="three-quarters"] {
top: 75%;
}
.pictureInPictureToggleButton[policy="bottom"] {
top: 100%;
translate: var(--pip-toggle-translate-x) -100%;
}
.pictureInPictureToggleIcon {
display: inline-block;
background-image: url(chrome://global/skin/media/pictureinpicture.svg);
@ -496,7 +519,15 @@
.controlsOverlay.hovering > .pictureInPictureToggleButton.hovering {
opacity: 1;
transform: translateY(-50%) translateX(0);
translate: 0 -50%;
}
.controlsOverlay.hovering > .pictureInPictureToggleButton.hovering[policy="top"] {
translate: 0;
}
.controlsOverlay.hovering > .pictureInPictureToggleButton.hovering[policy="bottom"] {
translate: 0 -100%
}
@supports -moz-bool-pref("media.videocontrols.picture-in-picture.video-toggle.testing") {