mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
cb2d95a146
--HG-- rename : accessible/tests/mochitest/nsIAccessible_actions.js => accessible/tests/mochitest/actions.js
806 lines
37 KiB
XML
806 lines
37 KiB
XML
<?xml version="1.0"?>
|
|
|
|
<!DOCTYPE bindings [
|
|
<!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
|
|
%videocontrolsDTD;
|
|
]>
|
|
|
|
<bindings id="videoContolBindings"
|
|
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>
|
|
|
|
<field name="thumb">null</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;
|
|
]]>
|
|
</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":
|
|
// Update the time shown in the thumb.
|
|
if (this.type == "scrubber")
|
|
this.thumb.setTime(newValue);
|
|
|
|
// 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":
|
|
this.setAttribute("max", newValue);
|
|
break;
|
|
}
|
|
]]>
|
|
</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">
|
|
<stack flex="1">
|
|
<vbox class="statusOverlay" hidden="true">
|
|
<box class="statusIcon" flex="1"/>
|
|
</vbox>
|
|
|
|
<vbox>
|
|
<spacer 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="durationLabel"/>
|
|
</vbox>
|
|
<button class="muteButton"
|
|
mutelabel="&muteButton.muteLabel;"
|
|
unmutelabel="&muteButton.unmuteLabel;"/>
|
|
<stack class="volumeStack" hidden="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,
|
|
playButton : null,
|
|
muteButton : null,
|
|
volumeStack : null,
|
|
volumeControl : null,
|
|
durationLabel : null,
|
|
scrubberThumb : null,
|
|
scrubber : null,
|
|
progressBar : null,
|
|
bufferBar : null,
|
|
|
|
randomID : 0,
|
|
videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
|
|
"loadstart", "durationchange", "timeupdate", "progress",
|
|
"playing", "waiting", "canplaythrough", "seeking",
|
|
"seeked", "emptied", "loadedmetadata", "error"],
|
|
|
|
// controlFader holds the fade state for the control bar.
|
|
controlFader : {
|
|
name : "controls", // shorthand for debugging
|
|
element : null, // the element to fade in/out
|
|
runtime : 0, // duration of active animation
|
|
fadingIn : false, // are we fading in, or fading out?
|
|
isVisible : false, // is it at all visible?
|
|
timer : null, // handle from setInterval()
|
|
delayTimer : null, // handle from setTimeout()
|
|
START_DELAY : 0, // ms, delay before fading in
|
|
RUNTIME_MAX : 200, // ms
|
|
RUNTIME_STEP : 30 // ms
|
|
},
|
|
|
|
// statusFader holds the fade state for the status overlay (inc. throbber)
|
|
statusFader : {
|
|
name : "status",
|
|
element : null,
|
|
runtime : 0,
|
|
fadingIn : false,
|
|
isVisible : false,
|
|
timer : null,
|
|
delayTimer : null,
|
|
START_DELAY : 750,
|
|
RUNTIME_MAX : 300,
|
|
RUNTIME_STEP : 20
|
|
},
|
|
|
|
// volumeFader holds the fade state for the volume <scale>.
|
|
volumeFader : {
|
|
name : "volume",
|
|
element : null,
|
|
maxSlide : null, // height when extended, set in init()
|
|
runtime : 0,
|
|
fadingIn : false,
|
|
isVisible : false,
|
|
timer : null,
|
|
delayTimer : null,
|
|
START_DELAY : 0,
|
|
RUNTIME_MAX : 200,
|
|
RUNTIME_STEP : 15
|
|
},
|
|
|
|
firstFrameShown : false,
|
|
lastTimeUpdate : 0,
|
|
maxCurrentTimeSeen : 0,
|
|
isAudioOnly : 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 : 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.durationChange(duration);
|
|
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;
|
|
else
|
|
this.startFadeIn(this.statusFader);
|
|
|
|
// 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.networkState == this.video.NETWORK_LOADED)
|
|
this.bufferBar.setAttribute("value", 100);
|
|
else
|
|
this.bufferBar.setAttribute("value", 0);
|
|
|
|
// Set the current status icon. If the video is in an error state,
|
|
// show the status overlay now.
|
|
if (this.video.error) {
|
|
this.statusIcon.setAttribute("type", "error");
|
|
this.startFadeIn(this.statusFader, true);
|
|
} else {
|
|
this.statusIcon.setAttribute("type", "throbber");
|
|
}
|
|
},
|
|
|
|
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)
|
|
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();
|
|
|
|
switch (aEvent.type) {
|
|
case "play":
|
|
this.setPlayButtonState(false);
|
|
break;
|
|
case "pause":
|
|
case "ended":
|
|
this.setPlayButtonState(true);
|
|
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.controlFader);
|
|
}
|
|
break;
|
|
case "loadeddata":
|
|
this.firstFrameShown = true;
|
|
break;
|
|
case "loadstart":
|
|
this.maxCurrentTimeSeen = 0;
|
|
this.statusIcon.setAttribute("type", "throbber");
|
|
this.isAudioOnly = (this.video instanceof HTMLAudioElement);
|
|
break;
|
|
case "durationchange":
|
|
var duration = Math.round(this.video.duration * 1000); // in ms
|
|
this.durationChange(duration);
|
|
break;
|
|
case "progress":
|
|
var loaded = aEvent.loaded;
|
|
var total = aEvent.total;
|
|
this.log("+++ load, " + loaded + " of " + total);
|
|
// When the source is streaming, the value of .total is -1. Set the
|
|
// progress bar to the maximum, since it's not useful.
|
|
if (total == -1)
|
|
total = loaded;
|
|
this.bufferBar.max = total;
|
|
this.bufferBar.value = loaded;
|
|
break;
|
|
case "timeupdate":
|
|
var currentTime = Math.round(this.video.currentTime * 1000); // in ms
|
|
var duration = Math.round(this.video.duration * 1000); // in ms
|
|
|
|
// Timeupdate events are dispatched *every frame*. Reduce workload by
|
|
// ignoring position changes that are within 333ms of the current position.
|
|
if (Math.abs(currentTime - this.lastTimeUpdate) < 333)
|
|
return;
|
|
this.lastTimeUpdate = currentTime;
|
|
|
|
this.showPosition(currentTime, duration);
|
|
break;
|
|
case "emptied":
|
|
this.bufferBar.value = 0;
|
|
break;
|
|
case "seeking":
|
|
case "waiting":
|
|
this.statusIcon.setAttribute("type", "throbber");
|
|
this.startFadeIn(this.statusFader);
|
|
break;
|
|
case "seeked":
|
|
// Normally we'd expect canplaythough to fire, but if we already
|
|
// have the data cached it shouldn't fire again.
|
|
if (this.video.readyState == this.video.HAVE_ENOUGH_DATA)
|
|
this.startFadeOut(this.statusFader);
|
|
break;
|
|
case "playing":
|
|
case "canplaythrough":
|
|
this.startFadeOut(this.statusFader);
|
|
break;
|
|
case "error":
|
|
this.statusIcon.setAttribute("type", "error");
|
|
this.startFadeIn(this.statusFader, true);
|
|
// If video hasn't shown anything yet, disable the controls.
|
|
if (!this.firstFrameShown)
|
|
this.startFadeOut(this.controlFader);
|
|
break;
|
|
default:
|
|
this.log("!!! event " + aEvent.type + " not handled!");
|
|
}
|
|
},
|
|
|
|
terminateEventListeners : function () {
|
|
for each (var event in this.videoEvents)
|
|
this.video.removeEventListener(event, this, false);
|
|
|
|
if (this.controlFader.timer)
|
|
clearInterval(this.controlFader.timer);
|
|
if (this.controlFader.delayTimer)
|
|
clearInterval(this.controlFader.delayTimer);
|
|
if (this.statusFader.timer)
|
|
clearInterval(this.statusFader.timer);
|
|
if (this.statusFader.delayTimer)
|
|
clearTimeout(this.statusFader.delayTimer);
|
|
this.log("--- videocontrols terminated ---");
|
|
},
|
|
|
|
durationChange : function (duration) {
|
|
if (isNaN(duration))
|
|
duration = this.maxCurrentTimeSeen;
|
|
this.log("Duration is " + duration + "ms");
|
|
|
|
// Format the duration as "h:mm:ss" or "m:ss"
|
|
var hours = Math.floor(duration / 3600000);
|
|
var mins = Math.floor(duration % 3600000 / 60000);
|
|
var secs = Math.floor(duration % 60000 / 1000);
|
|
var timeString;
|
|
if (secs < 10)
|
|
secs = "0" + secs;
|
|
if (hours) {
|
|
if (mins < 10)
|
|
mins = "0" + mins;
|
|
timeString = hours + ":" + mins + ":" + secs;
|
|
} else {
|
|
timeString = mins + ":" + secs;
|
|
}
|
|
this.durationLabel.setAttribute("value", 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.durationChange(duration);
|
|
}
|
|
|
|
this.log("time update @ " + currentTime + "ms of " + duration + "ms");
|
|
this.scrubber.value = currentTime;
|
|
|
|
// Extend the progressBar to the middle of the scrubber thumb.
|
|
var percent = currentTime / duration;
|
|
// The progressBar has max=10000
|
|
this.progressBar.value = Math.round(percent * 10000);
|
|
},
|
|
|
|
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.volumeFader, 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.controlFader, isMouseOver);
|
|
},
|
|
|
|
startFadeIn : function (fader, immediate) {
|
|
this.startFade(fader, true, immediate);
|
|
},
|
|
|
|
startFadeOut : function (fader, immediate) {
|
|
this.startFade(fader, false, immediate);
|
|
},
|
|
|
|
startFade : function (fader, fadeIn, immediate) {
|
|
// If the fader specifies a start delay, don't immediately fade in...
|
|
// Unless there's already a fade underway, in which case we want to be
|
|
// able to immediately reverse it (eg, a seeking event right after seeked).
|
|
if (fadeIn && fader.START_DELAY && !immediate && !fader.timer) {
|
|
function delayedFadeStart(self, fader) {
|
|
self.log("Delated start timer fired.");
|
|
fader.delayTimer = null;
|
|
self.startFade(fader, true, true);
|
|
}
|
|
|
|
// If there's already a timer running, let it handle things.
|
|
if (fader.delayTimer)
|
|
return;
|
|
|
|
this.log("Delaying " + fader.name + " fade-in...");
|
|
fader.delayTimer = setTimeout(delayedFadeStart, fader.START_DELAY, this, fader);
|
|
return;
|
|
}
|
|
|
|
// Cancel any delay timer (eg, if we start fading-out before it fires)
|
|
if (fader.delayTimer) {
|
|
this.log("Canceling " + fader.name + " fade-in delay...");
|
|
clearTimeout(fader.delayTimer);
|
|
fader.delayTimer = null;
|
|
}
|
|
|
|
|
|
// If we're already fading towards the desired state (or are
|
|
// already there), then we don't need to do anything more.
|
|
var directionChange = (fader.fadingIn != fadeIn);
|
|
if (!directionChange)
|
|
return;
|
|
|
|
fader.fadingIn = fadeIn;
|
|
this.log("Fading " + fader.name + (fader.fadingIn ? " in" : " out"));
|
|
|
|
// When switching direction mid-fade, we want the reversed fade
|
|
// to complete in the same amount of time as the current fade has
|
|
// been running. So we invert the runtime.
|
|
//
|
|
// For example, if we're 20ms into a 100ms fade-in, then we want to
|
|
// fade-out over 20ms. This is done by setting the .runtime to 80ms
|
|
// (100-20), so that doFade will only animate for 20ms more.
|
|
if (fader.runtime)
|
|
fader.runtime = fader.RUNTIME_MAX - fader.runtime;
|
|
|
|
if (!fader.timer) {
|
|
fader.timer = setInterval(this.doFade, fader.RUNTIME_STEP, this, fader);
|
|
// Perform the first fade step now, notably to make a fade-in
|
|
// immediately activate the controls.
|
|
this.doFade(this, fader, -(fader.RUNTIME_STEP - 1));
|
|
}
|
|
},
|
|
|
|
doFade : function (self, fader, lateness) {
|
|
// Update elapsed time, and compute position as a percent
|
|
// of total. Last frame could run over, so clamp to 1.
|
|
fader.runtime += fader.RUNTIME_STEP + lateness;
|
|
var pos = fader.runtime / fader.RUNTIME_MAX;
|
|
if (pos > 1)
|
|
pos = 1;
|
|
|
|
// Calculate the opacity for our position in the animation.
|
|
var opacity;
|
|
if (fader.fadingIn)
|
|
opacity = Math.pow(pos, 0.5);
|
|
else
|
|
opacity = Math.pow(1 - pos, 0.5);
|
|
fader.isVisible = (opacity ? true : false);
|
|
fader.element.style.opacity = opacity;
|
|
// Hide the element to ignore mouse clicks and reduce throbber CPU usage.
|
|
fader.element.setAttribute("hidden", !fader.isVisible);
|
|
|
|
// If this fader also has a slide effect, change the CSS margin-top too.
|
|
if (fader.maxSlide) {
|
|
var marginTop;
|
|
if (fader.fadingIn)
|
|
marginTop = Math.round(fader.maxSlide * Math.pow(pos, 0.5));
|
|
else
|
|
marginTop = Math.round(fader.maxSlide * Math.pow(1 - pos, 0.5));
|
|
|
|
fader.element.style.marginTop = marginTop;
|
|
}
|
|
|
|
// Is the animation done?
|
|
if (pos == 1) {
|
|
clearInterval(fader.timer);
|
|
fader.timer = null;
|
|
fader.runtime = 0;
|
|
}
|
|
},
|
|
|
|
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);
|
|
},
|
|
|
|
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.controlFader.element = document.getAnonymousElementByAttribute(binding, "class", "controlBar");
|
|
this.statusFader.element = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
|
|
this.volumeFader.element = document.getAnonymousElementByAttribute(binding, "class", "volumeStack");
|
|
|
|
this.statusIcon = document.getAnonymousElementByAttribute(binding, "class", "statusIcon");
|
|
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.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)
|
|
if (!(this.video.autoplay && this.video.mozAutoplayEnabled) || !this.dynamicControls) {
|
|
var fader = this.controlFader;
|
|
fader.element.setAttribute("hidden", "false");
|
|
fader.isVisible = true;
|
|
fader.fadingIn = true;
|
|
}
|
|
|
|
// Use the handleEvent() callback for all media events.
|
|
for each (var event in this.videoEvents)
|
|
this.video.addEventListener(event, this, false);
|
|
|
|
// Determine the height of the volumeFader when extended (which is controlled by CSS).
|
|
// Its .clientHeight seems to be 0 here, so use the theme's initial value. (eg "-70px")
|
|
this.volumeFader.maxSlide = parseInt(window.getComputedStyle(this.volumeStack, null)
|
|
.getPropertyValue("margin-top"));
|
|
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.log("--- videocontrols initialized ---");
|
|
}
|
|
}) ]]>
|
|
</field>
|
|
|
|
|
|
</implementation>
|
|
|
|
<handlers>
|
|
<handler event="mouseover">
|
|
this.Utils.onMouseInOut(event);
|
|
</handler>
|
|
<handler event="mouseout">
|
|
this.Utils.onMouseInOut(event);
|
|
</handler>
|
|
</handlers>
|
|
</binding>
|
|
</bindings>
|