Bug 462113 - Implement progress bar / scrubber for video controls. r=enn, ui-r=boriss
@ -5,3 +5,7 @@
|
||||
visibility: hidden;
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
.scrubber {
|
||||
-moz-binding: url("chrome://global/content/bindings/videocontrols.xml#suppressChangeEvent");
|
||||
}
|
||||
|
@ -6,6 +6,43 @@
|
||||
xmlns:xbl="http://www.mozilla.org/xbl"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
|
||||
<binding id="suppressChangeEvent"
|
||||
extends="chrome://global/content/bindings/scale.xml#scale">
|
||||
<implementation>
|
||||
<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":
|
||||
// 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);
|
||||
this.parentNode.parentNode.parentNode.Utils.seekToPosition();
|
||||
break;
|
||||
|
||||
case "minpos":
|
||||
this.setAttribute("min", newValue);
|
||||
break;
|
||||
|
||||
case "maxpos":
|
||||
this.setAttribute("max", newValue);
|
||||
break;
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
</implementation>
|
||||
</binding>
|
||||
|
||||
<binding id="videoControls">
|
||||
|
||||
<resources>
|
||||
@ -17,7 +54,12 @@
|
||||
<spacer flex="1"/>
|
||||
<hbox class="controlBar">
|
||||
<button class="playButton" oncommand="this.parentNode.parentNode.Utils.togglePause();"/>
|
||||
<box class="controlsMiddle" flex="1"/>
|
||||
<stack class="scrubberStack" flex="1">
|
||||
<box class="backgroundBar" flex="1"/>
|
||||
<progressmeter class="bufferBar" flex="1"/>
|
||||
<progressmeter class="progressBar" flex="1" max="10000"/>
|
||||
<scale class="scrubber" flex="1"/>
|
||||
</stack>
|
||||
<button class="muteButton" oncommand="this.parentNode.parentNode.Utils.toggleMute();"/>
|
||||
</hbox>
|
||||
</xbl:content>
|
||||
@ -81,6 +123,11 @@
|
||||
playButton : null,
|
||||
muteButton : null,
|
||||
|
||||
scrubber : null,
|
||||
progressBar : null,
|
||||
bufferBar : null,
|
||||
thumbWidth : 0,
|
||||
|
||||
FADE_TIME_MAX : 200, // ms
|
||||
FADE_TIME_STEP : 30, // ms
|
||||
|
||||
@ -90,6 +137,8 @@
|
||||
controlsVisible : false,
|
||||
|
||||
firstFrameShown : false,
|
||||
lastTimeUpdate : 0,
|
||||
maxCurrentTimeSeen : 0,
|
||||
|
||||
get dynamicControls() {
|
||||
// Don't fade controls for <audio> elements.
|
||||
@ -118,11 +167,92 @@
|
||||
case "loadeddata":
|
||||
this.firstFrameShown = true;
|
||||
break;
|
||||
case "loadstart":
|
||||
this.maxCurrentTimeSeen = 0;
|
||||
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;
|
||||
default:
|
||||
this.log("!!! event " + aEvent.type + " not handled!");
|
||||
}
|
||||
},
|
||||
|
||||
durationChange : function (duration) {
|
||||
if (isNaN(duration))
|
||||
duration = this.maxCurrentTimeSeen;
|
||||
this.log("Duration is " + duration + "ms");
|
||||
|
||||
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() {
|
||||
var newPosition = this.scrubber.getAttribute("value");
|
||||
newPosition /= 1000; // convert from ms
|
||||
this.log("+++ seeking to " + newPosition);
|
||||
this.video.currentTime = newPosition;
|
||||
},
|
||||
|
||||
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 so it goes to the right-edge of the
|
||||
// scrubber thumb. This is a bit tricky, due to how the thumb is
|
||||
// positioned... It's flush-left at the minimum position, but
|
||||
// flush-right at the maximum position (so that the full thumb is
|
||||
// always visible in the <scale>'s box).
|
||||
var percent = currentTime / duration;
|
||||
var leftEdge = Math.floor(percent * (this.scrubber.clientWidth - this.thumbWidth));
|
||||
var rightEdge = leftEdge + this.thumbWidth;
|
||||
var adjPercent = rightEdge / this.scrubber.clientWidth;
|
||||
|
||||
// The progressBar has max=10000
|
||||
this.progressBar.value = Math.round(adjPercent * 10000);
|
||||
},
|
||||
|
||||
onMouseInOut : function (event) {
|
||||
// If the controls are static, don't change anything.
|
||||
if (!this.dynamicControls)
|
||||
@ -251,7 +381,15 @@
|
||||
this.Utils.controlBar = document.getAnonymousElementByAttribute(this, "class", "controlBar");
|
||||
this.Utils.playButton = document.getAnonymousElementByAttribute(this, "class", "playButton");
|
||||
this.Utils.muteButton = document.getAnonymousElementByAttribute(this, "class", "muteButton");
|
||||
this.Utils.progressBar = document.getAnonymousElementByAttribute(this, "class", "progressBar");
|
||||
this.Utils.bufferBar = document.getAnonymousElementByAttribute(this, "class", "bufferBar");
|
||||
this.Utils.scrubber = document.getAnonymousElementByAttribute(this, "class", "scrubber");
|
||||
|
||||
// Get the width of the scrubber thumb.
|
||||
var thumb = document.getAnonymousElementByAttribute(this.Utils.scrubber, "class", "scale-thumb");
|
||||
if (thumb)
|
||||
this.Utils.thumbWidth = thumb.clientWidth;
|
||||
|
||||
// Set initial state of play/pause button.
|
||||
this.Utils.playButton.setAttribute("paused", video.paused);
|
||||
|
||||
@ -278,6 +416,11 @@
|
||||
video.addEventListener("ended", this.Utils, false);
|
||||
video.addEventListener("volumechange", this.Utils, false);
|
||||
video.addEventListener("loadeddata", this.Utils, false);
|
||||
video.addEventListener("loadstart", this.Utils, false);
|
||||
video.addEventListener("durationchange", this.Utils, false);
|
||||
video.addEventListener("timeupdate", this.Utils, false);
|
||||
video.addEventListener("progress", this.Utils, false);
|
||||
video.addEventListener("emptied", this.Utils, false);
|
||||
|
||||
this.Utils.log("--- videocontrols initialized ---");
|
||||
]]>
|
||||
|
@ -178,6 +178,7 @@ classic.jar:
|
||||
+ skin/classic/global/media/playButton.png (media/playButton.png)
|
||||
+ skin/classic/global/media/muteButton.png (media/muteButton.png)
|
||||
+ skin/classic/global/media/unmuteButton.png (media/unmuteButton.png)
|
||||
+ skin/classic/global/media/scrubberThumb.png (media/scrubberThumb.png)
|
||||
+ skin/classic/global/menu/menu-arrow-dis.gif (menu/menu-arrow-dis.gif)
|
||||
+ skin/classic/global/menu/menu-arrow-hov.gif (menu/menu-arrow-hov.gif)
|
||||
+ skin/classic/global/menu/menu-arrow.gif (menu/menu-arrow.gif)
|
||||
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 229 B |
Before Width: | Height: | Size: 113 B After Width: | Height: | Size: 126 B |
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 211 B |
BIN
toolkit/themes/pinstripe/global/media/scrubberThumb.png
Normal file
After Width: | Height: | Size: 167 B |
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 188 B |
@ -1,7 +1,7 @@
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
.controlBar {
|
||||
height: 24px;
|
||||
height: 28px;
|
||||
background-color: rgba(35,31,32,0.74);
|
||||
}
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
-moz-appearance: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
min-height: 24px;
|
||||
min-width: 24px;
|
||||
min-height: 28px;
|
||||
min-width: 28px;
|
||||
}
|
||||
.playButton[paused="true"] {
|
||||
background: url(chrome://global/skin/media/playButton.png) no-repeat center;
|
||||
@ -24,10 +24,58 @@
|
||||
-moz-appearance: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
min-height: 24px;
|
||||
min-width: 24px;
|
||||
min-height: 28px;
|
||||
min-width: 33px;
|
||||
}
|
||||
.muteButton[muted="true"] {
|
||||
background: url(chrome://global/skin/media/unmuteButton.png) no-repeat center;
|
||||
}
|
||||
|
||||
.backgroundBar {
|
||||
/* make bar 8px tall (control height = 28, minus 2 * 10 margin) */
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
-moz-border-radius: 4px 4px;
|
||||
}
|
||||
|
||||
.bufferBar, .progressBar {
|
||||
/* make bar 8px tall (control height = 28, minus 2 * 10 margin) */
|
||||
margin: 10px 0px 10px 0px;
|
||||
-moz-appearance: none;
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
/* .progress-bar is an element inside the <progressmeter> implementation. */
|
||||
.bufferBar .progress-bar {
|
||||
/*
|
||||
* Note that this is drawn on top of the .backgroundBar. So although this
|
||||
* has the same background-color specified, the semitransparent
|
||||
* compositing gives it a different visual appearance.
|
||||
*/
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
-moz-border-radius: 4px 4px;
|
||||
}
|
||||
|
||||
.progressBar .progress-bar {
|
||||
background-color: white;
|
||||
-moz-border-radius: 4px 4px;
|
||||
}
|
||||
|
||||
/* .scale-slider is an element inside the <scale> implementation. */
|
||||
.scale-slider {
|
||||
/* Hide the default horizontal bar. */
|
||||
-moz-appearance: none;
|
||||
background: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* .scale-thumb is an element inside the <scale> implementation. */
|
||||
.scale-thumb {
|
||||
/* Override the default thumb appearance with a custom image. */
|
||||
-moz-appearance: none;
|
||||
background: url(chrome://global/skin/media/scrubberThumb.png) no-repeat center;
|
||||
border: none;
|
||||
min-width: 11px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
@ -151,6 +151,7 @@ classic.jar:
|
||||
skin/classic/global/media/playButton.png (media/playButton.png)
|
||||
skin/classic/global/media/muteButton.png (media/muteButton.png)
|
||||
skin/classic/global/media/unmuteButton.png (media/unmuteButton.png)
|
||||
skin/classic/global/media/scrubberThumb.png (media/scrubberThumb.png)
|
||||
skin/classic/global/radio/radio-check.gif (radio/radio-check.gif)
|
||||
skin/classic/global/radio/radio-check-dis.gif (radio/radio-check-dis.gif)
|
||||
skin/classic/global/scrollbar/slider.gif (scrollbar/slider.gif)
|
||||
@ -322,6 +323,7 @@ classic.jar:
|
||||
skin/classic/aero/global/media/playButton.png (media/playButton.png)
|
||||
skin/classic/aero/global/media/muteButton.png (media/muteButton.png)
|
||||
skin/classic/aero/global/media/unmuteButton.png (media/unmuteButton.png)
|
||||
skin/classic/aero/global/media/scrubberThumb.png (media/scrubberThumb.png)
|
||||
skin/classic/aero/global/radio/radio-check.gif (radio/radio-check.gif)
|
||||
skin/classic/aero/global/radio/radio-check-dis.gif (radio/radio-check-dis.gif)
|
||||
skin/classic/aero/global/scrollbar/slider.gif (scrollbar/slider.gif)
|
||||
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 229 B |
Before Width: | Height: | Size: 113 B After Width: | Height: | Size: 126 B |
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 211 B |
BIN
toolkit/themes/winstripe/global/media/scrubberThumb.png
Normal file
After Width: | Height: | Size: 167 B |
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 188 B |
@ -1,7 +1,7 @@
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
.controlBar {
|
||||
height: 24px;
|
||||
height: 28px;
|
||||
background-color: rgba(35,31,32,0.74);
|
||||
}
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
-moz-appearance: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
min-height: 24px;
|
||||
min-width: 24px;
|
||||
min-height: 28px;
|
||||
min-width: 28px;
|
||||
border: none;
|
||||
}
|
||||
.playButton[paused="true"] {
|
||||
@ -25,11 +25,64 @@
|
||||
-moz-appearance: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
min-height: 24px;
|
||||
min-width: 24px;
|
||||
min-height: 28px;
|
||||
min-width: 33px;
|
||||
border: none;
|
||||
}
|
||||
.muteButton[muted="true"] {
|
||||
background: url(chrome://global/skin/media/unmuteButton.png) no-repeat center;
|
||||
}
|
||||
|
||||
.backgroundBar {
|
||||
/* make bar 8px tall (control height = 28, minus 2 * 10 margin) */
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
-moz-border-radius: 4px 4px;
|
||||
}
|
||||
|
||||
.bufferBar, .progressBar {
|
||||
/* make bar 8px tall (control height = 28, minus 2 * 10 margin) */
|
||||
margin: 10px 0px 10px 0px;
|
||||
-moz-appearance: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
min-width: 0px;
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
/* .progress-bar is an element inside the <progressmeter> implementation. */
|
||||
.bufferBar .progress-bar {
|
||||
/*
|
||||
* Note that this is drawn on top of the .backgroundBar. So although this
|
||||
* has the same background-color specified, the semitransparent
|
||||
* compositing gives it a different visual appearance.
|
||||
*/
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
-moz-border-radius: 4px 4px;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
.progressBar .progress-bar {
|
||||
background-color: white;
|
||||
-moz-border-radius: 4px 4px;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
/* .scale-slider is an element inside the <scale> implementation. */
|
||||
.scale-slider {
|
||||
/* Hide the default horizontal bar. */
|
||||
-moz-appearance: none;
|
||||
background: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* .scale-thumb is an element inside the <scale> implementation. */
|
||||
.scale-thumb {
|
||||
/* Override the default thumb appearance with a custom image. */
|
||||
-moz-appearance: none;
|
||||
background: url(chrome://global/skin/media/scrubberThumb.png) no-repeat center;
|
||||
border: none;
|
||||
min-width: 11px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|