copy from toolkit/content/widgets/videocontrols.xml
copy to toolkit/content/widgets/videocontrols.js
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.js
@@ -1,93 +1,88 @@
-<?xml version="1.0"?>
-<!-- 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/. -->
+/* 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/. */
-<!DOCTYPE bindings [
-<!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
-%videocontrolsDTD;
-]>
+"use strict";
-<bindings id="videoControlBindings"
- xmlns="http://www.mozilla.org/xbl"
- xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
- xmlns:xbl="http://www.mozilla.org/xbl"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns:html="http://www.w3.org/1999/xhtml">
+// This is a page widget. It runs in per-origin UA widget scope,
+// to be loaded by UAWidgetsChild.jsm.
-<binding id="videoControls">
- <resources>
- <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
- </resources>
+/*
+ * This is the class of entry. It will construct the actual implementation
+ * according to the value of the "controls" property.
+ */
+this.VideoControlsPageWidget = class {
+ constructor(shadowRoot) {
+ this.shadowRoot = shadowRoot;
+ this.element = shadowRoot.host;
+ this.document = this.element.ownerDocument;
+ this.window = this.document.defaultView;
- <xbl:content xmlns="http://www.w3.org/1999/xhtml" class="mediaControlsFrame">
- <div anonid="controlsContainer" class="controlsContainer" role="none">
- <div anonid="statusOverlay" class="statusOverlay stackItem" hidden="true">
- <div anonid="statusIcon" class="statusIcon"></div>
- <span class="errorLabel" anonid="errorAborted">&error.aborted;</span>
- <span class="errorLabel" anonid="errorNetwork">&error.network;</span>
- <span class="errorLabel" anonid="errorDecode">&error.decode;</span>
- <span class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</span>
- <span class="errorLabel" anonid="errorNoSource">&error.noSource2;</span>
- <span class="errorLabel" anonid="errorGeneric">&error.generic;</span>
- </div>
+ this.isMobile = this.window.navigator.appVersion.includes("Android");
+
+ this.switchImpl();
+ }
+
+ /*
+ * Callback called by UAWidgets when the "controls" property changes.
+ */
+ onattributechange() {
+ this.switchImpl();
+ }
- <div anonid="controlsOverlay" class="controlsOverlay stackItem">
- <div class="controlsSpacerStack">
- <div anonid="controlsSpacer" class="controlsSpacer stackItem" role="none"></div>
- <div anonid="clickToPlay" class="clickToPlay" hidden="true"></div>
- </div>
- <div anonid="controlBar" class="controlBar" hidden="true">
- <button anonid="playButton"
- class="button playButton"
- playlabel="&playButton.playLabel;"
- pauselabel="&playButton.pauseLabel;"
- tabindex="-1"/>
- <div anonid="scrubberStack" class="scrubberStack progressContainer" role="none">
- <div class="progressBackgroundBar stackItem" role="none">
- <div class="progressStack" role="none">
- <progress anonid="bufferBar" class="bufferBar" value="0" max="100" tabindex="-1"></progress>
- <progress anonid="progressBar" class="progressBar" value="0" max="100" tabindex="-1"></progress>
- </div>
- </div>
- <input type="range" anonid="scrubber" class="scrubber" tabindex="-1"/>
- </div>
- <span anonid="positionLabel" class="positionLabel" role="presentation"></span>
- <span anonid="durationLabel" class="durationLabel" role="presentation"></span>
- <span anonid="positionDurationBox" class="positionDurationBox" aria-hidden="true">
- &positionAndDuration.nameFormat;
- </span>
- <div anonid="controlBarSpacer" class="controlBarSpacer" hidden="true" role="none"></div>
- <button anonid="muteButton"
- class="button muteButton"
- mutelabel="&muteButton.muteLabel;"
- unmutelabel="&muteButton.unmuteLabel;"
- tabindex="-1"/>
- <div anonid="volumeStack" class="volumeStack progressContainer" role="none">
- <input type="range" anonid="volumeControl" class="volumeControl" min="0" max="100" step="1" tabindex="-1"/>
- </div>
- <button anonid="castingButton" class="button castingButton"
- aria-label="&castingButton.castingLabel;"/>
- <button anonid="closedCaptionButton" class="button closedCaptionButton"/>
- <button anonid="fullscreenButton"
- class="button fullscreenButton"
- enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
- exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
- </div>
- <div anonid="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></div>
- </div>
- </div>
- </xbl:content>
+ /*
+ * Actually switch the implementation.
+ * - With "controls" set, the VideoControlsImplPageWidget controls should load.
+ * - Without it, on mobile, the NoControlsImplPageWidget should load, so
+ * the user could see the click-to-play button when the video/audio is blocked.
+ */
+ switchImpl() {
+ let newImpl;
+ if (this.element.controls) {
+ newImpl = VideoControlsImplPageWidget;
+ } else if (this.isMobile) {
+ newImpl = NoControlsImplPageWidget;
+ }
+ // Skip if we are asked to load the same implementation.
+ // This can happen if the property is set again w/o value change.
+ if (this.impl && this.impl.constructor == newImpl) {
+ return;
+ }
+ if (this.impl) {
+ this.impl.destructor();
+ this.shadowRoot.firstChild.remove();
+ }
+ if (newImpl) {
+ this.impl = new newImpl(this.shadowRoot);
+ } else {
+ this.impl = undefined;
+ }
+ }
- <implementation>
+ destructor() {
+ if (!this.impl) {
+ return;
+ }
+ this.impl.destructor();
+ this.shadowRoot.firstChild.remove();
+ delete this.impl;
+ }
+};
- <constructor>
- <![CDATA[
+this.VideoControlsImplPageWidget = class {
+ constructor(shadowRoot) {
+ this.shadowRoot = shadowRoot;
+ this.element = shadowRoot.host;
+ this.document = this.element.ownerDocument;
+ this.window = this.document.defaultView;
+
+ this.generateContent();
+
this.randomID = 0;
this.Utils = {
debug: false,
video: null,
videocontrols: null,
controlBar: null,
playButton: null,
@@ -204,17 +199,17 @@
// to determine if the media source changed while we were detached.
this.initPositionDurationBox();
this.maxCurrentTimeSeen = currentTime;
this.showPosition(currentTime, duration);
// If we have metadata, check if this is a <video> without
// video data, or a video with no audio track.
if (this.video.readyState >= this.video.HAVE_METADATA) {
- if (this.video instanceof HTMLVideoElement &&
+ if (this.video instanceof this.window.HTMLVideoElement &&
(this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
this.isAudioOnly = true;
}
// We have to check again if the media has audio here,
// because of bug 718107: switching to fullscreen may
// cause the bindings to detach and reattach, hence
// unsetting the attribute.
@@ -266,18 +261,18 @@
break;
}
Object.defineProperties(control, {
// We should directly access CSSOM to get pre-defined style instead of
// retrieving computed dimensions from layout.
minWidth: {
get: () => {
- let controlAnonId = control.getAttribute("anonid");
- let propertyName = `--${controlAnonId}-width`;
+ let controlId = control.id;
+ let propertyName = `--${controlId}-width`;
if (control.modifier) {
propertyName += "-" + control.modifier;
}
let preDefinedSize = this.controlBarComputedStyles.getPropertyValue(propertyName);
return parseInt(preDefinedSize, 10);
}
},
@@ -398,27 +393,27 @@
*
* Once the queued seek operation is done, we dispatch a
* "canplay" event which indicates that the resuming operation
* is completed.
*/
SHOW_THROBBER_TIMEOUT_MS: 250,
_showThrobberTimer: null,
_delayShowThrobberWhileResumingVideoDecoder() {
- this._showThrobberTimer = setTimeout(() => {
+ this._showThrobberTimer = this.window.setTimeout(() => {
this.statusIcon.setAttribute("type", "throbber");
// Show the throbber immediately since we have waited for SHOW_THROBBER_TIMEOUT_MS.
// We don't want to wait for another animation delay(750ms) and the
// animation duration(300ms).
this.setupStatusFader(true);
}, this.SHOW_THROBBER_TIMEOUT_MS);
},
_cancelShowThrobberWhileResumingVideoDecoder() {
if (this._showThrobberTimer) {
- clearTimeout(this._showThrobberTimer);
+ this.window.clearTimeout(this._showThrobberTimer);
this._showThrobberTimer = null;
}
},
handleEvent(aEvent) {
if (!aEvent.isTrusted) {
this.log("Drop untrusted event ----> " + aEvent.type);
return;
@@ -472,25 +467,27 @@
break;
case "volumechange":
this.updateVolumeControls();
// Show the controls to highlight the changing volume,
// but only if the click-to-play overlay has already
// been hidden (we don't hide controls when the overlay is visible).
if (this.clickToPlay.hidden && !this.isAudioOnly) {
this.startFadeIn(this.controlBar);
- clearTimeout(this._hideControlsTimeout);
- this._hideControlsTimeout =
- setTimeout(() => this._hideControlsFn(), this.HIDE_CONTROLS_TIMEOUT_MS);
+ this.window.clearTimeout(this._hideControlsTimeout);
+ this._hideControlsTimeout = this.window.setTimeout(
+ () => this._hideControlsFn(),
+ this.HIDE_CONTROLS_TIMEOUT_MS
+ );
}
break;
case "loadedmetadata":
// If a <video> doesn't have any video data, treat it as <audio>
// and show the controls (they won't fade back out)
- if (this.video instanceof HTMLVideoElement &&
+ if (this.video instanceof this.window.HTMLVideoElement &&
(this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
this.isAudioOnly = true;
this.clickToPlay.hidden = true;
this.startFadeIn(this.controlBar);
this.setFullscreenButtonState();
}
this.showPosition(Math.round(this.video.currentTime * 1000), Math.round(this.video.duration * 1000));
if (!this.isAudioOnly && !this.video.mozHasAudio) {
@@ -503,17 +500,17 @@
this.firstFrameShown = true;
this.setupStatusFader();
break;
case "loadstart":
this.maxCurrentTimeSeen = 0;
this.controlsSpacer.removeAttribute("aria-label");
this.statusOverlay.removeAttribute("error");
this.statusIcon.setAttribute("type", "throbber");
- this.isAudioOnly = (this.video instanceof HTMLAudioElement);
+ this.isAudioOnly = this.video instanceof this.window.HTMLAudioElement;
this.setPlayButtonState(true);
this.setupNewLoadState();
this.setupStatusFader();
break;
case "progress":
this.statusIcon.removeAttribute("stalled");
this.showBuffered();
this.setupStatusFader();
@@ -711,18 +708,18 @@
}
try {
for (let { el, type, capture = false } of this.controlsEvents) {
el.removeEventListener(type, this, { mozSystemGroup: true, capture });
}
} catch (ex) {}
- clearTimeout(this._showControlsTimeout);
- clearTimeout(this._hideControlsTimeout);
+ this.window.clearTimeout(this._showControlsTimeout);
+ this.window.clearTimeout(this._hideControlsTimeout);
this._cancelShowThrobberWhileResumingVideoDecoder();
this.log("--- videocontrols terminated ---");
},
hasError() {
// We either have an explicit error, or the resource selection
// algorithm is running and we've tried to load something and failed.
@@ -739,17 +736,17 @@
hasSources() {
if (this.video.hasAttribute("src") &&
this.video.getAttribute("src") !== "") {
return true;
}
for (var child = this.video.firstChild;
child !== null;
child = child.nextElementSibling) {
- if (child instanceof HTMLSourceElement) {
+ if (child instanceof this.window.HTMLSourceElement) {
return true;
}
}
return false;
},
updateErrorText() {
let error;
@@ -778,17 +775,17 @@
break;
}
} else if (v.networkState == v.NETWORK_NO_SOURCE) {
error = "errorNoSource";
} else {
return; // No error found.
}
- let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
+ let label = this.shadowRoot.getElementById(error);
this.controlsSpacer.setAttribute("aria-label", label.textContent);
this.statusOverlay.setAttribute("error", error);
},
formatTime(aTime, showHours = false) {
// Format the duration as "h:mm:ss" or "m:ss"
aTime = Math.round(aTime / 1000);
let hours = Math.floor(aTime / 3600);
@@ -813,17 +810,17 @@
const positionTextNode = Array.prototype.find.call(
this.positionDurationBox.childNodes, (n) => !!~n.textContent.search("#1"));
const durationSpan = this.durationSpan;
const durationFormat = durationSpan.textContent;
const positionFormat = positionTextNode.textContent;
durationSpan.classList.add("duration");
durationSpan.setAttribute("role", "none");
- durationSpan.setAttribute("anonid", "durationSpan");
+ durationSpan.id = "durationSpan";
Object.defineProperties(this.positionDurationBox, {
durationSpan: {
value: durationSpan
},
position: {
set: (v) => {
positionTextNode.textContent = positionFormat.replace("#1", v);
@@ -1005,50 +1002,54 @@
},
HIDE_CONTROLS_TIMEOUT_MS: 2000,
onMouseMove(event) {
// If the controls are static, don't change anything.
if (!this.dynamicControls) {
return;
}
- clearTimeout(this._hideControlsTimeout);
+ this.window.clearTimeout(this._hideControlsTimeout);
// Suppress fading out the controls until the video has rendered
// its first frame. But since autoplay videos start off with no
// controls, let them fade-out so the controls don't get stuck on.
if (!this.firstFrameShown &&
!this.video.autoplay) {
return;
}
if (this._controlsHiddenByTimeout) {
- this._showControlsTimeout =
- setTimeout(() => this._showControlsFn(), this.SHOW_CONTROLS_TIMEOUT_MS);
+ this._showControlsTimeout = this.window.setTimeout(
+ () => this._showControlsFn(),
+ this.SHOW_CONTROLS_TIMEOUT_MS
+ );
} else {
this.startFade(this.controlBar, true);
}
// Hide the controls if the mouse cursor is left on top of the video
// but above the control bar and if the click-to-play overlay is hidden.
if ((this._controlsHiddenByTimeout ||
event.clientY < this.controlBar.getBoundingClientRect().top) &&
this.clickToPlay.hidden) {
- this._hideControlsTimeout =
- setTimeout(() => this._hideControlsFn(), this.HIDE_CONTROLS_TIMEOUT_MS);
+ this._hideControlsTimeout = this.window.setTimeout(
+ () => this._hideControlsFn(),
+ this.HIDE_CONTROLS_TIMEOUT_MS
+ );
}
},
onMouseInOut(event) {
// If the controls are static, don't change anything.
if (!this.dynamicControls) {
return;
}
- clearTimeout(this._hideControlsTimeout);
+ this.window.clearTimeout(this._hideControlsTimeout);
// Ignore events caused by transitions between child nodes.
// Note that the videocontrols element is the same
// size as the *content area* of the video element,
// but this is not the same as the video element's
// border area if the video has border or padding.
if (this.checkEventWithin(event, this.videocontrols)) {
return;
@@ -1075,17 +1076,17 @@
// Keep the controls visible if the click-to-play is visible.
if (!this.clickToPlay.hidden) {
return;
}
this.startFadeOut(this.controlBar, false);
this.textTrackList.hidden = true;
- clearTimeout(this._showControlsTimeout);
+ this.window.clearTimeout(this._showControlsTimeout);
this._controlsHiddenByTimeout = false;
}
},
startFadeIn(element, immediate) {
this.startFade(element, true, immediate);
},
@@ -1144,43 +1145,43 @@
},
then(fn) {
this.fn = fn;
}
};
// Note that handler is not a real Promise.
// All it offered is a then() method to register a callback
// to be triggered at the right time.
- animation.finished = handler;
+ Object.defineProperty(animation, "finished", { value: handler, configurable: true });
animation.addEventListener("finish", handler);
animation.addEventListener("cancel", handler);
},
startFade(element, fadeIn, immediate = false) {
// Bug 493523, the scrubber doesn't call valueChanged while hidden,
// so our dependent state (eg, timestamp in the thumb) will be stale.
// As a workaround, update it manually when it first becomes unhidden.
if (element == this.controlBar && fadeIn && element.hidden) {
this.scrubber.value = this.video.currentTime * 1000;
}
let animationProp =
- this.animationProps[element.getAttribute("anonid")];
+ this.animationProps[element.id];
if (!animationProp) {
- throw new Error("Element " + element.getAttribute("anonid") +
+ throw new Error("Element " + element.id +
" has no transition. Toggle the hidden property directly.");
}
let animation = this.animationMap.get(element);
if (!animation) {
// Create the animation object but don't start it.
// To be replaced with the following when the constructors ship
// (currently behind dom.animations-api.core.enabled)
/*
- animation = new Animation(new KeyframeEffect(
+ animation = new this.window.Animation(new this.window.KeyframeEffect(
element, animationProp.keyframes, animationProp.options));
*/
animation = element.animate(animationProp.keyframes, animationProp.options);
animation.cancel();
this.animationMap.set(element, animation);
}
@@ -1203,17 +1204,17 @@
element.hidden = false;
} else {
// No need to fade out if the element is already no visible.
if (element.hidden) {
return;
}
if (element == this.controlBar && !this.hasError() &&
- document.mozFullScreenElement == this.video) {
+ this.document.mozFullScreenElement == this.video) {
this.controlsSpacer.setAttribute("hideCursor", true);
}
}
element.classList.toggle("fadeout", !fadeIn);
element.classList.toggle("fadein", fadeIn);
let finishedPromise;
if (!immediate) {
@@ -1272,27 +1273,27 @@
}
// We'll handle style changes in the event listener for
// the "volumechange" event, same as if content script was
// controlling volume.
},
get isVideoInFullScreen() {
- return document.mozFullScreenElement == this.video;
+ return this.document.mozFullScreenElement == this.video;
},
toggleFullscreen() {
this.isVideoInFullScreen ?
- document.mozCancelFullScreen() :
+ this.document.mozCancelFullScreen() :
this.video.mozRequestFullScreen();
},
setFullscreenButtonState() {
- if (this.isAudioOnly || !document.mozFullScreenEnabled) {
+ if (this.isAudioOnly || !this.document.mozFullScreenEnabled) {
this.controlBar.setAttribute("fullscreen-unavailable", true);
this.adjustControlSize();
return;
}
this.controlBar.removeAttribute("fullscreen-unavailable");
this.adjustControlSize();
var attrName = this.isVideoInFullScreen ? "exitfullscreenlabel" : "enterfullscreenlabel";
@@ -1321,17 +1322,17 @@
// This is already broken by bug 718107 (controls will be hidden
// as soon as the video enters fullscreen).
// We can think about restoring the behavior here once the bug is
// fixed, or we could simply acknowledge the current behavior
// after-the-fact and try not to fix this.
if (this.isVideoInFullScreen) {
this._hideControlsTimeout =
- setTimeout(() => this._hideControlsFn(), this.HIDE_CONTROLS_TIMEOUT_MS);
+ this.window.setTimeout(() => this._hideControlsFn(), this.HIDE_CONTROLS_TIMEOUT_MS);
}
// Constructor will handle this correctly on the new DOM content in
// the new binding.
this.setFullscreenButtonState();
},
*/
@@ -1340,27 +1341,27 @@
return;
}
if (lock) {
if (this.video.mozIsOrientationLocked) {
return;
}
let dimenDiff = this.video.videoWidth - this.video.videoHeight;
if (dimenDiff > 0) {
- this.video.mozIsOrientationLocked = window.screen.mozLockOrientation("landscape");
+ this.video.mozIsOrientationLocked = this.window.screen.mozLockOrientation("landscape");
} else if (dimenDiff < 0) {
- this.video.mozIsOrientationLocked = window.screen.mozLockOrientation("portrait");
+ this.video.mozIsOrientationLocked = this.window.screen.mozLockOrientation("portrait");
} else {
- this.video.mozIsOrientationLocked = window.screen.mozLockOrientation(window.screen.orientation);
+ this.video.mozIsOrientationLocked = this.window.screen.mozLockOrientation(this.window.screen.orientation);
}
} else {
if (!this.video.mozIsOrientationLocked) {
return;
}
- window.screen.mozUnlockOrientation();
+ this.window.screen.mozUnlockOrientation();
this.video.mozIsOrientationLocked = false;
}
},
clickToPlayClickHandler(e) {
if (e.button != 0) {
return;
}
@@ -1423,65 +1424,65 @@
}
var attrName = muted ? "unmutelabel" : "mutelabel";
var value = this.muteButton.getAttribute(attrName);
this.muteButton.setAttribute("aria-label", value);
},
_getComputedPropertyValueAsInt(element, property) {
- let value = getComputedStyle(element, null).getPropertyValue(property);
+ let value = this.window.getComputedStyle(element).getPropertyValue(property);
return parseInt(value, 10);
},
keyHandler(event) {
// Ignore keys when content might be providing its own.
if (!this.video.hasAttribute("controls")) {
return;
}
var keystroke = "";
if (event.altKey) {
keystroke += "alt-";
}
if (event.shiftKey) {
keystroke += "shift-";
}
- if (navigator.platform.startsWith("Mac")) {
+ if (this.window.navigator.platform.startsWith("Mac")) {
if (event.metaKey) {
keystroke += "accel-";
}
if (event.ctrlKey) {
keystroke += "control-";
}
} else {
if (event.metaKey) {
keystroke += "meta-";
}
if (event.ctrlKey) {
keystroke += "accel-";
}
}
switch (event.keyCode) {
- case KeyEvent.DOM_VK_UP:
+ case this.window.KeyEvent.DOM_VK_UP:
keystroke += "upArrow";
break;
- case KeyEvent.DOM_VK_DOWN:
+ case this.window.KeyEvent.DOM_VK_DOWN:
keystroke += "downArrow";
break;
- case KeyEvent.DOM_VK_LEFT:
+ case this.window.KeyEvent.DOM_VK_LEFT:
keystroke += "leftArrow";
break;
- case KeyEvent.DOM_VK_RIGHT:
+ case this.window.KeyEvent.DOM_VK_RIGHT:
keystroke += "rightArrow";
break;
- case KeyEvent.DOM_VK_HOME:
+ case this.window.KeyEvent.DOM_VK_HOME:
keystroke += "home";
break;
- case KeyEvent.DOM_VK_END:
+ case this.window.KeyEvent.DOM_VK_END:
keystroke += "end";
break;
}
if (String.fromCharCode(event.charCode) == " ") {
keystroke += "space";
}
@@ -1646,18 +1647,18 @@
this.changeTextTrack(tt.index);
}
return;
}
tt.index = this.textTracksCount++;
const label = tt.label || "";
- const ttText = document.createTextNode(label);
- const ttBtn = document.createElement("button");
+ const ttText = this.document.createTextNode(label);
+ const ttBtn = this.document.createElement("button");
ttBtn.classList.add("textTrackItem");
ttBtn.setAttribute("index", tt.index);
ttBtn.appendChild(ttText);
this.textTrackList.appendChild(ttBtn);
if (tt.mode === "showing" && tt.index) {
@@ -1674,22 +1675,22 @@
}
}
this.textTrackList.hidden = true;
},
onControlBarAnimationFinished() {
this.textTrackList.hidden = true;
- this.video.dispatchEvent(new CustomEvent("controlbarchange"));
+ this.video.dispatchEvent(new this.window.CustomEvent("controlbarchange"));
this.adjustControlSize();
},
toggleCasting() {
- this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
+ this.videocontrols.dispatchEvent(new this.window.CustomEvent("VideoBindingCast"));
},
toggleClosedCaption() {
if (this.textTrackList.hidden) {
this.textTrackList.hidden = false;
} else {
this.textTrackList.hidden = true;
}
@@ -1711,17 +1712,17 @@
for (let tti of ttItems) {
const idx = +tti.getAttribute("index");
if (idx === toRemoveIndex) {
tti.remove();
this.textTracksCount--;
}
- this.video.dispatchEvent(new CustomEvent("texttrackchange"));
+ this.video.dispatchEvent(new this.window.CustomEvent("texttrackchange"));
}
this.setClosedCaptionButtonState();
},
initTextTracks() {
// add 'off' button anyway as new text track might be
// dynamically added after initialization.
@@ -1748,24 +1749,22 @@
}
return false;
}
return isDescendant(event.target) && isDescendant(event.relatedTarget);
},
log(msg) {
if (this.debug) {
- console.log("videoctl: " + msg + "\n");
+ this.window.console.log("videoctl: " + msg + "\n");
}
},
get isTopLevelSyntheticDocument() {
- let doc = this.video.ownerDocument;
- let win = doc.defaultView;
- return doc.mozSyntheticDocument && win === win.top;
+ return this.document.mozSyntheticDocument && this.window === this.window.top;
},
controlBarMinHeight: 40,
controlBarMinVisibleHeight: 28,
adjustControlSize() {
const minControlBarPaddingWidth = 18;
this.fullscreenButton.isWanted = !this.controlBar.hasAttribute("fullscreen-unavailable");
@@ -1809,17 +1808,17 @@
// Use flexible spacer to separate controls when scrubber is hidden.
// As long as muteButton hidden, which means only play button presents,
// hide spacer and make playButton centered.
this.controlBarSpacer.hidden = !this.scrubberStack.hidden || this.muteButton.hidden;
// Since the size of videocontrols is expanded with controlBar in <audio>, we
// should fix the dimensions in order not to recursively trigger reflow afterwards.
- if (this.video instanceof HTMLAudioElement) {
+ if (this.video instanceof this.window.HTMLAudioElement) {
if (givenHeight) {
// The height of controlBar should be capped with the bounds between controlBarMinHeight
// and controlBarMinVisibleHeight.
let controlBarHeight = Math.max(Math.min(givenHeight, this.controlBarMinHeight), this.controlBarMinVisibleHeight);
this.controlBar.style.height = `${controlBarHeight}px`;
}
// Bug 1367875: Set minimum required width to controlBar if the given size is smaller than padding.
// This can help us expand the control and restore to the default size the next time we need
@@ -1854,75 +1853,78 @@
if (this.clickToPlay.hidden && !this.video.played.length && this.video.paused) {
this.clickToPlay.hiddenByAdjustment = false;
}
this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
}
},
- init(binding) {
- this.video = binding.parentNode;
- this.videocontrols = binding;
+ init(shadowRoot) {
+ this.video = shadowRoot.host;
+ this.videocontrols = shadowRoot.firstChild;
+ this.document = this.videocontrols.ownerDocument;
+ this.window = this.document.defaultView;
+ this.shadowRoot = shadowRoot;
- this.controlsContainer = document.getAnonymousElementByAttribute(binding, "anonid", "controlsContainer");
- this.statusIcon = document.getAnonymousElementByAttribute(binding, "anonid", "statusIcon");
- this.controlBar = document.getAnonymousElementByAttribute(binding, "anonid", "controlBar");
- this.playButton = document.getAnonymousElementByAttribute(binding, "anonid", "playButton");
- this.controlBarSpacer = document.getAnonymousElementByAttribute(binding, "anonid", "controlBarSpacer");
- this.muteButton = document.getAnonymousElementByAttribute(binding, "anonid", "muteButton");
- this.volumeStack = document.getAnonymousElementByAttribute(binding, "anonid", "volumeStack");
- this.volumeControl = document.getAnonymousElementByAttribute(binding, "anonid", "volumeControl");
- this.progressBar = document.getAnonymousElementByAttribute(binding, "anonid", "progressBar");
- this.bufferBar = document.getAnonymousElementByAttribute(binding, "anonid", "bufferBar");
- this.scrubberStack = document.getAnonymousElementByAttribute(binding, "anonid", "scrubberStack");
- this.scrubber = document.getAnonymousElementByAttribute(binding, "anonid", "scrubber");
- this.durationLabel = document.getAnonymousElementByAttribute(binding, "anonid", "durationLabel");
- this.positionLabel = document.getAnonymousElementByAttribute(binding, "anonid", "positionLabel");
- this.positionDurationBox = document.getAnonymousElementByAttribute(binding, "anonid", "positionDurationBox");
- this.statusOverlay = document.getAnonymousElementByAttribute(binding, "anonid", "statusOverlay");
- this.controlsOverlay = document.getAnonymousElementByAttribute(binding, "anonid", "controlsOverlay");
- this.controlsSpacer = document.getAnonymousElementByAttribute(binding, "anonid", "controlsSpacer");
- this.clickToPlay = document.getAnonymousElementByAttribute(binding, "anonid", "clickToPlay");
- this.fullscreenButton = document.getAnonymousElementByAttribute(binding, "anonid", "fullscreenButton");
- this.castingButton = document.getAnonymousElementByAttribute(binding, "anonid", "castingButton");
- this.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "anonid", "closedCaptionButton");
- this.textTrackList = document.getAnonymousElementByAttribute(binding, "anonid", "textTrackList");
+ this.controlsContainer = this.shadowRoot.getElementById("controlsContainer");
+ this.statusIcon = this.shadowRoot.getElementById("statusIcon");
+ this.controlBar = this.shadowRoot.getElementById("controlBar");
+ this.playButton = this.shadowRoot.getElementById("playButton");
+ this.controlBarSpacer = this.shadowRoot.getElementById("controlBarSpacer");
+ this.muteButton = this.shadowRoot.getElementById("muteButton");
+ this.volumeStack = this.shadowRoot.getElementById("volumeStack");
+ this.volumeControl = this.shadowRoot.getElementById("volumeControl");
+ this.progressBar = this.shadowRoot.getElementById("progressBar");
+ this.bufferBar = this.shadowRoot.getElementById("bufferBar");
+ this.scrubberStack = this.shadowRoot.getElementById("scrubberStack");
+ this.scrubber = this.shadowRoot.getElementById("scrubber");
+ this.durationLabel = this.shadowRoot.getElementById("durationLabel");
+ this.positionLabel = this.shadowRoot.getElementById("positionLabel");
+ this.positionDurationBox = this.shadowRoot.getElementById("positionDurationBox");
+ this.statusOverlay = this.shadowRoot.getElementById("statusOverlay");
+ this.controlsOverlay = this.shadowRoot.getElementById("controlsOverlay");
+ this.controlsSpacer = this.shadowRoot.getElementById("controlsSpacer");
+ this.clickToPlay = this.shadowRoot.getElementById("clickToPlay");
+ this.fullscreenButton = this.shadowRoot.getElementById("fullscreenButton");
+ this.castingButton = this.shadowRoot.getElementById("castingButton");
+ this.closedCaptionButton = this.shadowRoot.getElementById("closedCaptionButton");
+ this.textTrackList = this.shadowRoot.getElementById("textTrackList");
if (this.positionDurationBox) {
this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
}
- let isMobile = navigator.appVersion.includes("Android");
+ let isMobile = this.window.navigator.appVersion.includes("Android");
if (isMobile) {
this.controlsContainer.classList.add("mobile");
}
// TODO: Switch to touch controls on touch-based desktops (bug 1447547)
this.videocontrols.isTouchControls = isMobile;
if (this.videocontrols.isTouchControls) {
this.controlsContainer.classList.add("touch");
}
- this.controlBarComputedStyles = getComputedStyle(this.controlBar);
+ this.controlBarComputedStyles = this.window.getComputedStyle(this.controlBar);
// Hide and show control in certain order.
this.prioritizedControls = [
this.playButton,
this.muteButton,
this.fullscreenButton,
this.castingButton,
this.closedCaptionButton,
this.positionDurationBox,
this.scrubberStack,
this.durationSpan,
this.volumeStack
];
- this.isAudioOnly = (this.video instanceof HTMLAudioElement);
+ this.isAudioOnly = this.video instanceof this.window.HTMLAudioElement;
this.setupInitialState();
this.setupNewLoadState();
this.initTextTracks();
// Use the handleEvent() callback for all media events.
// Only the "error" event listener must capture, so that it can trap error
// events from <source> children, which don't bubble. But we use capture
// for all events in order to simplify the event listener add/remove.
@@ -1946,17 +1948,17 @@
{ el: this.controlsSpacer, type: "click", nonTouchOnly: true },
{ el: this.controlsSpacer, type: "dblclick", nonTouchOnly: true },
{ el: this.textTrackList, type: "click" },
{ el: this.videocontrols, type: "resizevideocontrols" },
// See comment at onFullscreenChange on bug 718107.
- // { el: this.video.ownerDocument, type: "fullscreenchange" },
+ // { el: this.document, type: "fullscreenchange" },
{ el: this.video, type: "keypress", capture: true },
// Prevent any click event within media controls from dispatching through to video.
{ el: this.videocontrols, type: "click", mozSystemGroup: false },
// prevent dragging of controls image (bug 517114)
{ el: this.videocontrols, type: "dragstart" },
@@ -2015,25 +2017,24 @@
if (this.Utils.dynamicControls) {
this.Utils.startFadeIn(this.Utils.controlBar);
this.delayHideControls(this.controlsTimeout);
}
},
clearTimer() {
if (this.controlsTimer) {
- clearTimeout(this.controlsTimer);
+ this.window.clearTimeout(this.controlsTimer);
this.controlsTimer = null;
}
},
delayHideControls(aTimeout) {
this.clearTimer();
- this.controlsTimer =
- setTimeout(() => this.hideControls(), aTimeout);
+ this.controlsTimer = this.window.setTimeout(() => this.hideControls(), aTimeout);
},
hideControls() {
if (!this.Utils.dynamicControls) {
return;
}
this.Utils.startFadeOut(this.Utils.controlBar);
},
@@ -2082,19 +2083,20 @@
for (let { el, type, mozSystemGroup = true } of this.controlsEvents) {
el.removeEventListener(type, this, { mozSystemGroup });
}
} catch (ex) {}
this.clearTimer();
},
- init(binding) {
- this.videocontrols = binding;
- this.video = binding.parentNode;
+ init(shadowRoot) {
+ this.videocontrols = shadowRoot.firstChild;
+ this.video = shadowRoot.host;
+ this.shadowRoot = shadowRoot;
this.controlsEvents = [
{ el: this.Utils.playButton, type: "click" },
{ el: this.Utils.scrubber, type: "touchstart" },
{ el: this.Utils.scrubber, type: "touchend" },
{ el: this.Utils.muteButton, type: "click" },
{ el: this.Utils.controlsSpacer, type: "mouseup" }
];
@@ -2119,76 +2121,139 @@
// the controls to remain visible. this.controlsTimeout is a full
// 5s, which feels too long after the transition.
if (this.video.currentTime !== 0) {
this.delayHideControls(this.Utils.HIDE_CONTROLS_TIMEOUT_MS);
}
}
};
- this.Utils.init(this);
+ this.Utils.init(this.shadowRoot);
if (this.isTouchControls) {
- this.TouchUtils.init(this);
+ this.TouchUtils.init(this.shadowRoot);
}
- this.dispatchEvent(new CustomEvent("VideoBindingAttached"));
- ]]>
- </constructor>
- <destructor>
- <![CDATA[
+ this.shadowRoot.firstChild.dispatchEvent(new this.window.CustomEvent("VideoBindingAttached"));
+
+ this._setupEventListeners();
+ }
+
+ 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">
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin/media/videocontrols.css" />
+ <div id="controlsContainer" class="controlsContainer" role="none">
+ <div id="statusOverlay" class="statusOverlay stackItem" hidden="true">
+ <div id="statusIcon" class="statusIcon"></div>
+ <span class="errorLabel" id="errorAborted">&error.aborted;</span>
+ <span class="errorLabel" id="errorNetwork">&error.network;</span>
+ <span class="errorLabel" id="errorDecode">&error.decode;</span>
+ <span class="errorLabel" id="errorSrcNotSupported">&error.srcNotSupported;</span>
+ <span class="errorLabel" id="errorNoSource">&error.noSource2;</span>
+ <span class="errorLabel" id="errorGeneric">&error.generic;</span>
+ </div>
+
+ <div id="controlsOverlay" class="controlsOverlay stackItem">
+ <div class="controlsSpacerStack">
+ <div id="controlsSpacer" class="controlsSpacer stackItem" role="none"></div>
+ <div id="clickToPlay" class="clickToPlay" hidden="true"></div>
+ </div>
+
+ <div id="controlBar" class="controlBar" hidden="true">
+ <button id="playButton"
+ class="button playButton"
+ playlabel="&playButton.playLabel;"
+ pauselabel="&playButton.pauseLabel;"
+ tabindex="-1"/>
+ <div id="scrubberStack" class="scrubberStack progressContainer" role="none">
+ <div class="progressBackgroundBar stackItem" role="none">
+ <div class="progressStack" role="none">
+ <progress id="bufferBar" class="bufferBar" value="0" max="100" tabindex="-1"></progress>
+ <progress id="progressBar" class="progressBar" value="0" max="100" tabindex="-1"></progress>
+ </div>
+ </div>
+ <input type="range" id="scrubber" class="scrubber" tabindex="-1"/>
+ </div>
+ <span id="positionLabel" class="positionLabel" role="presentation"></span>
+ <span id="durationLabel" class="durationLabel" role="presentation"></span>
+ <span id="positionDurationBox" class="positionDurationBox" aria-hidden="true">
+ &positionAndDuration.nameFormat;
+ </span>
+ <div id="controlBarSpacer" class="controlBarSpacer" hidden="true" role="none"></div>
+ <button id="muteButton"
+ class="button muteButton"
+ mutelabel="&muteButton.muteLabel;"
+ unmutelabel="&muteButton.unmuteLabel;"
+ tabindex="-1"/>
+ <div id="volumeStack" class="volumeStack progressContainer" role="none">
+ <input type="range" id="volumeControl" class="volumeControl" min="0" max="100" step="1" tabindex="-1"/>
+ </div>
+ <button id="castingButton" class="button castingButton"
+ aria-label="&castingButton.castingLabel;"/>
+ <button id="closedCaptionButton" class="button closedCaptionButton"/>
+ <button id="fullscreenButton"
+ class="button fullscreenButton"
+ enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
+ exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
+ </div>
+ <div id="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></div>
+ </div>
+ </div>
+ </div>`, "application/xml");
+ this.shadowRoot.appendChild(this.document.importNode(parserDoc.documentElement, true));
+ }
+
+ destructor() {
this.Utils.terminate();
this.TouchUtils.terminate();
this.Utils.updateOrientationState(false);
// randomID used to be a <field>, which meant that the XBL machinery
// undefined the property when the element was unbound. The code in
// this file actually depends on this, so now that randomID is an
// expando, we need to make sure to explicitly delete it.
delete this.randomID;
- ]]>
- </destructor>
+ }
- </implementation>
-
- <handlers>
- <handler event="mouseover">
+ _setupEventListeners() {
+ this.shadowRoot.firstChild.addEventListener("mouseover", event => {
if (!this.isTouchControls) {
this.Utils.onMouseInOut(event);
}
- </handler>
- <handler event="mouseout">
+ });
+
+ this.shadowRoot.firstChild.addEventListener("mouseout", event => {
if (!this.isTouchControls) {
this.Utils.onMouseInOut(event);
}
- </handler>
- <handler event="mousemove">
+ });
+
+ this.shadowRoot.firstChild.addEventListener("mousemove", event => {
if (!this.isTouchControls) {
this.Utils.onMouseMove(event);
}
- </handler>
- </handlers>
-</binding>
-
-<binding id="noControls">
-
- <resources>
- <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
- </resources>
+ });
+ }
+};
- <xbl:content xmlns="http://www.w3.org/1999/xhtml" class="mediaControlsFrame">
- <div anonid="controlsContainer" class="controlsContainer" role="none" hidden="true">
- <div class="controlsOverlay stackItem">
- <div class="controlsSpacerStack">
- <div anonid="clickToPlay" class="clickToPlay"></div>
- </div>
- </div>
- </div>
- </xbl:content>
+this.NoControlsImplPageWidget = class {
+ constructor(shadowRoot) {
+ this.shadowRoot = shadowRoot;
+ this.element = shadowRoot.host;
+ this.document = this.element.ownerDocument;
+ this.window = this.document.defaultView;
- <implementation>
- <constructor>
- <![CDATA[
+ this.generateContent();
+
this.randomID = 0;
this.Utils = {
randomID: 0,
videoEvents: ["play",
"playing",
"MozNoControlsBlockedVideo"],
terminate() {
for (let event of this.videoEvents) {
@@ -2251,26 +2316,31 @@
} else if (e.button != 0) {
return;
}
this.noControlsOverlay.hidden = true;
this.video.play();
},
- init(binding) {
- this.videocontrols = binding;
+ init(shadowRoot) {
+ this.video = shadowRoot.host;
+ this.videocontrols = shadowRoot.firstChild;
+ this.document = this.videocontrols.ownerDocument;
+ this.window = this.document.defaultView;
+ this.shadowRoot = shadowRoot;
+
this.randomID = Math.random();
this.videocontrols.randomID = this.randomID;
- this.video = binding.parentNode;
- this.controlsContainer = document.getAnonymousElementByAttribute(binding, "anonid", "controlsContainer");
- this.clickToPlay = document.getAnonymousElementByAttribute(binding, "anonid", "clickToPlay");
- this.noControlsOverlay = document.getAnonymousElementByAttribute(binding, "anonid", "controlsContainer");
- let isMobile = navigator.appVersion.includes("Android");
+ this.controlsContainer = this.shadowRoot.getElementById("controlsContainer");
+ this.clickToPlay = this.shadowRoot.getElementById("clickToPlay");
+ this.noControlsOverlay = this.shadowRoot.getElementById("controlsContainer");
+
+ let isMobile = this.window.navigator.appVersion.includes("Android");
if (isMobile) {
this.controlsContainer.classList.add("mobile");
}
// TODO: Switch to touch controls on touch-based desktops (bug 1447547)
this.videocontrols.isTouchControls = isMobile;
if (this.videocontrols.isTouchControls) {
this.controlsContainer.classList.add("touch");
@@ -2281,26 +2351,44 @@
for (let event of this.videoEvents) {
this.video.addEventListener(event, this, {
capture: true,
mozSystemGroup: true
});
}
}
};
- this.Utils.init(this);
- this.Utils.video.dispatchEvent(new CustomEvent("MozNoControlsVideoBindingAttached"));
- ]]>
- </constructor>
- <destructor>
- <![CDATA[
+ this.Utils.init(this.shadowRoot);
+ this.Utils.video.dispatchEvent(new this.window.CustomEvent("MozNoControlsVideoBindingAttached"));
+ }
+
+ destructor() {
this.Utils.terminate();
// randomID used to be a <field>, which meant that the XBL machinery
// undefined the property when the element was unbound. The code in
// this file actually depends on this, so now that randomID is an
// expando, we need to make sure to explicitly delete it.
delete this.randomID;
- ]]>
- </destructor>
- </implementation>
-</binding>
+ }
-</bindings>
+ 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">
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin/media/videocontrols.css" />
+ <div id="controlsContainer" class="controlsContainer" role="none" hidden="true">
+ <div class="controlsOverlay stackItem">
+ <div class="controlsSpacerStack">
+ <div id="clickToPlay" class="clickToPlay"></div>
+ </div>
+ </div>
+ </div>
+ </div>`, "application/xml");
+ this.shadowRoot.appendChild(this.document.importNode(parserDoc.documentElement, true));
+ }
+};