Backed out 10 changesets (bug 1542756, bug 1543128, bug 1543122) for multiple media failures /test_setSinkId.html. CLOSED TREE

Backed out changeset ce3a15e1b737 (bug 1543128)
Backed out changeset cea8c1af70ad (bug 1543128)
Backed out changeset aeb23f8f45fb (bug 1543128)
Backed out changeset a2e73d143aba (bug 1543128)
Backed out changeset 1692fc6491a0 (bug 1543128)
Backed out changeset 9fbce4274cfd (bug 1542756)
Backed out changeset 20092bcebe6a (bug 1543122)
Backed out changeset 1645d577016c (bug 1543122)
Backed out changeset 3fce0b7586c1 (bug 1543122)
Backed out changeset aab68db4131b (bug 1543122)
This commit is contained in:
Csoregi Natalia 2019-04-13 06:22:47 +03:00
parent e2a9511e93
commit 9e8043e236
18 changed files with 412 additions and 359 deletions

View File

@ -65,6 +65,9 @@ let whitelist = [
intermittent: true,
errorMessage: /Property contained reference to invalid variable.*background/i,
isFromDevTools: true},
{sourceName: /pictureinpicture\/toggle.css$/i,
errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i,
isFromDevTools: false},
];
if (!Services.prefs.getBoolPref("layout.css.xul-box-display-values.content.enabled")) {

View File

@ -248,7 +248,7 @@ DownloadsPlacesView.prototype = {
let winUtils = window.windowUtils;
let nodes = winUtils.nodesFromRect(rlbRect.left, rlbRect.top,
0, rlbRect.width, rlbRect.height, 0,
true, false, false);
true, false);
// nodesFromRect returns nodes in z-index order, and for the same z-index
// sorts them in inverted DOM order, thus starting from the one that would
// be on top.

View File

@ -362,7 +362,6 @@ void DocumentOrShadowRoot::NodesFromRect(float aX, float aY, float aTopSize,
float aLeftSize,
bool aIgnoreRootScrollFrame,
bool aFlushLayout,
bool aOnlyVisible,
nsTArray<RefPtr<nsINode>>& aReturn) {
// Following the same behavior of elementFromPoint,
// we don't return anything if either coord is negative
@ -381,9 +380,6 @@ void DocumentOrShadowRoot::NodesFromRect(float aX, float aY, float aTopSize,
if (aIgnoreRootScrollFrame) {
options += FrameForPointOption::IgnoreRootScrollFrame;
}
if (aOnlyVisible) {
options += FrameForPointOption::OnlyVisible;
}
auto flush = aFlushLayout ? FlushLayout::Yes : FlushLayout::No;
QueryNodesFromRect(*this, rect, options, flush, Multiple::Yes, aReturn);

View File

@ -120,7 +120,7 @@ class DocumentOrShadowRoot {
void NodesFromRect(float aX, float aY, float aTopSize, float aRightSize,
float aBottomSize, float aLeftSize,
bool aIgnoreRootScrollFrame, bool aFlushLayout,
bool aOnlyVisible, nsTArray<RefPtr<nsINode>>&);
nsTArray<RefPtr<nsINode>>&);
/**
* This gets fired when the element that an id refers to changes.

View File

@ -1155,8 +1155,7 @@ NS_IMETHODIMP
nsDOMWindowUtils::NodesFromRect(float aX, float aY, float aTopSize,
float aRightSize, float aBottomSize,
float aLeftSize, bool aIgnoreRootScrollFrame,
bool aFlushLayout, bool aOnlyVisible,
nsINodeList** aReturn) {
bool aFlushLayout, nsINodeList** aReturn) {
nsCOMPtr<Document> doc = GetDocument();
NS_ENSURE_STATE(doc);
@ -1166,8 +1165,7 @@ nsDOMWindowUtils::NodesFromRect(float aX, float aY, float aTopSize,
AutoTArray<RefPtr<nsINode>, 8> nodes;
doc->NodesFromRect(aX, aY, aTopSize, aRightSize, aBottomSize, aLeftSize,
aIgnoreRootScrollFrame, aFlushLayout, aOnlyVisible,
nodes);
aIgnoreRootScrollFrame, aFlushLayout, nodes);
list->SetCapacity(nodes.Length());
for (auto& node : nodes) {
list->AppendElement(node->AsContent());

View File

@ -4097,7 +4097,19 @@ nsresult HTMLMediaElement::BindToTree(Document* aDocument, nsIContent* aParent,
if (IsInComposedDoc()) {
// Construct Shadow Root so web content can be hidden in the DOM.
AttachAndSetUAShadowRoot();
#ifdef ANDROID
NotifyUAWidgetSetupOrChange();
#else
// We don't want to call into JS if the website never asks for native
// video controls.
// If controls attribute is set later, controls is constructed lazily
// with the UAWidgetAttributeChanged event.
// This only applies to Desktop because on Fennec we would need to show
// an UI if the video is blocked.
if (Controls()) {
NotifyUAWidgetSetupOrChange();
}
#endif
}
mUnboundFromTree = false;

View File

@ -548,15 +548,5 @@ void HTMLVideoElement::EndCloningVisually() {
}
}
void HTMLVideoElement::TogglePictureInPicture(ErrorResult& error) {
// The MozTogglePictureInPicture event is listen for via the
// PictureInPictureChild actor, which is responsible for opening the new
// window and starting the visual clone.
nsresult rv = DispatchEvent(NS_LITERAL_STRING("MozTogglePictureInPicture"));
if (NS_FAILED(rv)) {
error.Throw(rv);
}
}
} // namespace dom
} // namespace mozilla

View File

@ -145,8 +145,6 @@ class HTMLVideoElement final : public HTMLMediaElement {
bool IsCloningElementVisually() const { return !!mVisualCloneTarget; }
void TogglePictureInPicture(ErrorResult& rv);
protected:
virtual ~HTMLVideoElement();

View File

@ -745,8 +745,6 @@ interface nsIDOMWindowUtils : nsISupports {
* frame when retrieving the element. If false, this method returns
* null for coordinates outside of the viewport.
* @param aFlushLayout flushes layout if true. Otherwise, no flush occurs.
* @param aOnlyVisible Set to true if you only want nodes that pass a visibility
* hit test.
*/
NodeList nodesFromRect(in float aX,
in float aY,
@ -755,8 +753,7 @@ interface nsIDOMWindowUtils : nsISupports {
in float aBottomSize,
in float aLeftSize,
in boolean aIgnoreRootScrollFrame,
in boolean aFlushLayout,
in boolean aOnlyVisible);
in boolean aFlushLayout);
/**

View File

@ -13,8 +13,20 @@
let dwu = window.windowUtils;
/*
NodeList nodesFromRect(in float aX,
in float aY,
in float aTopSize,
in float aRightSize,
in float aBottomSize,
in float aLeftSize,
in boolean aIgnoreRootScrollFrame,
in boolean aFlushLayout);
*/
function check(x, y, top, right, bottom, left, list) {
let nodes = dwu.nodesFromRect(x, y, top, right, bottom, left, true, false, false);
let nodes = dwu.nodesFromRect(x, y, top, right, bottom, left, true, false);
list.push(e.body);
list.push(e.html);

View File

@ -69,12 +69,6 @@ partial interface HTMLVideoElement {
// <video> element (see cloneElementVisually).
[Func="IsChromeOrXBLOrUAWidget"]
readonly attribute boolean isCloningElementVisually;
// Fires the privileged MozTogglePictureInPicture event to enter
// Picture-in-Picture. Call this when triggering Picture-in-Picture
// from the video controls UAWidget.
[Throws, Func="IsChromeOrXBLOrUAWidget"]
void togglePictureInPicture();
};
// https://dvcs.w3.org/hg/html-media/raw-file/default/media-source/media-source.html#idl-def-HTMLVideoElement

View File

@ -7,17 +7,25 @@
var EXPORTED_SYMBOLS = ["PictureInPictureChild", "PictureInPictureToggleChild"];
const {ActorChild} = ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.defineModuleGetter(this, "DeferredTask",
"resource://gre/modules/DeferredTask.jsm");
ChromeUtils.defineModuleGetter(this, "DOMLocalization",
"resource://gre/modules/DOMLocalization.jsm");
ChromeUtils.defineModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["InspectorUtils"]);
const TOGGLE_STYLESHEET = "chrome://global/skin/pictureinpicture/toggle.css";
const TOGGLE_ID = "picture-in-picture-toggle";
const FLYOUT_TOGGLE_ID = "picture-in-picture-flyout-toggle";
const FLYOUT_TOGGLE_CONTAINER = "picture-in-picture-flyout-container";
const TOGGLE_ENABLED_PREF =
"media.videocontrols.picture-in-picture.video-toggle.enabled";
const FLYOUT_ENABLED_PREF =
"media.videocontrols.picture-in-picture.video-toggle.flyout-enabled";
const FLYOUT_WAIT_MS_PREF =
"media.videocontrols.picture-in-picture.video-toggle.flyout-wait-ms";
const FLYOUT_ANIMATION_RUNTIME_MS = 400;
const MOUSEMOVE_PROCESSING_DELAY_MS = 50;
// A weak reference to the most recent <video> in this content
@ -26,11 +34,20 @@ var gWeakVideo = null;
// A weak reference to the content window of the most recent
// Picture-in-Picture window for this content process.
var gWeakPlayerContent = null;
// A process-global Promise that's set the first time the string for the
// flyout toggle label is requested from Fluent.
var gFlyoutLabelPromise = null;
// A process-global for the width of the toggle icon. We stash this here after
// computing it the first time to avoid repeatedly flushing styles.
var gToggleWidth = 0;
/**
* The PictureInPictureToggleChild is responsible for displaying the overlaid
* Picture-in-Picture toggle over top of <video> elements that the mouse is
* hovering.
*
* It's also responsible for showing the "flyout" version of the toggle, which
* currently displays on the first visible video per page.
*/
class PictureInPictureToggleChild extends ActorChild {
constructor(dispatcher) {
@ -42,6 +59,12 @@ class PictureInPictureToggleChild extends ActorChild {
// itself.
this.weakDocStates = new WeakMap();
this.toggleEnabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF);
this.flyoutEnabled = Services.prefs.getBoolPref(FLYOUT_ENABLED_PREF);
this.flyoutWaitMs = Services.prefs.getIntPref(FLYOUT_WAIT_MS_PREF);
this.l10n = new DOMLocalization([
"toolkit/global/videocontrols.ftl",
]);
}
/**
@ -61,12 +84,18 @@ class PictureInPictureToggleChild extends ActorChild {
weakVisibleVideos: new WeakSet(),
// The number of videos that are supposedly visible, according to the
// IntersectionObserver
visibleVideosCount: 0,
visibleVideos: 0,
// The DeferredTask that we'll arm every time a mousemove event occurs
// on a page where we have one or more visible videos.
mousemoveDeferredTask: null,
// A weak reference to the last video we displayed the toggle over.
weakOverVideo: null,
// A reference to the AnonymousContent returned after inserting the
// small toggle.
pipToggle: null,
// A reference to the AnonymousContent returned after inserting the
// flyout toggle.
flyoutToggle: null,
};
this.weakDocStates.set(this.content.document, state);
}
@ -79,14 +108,30 @@ class PictureInPictureToggleChild extends ActorChild {
case "canplay": {
if (this.toggleEnabled &&
event.target instanceof this.content.HTMLVideoElement &&
!event.target.controls &&
event.target.ownerDocument == this.content.document) {
this.registerVideo(event.target);
}
break;
}
case "mousedown": {
this.onMouseDown(event);
case "click": {
let state = this.docState;
let clickedFlyout = state.flyoutToggle &&
state.flyoutToggle.getTargetIdForEvent(event) == FLYOUT_TOGGLE_ID;
let clickedToggle = state.pipToggle &&
state.pipToggle.getTargetIdForEvent(event) == TOGGLE_ID;
if (clickedFlyout || clickedToggle) {
let video = state.weakOverVideo && state.weakOverVideo.get();
if (video) {
let pipEvent =
new this.content.CustomEvent("MozTogglePictureInPicture", {
bubbles: true,
});
video.dispatchEvent(pipEvent);
this.hideFlyout();
this.onMouseLeaveVideo(video);
}
}
break;
}
case "mousemove": {
@ -107,7 +152,7 @@ class PictureInPictureToggleChild extends ActorChild {
if (!state.intersectionObserver) {
let fn = this.onIntersection.bind(this);
state.intersectionObserver = new this.content.IntersectionObserver(fn, {
threshold: [0.0, 0.5],
threshold: [0.0, 1.0],
});
}
@ -126,7 +171,13 @@ class PictureInPictureToggleChild extends ActorChild {
* this registered video.
*/
worthTracking(intersectionEntry) {
return intersectionEntry.isIntersecting;
let video = intersectionEntry.target;
let rect = video.ownerGlobal.windowUtils.getBoundsWithoutFlushing(video);
let intRect = intersectionEntry.intersectionRect;
return intersectionEntry.isIntersecting &&
rect.width == intRect.width &&
rect.height == intRect.height;
}
/**
@ -143,25 +194,33 @@ class PictureInPictureToggleChild extends ActorChild {
// still alive and referrable from the WeakSet because the
// IntersectionObserverEntry holds a strong reference to the video.
let state = this.docState;
let oldVisibleVideosCount = state.visibleVideosCount;
let oldVisibleVideos = state.visibleVideos;
for (let entry of entries) {
let video = entry.target;
if (this.worthTracking(entry)) {
if (!state.weakVisibleVideos.has(video)) {
state.weakVisibleVideos.add(video);
state.visibleVideosCount++;
state.visibleVideos++;
// The very first video that we notice is worth tracking, we'll show
// the flyout toggle on.
if (this.flyoutEnabled) {
this.content.requestIdleCallback(() => {
this.maybeShowFlyout(video);
});
}
}
} else if (state.weakVisibleVideos.has(video)) {
state.weakVisibleVideos.delete(video);
state.visibleVideosCount--;
state.visibleVideos--;
}
}
if (!oldVisibleVideosCount && state.visibleVideosCount) {
if (!oldVisibleVideos && state.visibleVideos) {
this.content.requestIdleCallback(() => {
this.beginTrackingMouseOverVideos();
});
} else if (oldVisibleVideosCount && !state.visibleVideosCount) {
} else if (oldVisibleVideos && !state.visibleVideos) {
this.content.requestIdleCallback(() => {
this.stopTrackingMouseOverVideos();
});
@ -189,12 +248,9 @@ class PictureInPictureToggleChild extends ActorChild {
}, MOUSEMOVE_PROCESSING_DELAY_MS);
}
this.content.document.addEventListener("mousemove", this,
{ mozSystemGroup: true, capture: true });
// We want to try to cancel the mouse events from continuing
// on into content if the user has clicked on the toggle, so
// we don't use the mozSystemGroup here.
this.content.document.addEventListener("mousedown", this,
{ capture: true });
{ mozSystemGroup: true });
this.content.document.addEventListener("click", this,
{ mozSystemGroup: true });
}
/**
@ -206,65 +262,15 @@ class PictureInPictureToggleChild extends ActorChild {
let state = this.docState;
state.mousemoveDeferredTask.disarm();
this.content.document.removeEventListener("mousemove", this,
{ mozSystemGroup: true, capture: true });
this.content.document.removeEventListener("mousedown", this,
{ capture: true });
{ mozSystemGroup: true });
this.content.document.removeEventListener("click", this,
{ mozSystemGroup: true });
let oldOverVideo = state.weakOverVideo && state.weakOverVideo.get();
if (oldOverVideo) {
this.onMouseLeaveVideo(oldOverVideo);
}
}
/**
* If we're tracking <video> elements, this mousedown event handler is run anytime
* a mousedown occurs on the document. This function is responsible for checking
* if the user clicked on the Picture-in-Picture toggle. It does this by first
* checking if the video is visible beneath the point that was clicked. Then
* it tests whether or not the mousedown occurred within the rectangle of the
* toggle. If so, the event's default behaviour and propagation are stopped,
* and Picture-in-Picture is triggered.
*
* @param {Event} event The mousemove event.
*/
onMouseDown(event) {
let state = this.docState;
let video = state.weakOverVideo && state.weakOverVideo.get();
if (!video) {
return;
}
let shadowRoot = video.openOrClosedShadowRoot;
if (!shadowRoot) {
return;
}
let { clientX, clientY } = event;
let winUtils = this.content.windowUtils;
// We use winUtils.nodesFromRect instead of document.elementsFromPoint,
// since document.elementsFromPoint always flushes layout. The 1's in that
// function call are for the size of the rect that we want, which is 1x1.
//
// We pass the aOnlyVisible boolean argument to check that the video isn't
// occluded by anything visible at the point of mousedown. If it is, we'll
// ignore the mousedown.
let elements = winUtils.nodesFromRect(clientX, clientY, 1, 1, 1, 1, true,
false, true /* aOnlyVisible */);
if (!Array.from(elements).includes(video)) {
return;
}
let toggle = shadowRoot.getElementById("pictureInPictureToggleButton");
if (this.isMouseOverToggle(toggle, event)) {
event.preventDefault();
event.stopPropagation();
let pipEvent =
new this.content.CustomEvent("MozTogglePictureInPicture", {
bubbles: true,
});
video.dispatchEvent(pipEvent);
}
}
/**
* Called for each mousemove event when we're tracking those events to
* determine if the cursor is hovering over a <video>.
@ -292,12 +298,12 @@ class PictureInPictureToggleChild extends ActorChild {
// since document.elementsFromPoint always flushes layout. The 1's in that
// function call are for the size of the rect that we want, which is 1x1.
let elements = winUtils.nodesFromRect(clientX, clientY, 1, 1, 1, 1, true,
false, false);
false);
for (let element of elements) {
if (state.weakVisibleVideos.has(element) &&
!element.isCloningElementVisually) {
this.onMouseOverVideo(element, event);
this.onMouseOverVideo(element);
return;
}
}
@ -314,62 +320,15 @@ class PictureInPictureToggleChild extends ActorChild {
*
* @param {Element} video The video the mouse is over.
*/
onMouseOverVideo(video, event) {
onMouseOverVideo(video) {
let state = this.docState;
let oldOverVideo = state.weakOverVideo && state.weakOverVideo.get();
let shadowRoot = video.openOrClosedShadowRoot;
// It seems from automated testing that if it's still very early on in the
// lifecycle of a <video> element, it might not yet have a shadowRoot,
// in which case, we can bail out here early.
if (!shadowRoot) {
if (oldOverVideo) {
// We also clear the hover state on the old video we were hovering,
// if there was one.
this.onMouseLeaveVideo(oldOverVideo);
}
if (oldOverVideo && oldOverVideo == video) {
return;
}
let toggle = shadowRoot.getElementById("pictureInPictureToggleButton");
if (oldOverVideo) {
if (oldOverVideo == video) {
// If we're still hovering the old video, we might have entered or
// exited the toggle region.
this.checkHoverToggle(toggle, event);
return;
}
// We had an old video that we were hovering, and we're not hovering
// it anymore. Let's leave it.
this.onMouseLeaveVideo(oldOverVideo);
}
state.weakOverVideo = Cu.getWeakReference(video);
let controlsOverlay = shadowRoot.querySelector(".controlsOverlay");
InspectorUtils.addPseudoClassLock(controlsOverlay, ":hover");
// Now that we're hovering the video, we'll check to see if we're
// hovering the toggle too.
this.checkHoverToggle(toggle, event);
}
/**
* Checks if a mouse event is happening over a toggle element. If it is,
* sets the :hover pseudoclass on it. Otherwise, it clears the :hover
* pseudoclass.
*
* @param {Element} toggle The Picture-in-Picture toggle to check.
* @param {MouseEvent} event A MouseEvent to test.
*/
checkHoverToggle(toggle, event) {
if (this.isMouseOverToggle(toggle, event)) {
InspectorUtils.addPseudoClassLock(toggle, ":hover");
} else {
InspectorUtils.removePseudoClassLock(toggle, ":hover");
}
this.moveToggleToVideo(video);
}
/**
@ -380,35 +339,203 @@ class PictureInPictureToggleChild extends ActorChild {
*/
onMouseLeaveVideo(video) {
let state = this.docState;
let shadowRoot = video.openOrClosedShadowRoot;
if (shadowRoot) {
let controlsOverlay = shadowRoot.querySelector(".controlsOverlay");
let toggle = shadowRoot.getElementById("pictureInPictureToggleButton");
InspectorUtils.removePseudoClassLock(controlsOverlay, ":hover");
InspectorUtils.removePseudoClassLock(toggle, ":hover");
}
state.weakOverVideo = null;
state.pipToggle.setAttributeForElement(TOGGLE_ID, "hidden", "true");
}
/**
* Given a reference to a Picture-in-Picture toggle element, determines
* if a MouseEvent event is occurring within its bounds.
* The toggle is injected as AnonymousContent that is positioned absolutely.
* This method takes the <video> that we want to display the toggle on and
* calculates where exactly we need to position the AnonymousContent in
* absolute coordinates.
*
* @param {Element} toggle The Picture-in-Picture toggle.
* @param {MouseEvent} event A MouseEvent to test.
* @param {Element} video The video to display the toggle on.
* @param {AnonymousContent} anonymousContent The anonymousContent associated
* with the toggle about to be shown.
* @param {String} toggleID The ID of the toggle element with the CSS
* variables defining the toggle width and padding.
*
* @return {Boolean}
* @return {Object} with the following properties:
* {Number} top The top / y coordinate.
* {Number} left The left / x coordinate.
* {Number} width The width of the toggle icon, including padding.
*/
isMouseOverToggle(toggle, event) {
let toggleRect =
toggle.ownerGlobal.windowUtils.getBoundsWithoutFlushing(toggle);
let { clientX, clientY } = event;
return clientX >= toggleRect.left &&
clientX <= toggleRect.right &&
clientY >= toggleRect.top &&
clientY <= toggleRect.bottom;
calculateTogglePosition(video, anonymousContent, toggleID) {
let winUtils = this.content.windowUtils;
let scrollX = {}, scrollY = {};
winUtils.getScrollXY(false, scrollX, scrollY);
let rect = winUtils.getBoundsWithoutFlushing(video);
// For now, using AnonymousContent.getComputedStylePropertyValue causes
// a style flush, so we'll cache the value in this content process the
// first time we read it. See bug 1541207.
if (!gToggleWidth) {
let widthStr = anonymousContent.getComputedStylePropertyValue(toggleID,
"--pip-toggle-icon-width-height");
let paddingStr = anonymousContent.getComputedStylePropertyValue(toggleID,
"--pip-toggle-padding");
let iconWidth = parseInt(widthStr, 0);
let iconPadding = parseInt(paddingStr, 0);
gToggleWidth = iconWidth + (2 * iconPadding);
}
let originY = rect.top + scrollY.value;
let originX = rect.left + scrollX.value;
let top = originY + (rect.height / 2 - Math.round(gToggleWidth / 2));
let left = originX + (rect.width - gToggleWidth);
return { top, left, width: gToggleWidth };
}
/**
* Puts the small "Picture-in-Picture" toggle onto the passed in video.
*
* @param {Element} video The video to display the toggle on.
*/
moveToggleToVideo(video) {
let state = this.docState;
let winUtils = this.content.windowUtils;
if (!state.pipToggle) {
try {
winUtils.loadSheetUsingURIString(TOGGLE_STYLESHEET,
winUtils.AGENT_SHEET);
} catch (e) {
// This method can fail with NS_ERROR_INVALID_ARG if the sheet is
// already loaded - for example, from the flyout toggle.
if (e.result != Cr.NS_ERROR_INVALID_ARG) {
throw e;
}
}
let toggle = this.content.document.createElement("button");
toggle.classList.add("picture-in-picture-toggle-button");
toggle.id = TOGGLE_ID;
let icon = this.content.document.createElement("div");
icon.classList.add("icon");
toggle.appendChild(icon);
state.pipToggle = this.content.document.insertAnonymousContent(toggle);
}
let { top, left } = this.calculateTogglePosition(video, state.pipToggle,
TOGGLE_ID);
let styles = `
top: ${top}px;
left: ${left}px;
`;
let toggle = state.pipToggle;
toggle.setAttributeForElement(TOGGLE_ID, "style", styles);
// The toggle might have been hidden after a previous appearance.
toggle.removeAttributeForElement(TOGGLE_ID, "hidden");
}
/**
* Lazy getter that returns a Promise that resolves to the flyout toggle
* label string. Sets a process-global variable to the Promise so that
* subsequent calls within the same process don't cause us to go through
* the Fluent look-up path again.
*/
get flyoutLabel() {
if (gFlyoutLabelPromise) {
return gFlyoutLabelPromise;
}
gFlyoutLabelPromise =
this.l10n.formatValue("picture-in-picture-flyout-toggle");
return gFlyoutLabelPromise;
}
/**
* If configured to, will display the "Picture-in-Picture" flyout toggle on
* the passed-in video. This is an asynchronous function that handles the
* entire lifecycle of the flyout animation. If a flyout toggle has already
* been seen on this page, this function does nothing.
*
* @param {Element} video The video to display the flyout on.
*
* @return {Promise}
* @resolves {undefined} Once the flyout toggle animation has completed.
*/
async maybeShowFlyout(video) {
let state = this.docState;
if (state.flyoutToggle) {
return;
}
let winUtils = this.content.windowUtils;
try {
winUtils.loadSheetUsingURIString(TOGGLE_STYLESHEET, winUtils.AGENT_SHEET);
} catch (e) {
// This method can fail with NS_ERROR_INVALID_ARG if the sheet is
// already loaded.
if (e.result != Cr.NS_ERROR_INVALID_ARG) {
throw e;
}
}
let container = this.content.document.createElement("div");
container.id = FLYOUT_TOGGLE_CONTAINER;
let toggle = this.content.document.createElement("button");
toggle.classList.add("picture-in-picture-toggle-button");
toggle.id = FLYOUT_TOGGLE_ID;
let icon = this.content.document.createElement("div");
icon.classList.add("icon");
toggle.appendChild(icon);
let label = this.content.document.createElement("span");
label.classList.add("label");
label.textContent = await this.flyoutLabel;
toggle.appendChild(label);
container.appendChild(toggle);
state.flyoutToggle =
this.content.document.insertAnonymousContent(container);
let { top, left, width } =
this.calculateTogglePosition(video, state.flyoutToggle, FLYOUT_TOGGLE_ID);
let styles = `
top: ${top}px;
left: ${left}px;
`;
let flyout = state.flyoutToggle;
flyout.setAttributeForElement(FLYOUT_TOGGLE_CONTAINER, "style", styles);
let flyoutAnim = flyout.setAnimationForElement(FLYOUT_TOGGLE_ID, [
{ transform: `translateX(calc(100% - ${width}px))`, opacity: "0.2" },
{ transform: `translateX(calc(100% - ${width}px))`, opacity: "0.8" },
{ transform: "translateX(0)", opacity: "1" },
], FLYOUT_ANIMATION_RUNTIME_MS);
await flyoutAnim.finished;
await new Promise(resolve => this.content.setTimeout(resolve,
this.flyoutWaitMs));
flyoutAnim.reverse();
await flyoutAnim.finished;
this.hideFlyout();
}
/**
* Once the flyout has finished animating, or Picture-in-Picture has been
* requested, this function can be called to hide it.
*/
hideFlyout() {
let state = this.docState;
let flyout = state.flyoutToggle;
if (flyout) {
flyout.setAttributeForElement(FLYOUT_TOGGLE_CONTAINER, "hidden", "true");
}
}
}

View File

@ -14,7 +14,6 @@ class UAWidgetsChild extends ActorChild {
super(dispatcher);
this.widgets = new WeakMap();
this.prefsCache = new Map();
}
handleEvent(aEvent) {
@ -50,15 +49,11 @@ class UAWidgetsChild extends ActorChild {
setupWidget(aElement) {
let uri;
let widgetName;
let prefKeys = [];
switch (aElement.localName) {
case "video":
case "audio":
uri = "chrome://global/content/elements/videocontrols.js";
widgetName = "VideoControlsWidget";
prefKeys = [
"media.videocontrols.picture-in-picture.video-toggle.enabled",
];
break;
case "input":
uri = "chrome://global/content/elements/datetimebox.js";
@ -94,9 +89,7 @@ class UAWidgetsChild extends ActorChild {
Services.scriptloader.loadSubScript(uri, sandbox);
}
let prefs = Cu.cloneInto(this.getPrefsForUAWidget(widgetName, prefKeys), sandbox);
let widget = new sandbox[widgetName](shadowRoot, prefs);
let widget = new sandbox[widgetName](shadowRoot);
if (!isSystemPrincipal) {
widget = widget.wrappedJSObject;
}
@ -122,32 +115,4 @@ class UAWidgetsChild extends ActorChild {
}
this.widgets.delete(aElement);
}
getPrefsForUAWidget(aWidgetName, aPrefKeys) {
let result = this.prefsCache.get(aWidgetName);
if (result) {
return result;
}
result = {};
for (let key of aPrefKeys) {
switch (Services.prefs.getPrefType(key)) {
case Ci.nsIPrefBranch.PREF_BOOL: {
result[key] = Services.prefs.getBoolPref(key);
break;
}
case Ci.nsIPrefBranch.PREF_INT: {
result[key] = Services.prefs.getIntPref(key);
break;
}
case Ci.nsIPrefBranch.PREF_STRING: {
result[key] = Services.prefs.getStringPref(key);
break;
}
}
}
this.prefsCache.set(aWidgetName, result);
return result;
}
}

View File

@ -12,9 +12,8 @@
* according to the value of the "controls" property.
*/
this.VideoControlsWidget = class {
constructor(shadowRoot, prefs) {
constructor(shadowRoot) {
this.shadowRoot = shadowRoot;
this.prefs = prefs;
this.element = shadowRoot.host;
this.document = this.element.ownerDocument;
this.window = this.document.defaultView;
@ -52,8 +51,6 @@ this.VideoControlsWidget = class {
newImpl = NoControlsMobileImplWidget;
} else if (VideoControlsWidget.isPictureInPictureVideo(this.element)) {
newImpl = NoControlsPictureInPictureImplWidget;
} else {
newImpl = NoControlsDesktopImplWidget;
}
// Skip if we are asked to load the same implementation, and
@ -70,7 +67,7 @@ this.VideoControlsWidget = class {
this.shadowRoot.firstChild.remove();
}
if (newImpl) {
this.impl = new newImpl(this.shadowRoot, this.prefs);
this.impl = new newImpl(this.shadowRoot);
this.impl.onsetup();
} else {
this.impl = undefined;
@ -92,9 +89,8 @@ this.VideoControlsWidget = class {
};
this.VideoControlsImplWidget = class {
constructor(shadowRoot, prefs) {
constructor(shadowRoot) {
this.shadowRoot = shadowRoot;
this.prefs = prefs;
this.element = shadowRoot.host;
this.document = this.element.ownerDocument;
this.window = this.document.defaultView;
@ -269,10 +265,6 @@ this.VideoControlsImplWidget = class {
this.setShowPictureInPictureMessage(true);
}
if (!this.pipToggleEnabled || this.isShowingPictureInPictureMessage) {
this.pictureInPictureToggleButton.setAttribute("hidden", true);
}
let adjustableControls = [
...this.prioritizedControls,
this.controlBar,
@ -675,9 +667,6 @@ this.VideoControlsImplWidget = class {
// Prevent any click event within media controls from dispatching through to video.
aEvent.stopPropagation();
break;
case this.pictureInPictureToggleButton:
this.video.togglePictureInPicture();
break;
}
break;
case "dblclick":
@ -1954,18 +1943,13 @@ this.VideoControlsImplWidget = class {
}
},
get pipToggleEnabled() {
return this.prefs["media.videocontrols.picture-in-picture.video-toggle.enabled"];
},
init(shadowRoot, prefs) {
init(shadowRoot) {
this.shadowRoot = shadowRoot;
this.video = this.installReflowCallValidator(shadowRoot.host);
this.videocontrols = this.installReflowCallValidator(shadowRoot.firstChild);
this.document = this.videocontrols.ownerDocument;
this.window = this.document.defaultView;
this.shadowRoot = shadowRoot;
this.prefs = prefs;
this.controlsContainer = this.shadowRoot.getElementById("controlsContainer");
this.statusIcon = this.shadowRoot.getElementById("statusIcon");
@ -1991,8 +1975,6 @@ this.VideoControlsImplWidget = class {
this.castingButton = this.shadowRoot.getElementById("castingButton");
this.closedCaptionButton = this.shadowRoot.getElementById("closedCaptionButton");
this.textTrackList = this.shadowRoot.getElementById("textTrackList");
this.pictureInPictureToggleButton =
this.shadowRoot.getElementById("pictureInPictureToggleButton");
if (this.positionDurationBox) {
this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
@ -2080,8 +2062,6 @@ this.VideoControlsImplWidget = class {
{ el: this.video.textTracks, type: "change" },
{ el: this.video, type: "media-videoCasting", touchOnly: true },
{ el: this.pictureInPictureToggleButton, type: "click" },
];
for (let { el, type, nonTouchOnly = false, touchOnly = false,
@ -2230,7 +2210,7 @@ this.VideoControlsImplWidget = class {
},
};
this.Utils.init(this.shadowRoot, this.prefs);
this.Utils.init(this.shadowRoot);
if (this.Utils.isTouchControls) {
this.TouchUtils.init(this.shadowRoot, this.Utils);
}
@ -2273,10 +2253,6 @@ this.VideoControlsImplWidget = class {
<div id="clickToPlay" class="clickToPlay" hidden="true"></div>
</div>
<button id="pictureInPictureToggleButton" class="pictureInPictureToggleButton">
<div id="pictureInPictureToggleIcon" class="pictureInPictureToggleIcon"></div>
</button>
<div id="controlBar" class="controlBar" role="none" hidden="true">
<button id="playButton"
class="button playButton"
@ -2492,9 +2468,8 @@ this.NoControlsMobileImplWidget = class {
};
this.NoControlsPictureInPictureImplWidget = class {
constructor(shadowRoot, prefs) {
constructor(shadowRoot) {
this.shadowRoot = shadowRoot;
this.prefs = prefs;
this.element = shadowRoot.host;
this.document = this.element.ownerDocument;
this.window = this.document.defaultView;
@ -2534,71 +2509,3 @@ this.NoControlsPictureInPictureImplWidget = class {
this.shadowRoot.importNodeAndAppendChildAt(this.shadowRoot, parserDoc.documentElement, true);
}
};
this.NoControlsDesktopImplWidget = class {
constructor(shadowRoot, prefs) {
this.shadowRoot = shadowRoot;
this.element = shadowRoot.host;
this.document = this.element.ownerDocument;
this.window = this.document.defaultView;
this.prefs = prefs;
}
onsetup() {
this.generateContent();
this.Utils = {
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");
if (!this.pipToggleEnabled) {
this.pictureInPictureToggleButton.setAttribute("hidden", true);
}
},
get pipToggleEnabled() {
return this.prefs["media.videocontrols.picture-in-picture.video-toggle.enabled"];
},
};
this.Utils.init(this.shadowRoot, this.prefs);
}
elementStateMatches(element) {
return true;
}
destructor() {
}
generateContent() {
/*
* Pass the markup through XML parser purely for the reason of loading the localization DTD.
* Remove it when migrate to Fluent.
*/
const parser = new this.window.DOMParser();
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" type="text/css" href="chrome://global/skin/media/videocontrols.css" />
<div id="controlsContainer" class="controlsContainer" role="none">
<div class="controlsOverlay stackItem">
<button id="pictureInPictureToggleButton" class="pictureInPictureToggleButton">
<div id="pictureInPictureToggleIcon" class="pictureInPictureToggleIcon"></div>
</button>
</div>
</div>
</div>`, "application/xml");
this.shadowRoot.importNodeAndAppendChildAt(this.shadowRoot, parserDoc.documentElement, true);
}
};

View File

@ -0,0 +1,13 @@
# 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/.
### These strings are used in the video controls.
# This string is used when displaying the Picture-in-Picture "flyout" toggle.
# The "flyout" toggle is a variation of the Picture-in-Picture video toggle that
# appears in a ribbon over top of <video> elements when Picture-in-Picture is
# enabled. This variation only appears on the first <video> that's displayed to
# a user on a page. It animates out, displaying this string, and after 5
# seconds, animates away again.
picture-in-picture-flyout-toggle = Picture-in-Picture

View File

@ -112,5 +112,6 @@ toolkit.jar:
skin/classic/global/plugins/contentPluginCrashed.png (../../shared/plugins/contentPluginCrashed.png)
skin/classic/global/plugins/contentPluginStripe.png (../../shared/plugins/contentPluginStripe.png)
skin/classic/global/pictureinpicture/player.css (../../shared/pictureinpicture/player.css)
skin/classic/global/pictureinpicture/toggle.css (../../shared/pictureinpicture/toggle.css)
skin/classic/global/media/pictureinpicture.svg (../../shared/media/pictureinpicture.svg)

View File

@ -28,10 +28,6 @@
--track-size: 5px;
--thumb-size: 13px;
--label-font-size: 13px;
--pip-toggle-bgcolor: rgb(0, 96, 223);
--pip-toggle-text-and-icon-color: rgb(255, 255, 255);
--pip-toggle-padding: 5px;
--pip-toggle-icon-width-height: 16px;
}
.controlsContainer.touch {
--clickToPlay-size: 64px;
@ -69,8 +65,7 @@
}
.controlsContainer [hidden],
.controlBar[hidden],
.pictureInPictureToggleButton[hidden] {
.controlBar[hidden] {
display: none;
}
@ -438,44 +433,6 @@
stroke: #fff;
}
.pictureInPictureToggleButton {
display: flex;
-moz-appearance: none;
position: absolute;
background-color: var(--pip-toggle-bgcolor);
color: var(--pip-toggle-text-and-icon-color);
border: 0;
padding: var(--pip-toggle-padding);
right: 0;
top: 50%;
transform: translateY(-50%);
transition: opacity 160ms linear;
min-width: max-content;
pointer-events: auto;
opacity: 0;
}
.pictureInPictureToggleIcon {
display: inline-block;
background-image: url(chrome://global/skin/media/pictureinpicture.svg);
background-position: center left;
background-repeat: no-repeat;
-moz-context-properties: fill, stroke;
fill: var(--pip-toggle-text-and-icon-color);
stroke: var(--pip-toggle-text-and-icon-color);
width: var(--pip-toggle-icon-width-height);
height: var(--pip-toggle-icon-width-height);
min-width: max-content;
}
.controlsOverlay:hover > .pictureInPictureToggleButton {
opacity: 0.8;
}
.controlsOverlay:hover > .pictureInPictureToggleButton:hover {
opacity: 1;
}
/* Overlay Play button */
.clickToPlay {
min-width: var(--clickToPlay-size);

View File

@ -0,0 +1,83 @@
/**
* We add the #picture-in-picture-flyout-container and
* #picture-in-picture-toggle IDs here so that it's easier to read these
* property values in script, since they're AnonymousContent, and we need
* IDs and can't use classes to query AnonymousContent property values.
*/
#picture-in-picture-flyout-container:-moz-native-anonymous,
#picture-in-picture-toggle:-moz-native-anonymous,
.picture-in-picture-toggle-button:-moz-native-anonymous {
--pip-toggle-bgcolor: rgb(0, 96, 223);
--pip-toggle-text-and-icon-color: rgb(255, 255, 255);
--pip-toggle-padding: 5px;
--pip-toggle-icon-width-height: 16px;
}
.picture-in-picture-toggle-button:-moz-native-anonymous {
-moz-appearance: none;
display: flex;
position: absolute;
background-color: var(--pip-toggle-bgcolor);
border: 0;
padding: var(--pip-toggle-padding);
color: var(--pip-toggle-text-and-icon-color);
transform: translateX(0);
transition: transform 350ms linear;
min-width: max-content;
pointer-events: auto;
opacity: 0.8;
}
.picture-in-picture-toggle-button:-moz-native-anonymous:hover,
.picture-in-picture-toggle-button:-moz-native-anonymous:active {
opacity: 1;
background-color: var(--pip-toggle-bgcolor);
color: var(--pip-toggle-text-and-icon-color);
padding: var(--pip-toggle-padding);
}
#picture-in-picture-flyout-container[hidden]:-moz-native-anonymous,
.picture-in-picture-toggle-button[hidden]:-moz-native-anonymous {
display: none;
}
.picture-in-picture-toggle-button:-moz-native-anonymous > .icon {
display: inline-block;
background-image: url(chrome://global/skin/media/pictureinpicture.svg);
background-position: center left;
background-repeat: no-repeat;
-moz-context-properties: fill, stroke;
fill: var(--pip-toggle-text-and-icon-color);
stroke: var(--pip-toggle-text-and-icon-color);
width: var(--pip-toggle-icon-width-height);
height: var(--pip-toggle-icon-width-height);
min-width: max-content;
pointer-events: none;
}
.picture-in-picture-toggle-button:-moz-native-anonymous > .label {
margin-left: var(--pip-toggle-padding);
min-width: max-content;
pointer-events: none;
}
#picture-in-picture-flyout-container:-moz-native-anonymous {
position: absolute;
/**
* A higher z-index makes sure that the flyout always appears on top of the
* other toggle, so that we avoid seeing double-toggles.
*/
z-index: 2;
overflow: hidden;
/**
* This places the container for the flyout in the position where the flyout
* eventually ends up. This, coupled with the overflow: hidden, gives the
* effect that the flyout is sliding out from the edge of the video.
*/
transform: translateX(calc(-100% + var(--pip-toggle-icon-width-height) + 2 * var(--pip-toggle-padding)));
}
#picture-in-picture-flyout-container:-moz-native-anonymous > .picture-in-picture-toggle-button {
position: relative;
opacity: 1;
}