--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -1,2208 +1,2275 @@
<?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/. -->
-
<!DOCTYPE bindings [
- <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
- %videocontrolsDTD;
+<!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
+%videocontrolsDTD;
]>
<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">
+ 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">
+
+<binding id="timeThumb"
+ extends="chrome://global/content/bindings/scale.xml#scalethumb">
+ <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <xbl:children/>
+ <hbox class="timeThumb" xbl:inherits="showhours">
+ <label class="timeLabel"/>
+ </hbox>
+ </xbl:content>
+ <implementation>
+
+ <constructor>
+ <![CDATA[
+ this.timeLabel = document.getAnonymousElementByAttribute(this, "class", "timeLabel");
+ this.timeLabel.setAttribute("value", "0:00");
+ ]]>
+ </constructor>
+
+ <property name="showHours">
+ <getter>
+ <![CDATA[
+ return this.getAttribute("showhours") == "true";
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ this.setAttribute("showhours", val);
+ // If the duration becomes known while we're still showing the value
+ // for time=0, immediately update the value to show or hide the hours.
+ // It's less intrusive to do it now than when the user clicks play and
+ // is looking right next to the thumb.
+ if (!this.timeLabel) {
+ return;
+ }
+ var displayedTime = this.timeLabel.getAttribute("value");
+ if (val && displayedTime == "0:00") {
+ this.timeLabel.setAttribute("value", "0:00:00");
+ } else if (!val && displayedTime == "0:00:00") {
+ this.timeLabel.setAttribute("value", "0:00");
+ }
+ ]]>
+ </setter>
+ </property>
- <binding id="timeThumb"
- extends="chrome://global/content/bindings/scale.xml#scalethumb">
- <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
- <xbl:children/>
- <hbox class="timeThumb" xbl:inherits="showhours">
- <label class="timeLabel"/>
- </hbox>
- </xbl:content>
- <implementation>
+ <method name="setTime">
+ <parameter name="time"/>
+ <body>
+ <![CDATA[
+ var timeString;
+ time = Math.round(time / 1000);
+ var hours = Math.floor(time / 3600);
+ var mins = Math.floor((time % 3600) / 60);
+ var secs = Math.floor(time % 60);
+ if (secs < 10) {
+ secs = "0" + secs;
+ }
+ if (hours || this.showHours) {
+ if (mins < 10) {
+ mins = "0" + mins;
+ }
+ timeString = hours + ":" + mins + ":" + secs;
+ } else {
+ timeString = mins + ":" + secs;
+ }
- <constructor>
- <![CDATA[
- this.timeLabel = document.getAnonymousElementByAttribute(this, "class", "timeLabel");
- this.timeLabel.setAttribute("value", "0:00");
- ]]>
- </constructor>
+ this.timeLabel.setAttribute("value", timeString);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+</binding>
+
+<binding id="suppressChangeEvent"
+ extends="chrome://global/content/bindings/scale.xml#scale">
+<implementation implements="nsIXBLAccessible">
+ <!-- nsIXBLAccessible -->
+ <property name="accessibleName" readonly="true">
+ <getter>
+ if (this.type != "scrubber") {
+ return "";
+ }
+
+ var currTime = this.thumb.timeLabel.getAttribute("value");
+ var totalTime = this.durationValue;
+
+ return this.scrubberNameFormat
+ .replace(/#1/, currTime)
+ .replace(/#2/, totalTime);
+ </getter>
+ </property>
+
+ <constructor>
+ <![CDATA[
+ this.scrubberNameFormat = ]]>"&scrubberScale.nameFormat;"<![CDATA[;
+ this.durationValue = "";
+ this.valueBar = null;
+ this.isDragging = false;
+ this.isPausedByDragging = false;
- <property name="showHours">
- <getter>
- <![CDATA[
- return this.getAttribute("showhours") == "true";
- ]]>
- </getter>
- <setter>
- <![CDATA[
- this.setAttribute("showhours", val);
- // If the duration becomes known while we're still showing the value
- // for time=0, immediately update the value to show or hide the hours.
- // It's less intrusive to do it now than when the user clicks play and
- // is looking right next to the thumb.
- if (!this.timeLabel) return;
- var displayedTime = this.timeLabel.getAttribute("value");
- if (val && displayedTime == "0:00")
- this.timeLabel.setAttribute("value", "0:00:00");
- else if (!val && displayedTime == "0:00:00")
- this.timeLabel.setAttribute("value", "0:00");
- ]]>
- </setter>
- </property>
+ this.thumb = document.getAnonymousElementByAttribute(this, "class", "scale-thumb");
+ this.type = this.getAttribute("class");
+ this.Utils = document.getBindingParent(this.parentNode).Utils;
+ if (this.type == "scrubber") {
+ this.valueBar = this.Utils.progressBar;
+ }
+ ]]>
+ </constructor>
+
+ <method name="valueChanged">
+ <parameter name="which"/>
+ <parameter name="newValue"/>
+ <parameter name="userChanged"/>
+ <body>
+ <![CDATA[
+ // This method is a copy of the base binding's valueChanged(), except that it does
+ // not dispatch a |change| event (to avoid exposing the event to web content), and
+ // just calls the videocontrol's seekToPosition() method directly.
+ switch (which) {
+ case "curpos":
+ if (this.type == "scrubber") {
+ // Update the time shown in the thumb.
+ this.thumb.setTime(newValue);
+ this.Utils.positionLabel.setAttribute("value", this.thumb.timeLabel.value);
+ // Update the value bar to match the thumb position.
+ let percent = newValue / this.max;
+ if (!isNaN(percent) && percent != Infinity) {
+ this.valueBar.value = Math.round(percent * 10000); // has max=10000
+ } else {
+ this.valueBar.removeAttribute("value");
+ }
+ }
+
+ // The value of userChanged is true when changing the position with the mouse,
+ // but not when pressing an arrow key. However, the base binding sets
+ // ._userChanged in its keypress handlers, so we just need to check both.
+ if (!userChanged && !this._userChanged) {
+ return;
+ }
+ this.setAttribute("value", newValue);
+
+ if (this.type == "scrubber") {
+ this.Utils.seekToPosition(newValue);
+ } else if (this.type == "volumeControl") {
+ this.Utils.setVolume(newValue / 100);
+ }
+ break;
+
+ case "minpos":
+ this.setAttribute("min", newValue);
+ break;
+
+ case "maxpos":
+ if (this.type == "scrubber") {
+ // Update the value bar to match the thumb position.
+ let percent = this.value / newValue;
+ this.valueBar.value = Math.round(percent * 10000); // has max=10000
+ }
+ this.setAttribute("max", newValue);
+ break;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="dragStateChanged">
+ <parameter name="isDragging"/>
+ <body>
+ <![CDATA[
+ if (this.type == "scrubber") {
+ this.Utils.log("--- dragStateChanged: " + isDragging + " ---");
+ this.isDragging = isDragging;
+ if (this.isPausedByDragging && !isDragging) {
+ // After the drag ends, resume playing.
+ this.Utils.video.play();
+ this.isPausedByDragging = false;
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="pauseVideoDuringDragging">
+ <body>
+ <![CDATA[
+ if (this.isDragging &&
+ !this.Utils.video.paused && !this.isPausedByDragging) {
+ this.isPausedByDragging = true;
+ this.Utils.video.pause();
+ }
+ ]]>
+ </body>
+ </method>
+
+</implementation>
+</binding>
+
+<binding id="videoControls">
+ <resources>
+ <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
+ <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
+ </resources>
+
+ <xbl:content xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ 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>
- <method name="setTime">
- <parameter name="time"/>
- <body>
- <![CDATA[
- var timeString;
- time = Math.round(time / 1000);
- var hours = Math.floor(time / 3600);
- var mins = Math.floor((time % 3600) / 60);
- var secs = Math.floor(time % 60);
- if (secs < 10)
- secs = "0" + secs;
- if (hours || this.showHours) {
- if (mins < 10)
- mins = "0" + mins;
- timeString = hours + ":" + mins + ":" + secs;
- } else {
- timeString = mins + ":" + secs;
- }
+ <div anonid="controlsOverlay" class="controlsOverlay stackItem">
+ <div class="controlsSpacerStack" aria-hideen="true">
+ <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="playButton"
+ playlabel="&playButton.playLabel;"
+ pauselabel="&playButton.pauseLabel;"/>
+ <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"></progress>
+ <progress anonid="progressBar" class="progressBar" value="0" max="100"></progress>
+ </div>
+ </div>
+ <input type="range" anonid="scrubber" class="scrubber"/>
+ </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="muteButton"
+ mutelabel="&muteButton.muteLabel;"
+ unmutelabel="&muteButton.unmuteLabel;"/>
+ <div anonid="volumeStack" class="volumeStack progressContainer" role="none">
+ <input type="range" anonid="volumeControl" class="volumeControl" min="0" max="100" step="1"/>
+ </div>
+ <button anonid="closedCaptionButton" class="closedCaptionButton"/>
+ <button anonid="fullscreenButton"
+ class="fullscreenButton"
+ enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
+ exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
+ </div>
+ <div anonid="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></div>
+ </div>
+ </div>
+ </xbl:content>
+
+ <implementation>
+
+ <constructor>
+ <![CDATA[
+ this.isTouchControls = false;
+ this.randomID = 0;
- this.timeLabel.setAttribute("value", timeString);
- ]]>
- </body>
- </method>
- </implementation>
- </binding>
+ this.Utils = {
+ debug : false,
+ video : null,
+ videocontrols : null,
+ controlBar : null,
+ playButton : null,
+ muteButton : null,
+ volumeControl : null,
+ durationLabel : null,
+ positionLabel : null,
+ scrubber : null,
+ progressBar : null,
+ bufferBar : null,
+ statusOverlay : null,
+ controlsSpacer : null,
+ clickToPlay : null,
+ controlsOverlay : null,
+ fullscreenButton : null,
+ currentTextTrackIndex: 0,
+
+ textTracksCount: 0,
+ randomID : 0,
+ videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
+ "loadstart", "timeupdate", "progress",
+ "playing", "waiting", "canplay", "canplaythrough",
+ "seeking", "seeked", "emptied", "loadedmetadata",
+ "error", "suspend", "stalled",
+ "mozinterruptbegin", "mozinterruptend" ],
- <binding id="suppressChangeEvent"
- extends="chrome://global/content/bindings/scale.xml#scale">
- <implementation implements="nsIXBLAccessible">
- <!-- nsIXBLAccessible -->
- <property name="accessibleName" readonly="true">
- <getter>
- if (this.type != "scrubber")
- return "";
+ firstFrameShown : false,
+ timeUpdateCount : 0,
+ maxCurrentTimeSeen : 0,
+ isPausedByDragging: false,
+ _isAudioOnly : false,
+
+ get isAudioOnly() { return this._isAudioOnly; },
+ set isAudioOnly(val) {
+ this._isAudioOnly = val;
+ this.setFullscreenButtonState();
- var currTime = this.thumb.timeLabel.getAttribute("value");
- var totalTime = this.durationValue;
+ if (!this.isTopLevelSyntheticDocument) {
+ return;
+ }
+ if (this._isAudioOnly) {
+ this.video.style.height = this.controlBar.minHeight + "px";
+ this.video.style.width = "66%";
+ } else {
+ this.video.style.removeProperty("height");
+ this.video.style.removeProperty("width");
+ }
+ },
+ suppressError : false,
+
+ setupStatusFader(immediate) {
+ // Since the play button will be showing, we don't want to
+ // show the throbber behind it. The throbber here will
+ // only show if needed after the play button has been pressed.
+ if (!this.clickToPlay.hidden) {
+ this.startFadeOut(this.statusOverlay, true);
+ return;
+ }
- return this.scrubberNameFormat.replace(/#1/, currTime).
- replace(/#2/, totalTime);
- </getter>
- </property>
+ var show = false;
+ if (this.video.seeking ||
+ (this.video.error && !this.suppressError) ||
+ this.video.networkState == this.video.NETWORK_NO_SOURCE ||
+ (this.video.networkState == this.video.NETWORK_LOADING &&
+ (this.video.paused || this.video.ended
+ ? this.video.readyState < this.video.HAVE_CURRENT_DATA
+ : this.video.readyState < this.video.HAVE_FUTURE_DATA)) ||
+ (this.timeUpdateCount <= 1 && !this.video.ended &&
+ this.video.readyState < this.video.HAVE_FUTURE_DATA &&
+ this.video.networkState == this.video.NETWORK_LOADING)) {
+ show = true;
+ }
+
+ // Explicitly hide the status fader if this
+ // is audio only until bug 619421 is fixed.
+ if (this.isAudioOnly) {
+ show = false;
+ }
+
+ this.log("Status overlay: seeking=" + this.video.seeking +
+ " error=" + this.video.error + " readyState=" + this.video.readyState +
+ " paused=" + this.video.paused + " ended=" + this.video.ended +
+ " networkState=" + this.video.networkState +
+ " timeUpdateCount=" + this.timeUpdateCount +
+ " --> " + (show ? "SHOW" : "HIDE"));
+ this.startFade(this.statusOverlay, show, immediate);
+ },
- <constructor>
- <![CDATA[
- this.scrubberNameFormat = ]]>"&scrubberScale.nameFormat;"<![CDATA[;
- this.durationValue = "";
- this.valueBar = null;
- this.isDragging = false;
- this.isPausedByDragging = false;
+ /*
+ * Set the initial state of the controls. The binding is normally created along
+ * with video element, but could be attached at any point (eg, if the video is
+ * removed from the document and then reinserted). Thus, some one-time events may
+ * have already fired, and so we'll need to explicitly check the initial state.
+ */
+ setupInitialState() {
+ this.randomID = Math.random();
+ this.videocontrols.randomID = this.randomID;
+
+ this.setPlayButtonState(this.video.paused);
+
+ this.setFullscreenButtonState();
- this.thumb = document.getAnonymousElementByAttribute(this, "class", "scale-thumb");
- this.type = this.getAttribute("class");
- this.Utils = document.getBindingParent(this.parentNode).Utils;
- if (this.type == "scrubber")
- this.valueBar = this.Utils.progressBar;
- ]]>
- </constructor>
+ var duration = Math.round(this.video.duration * 1000); // in ms
+ var currentTime = Math.round(this.video.currentTime * 1000); // in ms
+ this.log("Initial playback position is at " + currentTime + " of " + duration);
+ // It would be nice to retain maxCurrentTimeSeen, but it would be difficult
+ // 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 &&
+ (this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
+ this.isAudioOnly = true;
+ }
- <method name="valueChanged">
- <parameter name="which"/>
- <parameter name="newValue"/>
- <parameter name="userChanged"/>
- <body>
- <![CDATA[
- // This method is a copy of the base binding's valueChanged(), except that it does
- // not dispatch a |change| event (to avoid exposing the event to web content), and
- // just calls the videocontrol's seekToPosition() method directly.
- switch (which) {
- case "curpos":
- if (this.type == "scrubber") {
- // Update the time shown in the thumb.
- this.thumb.setTime(newValue);
- this.Utils.positionLabel.setAttribute("value", this.thumb.timeLabel.value);
- // Update the value bar to match the thumb position.
- let percent = newValue / this.max;
- if (!isNaN(percent) && percent != Infinity) {
- this.valueBar.value = Math.round(percent * 10000); // has max=10000
- } else {
- this.valueBar.removeAttribute("value");
- }
+ // 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.
+ if (!this.isAudioOnly && !this.video.mozHasAudio) {
+ this.muteButton.setAttribute("noAudio", "true");
+ this.muteButton.setAttribute("disabled", "true");
+ }
+ }
+
+ if (this.isAudioOnly) {
+ this.clickToPlay.hidden = true;
+ }
+
+ // If the first frame hasn't loaded, kick off a throbber fade-in.
+ if (this.video.readyState >= this.video.HAVE_CURRENT_DATA) {
+ this.firstFrameShown = true;
+ }
+
+ // We can't determine the exact buffering status, but do know if it's
+ // fully loaded. (If it's still loading, it will fire a progress event
+ // and we'll figure out the exact state then.)
+ this.bufferBar.max = 100;
+ if (this.video.readyState >= this.video.HAVE_METADATA) {
+ this.showBuffered();
+ } else {
+ this.bufferBar.value = 0;
+ }
+
+ // Set the current status icon.
+ if (this.hasError()) {
+ this.clickToPlay.hidden = true;
+ this.statusIcon.setAttribute("type", "error");
+ this.updateErrorText();
+ this.setupStatusFader(true);
+ }
+
+ // An event handler for |onresize| should be added when bug 227495 is fixed.
+ this.controlBar.hidden = false;
+
+ let layoutControls = [
+ ...this.controlBar.children,
+ this.durationSpan,
+ this.controlBar,
+ this.clickToPlay
+ ];
+
+ for (let control of layoutControls) {
+ if (!control) {
+ break;
+ }
+
+ Object.defineProperties(control, {
+ minWidth: {
+ value: control.clientWidth,
+ writable: true
+ },
+ minHeight: {
+ value: control.clientHeight,
+ writable: true
+ },
+ isAdjustableControl: {
+ value: true
+ },
+ isWanted: {
+ value: true,
+ writable: true
+ },
+ hideByAdjustment: {
+ set: (v) => {
+ if (v) {
+ control.setAttribute("hidden", "true");
+ } else {
+ control.removeAttribute("hidden");
}
- // The value of userChanged is true when changing the position with the mouse,
- // but not when pressing an arrow key. However, the base binding sets
- // ._userChanged in its keypress handlers, so we just need to check both.
- if (!userChanged && !this._userChanged)
- return;
- this.setAttribute("value", newValue);
-
- if (this.type == "scrubber")
- this.Utils.seekToPosition(newValue);
- else if (this.type == "volumeControl")
- this.Utils.setVolume(newValue / 100);
- break;
+ control._isHiddenByAdjustment = v;
+ },
+ get: () => control._isHiddenByAdjustment
+ },
+ _isHiddenByAdjustment: {
+ value: false,
+ writable: true
+ }
+ });
+ }
+ // Cannot get minimal width of flexible scrubber and clickToPlay.
+ // Rewrite to empirical value for now.
+ this.controlBar.minHeight = 40;
+ this.scrubberStack.minWidth = 64;
+ this.volumeControl.minWidth = 48;
+ this.clickToPlay.minWidth = 48;
- case "minpos":
- this.setAttribute("min", newValue);
- break;
+ if (this.positionDurationBox) {
+ this.positionDurationBox.minWidth -= this.durationSpan.minWidth;
+ }
- case "maxpos":
- if (this.type == "scrubber") {
- // Update the value bar to match the thumb position.
- let percent = this.value / newValue;
- this.valueBar.value = Math.round(percent * 10000); // has max=10000
- }
- this.setAttribute("max", newValue);
- break;
- }
- ]]>
- </body>
- </method>
+ this.adjustControlSize();
+ this.controlBar.hidden = true;
+
+ // Can only update the volume controls once we've computed
+ // _volumeControlWidth, since the volume slider implementation
+ // depends on it.
+ this.updateVolumeControls();
+ },
- <method name="dragStateChanged">
- <parameter name="isDragging"/>
- <body>
- <![CDATA[
- if (this.type == "scrubber") {
- this.Utils.log("--- dragStateChanged: " + isDragging + " ---");
- this.isDragging = isDragging;
- if (this.isPausedByDragging && !isDragging) {
- // After the drag ends, resume playing.
- this.Utils.video.play();
- this.isPausedByDragging = false;
- }
- }
- ]]>
- </body>
- </method>
+ setupNewLoadState() {
+ // videocontrols.css hides the control bar by default, because if script
+ // is disabled our binding's script is disabled too (bug 449358). Thus,
+ // the controls are broken and we don't want them shown. But if script is
+ // enabled, the code here will run and can explicitly unhide the controls.
+ //
+ // For videos with |autoplay| set, we'll leave the controls initially hidden,
+ // so that they don't get in the way of the playing video. Otherwise we'll
+ // go ahead and reveal the controls now, so they're an obvious user cue.
+ //
+ // (Note: the |controls| attribute is already handled via layout/style/html.css)
+ var shouldShow = !this.dynamicControls ||
+ (this.video.paused &&
+ !(this.video.autoplay && this.video.mozAutoplayEnabled));
+ // Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107.
+ let shouldClickToPlayShow = shouldShow && !this.isAudioOnly &&
+ this.video.currentTime == 0 && !this.hasError();
+ this.startFade(this.clickToPlay, shouldClickToPlayShow, true);
+ this.startFade(this.controlsSpacer, shouldClickToPlayShow, true);
+ this.startFade(this.controlBar, shouldShow, true);
+ },
+
+ get dynamicControls() {
+ // Don't fade controls for <audio> elements.
+ var enabled = !this.isAudioOnly;
+
+ // Allow tests to explicitly suppress the fading of controls.
+ if (this.video.hasAttribute("mozNoDynamicControls")) {
+ enabled = false;
+ }
- <method name="pauseVideoDuringDragging">
- <body>
- <![CDATA[
- if (this.isDragging &&
- !this.Utils.video.paused && !this.isPausedByDragging) {
- this.isPausedByDragging = true;
- this.Utils.video.pause();
- }
- ]]>
- </body>
- </method>
+ // If the video hits an error, suppress controls if it
+ // hasn't managed to do anything else yet.
+ if (!this.firstFrameShown && this.hasError()) {
+ enabled = false;
+ }
- </implementation>
- </binding>
+ return enabled;
+ },
- <binding id="videoControls">
+ updateVolume() {
+ const volume = this.volumeControl.value;
+ this.setVolume(volume / 100);
+ },
- <resources>
- <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
- <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
- </resources>
+ updateVolumeControls() {
+ var volume = this.video.muted ? 0 : this.video.volume;
+ var volumePercentage = Math.round(volume * 100);
+ this.updateMuteButtonState();
+ this.volumeControl.value = volumePercentage;
+ },
- <xbl:content xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
- 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>
+ handleEvent(aEvent) {
+ this.log("Got media event ----> " + aEvent.type);
+
+ // If the binding is detached (or has been replaced by a
+ // newer instance of the binding), nuke our event-listeners.
+ if (this.videocontrols.randomID != this.randomID) {
+ this.terminateEventListeners();
+ return;
+ }
- <div anonid="controlsOverlay" class="controlsOverlay stackItem">
- <div class="controlsSpacerStack" aria-hideen="true">
- <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="playButton"
- playlabel="&playButton.playLabel;"
- pauselabel="&playButton.pauseLabel;"/>
- <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"></progress>
- <progress anonid="progressBar" class="progressBar" value="0" max="100"></progress>
- </div>
- </div>
- <input type="range" anonid="scrubber" class="scrubber"/>
- </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="muteButton"
- mutelabel="&muteButton.muteLabel;"
- unmutelabel="&muteButton.unmuteLabel;"/>
- <div anonid="volumeStack" class="volumeStack progressContainer" role="none">
- <input type="range" anonid="volumeControl" class="volumeControl" min="0" max="100" step="1"/>
- </div>
- <button anonid="closedCaptionButton" class="closedCaptionButton"/>
- <button anonid="fullscreenButton"
- class="fullscreenButton"
- enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
- exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
- </div>
- <div anonid="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></div>
- </div>
- </div>
- </xbl:content>
-
- <implementation>
-
- <constructor>
- <![CDATA[
- this.isTouchControls = false;
- this.randomID = 0;
-
- this.Utils = {
- debug : false,
- video : null,
- videocontrols : null,
- controlBar : null,
- playButton : null,
- muteButton : null,
- volumeControl : null,
- durationLabel : null,
- positionLabel : null,
- scrubber : null,
- progressBar : null,
- bufferBar : null,
- statusOverlay : null,
- controlsSpacer : null,
- clickToPlay : null,
- controlsOverlay : null,
- fullscreenButton : null,
- currentTextTrackIndex: 0,
-
- textTracksCount: 0,
- randomID : 0,
- videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
- "loadstart", "timeupdate", "progress",
- "playing", "waiting", "canplay", "canplaythrough",
- "seeking", "seeked", "emptied", "loadedmetadata",
- "error", "suspend", "stalled",
- "mozinterruptbegin", "mozinterruptend" ],
+ switch (aEvent.type) {
+ case "play":
+ this.setPlayButtonState(false);
+ this.setupStatusFader();
+ if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControls) {
+ this.startFadeOut(this.controlBar);
+ }
+ if (!this._triggeredByControls) {
+ this.clickToPlay.hidden = true;
+ this.controlsSpacer.setAttribute("fadeout", "true");
+ }
+ this._triggeredByControls = false;
+ break;
+ case "pause":
+ // Little white lie: if we've internally paused the video
+ // while dragging the scrubber, don't change the button state.
+ if (!this.scrubber.isDragging) {
+ this.setPlayButtonState(true);
+ }
+ this.setupStatusFader();
+ break;
+ case "ended":
+ this.setPlayButtonState(true);
+ // We throttle timechange events, so the thumb might not be
+ // exactly at the end when the video finishes.
+ this.showPosition(Math.round(this.video.currentTime * 1000),
+ Math.round(this.video.duration * 1000));
+ this.startFadeIn(this.controlBar);
+ this.setupStatusFader();
+ 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);
+ }
+ break;
+ case "loadedmetadata":
+ this.adjustControlSize();
+ // 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 &&
+ (this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
+ this.isAudioOnly = true;
+ this.clickToPlay.hidden = true;
+ this.startFadeIn(this.controlBar);
+ this.setFullscreenButtonState();
+ }
+ this.showDuration(Math.round(this.video.duration * 1000));
+ if (!this.isAudioOnly && !this.video.mozHasAudio) {
+ this.muteButton.setAttribute("noAudio", "true");
+ this.muteButton.setAttribute("disabled", "true");
+ }
+ break;
+ case "loadeddata":
+ 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.setPlayButtonState(true);
+ this.setupNewLoadState();
+ this.setupStatusFader();
+ break;
+ case "progress":
+ this.statusIcon.removeAttribute("stalled");
+ this.showBuffered();
+ this.setupStatusFader();
+ break;
+ case "stalled":
+ this.statusIcon.setAttribute("stalled", "true");
+ this.statusIcon.setAttribute("type", "throbber");
+ this.setupStatusFader();
+ break;
+ case "suspend":
+ this.setupStatusFader();
+ break;
+ case "timeupdate":
+ var currentTime = Math.round(this.video.currentTime * 1000); // in ms
+ var duration = Math.round(this.video.duration * 1000); // in ms
- firstFrameShown : false,
- timeUpdateCount : 0,
- maxCurrentTimeSeen : 0,
- isPausedByDragging: false,
- _isAudioOnly : false,
- get isAudioOnly() { return this._isAudioOnly; },
- set isAudioOnly(val) {
- this._isAudioOnly = val;
- this.setFullscreenButtonState();
-
- if (!this.isTopLevelSyntheticDocument)
- return;
- if (this._isAudioOnly) {
- this.video.style.height = this.controlBar.minHeight + "px";
- this.video.style.width = "66%";
- } else {
- this.video.style.removeProperty("height");
- this.video.style.removeProperty("width");
- }
- },
- suppressError : false,
-
- setupStatusFader : function(immediate) {
- // Since the play button will be showing, we don't want to
- // show the throbber behind it. The throbber here will
- // only show if needed after the play button has been pressed.
- if (!this.clickToPlay.hidden) {
- this.startFadeOut(this.statusOverlay, true);
- return;
- }
-
- var show = false;
- if (this.video.seeking ||
- (this.video.error && !this.suppressError) ||
- this.video.networkState == this.video.NETWORK_NO_SOURCE ||
- (this.video.networkState == this.video.NETWORK_LOADING &&
- (this.video.paused || this.video.ended
- ? this.video.readyState < this.video.HAVE_CURRENT_DATA
- : this.video.readyState < this.video.HAVE_FUTURE_DATA)) ||
- (this.timeUpdateCount <= 1 && !this.video.ended &&
- this.video.readyState < this.video.HAVE_FUTURE_DATA &&
- this.video.networkState == this.video.NETWORK_LOADING))
- show = true;
+ // If playing/seeking after the video ended, we won't get a "play"
+ // event, so update the button state here.
+ if (!this.video.paused) {
+ this.setPlayButtonState(false);
+ }
- // Explicitly hide the status fader if this
- // is audio only until bug 619421 is fixed.
- if (this.isAudioOnly)
- show = false;
-
- this.log("Status overlay: seeking=" + this.video.seeking +
- " error=" + this.video.error + " readyState=" + this.video.readyState +
- " paused=" + this.video.paused + " ended=" + this.video.ended +
- " networkState=" + this.video.networkState +
- " timeUpdateCount=" + this.timeUpdateCount +
- " --> " + (show ? "SHOW" : "HIDE"));
- this.startFade(this.statusOverlay, show, immediate);
- },
-
- /*
- * Set the initial state of the controls. The binding is normally created along
- * with video element, but could be attached at any point (eg, if the video is
- * removed from the document and then reinserted). Thus, some one-time events may
- * have already fired, and so we'll need to explicitly check the initial state.
- */
- setupInitialState : function() {
- this.randomID = Math.random();
- this.videocontrols.randomID = this.randomID;
-
- this.setPlayButtonState(this.video.paused);
-
- this.setFullscreenButtonState();
-
- var duration = Math.round(this.video.duration * 1000); // in ms
- var currentTime = Math.round(this.video.currentTime * 1000); // in ms
- this.log("Initial playback position is at " + currentTime + " of " + duration);
- // It would be nice to retain maxCurrentTimeSeen, but it would be difficult
- // 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 &&
- (this.video.videoWidth == 0 || this.video.videoHeight == 0))
- this.isAudioOnly = true;
+ this.timeUpdateCount++;
+ // Whether we show the statusOverlay sometimes depends
+ // on whether we've seen more than one timeupdate
+ // event (if we haven't, there hasn't been any
+ // "playback activity" and we may wish to show the
+ // statusOverlay while we wait for HAVE_ENOUGH_DATA).
+ // If we've seen more than 2 timeupdate events,
+ // the count is no longer relevant to setupStatusFader.
+ if (this.timeUpdateCount <= 2) {
+ this.setupStatusFader();
+ }
- // 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.
- if (!this.isAudioOnly && !this.video.mozHasAudio) {
- this.muteButton.setAttribute("noAudio", "true");
- this.muteButton.setAttribute("disabled", "true");
- }
- }
-
- if (this.isAudioOnly)
- this.clickToPlay.hidden = true;
-
- // If the first frame hasn't loaded, kick off a throbber fade-in.
- if (this.video.readyState >= this.video.HAVE_CURRENT_DATA)
- this.firstFrameShown = true;
-
- // We can't determine the exact buffering status, but do know if it's
- // fully loaded. (If it's still loading, it will fire a progress event
- // and we'll figure out the exact state then.)
- this.bufferBar.max = 100;
- if (this.video.readyState >= this.video.HAVE_METADATA)
- this.showBuffered();
- else
- this.bufferBar.value = 0;
-
- // Set the current status icon.
- if (this.hasError()) {
- this.clickToPlay.hidden = true;
- this.statusIcon.setAttribute("type", "error");
- this.updateErrorText();
- this.setupStatusFader(true);
- }
-
- // An event handler for |onresize| should be added when bug 227495 is fixed.
- this.controlBar.hidden = false;
-
- let layoutControls = [
- ...this.controlBar.children,
- this.durationSpan,
- this.controlBar,
- this.clickToPlay
- ];
-
- for (let control of layoutControls) {
- if (!control) {
- break;
- }
+ // If the user is dragging the scrubber ignore the delayed seek
+ // responses (don't yank the thumb away from the user)
+ if (this.scrubber.isDragging || this.scrubber.startToDrag) {
+ return;
+ }
+ this.showPosition(currentTime, duration);
+ this.showBuffered();
+ break;
+ case "emptied":
+ this.bufferBar.value = 0;
+ this.showPosition(0, 0);
+ break;
+ case "seeking":
+ this.showBuffered();
+ this.statusIcon.setAttribute("type", "throbber");
+ this.setupStatusFader();
+ break;
+ case "waiting":
+ this.statusIcon.setAttribute("type", "throbber");
+ this.setupStatusFader();
+ break;
+ case "seeked":
+ case "playing":
+ case "canplay":
+ case "canplaythrough":
+ this.setupStatusFader();
+ break;
+ case "error":
+ // We'll show the error status icon when we receive an error event
+ // under either of the following conditions:
+ // 1. The video has its error attribute set; this means we're loading
+ // from our src attribute, and the load failed, or we we're loading
+ // from source children and the decode or playback failed after we
+ // determined our selected resource was playable.
+ // 2. The video's networkState is NETWORK_NO_SOURCE. This means we we're
+ // loading from child source elements, but we were unable to select
+ // any of the child elements for playback during resource selection.
+ if (this.hasError()) {
+ this.suppressError = false;
+ this.clickToPlay.hidden = true;
+ this.statusIcon.setAttribute("type", "error");
+ this.updateErrorText();
+ this.setupStatusFader(true);
+ // If video hasn't shown anything yet, disable the controls.
+ if (!this.firstFrameShown) {
+ this.startFadeOut(this.controlBar);
+ }
+ this.controlsSpacer.removeAttribute("hideCursor");
+ }
+ break;
+ case "mozinterruptbegin":
+ case "mozinterruptend":
+ // Nothing to do...
+ break;
+ default:
+ this.log("!!! event " + aEvent.type + " not handled!");
+ }
+ },
- Object.defineProperties(control, {
- minWidth: {
- value: control.clientWidth,
- writable: true
- },
- minHeight: {
- value: control.clientHeight,
- writable: true
- },
- isAdjustableControl: {
- value: true
- },
- isWanted: {
- value: true,
- writable: true
- },
- hideByAdjustment: {
- set: (v) => {
- if (v) {
- control.setAttribute("hidden", "true");
- } else {
- control.removeAttribute("hidden");
- }
+ terminateEventListeners() {
+ if (this.videoEvents) {
+ for (let event of this.videoEvents) {
+ this.video.removeEventListener(event, this, {
+ capture: true,
+ mozSystemGroup: true
+ });
+ }
+ }
+
+ if (this.controlListeners) {
+ for (let element of this.controlListeners) {
+ element.item.removeEventListener(element.event, element.func,
+ { mozSystemGroup: true });
+ }
+
+ delete this.controlListeners;
+ }
+
+ this.log("--- videocontrols terminated ---");
+ },
- control._isHiddenByAdjustment = v;
- },
- get: () => control._isHiddenByAdjustment
- },
- _isHiddenByAdjustment: {
- value: false,
- writable: true
- }
- });
- }
- // Cannot get minimal width of flexible scrubber and clickToPlay.
- // Rewrite to empirical value for now.
- this.controlBar.minHeight = 40;
- this.scrubberStack.minWidth = 64;
- this.volumeControl.minWidth = 48;
- this.clickToPlay.minWidth = 48;
-
- if (this.positionDurationBox) {
- this.positionDurationBox.minWidth -= this.durationSpan.minWidth;
- }
-
- this.adjustControlSize();
- this.controlBar.hidden = true;
+ hasError() {
+ // We either have an explicit error, or the resource selection
+ // algorithm is running and we've tried to load something and failed.
+ // Note: we don't consider the case where we've tried to load but
+ // there's no sources to load as an error condition, as sites may
+ // do this intentionally to work around requires-user-interaction to
+ // play restrictions, and we don't want to display a debug message
+ // if that's the case.
+ return this.video.error != null ||
+ (this.video.networkState == this.video.NETWORK_NO_SOURCE &&
+ this.hasSources());
+ },
- // Can only update the volume controls once we've computed
- // _volumeControlWidth, since the volume slider implementation
- // depends on it.
- this.updateVolumeControls();
- },
-
- setupNewLoadState : function() {
- // videocontrols.css hides the control bar by default, because if script
- // is disabled our binding's script is disabled too (bug 449358). Thus,
- // the controls are broken and we don't want them shown. But if script is
- // enabled, the code here will run and can explicitly unhide the controls.
- //
- // For videos with |autoplay| set, we'll leave the controls initially hidden,
- // so that they don't get in the way of the playing video. Otherwise we'll
- // go ahead and reveal the controls now, so they're an obvious user cue.
- //
- // (Note: the |controls| attribute is already handled via layout/style/html.css)
- var shouldShow = !this.dynamicControls ||
- (this.video.paused &&
- !(this.video.autoplay && this.video.mozAutoplayEnabled));
- // Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107.
- let shouldClickToPlayShow = shouldShow && !this.isAudioOnly &&
- this.video.currentTime == 0 && !this.hasError();
- this.startFade(this.clickToPlay, shouldClickToPlayShow, true);
- this.startFade(this.controlsSpacer, shouldClickToPlayShow, true);
- this.startFade(this.controlBar, shouldShow, true);
- },
+ 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) {
+ return true;
+ }
+ }
+ return false;
+ },
- get dynamicControls() {
- // Don't fade controls for <audio> elements.
- var enabled = !this.isAudioOnly;
-
- // Allow tests to explicitly suppress the fading of controls.
- if (this.video.hasAttribute("mozNoDynamicControls"))
- enabled = false;
-
- // If the video hits an error, suppress controls if it
- // hasn't managed to do anything else yet.
- if (!this.firstFrameShown && this.hasError())
- enabled = false;
-
- return enabled;
- },
+ updateErrorText() {
+ let error;
+ let v = this.video;
+ // It is possible to have both v.networkState == NETWORK_NO_SOURCE
+ // as well as v.error being non-null. In this case, we will show
+ // the v.error.code instead of the v.networkState error.
+ if (v.error) {
+ switch (v.error.code) {
+ case v.error.MEDIA_ERR_ABORTED:
+ error = "errorAborted";
+ break;
+ case v.error.MEDIA_ERR_NETWORK:
+ error = "errorNetwork";
+ break;
+ case v.error.MEDIA_ERR_DECODE:
+ error = "errorDecode";
+ break;
+ case v.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
+ error = "errorSrcNotSupported";
+ break;
+ default:
+ error = "errorGeneric";
+ break;
+ }
+ } else if (v.networkState == v.NETWORK_NO_SOURCE) {
+ error = "errorNoSource";
+ } else {
+ return; // No error found.
+ }
- updateVolume() {
- const volume = this.volumeControl.value;
- this.setVolume(volume / 100);
- },
+ let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
+ this.controlsSpacer.setAttribute("aria-label", label.textContent);
+ this.statusOverlay.setAttribute("error", error);
+ },
- updateVolumeControls() {
- var volume = this.video.muted ? 0 : this.video.volume;
- var volumePercentage = Math.round(volume * 100);
- this.updateMuteButtonState();
- this.volumeControl.value = volumePercentage;
- },
+ formatTime(aTime) {
+ // Format the duration as "h:mm:ss" or "m:ss"
+ aTime = Math.round(aTime / 1000);
+ let hours = Math.floor(aTime / 3600);
+ let mins = Math.floor((aTime % 3600) / 60);
+ let secs = Math.floor(aTime % 60);
+ let timeString;
+ if (secs < 10) {
+ secs = "0" + secs;
+ }
+ if (hours) {
+ if (mins < 10) {
+ mins = "0" + mins;
+ }
+ timeString = hours + ":" + mins + ":" + secs;
+ } else {
+ timeString = mins + ":" + secs;
+ }
+ return timeString;
+ },
- handleEvent : function(aEvent) {
- this.log("Got media event ----> " + aEvent.type);
+ initPositionDurationBox() {
+ if (this.videocontrols.isTouchControls) {
+ return;
+ }
- // If the binding is detached (or has been replaced by a
- // newer instance of the binding), nuke our event-listeners.
- if (this.videocontrols.randomID != this.randomID) {
- this.terminateEventListeners();
- return;
- }
+ 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;
- switch (aEvent.type) {
- case "play":
- this.setPlayButtonState(false);
- this.setupStatusFader();
- if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControls)
- this.startFadeOut(this.controlBar);
- if (!this._triggeredByControls) {
- this.clickToPlay.hidden = true;
- this.controlsSpacer.setAttribute("fadeout", "true");
- }
- this._triggeredByControls = false;
- break;
- case "pause":
- // Little white lie: if we've internally paused the video
- // while dragging the scrubber, don't change the button state.
- if (!this.scrubber.isDragging)
- this.setPlayButtonState(true);
- this.setupStatusFader();
- break;
- case "ended":
- this.setPlayButtonState(true);
- // We throttle timechange events, so the thumb might not be
- // exactly at the end when the video finishes.
- this.showPosition(Math.round(this.video.currentTime * 1000),
- Math.round(this.video.duration * 1000));
- this.startFadeIn(this.controlBar);
- this.setupStatusFader();
- 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);
- }
- break;
- case "loadedmetadata":
- this.adjustControlSize();
- // 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 &&
- (this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
- this.isAudioOnly = true;
- this.clickToPlay.hidden = true;
- this.startFadeIn(this.controlBar);
- this.setFullscreenButtonState();
- }
- this.showDuration(Math.round(this.video.duration * 1000));
- if (!this.isAudioOnly && !this.video.mozHasAudio) {
- this.muteButton.setAttribute("noAudio", "true");
- this.muteButton.setAttribute("disabled", "true");
- }
- break;
- case "loadeddata":
- 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.setPlayButtonState(true);
- this.setupNewLoadState();
- this.setupStatusFader();
- break;
- case "progress":
- this.statusIcon.removeAttribute("stalled");
- this.showBuffered();
- this.setupStatusFader();
- break;
- case "stalled":
- this.statusIcon.setAttribute("stalled", "true");
- this.statusIcon.setAttribute("type", "throbber");
- this.setupStatusFader();
- break;
- case "suspend":
- this.setupStatusFader();
- break;
- case "timeupdate":
- var currentTime = Math.round(this.video.currentTime * 1000); // in ms
- var duration = Math.round(this.video.duration * 1000); // in ms
+ durationSpan.classList.add("duration");
+ durationSpan.setAttribute("role", "none");
+
+ Object.defineProperties(this.positionDurationBox, {
+ durationSpan: {
+ value: durationSpan
+ },
+ position: {
+ set: (v) => {
+ positionTextNode.textContent = positionFormat.replace("#1", v);
+ }
+ },
+ duration: {
+ set: (v) => {
+ durationSpan.textContent = v ? durationFormat.replace("#2", v) : "";
+ }
+ }
+ });
+ },
+
+ showDuration(duration) {
+ let isInfinite = (duration == Infinity);
+ this.log("Duration is " + duration + "ms.\n");
+
+ if (isNaN(duration) || isInfinite) {
+ duration = this.maxCurrentTimeSeen;
+ }
- // If playing/seeking after the video ended, we won't get a "play"
- // event, so update the button state here.
- if (!this.video.paused)
- this.setPlayButtonState(false);
+ // Format the duration as "h:mm:ss" or "m:ss"
+ let timeString = isInfinite ? "" : this.formatTime(duration);
+ if (this.videocontrols.isTouchControls) {
+ this.durationLabel.setAttribute("value", timeString);
+ } else {
+ this.positionDurationBox.duration = timeString;
+ }
+
+ // "durationValue" property is used by scale binding to
+ // generate accessible name.
+ this.scrubber.durationValue = timeString;
+
+ // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss
+
+ this.scrubber.max = duration;
+ // XXX Can't set increment here, due to bug 473103. Also, doing so causes
+ // snapping when dragging with the mouse, so we can't just set a value for
+ // the arrow-keys.
+ this.scrubber.pageIncrement = Math.round(duration / 10);
+ },
+
+ pauseVideoDuringDragging() {
+ if (!this.video.paused &&
+ !this.isPausedByDragging &&
+ this.scrubber.isDragging) {
+ this.isPausedByDragging = true;
+ this.video.pause();
+ }
+ },
+
+ onScrubberInput(e) {
+ const duration = Math.round(this.video.duration * 1000); // in ms
+ let time = this.scrubber.value;
+
+ if (!this.scrubber.startToDrag || this.scrubber.isDragging) {
+ this.seekToPosition(time);
+ this.showPosition(time, duration);
+ }
- this.timeUpdateCount++;
- // Whether we show the statusOverlay sometimes depends
- // on whether we've seen more than one timeupdate
- // event (if we haven't, there hasn't been any
- // "playback activity" and we may wish to show the
- // statusOverlay while we wait for HAVE_ENOUGH_DATA).
- // If we've seen more than 2 timeupdate events,
- // the count is no longer relevant to setupStatusFader.
- if (this.timeUpdateCount <= 2)
- this.setupStatusFader();
+ this.scrubber.startToDrag = true;
+ },
+
+ onScrubberChange(e) {
+ this.scrubber.startToDrag = false;
+ this.scrubber.isDragging = false;
+
+ if (this.isPausedByDragging) {
+ this.video.play();
+ this.isPausedByDragging = false;
+ }
+ },
+
+ updateScrubberProgress() {
+ if (this.videocontrols.isTouchControls) {
+ return;
+ }
+
+ const positionPercent = this.scrubber.value / this.scrubber.max * 100;
- // If the user is dragging the scrubber ignore the delayed seek
- // responses (don't yank the thumb away from the user)
- if (this.scrubber.isDragging || this.scrubber.startToDrag)
- return;
+ if (!isNaN(positionPercent) && positionPercent != Infinity) {
+ this.progressBar.value = positionPercent;
+ } else {
+ this.progressBar.value = 0;
+ }
+ },
+
+ seekToPosition(newPosition) {
+ newPosition /= 1000; // convert from ms
+ this.log("+++ seeking to " + newPosition);
+ if (this.videocontrols.isGonk) {
+ // We use fastSeek() on B2G, and an accurate (but slower)
+ // seek on other platforms (that are likely to be higher
+ // perf).
+ this.video.fastSeek(newPosition);
+ } else {
+ this.video.currentTime = newPosition;
+ }
+ },
+
+ setVolume(newVolume) {
+ this.log("*** setting volume to " + newVolume);
+ this.video.volume = newVolume;
+ this.video.muted = false;
+ },
- this.showPosition(currentTime, duration);
- this.showBuffered();
- break;
- case "emptied":
- this.bufferBar.value = 0;
- this.showPosition(0, 0);
- break;
- case "seeking":
- this.showBuffered();
- this.statusIcon.setAttribute("type", "throbber");
- this.setupStatusFader();
- break;
- case "waiting":
- this.statusIcon.setAttribute("type", "throbber");
- this.setupStatusFader();
- break;
- case "seeked":
- case "playing":
- case "canplay":
- case "canplaythrough":
- this.setupStatusFader();
- break;
- case "error":
- // We'll show the error status icon when we receive an error event
- // under either of the following conditions:
- // 1. The video has its error attribute set; this means we're loading
- // from our src attribute, and the load failed, or we we're loading
- // from source children and the decode or playback failed after we
- // determined our selected resource was playable.
- // 2. The video's networkState is NETWORK_NO_SOURCE. This means we we're
- // loading from child source elements, but we were unable to select
- // any of the child elements for playback during resource selection.
- if (this.hasError()) {
- this.suppressError = false;
- this.clickToPlay.hidden = true;
- this.statusIcon.setAttribute("type", "error");
- this.updateErrorText();
- this.setupStatusFader(true);
- // If video hasn't shown anything yet, disable the controls.
- if (!this.firstFrameShown)
- this.startFadeOut(this.controlBar);
- this.controlsSpacer.removeAttribute("hideCursor");
- }
- break;
- case "mozinterruptbegin":
- case "mozinterruptend":
- // Nothing to do...
- break;
- default:
- this.log("!!! event " + aEvent.type + " not handled!");
- }
- },
+ showPosition(currentTime, duration) {
+ // If the duration is unknown (because the server didn't provide
+ // it, or the video is a stream), then we want to fudge the duration
+ // by using the maximum playback position that's been seen.
+ if (currentTime > this.maxCurrentTimeSeen) {
+ this.maxCurrentTimeSeen = currentTime;
+ }
+ this.showDuration(duration);
+
+ this.log("time update @ " + currentTime + "ms of " + duration + "ms");
+
+ let positionTime = this.formatTime(currentTime);
+
+ this.scrubber.value = currentTime;
+ if (this.videocontrols.isTouchControls) {
+ this.positionLabel.setAttribute("value", positionTime);
+ } else {
+ this.positionDurationBox.position = positionTime;
+ this.updateScrubberProgress();
+ }
+ },
- terminateEventListeners : function() {
- if (this.videoEvents) {
- for (let event of this.videoEvents) {
- this.video.removeEventListener(event, this, {
- capture: true,
- mozSystemGroup: true
- });
- }
- }
-
- if (this.controlListeners) {
- for (let element of this.controlListeners) {
- element.item.removeEventListener(element.event, element.func,
- { mozSystemGroup: true });
- }
-
- delete this.controlListeners;
- }
-
- this.log("--- videocontrols terminated ---");
- },
+ showBuffered() {
+ function bsearch(haystack, needle, cmp) {
+ var length = haystack.length;
+ var low = 0;
+ var high = length;
+ while (low < high) {
+ var probe = low + ((high - low) >> 1);
+ var r = cmp(haystack, probe, needle);
+ if (r == 0) {
+ return probe;
+ } else if (r > 0) {
+ low = probe + 1;
+ } else {
+ high = probe;
+ }
+ }
+ return -1;
+ }
- hasError : function() {
- // We either have an explicit error, or the resource selection
- // algorithm is running and we've tried to load something and failed.
- // Note: we don't consider the case where we've tried to load but
- // there's no sources to load as an error condition, as sites may
- // do this intentionally to work around requires-user-interaction to
- // play restrictions, and we don't want to display a debug message
- // if that's the case.
- return this.video.error != null ||
- (this.video.networkState == this.video.NETWORK_NO_SOURCE &&
- this.hasSources());
- },
+ function bufferedCompare(buffered, i, time) {
+ if (time > buffered.end(i)) {
+ return 1;
+ } else if (time >= buffered.start(i)) {
+ return 0;
+ }
+ return -1;
+ }
- hasSources : function() {
- 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) {
- return true;
- }
- }
- return false;
- },
+ var duration = Math.round(this.video.duration * 1000);
+ if (isNaN(duration) || duration == Infinity) {
+ duration = this.maxCurrentTimeSeen;
+ }
- updateErrorText : function() {
- let error;
- let v = this.video;
- // It is possible to have both v.networkState == NETWORK_NO_SOURCE
- // as well as v.error being non-null. In this case, we will show
- // the v.error.code instead of the v.networkState error.
- if (v.error) {
- switch (v.error.code) {
- case v.error.MEDIA_ERR_ABORTED:
- error = "errorAborted";
- break;
- case v.error.MEDIA_ERR_NETWORK:
- error = "errorNetwork";
- break;
- case v.error.MEDIA_ERR_DECODE:
- error = "errorDecode";
- break;
- case v.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
- error = "errorSrcNotSupported";
- break;
- default:
- error = "errorGeneric";
- break;
- }
- } else if (v.networkState == v.NETWORK_NO_SOURCE) {
- error = "errorNoSource";
- } else {
- return; // No error found.
- }
+ // Find the range that the current play position is in and use that
+ // range for bufferBar. At some point we may support multiple ranges
+ // displayed in the bar.
+ var currentTime = this.video.currentTime;
+ var buffered = this.video.buffered;
+ var index = bsearch(buffered, currentTime, bufferedCompare);
+ var endTime = 0;
+ if (index >= 0) {
+ endTime = Math.round(buffered.end(index) * 1000);
+ }
+ this.bufferBar.max = duration;
+ this.bufferBar.value = endTime;
+ },
+
+ _controlsHiddenByTimeout : false,
+ _showControlsTimeout : 0,
+ SHOW_CONTROLS_TIMEOUT_MS: 500,
+ _showControlsFn() {
+ if (Utils.video.matches("video:hover")) {
+ Utils.startFadeIn(Utils.controlBar, false);
+ Utils._showControlsTimeout = 0;
+ Utils._controlsHiddenByTimeout = false;
+ }
+ },
- let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
- this.controlsSpacer.setAttribute("aria-label", label.textContent);
- this.statusOverlay.setAttribute("error", error);
- },
+ _hideControlsTimeout : 0,
+ _hideControlsFn() {
+ if (!Utils.scrubber.isDragging) {
+ Utils.startFade(Utils.controlBar, false);
+ Utils._hideControlsTimeout = 0;
+ Utils._controlsHiddenByTimeout = true;
+ }
+ },
+ HIDE_CONTROLS_TIMEOUT_MS : 2000,
+ onMouseMove(event) {
+ // Pause playing video when the mouse is dragging over the control bar.
+ if (this.scrubber.startToDrag) {
+ this.scrubber.isDragging = true;
+ this.pauseVideoDuringDragging();
+ }
- formatTime : function(aTime) {
- // Format the duration as "h:mm:ss" or "m:ss"
- aTime = Math.round(aTime / 1000);
- let hours = Math.floor(aTime / 3600);
- let mins = Math.floor((aTime % 3600) / 60);
- let secs = Math.floor(aTime % 60);
- let timeString;
- if (secs < 10)
- secs = "0" + secs;
- if (hours) {
- if (mins < 10)
- mins = "0" + mins;
- timeString = hours + ":" + mins + ":" + secs;
- } else {
- timeString = mins + ":" + secs;
- }
- return timeString;
- },
+ // If the controls are static, don't change anything.
+ if (!this.dynamicControls) {
+ return;
+ }
+
+ clearTimeout(this._hideControlsTimeout);
- initPositionDurationBox : function() {
- if (this.videocontrols.isTouchControls) {
- return;
- }
-
- 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");
+ // 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 && this.video.mozAutoplayEnabled)) {
+ return;
+ }
- Object.defineProperties(this.positionDurationBox, {
- durationSpan: {
- value: durationSpan
- },
- position: {
- set: (v) => {
- positionTextNode.textContent = positionFormat.replace("#1", v);
- }
- },
- duration: {
- set: (v) => {
- durationSpan.textContent = v ? durationFormat.replace("#2", v) : "";
- }
- }
- });
- },
+ if (this._controlsHiddenByTimeout) {
+ this._showControlsTimeout = setTimeout(this._showControlsFn, this.SHOW_CONTROLS_TIMEOUT_MS);
+ } else {
+ this.startFade(this.controlBar, true);
+ }
- showDuration : function(duration) {
- let isInfinite = (duration == Infinity);
- this.log("Duration is " + duration + "ms.\n");
-
- if (isNaN(duration) || isInfinite)
- duration = this.maxCurrentTimeSeen;
+ // 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);
+ }
+ },
- // Format the duration as "h:mm:ss" or "m:ss"
- let timeString = isInfinite ? "" : this.formatTime(duration);
- if (this.videocontrols.isTouchControls) {
- this.durationLabel.setAttribute("value", timeString);
- } else {
- this.positionDurationBox.duration = timeString;
- }
+ onMouseInOut(event) {
+ // If the controls are static, don't change anything.
+ if (!this.dynamicControls) {
+ return;
+ }
+
+ clearTimeout(this._hideControlsTimeout);
- // "durationValue" property is used by scale binding to
- // generate accessible name.
- this.scrubber.durationValue = timeString;
-
- // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss
+ // 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.isEventWithin(event, this.videocontrols)) {
+ return;
+ }
- this.scrubber.max = duration;
- // XXX Can't set increment here, due to bug 473103. Also, doing so causes
- // snapping when dragging with the mouse, so we can't just set a value for
- // the arrow-keys.
- this.scrubber.pageIncrement = Math.round(duration / 10);
- },
+ var isMouseOver = (event.type == "mouseover");
+
+ var controlRect = this.controlBar.getBoundingClientRect();
+ var isMouseInControls = event.clientY > controlRect.top &&
+ event.clientY < controlRect.bottom &&
+ event.clientX > controlRect.left &&
+ event.clientX < controlRect.right;
- pauseVideoDuringDragging: function() {
- if (!this.video.paused &&
- !this.isPausedByDragging &&
- this.scrubber.isDragging) {
- this.isPausedByDragging = true;
- this.video.pause();
- }
- },
+ // 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 && !isMouseOver &&
+ !(this.video.autoplay && this.video.mozAutoplayEnabled)) {
+ return;
+ }
+
+ if (!isMouseOver && !isMouseInControls) {
+ this.adjustControlSize();
- onScrubberInput: function(e) {
- const duration = Math.round(this.video.duration * 1000); // in ms
- let time = this.scrubber.value;
+ // Keep the controls visible if the click-to-play is visible.
+ if (!this.clickToPlay.hidden) {
+ return;
+ }
- if (!this.scrubber.startToDrag || this.scrubber.isDragging) {
- this.seekToPosition(time);
- this.showPosition(time, duration);
- }
-
- this.scrubber.startToDrag = true;
- },
-
- onScrubberChange: function(e) {
- this.scrubber.startToDrag = false;
- this.scrubber.isDragging = false;
+ this.startFadeOut(this.controlBar, false);
+ this.textTrackList.setAttribute("hidden", "true");
+ clearTimeout(this._showControlsTimeout);
+ Utils._controlsHiddenByTimeout = false;
+ }
+ },
- if (this.isPausedByDragging) {
- this.video.play();
- this.isPausedByDragging = false;
- }
- },
+ startFadeIn(element, immediate) {
+ this.startFade(element, true, immediate);
+ },
+
+ startFadeOut(element, immediate) {
+ this.startFade(element, false, immediate);
+ },
- updateScrubberProgress() {
- if (this.videocontrols.isTouchControls) {
- return;
- }
-
- const positionPercent = this.scrubber.value / this.scrubber.max * 100;
-
- if (!isNaN(positionPercent) && positionPercent != Infinity) {
- this.progressBar.value = positionPercent;
- } else {
- this.progressBar.value = 0;
- }
- },
+ startFade(element, fadeIn, immediate) {
+ if (element.classList.contains("controlBar") && fadeIn) {
+ // 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.hidden) {
+ if (this.videocontrols.isTouchControls) {
+ this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
+ } else {
+ this.scrubber.value = this.video.currentTime * 1000;
+ }
+ }
+ }
- seekToPosition : function(newPosition) {
- newPosition /= 1000; // convert from ms
- this.log("+++ seeking to " + newPosition);
- if (this.videocontrols.isGonk) {
- // We use fastSeek() on B2G, and an accurate (but slower)
- // seek on other platforms (that are likely to be higher
- // perf).
- this.video.fastSeek(newPosition);
- } else {
- this.video.currentTime = newPosition;
- }
- },
+ if (immediate) {
+ element.setAttribute("immediate", true);
+ } else {
+ element.removeAttribute("immediate");
+ }
- setVolume : function(newVolume) {
- this.log("*** setting volume to " + newVolume);
- this.video.volume = newVolume;
- this.video.muted = false;
- },
+ if (fadeIn && !(element.isAdjustableControl && element.hideByAdjustment)) {
+ element.hidden = false;
+ // force style resolution, so that transition begins
+ // when we remove the attribute.
+ element.clientTop;
+ element.removeAttribute("fadeout");
+ if (element.classList.contains("controlBar")) {
+ this.controlsSpacer.removeAttribute("hideCursor");
+ }
+ } else {
+ element.setAttribute("fadeout", true);
+ if (element.classList.contains("controlBar") && !this.hasError() &&
+ document.mozFullScreenElement == this.video) {
+ this.controlsSpacer.setAttribute("hideCursor", true);
+ }
+ }
+ },
- showPosition : function(currentTime, duration) {
- // If the duration is unknown (because the server didn't provide
- // it, or the video is a stream), then we want to fudge the duration
- // by using the maximum playback position that's been seen.
- if (currentTime > this.maxCurrentTimeSeen)
- this.maxCurrentTimeSeen = currentTime;
- this.showDuration(duration);
+ onTransitionEnd(event) {
+ // Ignore events for things other than opacity changes.
+ if (event.propertyName != "opacity") {
+ return;
+ }
- this.log("time update @ " + currentTime + "ms of " + duration + "ms");
-
- let positionTime = this.formatTime(currentTime);
+ var element = event.originalTarget;
- this.scrubber.value = currentTime;
- if (this.videocontrols.isTouchControls) {
- this.positionLabel.setAttribute("value", positionTime);
- } else {
- this.positionDurationBox.position = positionTime;
- this.updateScrubberProgress();
- }
- },
+ // Nothing to do when a fade *in* finishes.
+ if (!element.hasAttribute("fadeout")) {
+ return;
+ }
+
+ if (this.videocontrols.isTouchControls) {
+ this.scrubber.dragStateChanged(false);
+ }
+ element.hidden = true;
+ },
+
+ _triggeredByControls: false,
- showBuffered : function() {
- function bsearch(haystack, needle, cmp) {
- var length = haystack.length;
- var low = 0;
- var high = length;
- while (low < high) {
- var probe = low + ((high - low) >> 1);
- var r = cmp(haystack, probe, needle);
- if (r == 0) {
- return probe;
- } else if (r > 0) {
- low = probe + 1;
- } else {
- high = probe;
- }
- }
- return -1;
- }
+ startPlay() {
+ this._triggeredByControls = true;
+ this.hideClickToPlay();
+ this.video.play();
+ },
+
+ togglePause() {
+ if (this.video.paused || this.video.ended) {
+ this.startPlay();
+ } else {
+ this.video.pause();
+ }
- function bufferedCompare(buffered, i, time) {
- if (time > buffered.end(i)) {
- return 1;
- } else if (time >= buffered.start(i)) {
- return 0;
- }
- return -1;
- }
+ // We'll handle style changes in the event listener for
+ // the "play" and "pause" events, same as if content
+ // script was controlling video playback.
+ },
- var duration = Math.round(this.video.duration * 1000);
- if (isNaN(duration) || duration == Infinity) {
- duration = this.maxCurrentTimeSeen;
- }
+ isVideoWithoutAudioTrack() {
+ return this.video.readyState >= this.video.HAVE_METADATA &&
+ !this.isAudioOnly &&
+ !this.video.mozHasAudio;
+ },
- // Find the range that the current play position is in and use that
- // range for bufferBar. At some point we may support multiple ranges
- // displayed in the bar.
- var currentTime = this.video.currentTime;
- var buffered = this.video.buffered;
- var index = bsearch(buffered, currentTime, bufferedCompare);
- var endTime = 0;
- if (index >= 0) {
- endTime = Math.round(buffered.end(index) * 1000);
- }
- this.bufferBar.max = duration;
- this.bufferBar.value = endTime;
- },
+ toggleMute() {
+ if (this.isVideoWithoutAudioTrack()) {
+ return;
+ }
+ this.video.muted = !this.isEffectivelyMuted();
+ if (this.video.volume === 0) {
+ this.video.volume = 0.5;
+ }
- _controlsHiddenByTimeout : false,
- _showControlsTimeout : 0,
- SHOW_CONTROLS_TIMEOUT_MS: 500,
- _showControlsFn : function() {
- if (Utils.video.matches("video:hover")) {
- Utils.startFadeIn(Utils.controlBar, false);
- Utils._showControlsTimeout = 0;
- Utils._controlsHiddenByTimeout = false;
- }
- },
+ // We'll handle style changes in the event listener for
+ // the "volumechange" event, same as if content script was
+ // controlling volume.
+ },
+
+ isVideoInFullScreen() {
+ return document.mozFullScreenElement == this.video;
+ },
- _hideControlsTimeout : 0,
- _hideControlsFn : function() {
- if (!Utils.scrubber.isDragging) {
- Utils.startFade(Utils.controlBar, false);
- Utils._hideControlsTimeout = 0;
- Utils._controlsHiddenByTimeout = true;
- }
- },
- HIDE_CONTROLS_TIMEOUT_MS : 2000,
- onMouseMove : function(event) {
- // Pause playing video when the mouse is dragging over the control bar.
- if (this.scrubber.startToDrag) {
- this.scrubber.isDragging = true;
- this.pauseVideoDuringDragging();
- }
+ toggleFullscreen() {
+ this.isVideoInFullScreen() ?
+ document.mozCancelFullScreen() :
+ this.video.mozRequestFullScreen();
+ },
- // If the controls are static, don't change anything.
- if (!this.dynamicControls)
- return;
-
- 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 && this.video.mozAutoplayEnabled))
- return;
-
- if (this._controlsHiddenByTimeout)
- this._showControlsTimeout = setTimeout(this._showControlsFn, this.SHOW_CONTROLS_TIMEOUT_MS);
- else
- this.startFade(this.controlBar, true);
+ setFullscreenButtonState() {
+ if (this.isAudioOnly || !document.mozFullScreenEnabled) {
+ this.controlBar.setAttribute("fullscreen-unavailable", true);
+ this.adjustControlSize();
+ return;
+ }
+ this.controlBar.removeAttribute("fullscreen-unavailable");
+ this.adjustControlSize();
- // 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);
- }
- },
-
- onMouseInOut : function(event) {
- // If the controls are static, don't change anything.
- if (!this.dynamicControls)
- return;
-
- clearTimeout(this._hideControlsTimeout);
+ var attrName = this.isVideoInFullScreen() ? "exitfullscreenlabel" : "enterfullscreenlabel";
+ var value = this.fullscreenButton.getAttribute(attrName);
+ this.fullscreenButton.setAttribute("aria-label", value);
- // 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.isEventWithin(event, this.videocontrols))
- return;
-
- var isMouseOver = (event.type == "mouseover");
+ if (this.isVideoInFullScreen()) {
+ this.fullscreenButton.setAttribute("fullscreened", "true");
+ } else {
+ this.fullscreenButton.removeAttribute("fullscreened");
+ }
+ },
- var controlRect = this.controlBar.getBoundingClientRect();
- var isMouseInControls = event.clientY > controlRect.top &&
- event.clientY < controlRect.bottom &&
- event.clientX > controlRect.left &&
- event.clientX < controlRect.right;
-
- // 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 && !isMouseOver &&
- !(this.video.autoplay && this.video.mozAutoplayEnabled))
- return;
+ onFullscreenChange() {
+ if (this.isVideoInFullScreen()) {
+ Utils._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
+ }
+ this.setFullscreenButtonState();
+ },
- if (!isMouseOver && !isMouseInControls) {
- this.adjustControlSize();
-
- // Keep the controls visible if the click-to-play is visible.
- if (!this.clickToPlay.hidden)
- return;
-
- this.startFadeOut(this.controlBar, false);
- this.textTrackList.setAttribute("hidden", "true");
- clearTimeout(this._showControlsTimeout);
- Utils._controlsHiddenByTimeout = false;
- }
- },
+ clickToPlayClickHandler(e) {
+ if (e.button != 0) {
+ return;
+ }
+ if (this.hasError() && !this.suppressError) {
+ // Errors that can be dismissed should be placed here as we discover them.
+ if (this.video.error.code != this.video.error.MEDIA_ERR_ABORTED) {
+ return;
+ }
+ this.statusOverlay.hidden = true;
+ this.suppressError = true;
+ return;
+ }
+ if (e.defaultPrevented) {
+ return;
+ }
+ if (this.playButton.hasAttribute("paused")) {
+ this.startPlay();
+ } else {
+ this.video.pause();
+ }
+ },
+ hideClickToPlay() {
+ let videoHeight = this.video.clientHeight;
+ let videoWidth = this.video.clientWidth;
- startFadeIn : function(element, immediate) {
- this.startFade(element, true, immediate);
- },
-
- startFadeOut : function(element, immediate) {
- this.startFade(element, false, immediate);
- },
-
- startFade : function(element, fadeIn, immediate) {
- if (element.classList.contains("controlBar") && fadeIn) {
- // 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.hidden)
- if (this.videocontrols.isTouchControls) {
- this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
- } else {
- this.scrubber.value = this.video.currentTime * 1000;
- }
- }
+ // The play button will animate to 3x its size. This
+ // shows the animation unless the video is too small
+ // to show 2/3 of the animation.
+ let animationScale = 2;
+ let animationMinSize = this.clickToPlay.minWidth * animationScale;
- if (immediate)
- element.setAttribute("immediate", true);
- else
- element.removeAttribute("immediate");
+ if (animationMinSize > videoWidth ||
+ animationMinSize > (videoHeight - this.controlBar.minHeight)) {
+ this.clickToPlay.setAttribute("immediate", "true");
+ this.clickToPlay.hidden = true;
+ } else {
+ this.clickToPlay.removeAttribute("immediate");
+ }
+ this.clickToPlay.setAttribute("fadeout", "true");
+ this.controlsSpacer.setAttribute("fadeout", "true");
+ },
- if (fadeIn && !(element.isAdjustableControl && element.hideByAdjustment)) {
- element.hidden = false;
- // force style resolution, so that transition begins
- // when we remove the attribute.
- element.clientTop;
- element.removeAttribute("fadeout");
- if (element.classList.contains("controlBar"))
- this.controlsSpacer.removeAttribute("hideCursor");
- } else {
- element.setAttribute("fadeout", true);
- if (element.classList.contains("controlBar") && !this.hasError() &&
- document.mozFullScreenElement == this.video)
- this.controlsSpacer.setAttribute("hideCursor", true);
+ setPlayButtonState(aPaused) {
+ if (aPaused) {
+ this.playButton.setAttribute("paused", "true");
+ } else {
+ this.playButton.removeAttribute("paused");
+ }
+
+ var attrName = aPaused ? "playlabel" : "pauselabel";
+ var value = this.playButton.getAttribute(attrName);
+ this.playButton.setAttribute("aria-label", value);
+ },
- }
- },
+ isEffectivelyMuted() {
+ return this.video.muted || !this.video.volume;
+ },
- onTransitionEnd : function(event) {
- // Ignore events for things other than opacity changes.
- if (event.propertyName != "opacity")
- return;
+ updateMuteButtonState() {
+ var muted = this.isEffectivelyMuted();
- var element = event.originalTarget;
+ if (muted) {
+ this.muteButton.setAttribute("muted", "true");
+ } else {
+ this.muteButton.removeAttribute("muted");
+ }
- // Nothing to do when a fade *in* finishes.
- if (!element.hasAttribute("fadeout"))
- return;
+ var attrName = muted ? "unmutelabel" : "mutelabel";
+ var value = this.muteButton.getAttribute(attrName);
+ this.muteButton.setAttribute("aria-label", value);
+ },
- if (this.videocontrols.isTouchControls) {
- this.scrubber.dragStateChanged(false);
- }
- element.hidden = true;
- },
+ _getComputedPropertyValueAsInt(element, property) {
+ let value = getComputedStyle(element, null).getPropertyValue(property);
+ return parseInt(value, 10);
+ },
- _triggeredByControls: false,
+ keyHandler(event) {
+ // Ignore keys when content might be providing its own.
+ if (!this.video.hasAttribute("controls")) {
+ return;
+ }
- startPlay : function() {
- this._triggeredByControls = true;
- this.hideClickToPlay();
- this.video.play();
- },
-
- togglePause : function() {
- if (this.video.paused || this.video.ended) {
- this.startPlay();
- } else {
- this.video.pause();
- }
-
- // We'll handle style changes in the event listener for
- // the "play" and "pause" events, same as if content
- // script was controlling video playback.
- },
-
- isVideoWithoutAudioTrack : function() {
- return this.video.readyState >= this.video.HAVE_METADATA &&
- !this.isAudioOnly &&
- !this.video.mozHasAudio;
- },
+ var keystroke = "";
+ if (event.altKey) {
+ keystroke += "alt-";
+ }
+ if (event.shiftKey) {
+ keystroke += "shift-";
+ }
+ if (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:
+ keystroke += "upArrow";
+ break;
+ case KeyEvent.DOM_VK_DOWN:
+ keystroke += "downArrow";
+ break;
+ case KeyEvent.DOM_VK_LEFT:
+ keystroke += "leftArrow";
+ break;
+ case KeyEvent.DOM_VK_RIGHT:
+ keystroke += "rightArrow";
+ break;
+ case KeyEvent.DOM_VK_HOME:
+ keystroke += "home";
+ break;
+ case KeyEvent.DOM_VK_END:
+ keystroke += "end";
+ break;
+ }
- toggleMute : function() {
- if (this.isVideoWithoutAudioTrack()) {
- return;
- }
- this.video.muted = !this.isEffectivelyMuted();
- if (this.video.volume === 0) {
- this.video.volume = 0.5;
- }
-
- // We'll handle style changes in the event listener for
- // the "volumechange" event, same as if content script was
- // controlling volume.
- },
-
- isVideoInFullScreen : function() {
- return document.mozFullScreenElement == this.video;
- },
+ if (String.fromCharCode(event.charCode) == ' ') {
+ keystroke += "space";
+ }
- toggleFullscreen : function() {
- this.isVideoInFullScreen() ?
- document.mozCancelFullScreen() :
- this.video.mozRequestFullScreen();
- },
-
- setFullscreenButtonState : function() {
- if (this.isAudioOnly || !document.mozFullScreenEnabled) {
- this.controlBar.setAttribute("fullscreen-unavailable", true);
- this.adjustControlSize();
- return;
- }
- this.controlBar.removeAttribute("fullscreen-unavailable");
- this.adjustControlSize();
-
- var attrName = this.isVideoInFullScreen() ? "exitfullscreenlabel" : "enterfullscreenlabel";
- var value = this.fullscreenButton.getAttribute(attrName);
- this.fullscreenButton.setAttribute("aria-label", value);
+ this.log("Got keystroke: " + keystroke);
+ var oldval, newval;
- if (this.isVideoInFullScreen())
- this.fullscreenButton.setAttribute("fullscreened", "true");
- else
- this.fullscreenButton.removeAttribute("fullscreened");
- },
-
- onFullscreenChange: function() {
- if (this.isVideoInFullScreen()) {
- Utils._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
- }
- this.setFullscreenButtonState();
- },
+ try {
+ switch (keystroke) {
+ case "space": /* Play */
+ this.togglePause();
+ break;
+ case "downArrow": /* Volume decrease */
+ oldval = this.video.volume;
+ this.video.volume = (oldval < 0.1 ? 0 : oldval - 0.1);
+ this.video.muted = false;
+ break;
+ case "upArrow": /* Volume increase */
+ oldval = this.video.volume;
+ this.video.volume = (oldval > 0.9 ? 1 : oldval + 0.1);
+ this.video.muted = false;
+ break;
+ case "accel-downArrow": /* Mute */
+ this.video.muted = true;
+ break;
+ case "accel-upArrow": /* Unmute */
+ this.video.muted = false;
+ break;
+ case "leftArrow": /* Seek back 15 seconds */
+ case "accel-leftArrow": /* Seek back 10% */
+ oldval = this.video.currentTime;
+ if (keystroke == "leftArrow") {
+ newval = oldval - 15;
+ } else {
+ newval = oldval - (this.video.duration || this.maxCurrentTimeSeen / 1000) / 10;
+ }
+ this.video.currentTime = (newval >= 0 ? newval : 0);
+ break;
+ case "rightArrow": /* Seek forward 15 seconds */
+ case "accel-rightArrow": /* Seek forward 10% */
+ oldval = this.video.currentTime;
+ var maxtime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
+ if (keystroke == "rightArrow") {
+ newval = oldval + 15;
+ } else {
+ newval = oldval + maxtime / 10;
+ }
+ this.video.currentTime = (newval <= maxtime ? newval : maxtime);
+ break;
+ case "home": /* Seek to beginning */
+ this.video.currentTime = 0;
+ break;
+ case "end": /* Seek to end */
+ if (this.video.currentTime != this.video.duration) {
+ this.video.currentTime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
+ }
+ break;
+ default:
+ return;
+ }
+ } catch (e) { /* ignore any exception from setting .currentTime */ }
- clickToPlayClickHandler : function(e) {
- if (e.button != 0)
- return;
- if (this.hasError() && !this.suppressError) {
- // Errors that can be dismissed should be placed here as we discover them.
- if (this.video.error.code != this.video.error.MEDIA_ERR_ABORTED)
- return;
- this.statusOverlay.hidden = true;
- this.suppressError = true;
- return;
- }
- if (e.defaultPrevented)
- return;
- if (this.playButton.hasAttribute("paused")) {
- this.startPlay();
- } else {
- this.video.pause();
- }
- },
- hideClickToPlay : function() {
- let videoHeight = this.video.clientHeight;
- let videoWidth = this.video.clientWidth;
-
- // The play button will animate to 3x its size. This
- // shows the animation unless the video is too small
- // to show 2/3 of the animation.
- let animationScale = 2;
- let animationMinSize = this.clickToPlay.minWidth * animationScale;
-
- if (animationMinSize > videoWidth ||
- animationMinSize > (videoHeight - this.controlBar.minHeight)) {
- this.clickToPlay.setAttribute("immediate", "true");
- this.clickToPlay.hidden = true;
- } else {
- this.clickToPlay.removeAttribute("immediate");
- }
- this.clickToPlay.setAttribute("fadeout", "true");
- this.controlsSpacer.setAttribute("fadeout", "true");
- },
-
- setPlayButtonState : function(aPaused) {
- if (aPaused)
- this.playButton.setAttribute("paused", "true");
- else
- this.playButton.removeAttribute("paused");
-
- var attrName = aPaused ? "playlabel" : "pauselabel";
- var value = this.playButton.getAttribute(attrName);
- this.playButton.setAttribute("aria-label", value);
- },
+ event.preventDefault(); // Prevent page scrolling
+ },
- isEffectivelyMuted : function() {
- return this.video.muted || !this.video.volume;
- },
+ isSupportedTextTrack(textTrack) {
+ return textTrack.kind == "subtitles" ||
+ textTrack.kind == "captions";
+ },
- updateMuteButtonState : function() {
- var muted = this.isEffectivelyMuted();
-
- if (muted)
- this.muteButton.setAttribute("muted", "true");
- else
- this.muteButton.removeAttribute("muted");
+ get isClosedCaptionAvailable() {
+ return this.overlayableTextTracks.length && !this.videocontrols.isTouchControls;
+ },
- var attrName = muted ? "unmutelabel" : "mutelabel";
- var value = this.muteButton.getAttribute(attrName);
- this.muteButton.setAttribute("aria-label", value);
- },
+ get overlayableTextTracks() {
+ return Array.prototype.filter.call(this.video.textTracks, this.isSupportedTextTrack);
+ },
- _getComputedPropertyValueAsInt : function(element, property) {
- let value = getComputedStyle(element, null).getPropertyValue(property);
- return parseInt(value, 10);
- },
+ isClosedCaptionOn() {
+ for (let tt of this.overlayableTextTracks) {
+ if (tt.mode === "showing") {
+ return true;
+ }
+ }
- keyHandler : function(event) {
- // Ignore keys when content might be providing its own.
- if (!this.video.hasAttribute("controls"))
- return;
+ return false;
+ },
- var keystroke = "";
- if (event.altKey)
- keystroke += "alt-";
- if (event.shiftKey)
- keystroke += "shift-";
- if (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:
- keystroke += "upArrow";
- break;
- case KeyEvent.DOM_VK_DOWN:
- keystroke += "downArrow";
- break;
- case KeyEvent.DOM_VK_LEFT:
- keystroke += "leftArrow";
- break;
- case KeyEvent.DOM_VK_RIGHT:
- keystroke += "rightArrow";
- break;
- case KeyEvent.DOM_VK_HOME:
- keystroke += "home";
- break;
- case KeyEvent.DOM_VK_END:
- keystroke += "end";
- break;
- }
+ setClosedCaptionButtonState() {
+ if (!this.isClosedCaptionAvailable) {
+ this.closedCaptionButton.setAttribute("hidden", "true");
+ return;
+ }
+
+ this.closedCaptionButton.removeAttribute("hidden");
+
+ if (this.isClosedCaptionOn()) {
+ this.closedCaptionButton.setAttribute("enabled", "true");
+ } else {
+ this.closedCaptionButton.removeAttribute("enabled");
+ }
+ this.adjustControlSize();
+
+ let ttItems = this.textTrackList.childNodes;
+
+ for (let tti of ttItems) {
+ const idx = +tti.getAttribute("index");
+
+ if (idx == this.currentTextTrackIndex) {
+ tti.setAttribute("on", "true");
+ } else {
+ tti.removeAttribute("on");
+ }
+ }
+ },
+
+ addNewTextTrack(tt) {
+ if (!this.isSupportedTextTrack(tt)) {
+ return;
+ }
- if (String.fromCharCode(event.charCode) == ' ')
- keystroke += "space";
+ if (tt.index && tt.index < this.textTracksCount) {
+ return;
+ }
+
+ tt.index = this.textTracksCount++;
+
+ const label = tt.label || "";
+ const ttText = document.createTextNode(label);
+ const ttBtn = document.createElement("button");
+
+ ttBtn.classList.add("textTrackItem");
+ ttBtn.setAttribute("index", tt.index);
- this.log("Got keystroke: " + keystroke);
- var oldval, newval;
+ ttBtn.addEventListener("click", function(event) {
+ event.stopPropagation();
+
+ this.changeTextTrack(tt.index);
+ }.bind(this));
+
+ ttBtn.appendChild(ttText);
+
+ this.textTrackList.appendChild(ttBtn);
+
+ if (tt.mode === "showing" && tt.index) {
+ this.changeTextTrack(tt.index);
+ }
+ },
- try {
- switch (keystroke) {
- case "space": /* Play */
- this.togglePause();
- break;
- case "downArrow": /* Volume decrease */
- oldval = this.video.volume;
- this.video.volume = (oldval < 0.1 ? 0 : oldval - 0.1);
- this.video.muted = false;
- break;
- case "upArrow": /* Volume increase */
- oldval = this.video.volume;
- this.video.volume = (oldval > 0.9 ? 1 : oldval + 0.1);
- this.video.muted = false;
- break;
- case "accel-downArrow": /* Mute */
- this.video.muted = true;
- break;
- case "accel-upArrow": /* Unmute */
- this.video.muted = false;
- break;
- case "leftArrow": /* Seek back 15 seconds */
- case "accel-leftArrow": /* Seek back 10% */
- oldval = this.video.currentTime;
- if (keystroke == "leftArrow")
- newval = oldval - 15;
- else
- newval = oldval - (this.video.duration || this.maxCurrentTimeSeen / 1000) / 10;
- this.video.currentTime = (newval >= 0 ? newval : 0);
- break;
- case "rightArrow": /* Seek forward 15 seconds */
- case "accel-rightArrow": /* Seek forward 10% */
- oldval = this.video.currentTime;
- var maxtime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
- if (keystroke == "rightArrow")
- newval = oldval + 15;
- else
- newval = oldval + maxtime / 10;
- this.video.currentTime = (newval <= maxtime ? newval : maxtime);
- break;
- case "home": /* Seek to beginning */
- this.video.currentTime = 0;
- break;
- case "end": /* Seek to end */
- if (this.video.currentTime != this.video.duration)
- this.video.currentTime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
- break;
- default:
- return;
- }
- } catch (e) { /* ignore any exception from setting .currentTime */ }
+ changeTextTrack(index) {
+ for (let tt of this.overlayableTextTracks) {
+ if (tt.index === index) {
+ tt.mode = "showing";
+
+ this.currentTextTrackIndex = tt.index;
+ } else {
+ tt.mode = "disabled";
+ }
+ }
+
+ // should fallback to off
+ if (this.currentTextTrackIndex !== index) {
+ this.currentTextTrackIndex = 0;
+ }
- event.preventDefault(); // Prevent page scrolling
- },
+ this.textTrackList.setAttribute("hidden", "true");
+ this.setClosedCaptionButtonState();
+ },
- isSupportedTextTrack : function(textTrack) {
- return textTrack.kind == "subtitles" ||
- textTrack.kind == "captions";
- },
+ onControlBarTransitioned() {
+ this.textTrackList.setAttribute("hidden", "true");
+ this.video.dispatchEvent(new CustomEvent("controlbarchange"));
+ },
+
+ toggleClosedCaption() {
+ if (this.textTrackList.hasAttribute("hidden")) {
+ this.textTrackList.removeAttribute("hidden");
+ } else {
+ this.textTrackList.setAttribute("hidden", "true");
+ }
+ },
- get isClosedCaptionAvailable() {
- return this.overlayableTextTracks.length && !this.videocontrols.isTouchControls;
- },
+ onTextTrackAdd(trackEvent) {
+ this.addNewTextTrack(trackEvent.track);
+ this.setClosedCaptionButtonState();
+ },
- get overlayableTextTracks() {
- return Array.prototype.filter.call(this.video.textTracks, this.isSupportedTextTrack);
- },
+ onTextTrackRemove(trackEvent) {
+ const toRemoveIndex = trackEvent.track.index;
+ const ttItems = this.textTrackList.childNodes;
- isClosedCaptionOn : function() {
- for (let tt of this.overlayableTextTracks) {
- if (tt.mode === "showing") {
- return true;
- }
- }
+ if (!ttItems) {
+ return;
+ }
+
+ for (let tti of ttItems) {
+ const idx = +tti.getAttribute("index");
- return false;
- },
+ if (idx === toRemoveIndex) {
+ tti.remove();
+ this.textTracksCount--;
+ }
- setClosedCaptionButtonState : function() {
- if (!this.isClosedCaptionAvailable) {
- this.closedCaptionButton.setAttribute("hidden", "true");
- return;
- }
-
- this.closedCaptionButton.removeAttribute("hidden");
+ if (idx === this.currentTextTrackIndex) {
+ this.currentTextTrackIndex = 0;
- if (this.isClosedCaptionOn()) {
- this.closedCaptionButton.setAttribute("enabled", "true");
- } else {
- this.closedCaptionButton.removeAttribute("enabled");
- }
- this.adjustControlSize();
+ this.video.dispatchEvent(new CustomEvent("texttrackchange"));
+ }
+ }
- let ttItems = this.textTrackList.childNodes;
+ this.setClosedCaptionButtonState();
+ },
- for (let tti of ttItems) {
- const idx = +tti.getAttribute("index");
+ initTextTracks() {
+ if (!this.isClosedCaptionAvailable) {
+ this.closedCaptionButton.setAttribute("hidden", "true");
+ return;
+ }
- if (idx == this.currentTextTrackIndex) {
- tti.setAttribute("on", "true");
- } else {
- tti.removeAttribute("on");
- }
- }
- },
+ const offLabel = this.textTrackList.getAttribute("offlabel");
- addNewTextTrack : function(tt) {
- if (!this.isSupportedTextTrack(tt)) {
- return;
- }
+ this.addNewTextTrack({
+ label: offLabel,
+ kind: "subtitles"
+ });
- if (tt.index && tt.index < this.textTracksCount) {
- return;
- }
-
- tt.index = this.textTracksCount++;
+ for (let tt of this.overlayableTextTracks) {
+ this.addNewTextTrack(tt);
+ }
- const label = tt.label || "";
- const ttText = document.createTextNode(label);
- const ttBtn = document.createElement("button");
-
- ttBtn.classList.add("textTrackItem");
- ttBtn.setAttribute("index", tt.index);
+ this.setClosedCaptionButtonState();
+ },
- ttBtn.addEventListener("click", function(event) {
- event.stopPropagation();
-
- this.changeTextTrack(tt.index);
- }.bind(this));
-
- ttBtn.appendChild(ttText);
-
- this.textTrackList.appendChild(ttBtn);
-
- if (tt.mode === "showing" && tt.index) {
- this.changeTextTrack(tt.index);
- }
- },
+ isEventWithin(event, parent1, parent2) {
+ function isDescendant(node) {
+ while (node) {
+ if (node == parent1 || node == parent2) {
+ return true;
+ }
+ node = node.parentNode;
+ }
+ return false;
+ }
+ return isDescendant(event.target) && isDescendant(event.relatedTarget);
+ },
- changeTextTrack : function(index) {
- for (let tt of this.overlayableTextTracks) {
- if (tt.index === index) {
- tt.mode = "showing";
+ log(msg) {
+ if (this.debug) {
+ console.log("videoctl: " + msg + "\n");
+ }
+ },
- this.currentTextTrackIndex = tt.index;
- } else {
- tt.mode = "disabled";
- }
- }
-
- // should fallback to off
- if (this.currentTextTrackIndex !== index) {
- this.currentTextTrackIndex = 0;
- }
+ get isTopLevelSyntheticDocument() {
+ let doc = this.video.ownerDocument;
+ let win = doc.defaultView;
+ return doc.mozSyntheticDocument && win === win.top;
+ },
- this.textTrackList.setAttribute("hidden", "true");
- this.setClosedCaptionButtonState();
- },
+ adjustControlSize() {
+ if (!this.controlBar.minWidth || this.videocontrols.isTouchControls) {
+ return;
+ }
- onControlBarTransitioned : function() {
- this.textTrackList.setAttribute("hidden", "true");
- this.video.dispatchEvent(new CustomEvent("controlbarchange"));
- },
+ let videoWidth = this.video.clientWidth;
+ let videoHeight = this.video.clientHeight;
+ const minControlBarPaddingWidth = 18;
- toggleClosedCaption : function() {
- if (this.textTrackList.hasAttribute("hidden")) {
- this.textTrackList.removeAttribute("hidden");
- } else {
- this.textTrackList.setAttribute("hidden", "true");
- }
- },
+ // Hide and show control in order.
+ const prioritizedControls = [
+ this.playButton,
+ this.muteButton,
+ this.fullscreenButton,
+ this.closedCaptionButton,
+ this.positionDurationBox,
+ this.scrubberStack,
+ this.durationSpan,
+ this.volumeStack
+ ];
- onTextTrackAdd : function(trackEvent) {
- this.addNewTextTrack(trackEvent.track);
- this.setClosedCaptionButtonState();
- },
+ if (this.controlBar.hasAttribute("fullscreen-unavailable")) {
+ this.fullscreenButton.isWanted = false;
+ }
+ if (!this.isClosedCaptionAvailable) {
+ this.closedCaptionButton.isWanted = false;
+ }
+ if (this.muteButton.hasAttribute("noAudio")) {
+ this.volumeStack.isWanted = false;
+ }
- onTextTrackRemove : function(trackEvent) {
- const toRemoveIndex = trackEvent.track.index;
- const ttItems = this.textTrackList.childNodes;
-
- if (!ttItems) {
- return;
- }
-
- for (let tti of ttItems) {
- const idx = +tti.getAttribute("index");
+ let widthUsed = minControlBarPaddingWidth;
+ let preventAppendControl = false;
- if (idx === toRemoveIndex) {
- tti.remove();
- this.textTracksCount--;
- }
+ for (let control of prioritizedControls) {
+ if (!control.isWanted) {
+ control.hideByAdjustment = true;
+ continue;
+ }
- if (idx === this.currentTextTrackIndex) {
- this.currentTextTrackIndex = 0;
-
- this.video.dispatchEvent(new CustomEvent("texttrackchange"));
- }
- }
-
- this.setClosedCaptionButtonState();
- },
+ control.hideByAdjustment = preventAppendControl ||
+ widthUsed + control.minWidth > videoWidth;
- initTextTracks : function() {
- if (!this.isClosedCaptionAvailable) {
- this.closedCaptionButton.setAttribute("hidden", "true");
- return;
- }
-
- const offLabel = this.textTrackList.getAttribute("offlabel");
+ if (control.hideByAdjustment) {
+ preventAppendControl = true;
+ } else {
+ widthUsed += control.minWidth;
+ }
+ }
- this.addNewTextTrack({
- label: offLabel,
- kind: "subtitles"
- });
-
- for (let tt of this.overlayableTextTracks) {
- this.addNewTextTrack(tt);
- }
+ if (this.durationSpan.hideByAdjustment) {
+ this.positionDurationBox.setAttribute("positionOnly", "true");
+ } else {
+ this.positionDurationBox.removeAttribute("positionOnly");
+ }
- this.setClosedCaptionButtonState();
- },
+ if (videoHeight < this.controlBar.minHeight ||
+ widthUsed === minControlBarPaddingWidth) {
+ this.controlBar.setAttribute("size", "hidden");
+ this.controlBar.hideByAdjustment = true;
+ } else {
+ this.controlBar.removeAttribute("size");
+ this.controlBar.hideByAdjustment = false;
+ }
- isEventWithin : function(event, parent1, parent2) {
- function isDescendant(node) {
- while (node) {
- if (node == parent1 || node == parent2)
- return true;
- node = node.parentNode;
- }
- return false;
- }
- return isDescendant(event.target) && isDescendant(event.relatedTarget);
- },
+ // 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;
- log : function(msg) {
- if (this.debug)
- console.log("videoctl: " + msg + "\n");
- },
+ // Adjust clickToPlayButton size.
+ const minVideoSideLength = Math.min(videoWidth, videoHeight);
+ const clickToPlayViewRatio = 0.15;
+ const clickToPlayScaledSize = Math.max(
+ this.clickToPlay.minWidth, minVideoSideLength * clickToPlayViewRatio);
- get isTopLevelSyntheticDocument() {
- let doc = this.video.ownerDocument;
- let win = doc.defaultView;
- return doc.mozSyntheticDocument && win === win.top;
- },
-
- adjustControlSize : function adjustControlSize() {
- if (!this.controlBar.minWidth || this.videocontrols.isTouchControls) {
- return;
- }
-
- let videoWidth = this.video.clientWidth;
- let videoHeight = this.video.clientHeight;
- const minControlBarPaddingWidth = 18;
+ if (clickToPlayScaledSize >= videoWidth ||
+ (clickToPlayScaledSize + this.controlBar.minHeight / 2 >= videoHeight / 2 )) {
+ this.clickToPlay.hideByAdjustment = true;
+ } else {
+ if (this.clickToPlay.hidden && !this.video.played.length && this.video.paused) {
+ this.clickToPlay.hideByAdjustment = false;
+ }
+ this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
+ this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
+ }
+ },
- // Hide and show control in order.
- const prioritizedControls = [
- this.playButton,
- this.muteButton,
- this.fullscreenButton,
- this.closedCaptionButton,
- this.positionDurationBox,
- this.scrubberStack,
- this.durationSpan,
- this.volumeStack
- ];
+ init(binding) {
+ this.video = binding.parentNode;
+ this.videocontrols = binding;
- if (this.controlBar.hasAttribute("fullscreen-unavailable")) {
- this.fullscreenButton.isWanted = false;
- }
- if (!this.isClosedCaptionAvailable) {
- this.closedCaptionButton.isWanted = false;
- }
- if (this.muteButton.hasAttribute("noAudio")) {
- this.volumeStack.isWanted = false;
- }
-
- let widthUsed = minControlBarPaddingWidth;
- let preventAppendControl = false;
-
- for (let control of prioritizedControls) {
- if (!control.isWanted) {
- control.hideByAdjustment = true;
- continue;
- }
+ 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.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "anonid", "closedCaptionButton");
+ this.textTrackList = document.getAnonymousElementByAttribute(binding, "anonid", "textTrackList");
- control.hideByAdjustment = preventAppendControl ||
- widthUsed + control.minWidth > videoWidth;
+ if (this.positionDurationBox) {
+ this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
+ }
- if (control.hideByAdjustment) {
- preventAppendControl = true;
- } else {
- widthUsed += control.minWidth;
- }
- }
-
- if (this.durationSpan.hideByAdjustment) {
- this.positionDurationBox.setAttribute("positionOnly", "true");
- } else {
- this.positionDurationBox.removeAttribute("positionOnly");
- }
+ // XXX controlsContainer is a desktop only element. To determine whether
+ // isTouchControls or not during the whole initialization process, get
+ // this state overridden here.
+ this.videocontrols.isTouchControls = !this.controlsContainer;
+ this.isAudioOnly = (this.video instanceof HTMLAudioElement);
+ this.setupInitialState();
+ this.setupNewLoadState();
+ this.initTextTracks();
- if (videoHeight < this.controlBar.minHeight ||
- widthUsed === minControlBarPaddingWidth) {
- this.controlBar.setAttribute("size", "hidden");
- this.controlBar.hideByAdjustment = true;
- } else {
- this.controlBar.removeAttribute("size");
- this.controlBar.hideByAdjustment = false;
- }
+ // 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.
+ for (let event of this.videoEvents) {
+ this.video.addEventListener(event, this, {
+ capture: true,
+ mozSystemGroup: true
+ });
+ }
- // 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;
+ var self = this;
+ this.controlListeners = [];
- // Adjust clickToPlayButton size.
- const minVideoSideLength = Math.min(videoWidth, videoHeight);
- const clickToPlayViewRatio = 0.15;
- const clickToPlayScaledSize = Math.max(
- this.clickToPlay.minWidth, minVideoSideLength * clickToPlayViewRatio);
+ // Helper function to add an event listener to the given element
+ function addListener(elem, eventName, func) {
+ let boundFunc = func.bind(self);
+ self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
+ elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true });
+ }
- if (clickToPlayScaledSize >= videoWidth ||
- (clickToPlayScaledSize + this.controlBar.minHeight / 2 >= videoHeight / 2 )) {
+ addListener(this.muteButton, "click", this.toggleMute);
+ addListener(this.closedCaptionButton, "click", this.toggleClosedCaption);
+ addListener(this.fullscreenButton, "click", this.toggleFullscreen);
+ addListener(this.playButton, "click", this.clickToPlayClickHandler);
+ addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
+ addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
+ addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
- this.clickToPlay.hideByAdjustment = true;
- } else {
- if (this.clickToPlay.hidden && !this.video.played.length && this.video.paused) {
- this.clickToPlay.hideByAdjustment = false;
- }
- this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
- this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
- }
- },
-
- init : function(binding) {
- this.video = binding.parentNode;
- this.videocontrols = binding;
+ addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
+ addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
+ addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
+ addListener(this.controlBar, "transitionend", this.onControlBarTransitioned);
+ addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange);
+ addListener(this.video, "keypress", this.keyHandler);
- 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.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "anonid", "closedCaptionButton");
- this.textTrackList = document.getAnonymousElementByAttribute(binding, "anonid", "textTrackList");
+ addListener(this.videocontrols, "dragstart", function(event) {
+ event.preventDefault(); // prevent dragging of controls image (bug 517114)
+ });
- if (this.positionDurationBox) {
- this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
- }
+ if (!this.videocontrols.isTouchControls) {
+ addListener(this.scrubber, "input", this.onScrubberInput);
+ addListener(this.scrubber, "change", this.onScrubberChange);
+ addListener(this.volumeControl, "input", this.updateVolume);
+ addListener(this.video.textTracks, "addtrack", this.onTextTrackAdd);
+ addListener(this.video.textTracks, "removetrack", this.onTextTrackRemove);
+ }
- // XXX controlsContainer is a desktop only element. To determine whether
- // isTouchControls or not during the whole initialization process, get
- // this state overridden here.
- this.videocontrols.isTouchControls = !this.controlsContainer;
- this.isAudioOnly = (this.video instanceof HTMLAudioElement);
- this.setupInitialState();
- this.setupNewLoadState();
- this.initTextTracks();
+ this.log("--- videocontrols initialized ---");
+ }
+ };
- // 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.
- for (let event of this.videoEvents) {
- this.video.addEventListener(event, this, {
- capture: true,
- mozSystemGroup: true
- });
- }
-
- var self = this;
- this.controlListeners = [];
+ this.Utils.init(this);
+ ]]>
+ </constructor>
+ <destructor>
+ <![CDATA[
+ this.Utils.terminateEventListeners();
+ // 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>
- // Helper function to add an event listener to the given element
- function addListener(elem, eventName, func) {
- let boundFunc = func.bind(self);
- self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
- elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true });
- }
-
- addListener(this.muteButton, "click", this.toggleMute);
- addListener(this.closedCaptionButton, "click", this.toggleClosedCaption);
- addListener(this.fullscreenButton, "click", this.toggleFullscreen);
- addListener(this.playButton, "click", this.clickToPlayClickHandler);
- addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
- addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
- addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
+ </implementation>
- addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
- addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
- addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
- addListener(this.controlBar, "transitionend", this.onControlBarTransitioned);
- addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange);
- addListener(this.video, "keypress", this.keyHandler);
-
- addListener(this.videocontrols, "dragstart", function(event) {
- event.preventDefault(); // prevent dragging of controls image (bug 517114)
- });
-
- if (!this.videocontrols.isTouchControls) {
- addListener(this.scrubber, "input", this.onScrubberInput);
- addListener(this.scrubber, "change", this.onScrubberChange);
- addListener(this.volumeControl, "input", this.updateVolume);
- addListener(this.video.textTracks, "addtrack", this.onTextTrackAdd);
- addListener(this.video.textTracks, "removetrack", this.onTextTrackRemove);
- }
+ <handlers>
+ <handler event="mouseover">
+ if (!this.isTouchControls) {
+ this.Utils.onMouseInOut(event);
+ }
+ </handler>
+ <handler event="mouseout">
+ if (!this.isTouchControls) {
+ this.Utils.onMouseInOut(event);
+ }
+ </handler>
+ <handler event="mousemove">
+ if (!this.isTouchControls) {
+ this.Utils.onMouseMove(event);
+ }
+ </handler>
+ </handlers>
+</binding>
- this.log("--- videocontrols initialized ---");
- }
- };
+<binding id="touchControls" extends="chrome://global/content/bindings/videocontrols.xml#videoControls">
- this.Utils.init(this);
- ]]>
- </constructor>
- <destructor>
- <![CDATA[
- this.Utils.terminateEventListeners();
- // 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>
+ <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
+ <stack flex="1">
+ <vbox anonid="statusOverlay" flex="1" class="statusOverlay" hidden="true">
+ <box anonid="statusIcon" class="statusIcon"/>
+ <label class="errorLabel" anonid="errorAborted">&error.aborted;</label>
+ <label class="errorLabel" anonid="errorNetwork">&error.network;</label>
+ <label class="errorLabel" anonid="errorDecode">&error.decode;</label>
+ <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label>
+ <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
+ <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
+ </vbox>
- <handlers>
- <handler event="mouseover">
- if (!this.isTouchControls)
- this.Utils.onMouseInOut(event);
- </handler>
- <handler event="mouseout">
- if (!this.isTouchControls)
- this.Utils.onMouseInOut(event);
- </handler>
- <handler event="mousemove">
- if (!this.isTouchControls)
- this.Utils.onMouseMove(event);
- </handler>
- </handlers>
- </binding>
+ <vbox anonid="controlsOverlay" class="controlsOverlay">
+ <spacer anonid="controlsSpacer" class="controlsSpacer" flex="1"/>
+ <box flex="1" hidden="true">
+ <box anonid="clickToPlay" class="clickToPlay" hidden="true" flex="1"/>
+ <vbox anonid="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox>
+ </box>
+ <vbox anonid="controlBar" class="controlBar" hidden="true">
+ <hbox class="buttonsBar">
+ <button anonid="playButton"
+ class="playButton"
+ playlabel="&playButton.playLabel;"
+ pauselabel="&playButton.pauseLabel;"/>
+ <label anonid="positionLabel" class="positionLabel" role="presentation"/>
+ <stack anonid="scrubberStack" class="scrubberStack">
+ <box class="backgroundBar"/>
+ <progressmeter class="flexibleBar" value="100"/>
+ <progressmeter anonid="bufferBar" class="bufferBar"/>
+ <progressmeter anonid="progressBar" class="progressBar" max="10000"/>
+ <scale anonid="scrubber" class="scrubber" movetoclick="true"/>
+ </stack>
+ <label anonid="durationLabel" class="durationLabel" role="presentation"/>
+ <button anonid="muteButton"
+ class="muteButton"
+ mutelabel="&muteButton.muteLabel;"
+ unmutelabel="&muteButton.unmuteLabel;"/>
+ <stack anonid="volumeStack" class="volumeStack">
+ <box anonid="volumeBackground" class="volumeBackground"/>
+ <box anonid="volumeForeground" class="volumeForeground"/>
+ <scale anonid="volumeControl" class="volumeControl" movetoclick="true"/>
+ </stack>
+ <button anonid="castingButton" class="castingButton" hidden="true"
+ aria-label="&castingButton.castingLabel;"/>
+ <button anonid="closedCaptionButton" class="closedCaptionButton" hidden="true"/>
+ <button anonid="fullscreenButton"
+ class="fullscreenButton"
+ enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
+ exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </stack>
+ </xbl:content>
- <binding id="touchControls" extends="chrome://global/content/bindings/videocontrols.xml#videoControls">
+ <implementation>
+ <constructor>
+ <![CDATA[
+ this.isTouchControls = true;
+ this.TouchUtils = {
+ videocontrols: null,
+ video: null,
+ controlsTimer: null,
+ controlsTimeout: 5000,
+ positionLabel: null,
+ castingButton: null,
- <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
- <stack flex="1">
- <vbox anonid="statusOverlay" flex="1" class="statusOverlay" hidden="true">
- <box anonid="statusIcon" class="statusIcon"/>
- <label class="errorLabel" anonid="errorAborted">&error.aborted;</label>
- <label class="errorLabel" anonid="errorNetwork">&error.network;</label>
- <label class="errorLabel" anonid="errorDecode">&error.decode;</label>
- <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label>
- <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
- <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
- </vbox>
+ get Utils() {
+ return this.videocontrols.Utils;
+ },
+
+ get visible() {
+ return !this.Utils.controlBar.hasAttribute("fadeout") &&
+ !(this.Utils.controlBar.getAttribute("hidden") == "true");
+ },
+
+ _firstShow: false,
+ get firstShow() { return this._firstShow; },
+ set firstShow(val) {
+ this._firstShow = val;
+ this.Utils.controlBar.setAttribute("firstshow", val);
+ },
- <vbox anonid="controlsOverlay" class="controlsOverlay">
- <spacer anonid="controlsSpacer" class="controlsSpacer" flex="1"/>
- <box flex="1" hidden="true">
- <box anonid="clickToPlay" class="clickToPlay" hidden="true" flex="1"/>
- <vbox anonid="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox>
- </box>
- <vbox anonid="controlBar" class="controlBar" hidden="true">
- <hbox class="buttonsBar">
- <button anonid="playButton"
- class="playButton"
- playlabel="&playButton.playLabel;"
- pauselabel="&playButton.pauseLabel;"/>
- <label anonid="positionLabel" class="positionLabel" role="presentation"/>
- <stack anonid="scrubberStack" class="scrubberStack">
- <box class="backgroundBar"/>
- <progressmeter class="flexibleBar" value="100"/>
- <progressmeter anonid="bufferBar" class="bufferBar"/>
- <progressmeter anonid="progressBar" class="progressBar" max="10000"/>
- <scale anonid="scrubber" class="scrubber" movetoclick="true"/>
- </stack>
- <label anonid="durationLabel" class="durationLabel" role="presentation"/>
- <button anonid="muteButton"
- class="muteButton"
- mutelabel="&muteButton.muteLabel;"
- unmutelabel="&muteButton.unmuteLabel;"/>
- <stack anonid="volumeStack" class="volumeStack">
- <box anonid="volumeBackground" class="volumeBackground"/>
- <box anonid="volumeForeground" class="volumeForeground"/>
- <scale anonid="volumeControl" class="volumeControl" movetoclick="true"/>
- </stack>
- <button anonid="castingButton" class="castingButton" hidden="true"
- aria-label="&castingButton.castingLabel;"/>
- <button anonid="closedCaptionButton" class="closedCaptionButton" hidden="true"/>
- <button anonid="fullscreenButton"
- class="fullscreenButton"
- enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
- exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
- </hbox>
- </vbox>
- </vbox>
- </stack>
- </xbl:content>
+ toggleControls() {
+ if (!this.Utils.dynamicControls || !this.visible) {
+ this.showControls();
+ } else {
+ this.delayHideControls(0);
+ }
+ },
+
+ showControls() {
+ if (this.Utils.dynamicControls) {
+ this.Utils.startFadeIn(this.Utils.controlBar);
+ this.delayHideControls(this.controlsTimeout);
+ }
+ },
+
+ clearTimer() {
+ if (this.controlsTimer) {
+ clearTimeout(this.controlsTimer);
+ this.controlsTimer = null;
+ }
+ },
+
+ delayHideControls(aTimeout) {
+ this.clearTimer();
+ let self = this;
+ this.controlsTimer = setTimeout(function() {
+ self.hideControls();
+ }, aTimeout);
+ },
- <implementation>
- <constructor>
- <![CDATA[
- this.isTouchControls = true;
- this.TouchUtils = {
- videocontrols: null,
- video: null,
- controlsTimer: null,
- controlsTimeout: 5000,
- positionLabel: null,
- castingButton: null,
+ hideControls() {
+ if (!this.Utils.dynamicControls) {
+ return;
+ }
+ this.Utils.startFadeOut(this.Utils.controlBar);
+ if (this.firstShow) {
+ this.videocontrols.addEventListener("transitionend", this, false);
+ }
+ },
- get Utils() {
- return this.videocontrols.Utils;
- },
+ handleEvent(aEvent) {
+ if (aEvent.type == "transitionend") {
+ this.firstShow = false;
+ this.videocontrols.removeEventListener("transitionend", this, false);
+ return;
+ }
- get visible() {
- return !this.Utils.controlBar.hasAttribute("fadeout") &&
- !(this.Utils.controlBar.getAttribute("hidden") == "true");
- },
-
- _firstShow: false,
- get firstShow() { return this._firstShow; },
- set firstShow(val) {
- this._firstShow = val;
- this.Utils.controlBar.setAttribute("firstshow", val);
- },
+ if (this.videocontrols.randomID != this.Utils.randomID) {
+ this.terminateEventListeners();
+ }
+ },
- toggleControls: function() {
- if (!this.Utils.dynamicControls || !this.visible)
- this.showControls();
- else
- this.delayHideControls(0);
- },
+ terminateEventListeners() {
+ for (var event of this.videoEvents) {
+ this.Utils.video.removeEventListener(event, this, false);
+ }
+ },
- showControls : function() {
- if (this.Utils.dynamicControls) {
- this.Utils.startFadeIn(this.Utils.controlBar);
- this.delayHideControls(this.controlsTimeout);
- }
- },
+ isVideoCasting() {
+ return this.video.mozIsCasting;
+ },
- clearTimer: function() {
- if (this.controlsTimer) {
- clearTimeout(this.controlsTimer);
- this.controlsTimer = null;
- }
- },
+ updateCasting(eventDetail) {
+ let castingData = JSON.parse(eventDetail);
+ if ("allow" in castingData) {
+ this.video.mozAllowCasting = !!castingData.allow;
+ }
- delayHideControls : function(aTimeout) {
- this.clearTimer();
- let self = this;
- this.controlsTimer = setTimeout(function() {
- self.hideControls();
- }, aTimeout);
- },
-
- hideControls : function() {
- if (!this.Utils.dynamicControls)
- return;
- this.Utils.startFadeOut(this.Utils.controlBar);
- if (this.firstShow)
- this.videocontrols.addEventListener("transitionend", this, false);
- },
+ if ("active" in castingData) {
+ this.video.mozIsCasting = !!castingData.active;
+ }
+ this.setCastButtonState();
+ },
- handleEvent : function(aEvent) {
- if (aEvent.type == "transitionend") {
- this.firstShow = false;
- this.videocontrols.removeEventListener("transitionend", this, false);
- return;
- }
+ startCasting() {
+ this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
+ },
- if (this.videocontrols.randomID != this.Utils.randomID)
- this.terminateEventListeners();
-
- },
+ setCastButtonState() {
+ if (this.isAudioOnly || !this.video.mozAllowCasting) {
+ this.castingButton.hidden = true;
+ return;
+ }
- terminateEventListeners : function() {
- for (var event of this.videoEvents)
- this.Utils.video.removeEventListener(event, this, false);
- },
+ if (this.video.mozIsCasting) {
+ this.castingButton.setAttribute("active", "true");
+ } else {
+ this.castingButton.removeAttribute("active");
+ }
- isVideoCasting : function() {
- if (this.video.mozIsCasting)
- return true;
- return false;
- },
+ this.castingButton.hidden = false;
+ },
- updateCasting : function(eventDetail) {
- let castingData = JSON.parse(eventDetail);
- if ("allow" in castingData) {
- this.video.mozAllowCasting = !!castingData.allow;
- }
+ init(binding) {
+ this.videocontrols = binding;
+ this.video = binding.parentNode;
- if ("active" in castingData) {
- this.video.mozIsCasting = !!castingData.active;
- }
- this.setCastButtonState();
- },
-
- startCasting : function() {
- this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
- },
+ let self = this;
+ this.Utils.playButton.addEventListener("command", function() {
+ if (!self.video.paused) {
+ self.delayHideControls(0);
+ } else {
+ self.showControls();
+ }
+ }, false);
+ this.Utils.scrubber.addEventListener("touchstart", function() {
+ self.clearTimer();
+ }, false);
+ this.Utils.scrubber.addEventListener("touchend", function() {
+ self.delayHideControls(self.controlsTimeout);
+ }, false);
+ this.Utils.muteButton.addEventListener("click", function() { self.delayHideControls(self.controlsTimeout); }, false);
- setCastButtonState : function() {
- if (this.isAudioOnly || !this.video.mozAllowCasting) {
- this.castingButton.hidden = true;
- return;
- }
+ this.castingButton = document.getAnonymousElementByAttribute(binding, "anonid", "castingButton");
+ this.castingButton.addEventListener("command", function() {
+ self.startCasting();
+ }, false);
- if (this.video.mozIsCasting) {
- this.castingButton.setAttribute("active", "true");
- } else {
- this.castingButton.removeAttribute("active");
- }
-
- this.castingButton.hidden = false;
- },
-
- init : function(binding) {
- this.videocontrols = binding;
- this.video = binding.parentNode;
+ this.video.addEventListener("media-videoCasting", function(e) {
+ if (!e.isTrusted) {
+ return;
+ }
+ self.updateCasting(e.detail);
+ }, false, true);
- let self = this;
- this.Utils.playButton.addEventListener("command", function() {
- if (!self.video.paused)
- self.delayHideControls(0);
- else
- self.showControls();
- }, false);
- this.Utils.scrubber.addEventListener("touchstart", function() {
- self.clearTimer();
- }, false);
- this.Utils.scrubber.addEventListener("touchend", function() {
- self.delayHideControls(self.controlsTimeout);
- }, false);
- this.Utils.muteButton.addEventListener("click", function() { self.delayHideControls(self.controlsTimeout); }, false);
+ // The first time the controls appear we want to just display
+ // a play button that does not fade away. The firstShow property
+ // makes that happen. But because of bug 718107 this init() method
+ // may be called again when we switch in or out of fullscreen
+ // mode. So we only set firstShow if we're not autoplaying and
+ // if we are at the beginning of the video and not already playing
+ if (!this.video.autoplay && this.Utils.dynamicControls && this.video.paused &&
+ this.video.currentTime === 0) {
+ this.firstShow = true;
+ }
- this.castingButton = document.getAnonymousElementByAttribute(binding, "anonid", "castingButton");
- this.castingButton.addEventListener("command", function() {
- self.startCasting();
- }, false);
-
- this.video.addEventListener("media-videoCasting", function(e) {
- if (!e.isTrusted)
- return;
- self.updateCasting(e.detail);
- }, false, true);
-
- // The first time the controls appear we want to just display
- // a play button that does not fade away. The firstShow property
- // makes that happen. But because of bug 718107 this init() method
- // may be called again when we switch in or out of fullscreen
- // mode. So we only set firstShow if we're not autoplaying and
- // if we are at the beginning of the video and not already playing
- if (!this.video.autoplay && this.Utils.dynamicControls && this.video.paused &&
- this.video.currentTime === 0)
- this.firstShow = true;
+ // If the video is not at the start, then we probably just
+ // transitioned into or out of fullscreen mode, and we don't want
+ // 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);
+ }
+ }
+ };
- // If the video is not at the start, then we probably just
- // transitioned into or out of fullscreen mode, and we don't want
- // 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.TouchUtils.init(this)
+ this.dispatchEvent(new CustomEvent("VideoBindingAttached"));
+ ]]>
+ </constructor>
+ <destructor>
+ <![CDATA[
+ // XBL destructors don't appear to be inherited properly, so we need
+ // to do this here in addition to the videoControls destructor. :-(
+ delete this.randomID;
+ ]]>
+ </destructor>
+
+ </implementation>
- this.TouchUtils.init(this)
- this.dispatchEvent(new CustomEvent("VideoBindingAttached"));
- ]]>
- </constructor>
- <destructor>
- <![CDATA[
- // XBL destructors don't appear to be inherited properly, so we need
- // to do this here in addition to the videoControls destructor. :-(
- delete this.randomID;
- ]]>
- </destructor>
+ <handlers>
+ <handler event="mouseup">
+ if (event.originalTarget.nodeName == "vbox") {
+ if (this.TouchUtils.firstShow) {
+ this.Utils.video.play();
+ }
+ this.TouchUtils.toggleControls();
+ }
+ </handler>
+ </handlers>
+
+</binding>
- </implementation>
+<binding id="touchControlsGonk" extends="chrome://global/content/bindings/videocontrols.xml#touchControls">
+ <implementation>
+ <constructor>
+ this.isGonk = true;
+ </constructor>
+ </implementation>
+</binding>
- <handlers>
- <handler event="mouseup">
- if (event.originalTarget.nodeName == "vbox") {
- if (this.TouchUtils.firstShow)
- this.Utils.video.play();
- this.TouchUtils.toggleControls();
- }
- </handler>
- </handlers>
+<binding id="noControls">
- </binding>
+ <resources>
+ <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
+ <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
+ </resources>
- <binding id="touchControlsGonk" extends="chrome://global/content/bindings/videocontrols.xml#touchControls">
- <implementation>
- <constructor>
- this.isGonk = true;
- </constructor>
- </implementation>
- </binding>
-
- <binding id="noControls">
+ <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
+ <vbox flex="1" class="statusOverlay" hidden="true">
+ <box flex="1">
+ <box class="clickToPlay" flex="1"/>
+ </box>
+ </vbox>
+ </xbl:content>
- <resources>
- <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
- <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
- </resources>
-
- <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
- <vbox flex="1" class="statusOverlay" hidden="true">
- <box flex="1">
- <box class="clickToPlay" flex="1"/>
- </box>
- </vbox>
- </xbl:content>
+ <implementation>
+ <constructor>
+ <![CDATA[
+ this.randomID = 0;
+ this.Utils = {
+ randomID : 0,
+ videoEvents : ["play",
+ "playing"],
+ controlListeners: [],
+ terminateEventListeners() {
+ for (let event of this.videoEvents) {
+ this.video.removeEventListener(event, this, { mozSystemGroup: true });
+ }
- <implementation>
- <constructor>
- <![CDATA[
- this.randomID = 0;
- this.Utils = {
- randomID : 0,
- videoEvents : ["play",
- "playing"],
- controlListeners: [],
- terminateEventListeners : function() {
- for (let event of this.videoEvents)
- this.video.removeEventListener(event, this, { mozSystemGroup: true });
+ for (let element of this.controlListeners) {
+ element.item.removeEventListener(element.event, element.func,
+ { mozSystemGroup: true });
+ }
- for (let element of this.controlListeners) {
- element.item.removeEventListener(element.event, element.func,
- { mozSystemGroup: true });
- }
+ delete this.controlListeners;
+ },
- delete this.controlListeners;
- },
+ hasError() {
+ return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE);
+ },
- hasError : function() {
- return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE);
- },
+ handleEvent(aEvent) {
+ // If the binding is detached (or has been replaced by a
+ // newer instance of the binding), nuke our event-listeners.
+ if (this.binding.randomID != this.randomID) {
+ this.terminateEventListeners();
+ return;
+ }
- handleEvent : function(aEvent) {
- // If the binding is detached (or has been replaced by a
- // newer instance of the binding), nuke our event-listeners.
- if (this.binding.randomID != this.randomID) {
- this.terminateEventListeners();
- return;
- }
-
- switch (aEvent.type) {
- case "play":
- this.noControlsOverlay.hidden = true;
- break;
- case "playing":
- this.noControlsOverlay.hidden = true;
- break;
- }
- },
+ switch (aEvent.type) {
+ case "play":
+ this.noControlsOverlay.hidden = true;
+ break;
+ case "playing":
+ this.noControlsOverlay.hidden = true;
+ break;
+ }
+ },
- blockedVideoHandler : function() {
- if (this.binding.randomID != this.randomID) {
- this.terminateEventListeners();
- return;
- } else if (this.hasError()) {
- this.noControlsOverlay.hidden = true;
- return;
- }
- this.noControlsOverlay.hidden = false;
- },
+ blockedVideoHandler() {
+ if (this.binding.randomID != this.randomID) {
+ this.terminateEventListeners();
+ return;
+ } else if (this.hasError()) {
+ this.noControlsOverlay.hidden = true;
+ return;
+ }
+ this.noControlsOverlay.hidden = false;
+ },
- clickToPlayClickHandler : function(e) {
- if (this.binding.randomID != this.randomID) {
- this.terminateEventListeners();
- return;
- } else if (e.button != 0) {
- return;
- }
+ clickToPlayClickHandler(e) {
+ if (this.binding.randomID != this.randomID) {
+ this.terminateEventListeners();
+ return;
+ } else if (e.button != 0) {
+ return;
+ }
- this.noControlsOverlay.hidden = true;
- this.video.play();
- },
+ this.noControlsOverlay.hidden = true;
+ this.video.play();
+ },
- init : function(binding) {
- this.binding = binding;
- this.randomID = Math.random();
- this.binding.randomID = this.randomID;
- this.video = binding.parentNode;
- this.clickToPlay = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
- this.noControlsOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
+ init(binding) {
+ this.binding = binding;
+ this.randomID = Math.random();
+ this.binding.randomID = this.randomID;
+ this.video = binding.parentNode;
+ this.clickToPlay = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
+ this.noControlsOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
- let self = this;
- function addListener(elem, eventName, func) {
- let boundFunc = func.bind(self);
- self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
- elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true });
- }
- addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
- addListener(this.video, "MozNoControlsBlockedVideo", this.blockedVideoHandler);
+ let self = this;
+ function addListener(elem, eventName, func) {
+ let boundFunc = func.bind(self);
+ self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
+ elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true });
+ }
+ addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
+ addListener(this.video, "MozNoControlsBlockedVideo", this.blockedVideoHandler);
- for (let event of this.videoEvents) {
- this.video.addEventListener(event, this, { mozSystemGroup: true });
- }
+ for (let event of this.videoEvents) {
+ this.video.addEventListener(event, this, { mozSystemGroup: true });
+ }
- if (this.video.autoplay && !this.video.mozAutoplayEnabled) {
- this.blockedVideoHandler();
- }
- }
- };
- this.Utils.init(this);
- this.Utils.video.dispatchEvent(new CustomEvent("MozNoControlsVideoBindingAttached"));
- ]]>
- </constructor>
- <destructor>
- <![CDATA[
- this.Utils.terminateEventListeners();
- // 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>
+ if (this.video.autoplay && !this.video.mozAutoplayEnabled) {
+ this.blockedVideoHandler();
+ }
+ }
+ };
+ this.Utils.init(this);
+ this.Utils.video.dispatchEvent(new CustomEvent("MozNoControlsVideoBindingAttached"));
+ ]]>
+ </constructor>
+ <destructor>
+ <![CDATA[
+ this.Utils.terminateEventListeners();
+ // 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>