gecko-dev/toolkit/content/widgets/videocontrols.xml
Jared Wein b5f521fe19 Bug 1074744 - Apply the margin-end to the volume controls when fullscreen is unavailable. r=gijs r=dolske
The adjusted margin-end needs to be applied to the volume controls when the fullscreen button is hidden due to fullscreen being unavailable, such as within an iframe that is lacking the allowfullscreen attribute. Previously the margin-end was only applied when the video was determined to be audio-only.
2015-01-09 14:29:36 -05:00

1803 lines
86 KiB
XML

<?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;
]>
<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">
<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.
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>
<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;
}
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.wasPausedBeforeDrag = true;
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.
var percent = newValue / this.max;
this.valueBar.value = Math.round(percent * 10000); // has max=10000
}
// 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.
var 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 (isDragging) {
this.wasPausedBeforeDrag = this.Utils.video.paused;
this.previousPlaybackRate = this.Utils.video.playbackRate;
this.Utils.video.pause();
} else if (!this.wasPausedBeforeDrag) {
// After the drag ends, resume playing.
this.Utils.video.playbackRate = this.previousPlaybackRate;
this.Utils.video.play();
}
}
]]>
</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="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
class="mediaControlsFrame">
<stack flex="1">
<vbox flex="1" class="statusOverlay" hidden="true">
<box 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>
<vbox class="statsOverlay" hidden="true">
<html:div class="statsDiv" xmlns="http://www.w3.org/1999/xhtml">
<table class="statsTable">
<tr>
<td class="statLabel">&stats.media;</td>
<td class="statValue filename"><span class="statFilename"/></td>
</tr>
<tr>
<td class="statLabel">&stats.size;</td>
<td class="statValue size"><span class="statSize"/></td>
</tr>
<tr style="height: 1em;"/>
<tr>
<td class="statLabel">&stats.activity;</td>
<td class="statValue activity">
<span class="statActivity">
<span class="statActivityPaused">&stats.activityPaused;</span>
<span class="statActivityPlaying">&stats.activityPlaying;</span>
<span class="statActivityEnded">&stats.activityEnded;</span>
<span class="statActivitySeeking">&stats.activitySeeking;</span>
</span>
</td>
</tr>
<tr>
<td class="statLabel">&stats.volume;</td> <td class="statValue"><span class="statVolume"/></td>
</tr>
<tr>
<!-- Localization note: readyState is a HTML5 API MediaElement-specific attribute and should not be localized. -->
<td class="statLabel">readyState</td> <td class="statValue"><span class="statReadyState"/></td>
</tr>
<tr>
<!-- Localization note: networkState is a HTML5 API MediaElement-specific attribute and should not be localized. -->
<td class="statLabel">networkState</td> <td class="statValue"><span class="statNetState"/></td>
</tr>
<tr style="height: 1em;"/>
<tr>
<td class="statLabel">&stats.framesParsed;</td>
<td class="statValue"><span class="statFramesParsed"/></td>
</tr>
<tr>
<td class="statLabel">&stats.framesDecoded;</td>
<td class="statValue"><span class="statFramesDecoded"/></td>
</tr>
<tr>
<td class="statLabel">&stats.framesPresented;</td>
<td class="statValue"><span class="statFramesPresented"/></td>
</tr>
<tr>
<td class="statLabel">&stats.framesPainted;</td>
<td class="statValue"><span class="statFramesPainted"/></td>
</tr>
</table>
</html:div>
</vbox>
<vbox class="controlsOverlay">
<stack flex="1">
<spacer class="controlsSpacer" flex="1"/>
<box class="clickToPlay" hidden="true" flex="1"/>
</stack>
<hbox class="controlBar" hidden="true">
<button class="playButton"
playlabel="&playButton.playLabel;"
pauselabel="&playButton.pauseLabel;"/>
<stack class="scrubberStack" flex="1">
<box class="backgroundBar"/>
<progressmeter class="bufferBar"/>
<progressmeter class="progressBar" max="10000"/>
<scale class="scrubber" movetoclick="true"/>
</stack>
<vbox class="durationBox">
<label class="positionLabel" role="presentation"/>
<label class="durationLabel" role="presentation"/>
</vbox>
<button class="muteButton"
mutelabel="&muteButton.muteLabel;"
unmutelabel="&muteButton.unmuteLabel;"/>
<stack class="volumeStack">
<box class="volumeBackground"/>
<box class="volumeForeground" anonid="volumeForeground"/>
<scale class="volumeControl" movetoclick="true"/>
</stack>
<button class="fullscreenButton"
enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
</hbox>
</vbox>
</stack>
</xbl:content>
<implementation>
<constructor>
<![CDATA[
this.isTouchControl = false;
this.randomID = 0;
this.Utils = {
debug : false,
video : null,
videocontrols : null,
controlBar : null,
playButton : null,
muteButton : null,
volumeControl : null,
durationLabel : null,
positionLabel : null,
scrubberThumb : null,
scrubber : null,
progressBar : null,
bufferBar : null,
statusOverlay : null,
controlsSpacer : null,
clickToPlay : null,
stats : {},
controlsOverlay : null,
fullscreenButton : null,
randomID : 0,
videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
"loadstart", "timeupdate", "progress",
"playing", "waiting", "canplay", "canplaythrough",
"seeking", "seeked", "emptied", "loadedmetadata",
"error", "suspend", "stalled",
"mozinterruptbegin", "mozinterruptend" ],
firstFrameShown : false,
timeUpdateCount : 0,
maxCurrentTimeSeen : 0,
_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._controlBarHeight + "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_ENOUGH_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);
},
/*
* 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.updateMuteButtonState();
this.setFullscreenButtonState();
var volume = this.video.muted ? 0 : Math.round(this.video.volume * 100);
this.volumeControl.value = volume;
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.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;
// 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.setAttribute("max", 100);
if (this.video.readyState >= this.video.HAVE_METADATA)
this.showBuffered();
else
this.bufferBar.setAttribute("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;
this._playButtonWidth = this.playButton.clientWidth;
this._durationLabelWidth = this.durationLabel.clientWidth;
this._muteButtonWidth = this.muteButton.clientWidth;
this._volumeControlWidth = this.volumeControl.clientWidth;
this._fullscreenButtonWidth = this.fullscreenButton.clientWidth;
this._controlBarHeight = this.controlBar.clientHeight;
this.controlBar.hidden = true;
this.adjustControlSize();
// Preserve Statistics when toggling fullscreen mode due to bug 714071.
if (this.video.mozMediaStatisticsShowing)
this.showStatistics(true);
this._handleCustomEventsBound = this.handleCustomEvents.bind(this);
this.video.addEventListener("media-showStatistics", this._handleCustomEventsBound, false, true);
},
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.
this.startFade(this.clickToPlay, shouldShow && !this.isAudioOnly &&
this.video.currentTime == 0 && !this.hasError(), true);
this.startFade(this.controlBar, shouldShow, true);
},
handleCustomEvents : function (e) {
if (!e.isTrusted)
return;
this.showStatistics(e.detail);
},
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;
},
handleEvent : function (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;
}
switch (aEvent.type) {
case "play":
this.setPlayButtonState(false);
this.setupStatusFader();
if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControl)
this.startFadeOut(this.controlBar);
if (!this._triggeredByControls)
this.clickToPlay.hidden = 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":
var volume = this.video.muted ? 0 : this.video.volume;
var volumePercentage = Math.round(volume * 100);
this.updateMuteButtonState();
this.volumeControl.value = volumePercentage;
this.volumeForeground.style.paddingRight = (1 - volume) * this._volumeControlWidth + "px";
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
// 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);
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();
// 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)
return;
this.showPosition(currentTime, duration);
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!");
}
},
terminateEventListeners : function () {
if (this.statsInterval) {
clearInterval(this.statsInterval);
this.statsInterval = null;
}
for each (let event in this.videoEvents)
this.video.removeEventListener(event, this, false);
for each(let element in this.controlListeners)
element.item.removeEventListener(element.event, element.func, false);
delete this.controlListeners;
this.video.removeEventListener("media-showStatistics", this._handleCustomEventsBound, false);
delete this._handleCustomEventsBound;
this.log("--- videocontrols terminated ---");
},
hasError : function () {
return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE);
},
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.
}
let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
this.controlsSpacer.setAttribute("aria-label", label.textContent);
this.statusOverlay.setAttribute("error", error);
},
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;
},
showDuration : function (duration) {
let isInfinite = (duration == Infinity);
this.log("Duration is " + duration + "ms.\n");
if (isNaN(duration) || isInfinite)
duration = this.maxCurrentTimeSeen;
// Format the duration as "h:mm:ss" or "m:ss"
let timeString = isInfinite ? "" : this.formatTime(duration);
this.durationLabel.setAttribute("value", 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.scrubberThumb.showHours = (duration >= 3600000);
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.increment = duration / 50;
this.scrubber.pageIncrement = Math.round(duration / 10);
},
seekToPosition : function(newPosition) {
newPosition /= 1000; // convert from ms
this.log("+++ seeking to " + newPosition);
#ifdef MOZ_WIDGET_GONK
// 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;
#endif
},
setVolume : function(newVolume) {
this.log("*** setting volume to " + newVolume);
this.video.volume = newVolume;
this.video.muted = false;
},
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);
this.log("time update @ " + currentTime + "ms of " + duration + "ms");
this.positionLabel.setAttribute("value", this.formatTime(currentTime));
this.scrubber.value = currentTime;
},
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;
}
function bufferedCompare(buffered, i, time) {
if (time > buffered.end(i)) {
return 1;
} else if (time >= buffered.start(i)) {
return 0;
}
return -1;
}
var duration = Math.round(this.video.duration * 1000);
if (isNaN(duration))
duration = this.maxCurrentTimeSeen;
// 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 : function () {
if (Utils.video.matches("video:hover")) {
Utils.startFadeIn(Utils.controlBar, false);
Utils._showControlsTimeout = 0;
Utils._controlsHiddenByTimeout = false;
}
},
_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) {
// 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);
// 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);
// 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");
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;
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);
clearTimeout(this._showControlsTimeout);
Utils._controlsHiddenByTimeout = false;
}
},
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)
this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
}
if (immediate)
element.setAttribute("immediate", true);
else
element.removeAttribute("immediate");
if (fadeIn) {
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);
}
},
onTransitionEnd : function (event) {
// Ignore events for things other than opacity changes.
if (event.propertyName != "opacity")
return;
var element = event.originalTarget;
// Nothing to do when a fade *in* finishes.
if (!element.hasAttribute("fadeout"))
return;
element.hidden = true;
},
_triggeredByControls: false,
togglePause : function () {
if (this.video.paused || this.video.ended) {
this._triggeredByControls = true;
this.hideClickToPlay();
this.video.playbackRate = this.video.defaultPlaybackRate;
this.video.play();
} 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;
},
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;
},
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);
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();
},
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;
}
// Read defaultPrevented asynchronously, since Web content
// may want to consume the "click" event but will only
// receive it after us.
let self = this;
setTimeout(function clickToPlayCallback() {
if (!e.defaultPrevented)
self.togglePause();
}, 0);
},
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;
if (this._overlayPlayButtonHeight * animationScale > (videoHeight - this._controlBarHeight)||
this._overlayPlayButtonWidth * animationScale > videoWidth) {
this.clickToPlay.setAttribute("immediate", "true");
this.clickToPlay.hidden = true;
} else {
this.clickToPlay.removeAttribute("immediate");
}
this.clickToPlay.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);
},
isEffectivelyMuted : function() {
return this.video.muted || !this.video.volume;
},
updateMuteButtonState : function() {
var muted = this.isEffectivelyMuted();
if (muted)
this.muteButton.setAttribute("muted", "true");
else
this.muteButton.removeAttribute("muted");
var attrName = muted ? "unmutelabel" : "mutelabel";
var value = this.muteButton.getAttribute(attrName);
this.muteButton.setAttribute("aria-label", value);
},
_getComputedPropertyValueAsInt : function(element, property) {
let value = window.getComputedStyle(element, null).getPropertyValue(property);
return parseInt(value, 10);
},
STATS_INTERVAL_MS : 500,
statsInterval : null,
showStatistics : function(shouldShow) {
if (this.statsInterval) {
clearInterval(this.statsInterval);
this.statsInterval = null;
}
if (shouldShow) {
this.video.mozMediaStatisticsShowing = true;
this.statsOverlay.hidden = false;
this.statsInterval = setInterval(this.updateStats.bind(this), this.STATS_INTERVAL_MS);
this.updateStats();
} else {
this.video.mozMediaStatisticsShowing = false;
this.statsOverlay.hidden = true;
}
},
updateStats : function() {
if (this.videocontrols.randomID != this.randomID) {
this.terminateEventListeners();
return;
}
let v = this.video;
let s = this.stats;
let src = v.currentSrc || v.src || "(no source found)";
let srcParts = src.split('/');
let srcIdx = srcParts.length - 1;
if (src.lastIndexOf('/') == src.length - 1)
srcIdx--;
s.filename.textContent = decodeURI(srcParts[srcIdx]);
let size = v.videoWidth + "x" + v.videoHeight;
if (this._getComputedPropertyValueAsInt(this.video, "width") != v.videoWidth || this._getComputedPropertyValueAsInt(this.video, "height") != v.videoHeight)
size += " scaled to " + this._getComputedPropertyValueAsInt(this.video, "width") + "x" + this._getComputedPropertyValueAsInt(this.video, "height");
s.size.textContent = size;
let activity;
if (v.paused)
activity = "paused";
else
activity = "playing";
if (v.ended)
activity = "ended";
if (s.activity.getAttribute("activity") != activity)
s.activity.setAttribute("activity", activity);
if (v.seeking && !s.activity.hasAttribute("seeking"))
s.activity.setAttribute("seeking", true);
else if (s.activity.hasAttribute("seeking"))
s.activity.removeAttribute("seeking");
let readyState = v.readyState;
switch (readyState) {
case v.HAVE_NOTHING: readyState = "HAVE_NOTHING"; break;
case v.HAVE_METADATA: readyState = "HAVE_METADATA"; break;
case v.HAVE_CURRENT_DATA: readyState = "HAVE_CURRENT_DATA"; break;
case v.HAVE_FUTURE_DATA: readyState = "HAVE_FUTURE_DATA"; break;
case v.HAVE_ENOUGH_DATA: readyState = "HAVE_ENOUGH_DATA"; break;
}
s.readyState.textContent = readyState;
let networkState = v.networkState;
switch (networkState) {
case v.NETWORK_EMPTY: networkState = "NETWORK_EMPTY"; break;
case v.NETWORK_IDLE: networkState = "NETWORK_IDLE"; break;
case v.NETWORK_LOADING: networkState = "NETWORK_LOADING"; break;
case v.NETWORK_NO_SOURCE: networkState = "NETWORK_NO_SOURCE"; break;
}
s.netState.textContent = networkState;
s.framesParsed.textContent = v.mozParsedFrames;
s.framesDecoded.textContent = v.mozDecodedFrames;
s.framesPresented.textContent = v.mozPresentedFrames;
s.framesPainted.textContent = v.mozPaintedFrames;
let volume = Math.round(v.volume * 100) + "%";
if (v.muted)
volume += " (muted)";
s.volume.textContent = volume;
},
keyHandler : function(event) {
// Ignore keys when content might be providing its own.
if (!this.video.hasAttribute("controls"))
return;
var keystroke = "";
if (event.altKey)
keystroke += "alt-";
if (event.shiftKey)
keystroke += "shift-";
#ifdef XP_MACOSX
if (event.metaKey)
keystroke += "accel-";
if (event.ctrlKey)
keystroke += "control-";
#else
if (event.metaKey)
keystroke += "meta-";
if (event.ctrlKey)
keystroke += "accel-";
#endif
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;
}
if (String.fromCharCode(event.charCode) == ' ')
keystroke += "space";
this.log("Got keystroke: " + keystroke);
var oldval, newval;
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 */ }
event.preventDefault(); // Prevent page scrolling
},
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);
},
log : function (msg) {
if (this.debug)
dump("videoctl: " + msg + "\n");
},
get isTopLevelSyntheticDocument() {
let doc = this.video.ownerDocument;
let win = doc.defaultView;
return doc.mozSyntheticDocument && win === win.top;
},
_playButtonWidth : 0,
_durationLabelWidth : 0,
_muteButtonWidth : 0,
_volumeControlWidth : 0,
_fullscreenButtonWidth : 0,
_controlBarHeight : 0,
_overlayPlayButtonHeight : 64,
_overlayPlayButtonWidth : 64,
_volumeStackMarginEnd : 8,
adjustControlSize : function adjustControlSize() {
let doc = this.video.ownerDocument;
// The scrubber has |flex=1|, therefore |minScrubberWidth|
// was generated by empirical testing.
let minScrubberWidth = 25;
let minWidthAllControls = this._playButtonWidth +
minScrubberWidth +
this._durationLabelWidth +
this._muteButtonWidth +
this._volumeControlWidth +
this._fullscreenButtonWidth;
let isFullscreenUnavailable = this.controlBar.hasAttribute("fullscreen-unavailable");
if (isFullscreenUnavailable) {
// When the fullscreen button is hidden we add margin-end to the volume stack.
minWidthAllControls -= this._fullscreenButtonWidth - this._volumeStackMarginEnd;
}
let minHeightForControlBar = this._controlBarHeight;
let minWidthOnlyPlayPause = this._playButtonWidth + this._muteButtonWidth;
let isAudioOnly = this.isAudioOnly;
let videoHeight = isAudioOnly ? minHeightForControlBar : this.video.clientHeight;
let videoWidth = isAudioOnly ? minWidthAllControls : this.video.clientWidth;
// Adapt the size of the controls to the size of the video
if (this.video.readyState >= this.video.HAVE_METADATA) {
if (!this.isAudioOnly && this.video.videoWidth && this.video.videoHeight) {
var rect = this.video.getBoundingClientRect();
var widthRatio = rect.width / this.video.videoWidth;
var heightRatio = rect.height / this.video.videoHeight;
var width = this.video.videoWidth * Math.min(widthRatio, heightRatio);
this.controlsOverlay.setAttribute("scaled", true);
this.controlsOverlay.style.width = width + "px";
this.controlsSpacer.style.width = width + "px";
this.controlBar.style.width = width + "px";
} else {
this.controlsOverlay.removeAttribute("scaled");
this.controlsOverlay.style.width = "";
this.controlsSpacer.style.width = "";
this.controlBar.style.width = "";
}
}
if ((this._overlayPlayButtonHeight + this._controlBarHeight) > videoHeight ||
this._overlayPlayButtonWidth > videoWidth) {
this.clickToPlay.hidden = true;
} else if (this.clickToPlay.hidden &&
!this.video.played.length &&
this.video.paused) {
// Check this.video.paused to handle when a video is
// playing but hasn't processed any frames yet
this.clickToPlay.hidden = false;
}
let size = "normal";
if (videoHeight < minHeightForControlBar)
size = "hidden";
else if (videoWidth < minWidthOnlyPlayPause)
size = "hidden";
else if (videoWidth < minWidthAllControls)
size = "small";
this.controlBar.setAttribute("size", size);
},
init : function (binding) {
this.video = binding.parentNode;
this.videocontrols = binding;
this.statusIcon = document.getAnonymousElementByAttribute(binding, "class", "statusIcon");
this.controlBar = document.getAnonymousElementByAttribute(binding, "class", "controlBar");
this.playButton = document.getAnonymousElementByAttribute(binding, "class", "playButton");
this.muteButton = document.getAnonymousElementByAttribute(binding, "class", "muteButton");
this.volumeControl = document.getAnonymousElementByAttribute(binding, "class", "volumeControl");
this.progressBar = document.getAnonymousElementByAttribute(binding, "class", "progressBar");
this.bufferBar = document.getAnonymousElementByAttribute(binding, "class", "bufferBar");
this.scrubber = document.getAnonymousElementByAttribute(binding, "class", "scrubber");
this.scrubberThumb = document.getAnonymousElementByAttribute(this.scrubber, "class", "scale-thumb");
this.durationLabel = document.getAnonymousElementByAttribute(binding, "class", "durationLabel");
this.positionLabel = document.getAnonymousElementByAttribute(binding, "class", "positionLabel");
this.statusOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
this.statsOverlay = document.getAnonymousElementByAttribute(binding, "class", "statsOverlay");
this.controlsOverlay = document.getAnonymousElementByAttribute(binding, "class", "controlsOverlay");
this.controlsSpacer = document.getAnonymousElementByAttribute(binding, "class", "controlsSpacer");
this.clickToPlay = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
this.fullscreenButton = document.getAnonymousElementByAttribute(binding, "class", "fullscreenButton");
this.volumeForeground = document.getAnonymousElementByAttribute(binding, "anonid", "volumeForeground");
this.statsTable = document.getAnonymousElementByAttribute(binding, "class", "statsTable");
this.stats.filename = document.getAnonymousElementByAttribute(binding, "class", "statFilename");
this.stats.size = document.getAnonymousElementByAttribute(binding, "class", "statSize");
this.stats.activity = document.getAnonymousElementByAttribute(binding, "class", "statActivity");
this.stats.volume = document.getAnonymousElementByAttribute(binding, "class", "statVolume");
this.stats.readyState = document.getAnonymousElementByAttribute(binding, "class", "statReadyState");
this.stats.netState = document.getAnonymousElementByAttribute(binding, "class", "statNetState");
this.stats.framesParsed = document.getAnonymousElementByAttribute(binding, "class", "statFramesParsed");
this.stats.framesDecoded = document.getAnonymousElementByAttribute(binding, "class", "statFramesDecoded");
this.stats.framesPresented = document.getAnonymousElementByAttribute(binding, "class", "statFramesPresented");
this.stats.framesPainted = document.getAnonymousElementByAttribute(binding, "class", "statFramesPainted");
this.isAudioOnly = (this.video instanceof HTMLAudioElement);
this.setupInitialState();
this.setupNewLoadState();
// Use the handleEvent() callback for all media events.
// The "error" event listener must capture, so that it can trap error events
// from the <source> children, which don't bubble.
for each (let event in this.videoEvents)
this.video.addEventListener(event, this, (event == "error") ? true : false);
var self = this;
this.controlListeners = [];
// 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, false);
}
addListener(this.muteButton, "command", this.toggleMute);
addListener(this.playButton, "command", this.togglePause);
addListener(this.fullscreenButton, "command", this.toggleFullscreen);
addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
addListener(this.video, "keypress", this.keyHandler);
addListener(this.videocontrols, "dragstart", function(event) {
event.preventDefault(); //prevent dragging of controls image (bug 517114)
});
this.log("--- videocontrols initialized ---");
}
};
this.Utils.init(this);
]]>
</constructor>
<destructor>
<![CDATA[
// randomID used to be a <field>, which meant that the XBL machinery
// undefined the property when the element was unbound. The code in
// this file actually depends on this, so now that randomID is an
// expando, we need to make sure to explicitly delete it.
delete this.randomID;
]]>
</destructor>
</implementation>
<handlers>
<handler event="mouseover">
if (!this.isTouchControl)
this.Utils.onMouseInOut(event);
</handler>
<handler event="mouseout">
if (!this.isTouchControl)
this.Utils.onMouseInOut(event);
</handler>
<handler event="mousemove">
if (!this.isTouchControl)
this.Utils.onMouseMove(event);
</handler>
</handlers>
</binding>
<binding id="touchControls" extends="chrome://global/content/bindings/videocontrols.xml#videoControls">
<xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
<stack flex="1">
<vbox flex="1" class="statusOverlay" hidden="true">
<box 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>
<vbox class="controlsOverlay">
<spacer class="controlsSpacer" flex="1"/>
<box flex="1" hidden="true">
<box class="clickToPlay" hidden="true" flex="1"/>
</box>
<vbox class="controlBar" hidden="true">
<hbox class="buttonsBar">
<button class="castingButton" hidden="true"
aria-label="&castingButton.castingLabel;"/>
<button class="fullscreenButton"
enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
<spacer flex="1"/>
<button class="playButton"
playlabel="&playButton.playLabel;"
pauselabel="&playButton.pauseLabel;"/>
<spacer flex="1"/>
<button class="muteButton"
mutelabel="&muteButton.muteLabel;"
unmutelabel="&muteButton.unmuteLabel;"/>
<stack class="volumeStack">
<box class="volumeBackground"/>
<box class="volumeForeground" anonid="volumeForeground"/>
<scale class="volumeControl" movetoclick="true"/>
</stack>
</hbox>
<stack class="scrubberStack" flex="1">
<box class="backgroundBar"/>
<progressmeter class="bufferBar"/>
<progressmeter class="progressBar" max="10000"/>
<scale class="scrubber" movetoclick="true"/>
</stack>
<vbox class="durationBox">
<label class="positionLabel" role="presentation"/>
<label class="durationLabel" role="presentation"/>
</vbox>
</vbox>
</vbox>
</stack>
</xbl:content>
<implementation>
<constructor>
<![CDATA[
this.isTouchControl = true;
this.TouchUtils = {
videocontrols: null,
video: null,
controlsTimer: null,
controlsTimeout: 5000,
positionLabel: null,
castingButton: null,
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);
},
toggleControls: function() {
if (!this.Utils.dynamicControls || !this.visible)
this.showControls();
else
this.delayHideControls(0);
},
showControls : function() {
if (this.Utils.dynamicControls) {
this.Utils.startFadeIn(this.Utils.controlBar);
this.delayHideControls(this.controlsTimeout);
}
},
clearTimer: function() {
if (this.controlsTimer) {
clearTimeout(this.controlsTimer);
this.controlsTimer = null;
}
},
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);
},
handleEvent : function (aEvent) {
if (aEvent.type == "transitionend") {
this.firstShow = false;
this.videocontrols.removeEventListener("transitionend", this, false);
return;
}
if (this.videocontrols.randomID != this.Utils.randomID)
this.terminateEventListeners();
},
terminateEventListeners : function () {
for each (var event in this.videoEvents)
this.Utils.video.removeEventListener(event, this, false);
},
isVideoCasting : function () {
if (this.video.mozIsCasting)
return true;
return false;
},
updateCasting : function (eventDetail) {
let castingData = JSON.parse(eventDetail);
if ("allow" in castingData) {
this.video.mozAllowCasting = !!castingData.allow;
}
if ("active" in castingData) {
this.video.mozIsCasting = !!castingData.active;
}
this.setCastButtonState();
},
startCasting : function () {
this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
},
setCastButtonState : function () {
if (this.isAudioOnly || !this.video.mozAllowCasting) {
this.castingButton.hidden = true;
return;
}
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;
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);
this.castingButton = document.getAnonymousElementByAttribute(binding, "class", "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);
}
}
};
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>
<handlers>
<handler event="mouseup">
if(event.originalTarget.nodeName == "vbox") {
if (this.TouchUtils.firstShow)
this.Utils.video.play();
this.TouchUtils.toggleControls();
}
</handler>
</handlers>
</binding>
</bindings>