gecko/toolkit/content/widgets/videocontrols.xml
Frank Yan 5f4fd2b363 Bug 485696 - Video progress slider jitters on indefinite length streams. r=dolske a=blocking2.0final+
--HG--
extra : rebase_source : a2549d0ef1a853e71ef2f9765026dd9150736d41
2011-01-11 19:32:17 -08:00

1120 lines
50 KiB
XML

<?xml version="1.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">
<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>
<field name="timeLabel">null</field>
<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;
var hours = Math.floor(time / 3600000);
var mins = Math.floor(time % 3600000 / 60000);
var secs = Math.floor(time % 60000 / 1000);
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>
<!-- Public -->
<field name="scrubberNameFormat">"&scrubberScale.nameFormat;"</field>
<field name="durationValue">""</field>
<field name="thumb">null</field>
<field name="valueBar">null</field>
<field name="isDragging">false</field>
<field name="wasPausedBeforeDrag">true</field>
<field name="type">null</field>
<field name="Utils">null</field>
<constructor>
<![CDATA[
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);
// 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.Utils.video.pause();
} else if (!this.wasPausedBeforeDrag) {
// After the drag ends, resume playing.
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"/>
</vbox>
<vbox class="controlsOverlay">
<spacer class="controlsSpacer" flex="1"/>
<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" hidden="true" fadeout="true">
<box class="volumeBackgroundBar"/>
<scale class="volumeControl" orient="vertical" dir="reverse" movetoclick="true"/>
</stack>
</hbox>
</vbox>
</stack>
</xbl:content>
<implementation implements="nsISecurityCheckedComponent">
<!-- nsISecurityCheckedComponent -->
<method name="canCreateWrapper">
<parameter name="aIID"/>
<body>
return "AllAccess";
</body>
</method>
<method name="canCallMethod">
<parameter name="aIID"/>
<parameter name="aMethodName"/>
<body>
return "AllAccess";
</body>
</method>
<method name="canGetProperty">
<parameter name="aIID"/>
<parameter name="aPropertyName"/>
<body>
return "AllAccess";
</body>
</method>
<method name="canSetProperty">
<parameter name="aIID"/>
<parameter name="aPropertyName"/>
<body>
return "AllAccess";
</body>
</method>
<method name="QueryInterface">
<parameter name="aIID"/>
<body>
<![CDATA[
if (!iid.equals(Components.interfaces.nsISecurityCheckedComponent))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
]]>
</body>
</method>
<constructor>
<![CDATA[
this.Utils.init(this);
]]>
</constructor>
<field name="randomID">0</field>
<field name="Utils">
<![CDATA[ ({
debug : false,
video : null,
videocontrols : null,
controlBar : null,
playButton : null,
muteButton : null,
volumeStack : null,
volumeControl : null,
durationLabel : null,
positionLabel : null,
scrubberThumb : null,
scrubber : null,
progressBar : null,
bufferBar : null,
statusOverlay : null,
randomID : 0,
videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
"loadstart", "timeupdate", "progress",
"playing", "waiting", "canplay", "canplaythrough",
"seeking", "seeked", "emptied", "loadedmetadata",
"error", "suspend"],
firstFrameShown : false,
timeUpdateCount : 0,
maxCurrentTimeSeen : 0,
lastDurationSeen : NaN,
isAudioOnly : false,
setupStatusFader : function(immediate) {
var show = false;
if (this.video.seeking ||
this.video.error ||
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;
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.setMuteButtonState(this.video.muted);
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.
if (this.video.readyState >= this.video.HAVE_METADATA) {
if (this.video instanceof HTMLVideoElement &&
(this.video.videoWidth == 0 || this.videoHeight == 0))
this.isAudioOnly = 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.video.error || this.video.networkState == this.video.NETWORK_NO_SOURCE) {
this.statusIcon.setAttribute("type", "error");
this.setupStatusFader(true);
} else {
this.statusIcon.setAttribute("type", "throbber");
this.setupStatusFader();
}
},
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.video.error || this.video.networkState == this.video.NETWORK_NO_SOURCE))
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();
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.setupStatusFader();
break;
case "volumechange":
var volume = this.video.muted ? 0 : Math.round(this.video.volume * 100);
this.setMuteButtonState(this.video.muted);
this.volumeControl.value = volume;
break;
case "loadedmetadata":
// If a <video> doesn't have any video data, treat it as <audio>
// and show the controls (they won't fade back out)
if (this.video instanceof HTMLVideoElement &&
(this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
this.isAudioOnly = true;
this.startFadeIn(this.controlBar);
}
this.showDuration(Math.round(this.video.duration * 1000));
break;
case "loadeddata":
this.firstFrameShown = true;
this.setupStatusFader();
break;
case "loadstart":
this.maxCurrentTimeSeen = 0;
this.statusIcon.setAttribute("type", "throbber");
this.isAudioOnly = (this.video instanceof HTMLAudioElement);
this.setPlayButtonState(true);
break;
case "progress":
this.showBuffered();
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;
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.video.error || this.video.networkState == this.video.NETWORK_NO_SOURCE) {
this.statusIcon.setAttribute("type", "error");
this.setupStatusFader(true);
// If video hasn't shown anything yet, disable the controls.
if (!this.firstFrameShown)
this.startFadeOut(this.controlBar);
}
break;
default:
this.log("!!! event " + aEvent.type + " not handled!");
}
},
terminateEventListeners : function () {
for each (var event in this.videoEvents)
this.video.removeEventListener(event, this, false);
this.log("--- videocontrols terminated ---");
},
formatTime : function(aTime) {
// Format the duration as "h:mm:ss" or "m:ss"
let hours = Math.floor(aTime / 3600000);
let mins = Math.floor(aTime % 3600000 / 60000);
let secs = Math.floor(aTime % 60000 / 1000);
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) {
if (isNaN(duration))
duration = this.maxCurrentTimeSeen;
if (duration == this.lastDurationSeen)
return;
this.lastDurationSeen = duration;
this.log("Duration is " + duration + "ms");
// Format the duration as "h:mm:ss" or "m:ss"
let timeString = 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);
this.video.currentTime = newPosition;
},
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;
if (isNaN(duration))
duration = this.maxCurrentTimeSeen;
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;
},
onVolumeMouseInOut : function (event) {
// Ignore events caused by transitions between mute button and volumeStack,
// or between nodes inside these two elements.
if (this.isEventWithin(event, this.muteButton, this.volumeStack))
return;
var isMouseOver = (event.type == "mouseover");
this.startFade(this.volumeStack, isMouseOver);
},
onMouseInOut : function (event) {
// If the controls are static, don't change anything.
if (!this.dynamicControls)
return;
// 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");
// 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;
this.startFade(this.controlBar, isMouseOver);
},
startFadeIn : function (element, immediate) {
this.startFade(element, true, immediate);
},
startFadeOut : function (element, immediate) {
this.startFade(element, false, immediate);
},
startFade : function (element, fadeIn, immediate) {
// 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.className == "controlBar" && fadeIn && element.hidden)
this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
if (immediate)
element.setAttribute("immediate", true);
else
element.removeAttribute("immediate");
if (fadeIn) {
element.setAttribute("hidden", false);
// force style resolution, so that transition begins
// when we remove the attribute.
element.clientTop;
element.removeAttribute("fadeout");
} else {
element.setAttribute("fadeout", 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.setAttribute("hidden", true);
},
togglePause : function () {
if (this.video.paused || this.video.ended)
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.
},
toggleMute : function () {
this.video.muted = !this.video.muted;
// We'll handle style changes in the event listener for
// the "volumechange" event, same as if content script was
// controlling volume.
},
setPlayButtonState : function(aPaused) {
this.playButton.setAttribute("paused", aPaused);
var attrName = aPaused ? "playlabel" : "pauselabel";
var value = this.playButton.getAttribute(attrName);
this.playButton.setAttribute("aria-label", value);
},
setMuteButtonState : function(aMuted) {
this.muteButton.setAttribute("muted", aMuted);
var attrName = aMuted ? "unmutelabel" : "mutelabel";
var value = this.muteButton.getAttribute(attrName);
this.muteButton.setAttribute("aria-label", value);
},
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 */
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");
},
init : function (binding) {
this.video = binding.parentNode;
this.videocontrols = binding;
this.isAudioOnly = (this.video instanceof HTMLAudioElement);
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.volumeStack = document.getAnonymousElementByAttribute(binding, "class", "volumeStack");
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.setupInitialState();
// 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.video.autoplay && this.video.mozAutoplayEnabled) || !this.dynamicControls);
this.startFade(this.controlBar, shouldShow, true);
// 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 (var event in this.videoEvents)
this.video.addEventListener(event, this, (event == "error") ? true : false);
var self = this;
this.muteButton.addEventListener("command", function() { self.toggleMute(); }, false);
this.playButton.addEventListener("command", function() { self.togglePause(); }, false);
this.muteButton.addEventListener("mouseover", function(e) { self.onVolumeMouseInOut(e); }, false);
this.muteButton.addEventListener("mouseout", function(e) { self.onVolumeMouseInOut(e); }, false);
this.volumeStack.addEventListener("mouseover", function(e) { self.onVolumeMouseInOut(e); }, false);
this.volumeStack.addEventListener("mouseout", function(e) { self.onVolumeMouseInOut(e); }, false);
this.videocontrols.addEventListener("transitionend", function(e) { self.onTransitionEnd(e); }, false);
// Make the <video> element keyboard accessible.
this.video.setAttribute("tabindex", 0);
this.video.addEventListener("keypress", function (e) { self.keyHandler(e) }, false);
this.log("--- videocontrols initialized ---");
}
}) ]]>
</field>
</implementation>
<handlers>
<handler event="mouseover">
this.Utils.onMouseInOut(event);
</handler>
<handler event="mouseout">
this.Utils.onMouseInOut(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"/>
</vbox>
<vbox class="controlsOverlay">
<spacer class="controlsSpacer" flex="1"/>
<vbox class="controlBar" hidden="true">
<hbox class="buttonsBar">
<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" hidden="true" fadeout="true">
<box class="volumeBackgroundBar"/>
<scale class="volumeControl" orient="vertical" dir="reverse" 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>
this.TouchUtils.init(this);
</constructor>
<field name="TouchUtils">
<![CDATA[ ({
videocontrols: null,
controlsTimer : null,
controlsTimeout : 5000,
positionLabel: null,
get Utils() {
return this.videocontrols.Utils;
},
get visible() {
return !this.Utils.controlBar.hasAttribute("fadeout") || !this.Utils.controlBar.hasAttribute("hidden");
},
_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() {
let event = document.createEvent("MouseEvents");
event.initEvent("mouseover", true, true);
this.Utils.videocontrols.dispatchEvent(event);
this.delayHideControls(this.controlsTimeout);
},
delayHideControls : function(aTimeout) {
if (this.controlsTimer) {
clearTimeout(this.controlsTimer);
this.controlsTimer = null;
}
let self = this;
this.controlsTimer = setTimeout(function() {
self.hideControls();
}, aTimeout);
},
hideControls : function() {
let event = document.createEvent("MouseEvents");
event.initEvent("mouseout", true, true);
this.Utils.videocontrols.dispatchEvent(event);
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);
},
init : function (binding) {
this.videocontrols = binding;
var video = binding.parentNode;
let self = this;
this.Utils.playButton.addEventListener("command", function() {
if (!self.Utils.video.paused)
self.delayHideControls(0);
else
self.showControls();
}, false);
this.Utils.scrubber.addEventListener("click", function() { self.delayHideControls(self.controlsTimeout); }, false);
this.Utils.muteButton.addEventListener("click", function() { self.delayHideControls(self.controlsTimeout); }, false);
if (!video.autoplay && this.Utils.dynamicControls)
this.firstShow = true;
}
}) ]]>
</field>
</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>