mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-14 15:37:55 +00:00
1701 lines
81 KiB
XML
1701 lines
81 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" hidden="true" fadeout="true">
|
|
<box class="volumeBackgroundBar"/>
|
|
<scale class="volumeControl" orient="vertical" dir="reverse" 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,
|
|
volumeStack : null,
|
|
volumeControl : null,
|
|
durationLabel : null,
|
|
positionLabel : null,
|
|
scrubberThumb : null,
|
|
scrubber : null,
|
|
progressBar : null,
|
|
bufferBar : null,
|
|
statusOverlay : null,
|
|
controlsSpacer : null,
|
|
clickToPlay : null,
|
|
stats : {},
|
|
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;
|
|
if (!this.isTopLevelSyntheticDocument)
|
|
return;
|
|
this.video.style.height = this._controlBarHeight + "px";
|
|
this.video.style.width = "66%";
|
|
},
|
|
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.setMuteButtonState(this.video.muted);
|
|
|
|
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._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 (XPCNativeWrapper.unwrap(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 : Math.round(this.video.volume * 100);
|
|
this.setMuteButtonState(this.video.muted);
|
|
this.volumeControl.value = volume;
|
|
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);
|
|
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;
|
|
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) {
|
|
let doc = this.video.ownerDocument;
|
|
let win = doc.defaultView;
|
|
if (this.isVideoWithoutAudioTrack() ||
|
|
(this.isAudioOnly && this.isTopLevelSyntheticDocument)) {
|
|
return;
|
|
}
|
|
// 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);
|
|
},
|
|
|
|
_controlsHiddenByTimeout : false,
|
|
_showControlsTimeout : 0,
|
|
SHOW_CONTROLS_TIMEOUT_MS: 500,
|
|
_showControlsFn : function () {
|
|
if (Utils.video.mozMatchesSelector("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 isMouseInControls = event.clientY > this.controlBar.getBoundingClientRect().top &&
|
|
event.clientY < this.controlBar.getBoundingClientRect().bottom;
|
|
|
|
// 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.video.muted;
|
|
|
|
// 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.fullscreenButton.hidden = true;
|
|
return;
|
|
}
|
|
|
|
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);
|
|
},
|
|
|
|
setMuteButtonState : function(aMuted) {
|
|
if (aMuted)
|
|
this.muteButton.setAttribute("muted", "true");
|
|
else
|
|
this.muteButton.removeAttribute("muted");
|
|
|
|
var attrName = aMuted ? "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;
|
|
}
|
|
|
|
let unwrappedVideo = XPCNativeWrapper.unwrap(this.video);
|
|
if (shouldShow) {
|
|
unwrappedVideo.mozMediaStatisticsShowing = true;
|
|
|
|
this.statsOverlay.hidden = false;
|
|
this.statsInterval = setInterval(this.updateStats.bind(this), this.STATS_INTERVAL_MS);
|
|
this.updateStats();
|
|
} else {
|
|
delete unwrappedVideo.mozMediaStatisticsShowing;
|
|
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,
|
|
_fullscreenButtonWidth : 0,
|
|
_controlBarHeight : 0,
|
|
_overlayPlayButtonHeight : 64,
|
|
_overlayPlayButtonWidth : 64,
|
|
adjustControlSize : function adjustControlSize() {
|
|
let doc = this.video.ownerDocument;
|
|
let isAudioOnly = this.isAudioOnly;
|
|
if (isAudioOnly && !this.isTopLevelSyntheticDocument)
|
|
return;
|
|
|
|
// 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._fullscreenButtonWidth;
|
|
let minHeightForControlBar = this._controlBarHeight;
|
|
let minWidthOnlyPlayPause = this._playButtonWidth + this._muteButtonWidth;
|
|
|
|
let videoHeight = isAudioOnly ? minHeightForControlBar : this.video.clientHeight;
|
|
let videoWidth = isAudioOnly ? minWidthAllControls : this.video.clientWidth;
|
|
|
|
if (this._overlayPlayButtonHeight > videoHeight || this._overlayPlayButtonWidth > videoWidth)
|
|
this.clickToPlay.hidden = true;
|
|
|
|
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.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.statsOverlay = document.getAnonymousElementByAttribute(binding, "class", "statsOverlay");
|
|
this.controlsSpacer = document.getAnonymousElementByAttribute(binding, "class", "controlsSpacer");
|
|
this.clickToPlay = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
|
|
this.fullscreenButton = document.getAnonymousElementByAttribute(binding, "class", "fullscreenButton");
|
|
|
|
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.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);
|
|
|
|
if (this.isAudioOnly) {
|
|
this.controlBar.classList.add("audio");
|
|
} else {
|
|
addListener(this.muteButton, "mouseover", this.onVolumeMouseInOut);
|
|
addListener(this.muteButton, "mouseout", this.onVolumeMouseInOut);
|
|
addListener(this.volumeStack, "mouseover", this.onVolumeMouseInOut);
|
|
addListener(this.volumeStack, "mouseout", this.onVolumeMouseInOut);
|
|
}
|
|
|
|
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);
|
|
|
|
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="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" 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>
|
|
<![CDATA[
|
|
this.isTouchControl = true;
|
|
this.TouchUtils = {
|
|
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.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);
|
|
},
|
|
|
|
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("touchstart", function() {
|
|
self.clearTimer();
|
|
}, false);
|
|
this.Utils.scrubber.addEventListener("touchend", function() {
|
|
self.delayHideControls(self.controlsTimeout);
|
|
}, false);
|
|
this.Utils.muteButton.addEventListener("click", function() { self.delayHideControls(self.controlsTimeout); }, false);
|
|
|
|
// The first time the controls appear we want to just display
|
|
// a play button that does not fade away. The firstShow property
|
|
// makes that happen. But because of bug 718107 this init() method
|
|
// may be called again when we switch in or out of fullscreen
|
|
// mode. So we only set firstShow if we're not autoplaying and
|
|
// if we are at the beginning of the video and not already playing
|
|
if (!video.autoplay && this.Utils.dynamicControls && video.paused &&
|
|
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 (video.currentTime !== 0) {
|
|
this.delayHideControls(this.Utils.HIDE_CONTROLS_TIMEOUT_MS);
|
|
}
|
|
}
|
|
};
|
|
this.TouchUtils.init(this);
|
|
]]>
|
|
</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>
|