зеркало из https://github.com/mozilla/gecko-dev.git
Bug 669260 - Add statistics overlay to video element. r=dolske
This commit is contained in:
Родитель
1b2acf4856
Коммит
2ffc877a0e
|
@ -105,6 +105,14 @@
|
|||
label="&mediaHideControls.label;"
|
||||
accesskey="&mediaHideControls.accesskey;"
|
||||
oncommand="gContextMenu.mediaCommand('hidecontrols');"/>
|
||||
<menuitem id="context-video-showstats"
|
||||
accesskey="&videoShowStats.accesskey;"
|
||||
label="&videoShowStats.label;"
|
||||
oncommand="gContextMenu.mediaCommand('showstats');"/>
|
||||
<menuitem id="context-video-hidestats"
|
||||
accesskey="&videoHideStats.accesskey;"
|
||||
label="&videoHideStats.label;"
|
||||
oncommand="gContextMenu.mediaCommand('hidestats');"/>
|
||||
<menuitem id="context-video-fullscreen"
|
||||
accesskey="&videoFullScreen.accesskey;"
|
||||
label="&videoFullScreen.label;"
|
||||
|
|
|
@ -418,6 +418,10 @@ nsContextMenu.prototype = {
|
|||
this.showItem("context-media-showcontrols", onMedia && !this.target.controls);
|
||||
this.showItem("context-media-hidecontrols", onMedia && this.target.controls);
|
||||
this.showItem("context-video-fullscreen", this.onVideo);
|
||||
var statsShowing = this.onVideo && this.target.wrappedJSObject.mozMediaStatisticsShowing;
|
||||
this.showItem("context-video-showstats", this.onVideo && this.target.controls && !statsShowing);
|
||||
this.showItem("context-video-hidestats", this.onVideo && this.target.controls && statsShowing);
|
||||
|
||||
// Disable them when there isn't a valid media source loaded.
|
||||
if (onMedia) {
|
||||
var hasError = this.target.error != null ||
|
||||
|
@ -432,6 +436,8 @@ nsContextMenu.prototype = {
|
|||
let canSaveSnapshot = this.target.readyState >= this.target.HAVE_CURRENT_DATA;
|
||||
this.setItemAttr("context-video-saveimage", "disabled", !canSaveSnapshot);
|
||||
this.setItemAttr("context-video-fullscreen", "disabled", hasError);
|
||||
this.setItemAttr("context-video-showstats", "disabled", hasError);
|
||||
this.setItemAttr("context-video-hidestats", "disabled", hasError);
|
||||
}
|
||||
}
|
||||
this.showItem("context-media-sep-commands", onMedia);
|
||||
|
@ -1436,6 +1442,16 @@ nsContextMenu.prototype = {
|
|||
case "showcontrols":
|
||||
media.setAttribute("controls", "true");
|
||||
break;
|
||||
case "showstats":
|
||||
var event = document.createEvent("CustomEvent");
|
||||
event.initCustomEvent("media-showStatistics", false, true, true);
|
||||
media.dispatchEvent(event);
|
||||
break;
|
||||
case "hidestats":
|
||||
var event = document.createEvent("CustomEvent");
|
||||
event.initCustomEvent("media-showStatistics", false, true, false);
|
||||
media.dispatchEvent(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -12,9 +12,9 @@ Browser context menu subtest.
|
|||
<input id="test-input"><br>
|
||||
<img id="test-image" src="ctxmenu-image.png">
|
||||
<canvas id="test-canvas" width="100" height="100" style="background-color: blue"></canvas>
|
||||
<video id="test-video-ok" src="video.ogg" width="100" height="100" style="background-color: green"></video>
|
||||
<video id="test-video-bad" src="bogus.duh" width="100" height="100" style="background-color: orange"></video>
|
||||
<video id="test-video-bad2" width="100" height="100" style="background-color: yellow">
|
||||
<video controls id="test-video-ok" src="video.ogg" width="100" height="100" style="background-color: green"></video>
|
||||
<video controls id="test-video-bad" src="bogus.duh" width="100" height="100" style="background-color: orange"></video>
|
||||
<video controls id="test-video-bad2" width="100" height="100" style="background-color: yellow">
|
||||
<source src="bogus.duh" type="video/durrrr;">
|
||||
</video>
|
||||
<iframe id="test-iframe" width="98" height="98" style="border: 1px solid black"></iframe>
|
||||
|
|
|
@ -332,7 +332,8 @@ function runTest(testNum) {
|
|||
// Context menu for a video (with a VALID media source)
|
||||
checkContextMenu(["context-media-play", true,
|
||||
"context-media-mute", true,
|
||||
"context-media-showcontrols", true,
|
||||
"context-media-hidecontrols", true,
|
||||
"context-video-showstats", true,
|
||||
"context-video-fullscreen", true,
|
||||
"---", null,
|
||||
"context-viewvideo", true,
|
||||
|
@ -351,7 +352,8 @@ function runTest(testNum) {
|
|||
// Context menu for a video (with an INVALID media source)
|
||||
checkContextMenu(["context-media-play", false,
|
||||
"context-media-mute", false,
|
||||
"context-media-showcontrols", false,
|
||||
"context-media-hidecontrols", false,
|
||||
"context-video-showstats", false,
|
||||
"context-video-fullscreen", false,
|
||||
"---", null,
|
||||
"context-viewvideo", true,
|
||||
|
@ -370,7 +372,8 @@ function runTest(testNum) {
|
|||
// Context menu for a video (with an INVALID media source)
|
||||
checkContextMenu(["context-media-play", false,
|
||||
"context-media-mute", false,
|
||||
"context-media-showcontrols", false,
|
||||
"context-media-hidecontrols", false,
|
||||
"context-video-showstats", false,
|
||||
"context-video-fullscreen", false,
|
||||
"---", null,
|
||||
"context-viewvideo", false,
|
||||
|
|
|
@ -434,7 +434,10 @@ can reach it easily. -->
|
|||
<!ENTITY videoFullScreen.accesskey "F">
|
||||
<!ENTITY videoSaveImage.label "Save Snapshot As…">
|
||||
<!ENTITY videoSaveImage.accesskey "S">
|
||||
|
||||
<!ENTITY videoShowStats.label "Show Statistics">
|
||||
<!ENTITY videoShowStats.accesskey "t">
|
||||
<!ENTITY videoHideStats.label "Hide Statistics">
|
||||
<!ENTITY videoHideStats.accesskey "t">
|
||||
|
||||
<!-- LOCALIZATION NOTE :
|
||||
fullZoomEnlargeCmd.commandkey3, fullZoomReduceCmd.commandkey2 and
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
@namespace html url("http://www.w3.org/1999/xhtml");
|
||||
|
||||
.scrubber,
|
||||
.volumeControl {
|
||||
|
@ -54,6 +55,31 @@
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Statistics formatting */
|
||||
html|td.statLabel {
|
||||
font-weight: bold;
|
||||
max-width: 20%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
html|td.statValue {
|
||||
max-width: 30%;
|
||||
}
|
||||
html|td.filename {
|
||||
max-width: 80%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
html|span.statActivity > html|span {
|
||||
display: none;
|
||||
}
|
||||
html|span.statActivity[activity="paused"] > html|span.statActivityPaused,
|
||||
html|span.statActivity[activity="playing"] > html|span.statActivityPlaying,
|
||||
html|span.statActivity[activity="ended"] > html|span.statActivityEnded,
|
||||
html|span.statActivity[seeking] > html|span.statActivitySeeking {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.controlBar[size="hidden"],
|
||||
.controlBar[size="small"] .durationBox,
|
||||
.controlBar[size="small"] .durationLabel,
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
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: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">
|
||||
|
@ -199,6 +200,60 @@
|
|||
<box class="statusIcon"/>
|
||||
</vbox>
|
||||
|
||||
<vbox class="statsOverlay" hidden="true">
|
||||
<html:table class="statsTable" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<tr>
|
||||
<td class="statLabel">&stats.media;</td>
|
||||
<td colspan="3" class="statValue filename"><span class="statFilename"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="statLabel">&stats.size;</td>
|
||||
<td colspan="3" 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>
|
||||
<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>
|
||||
<td class="statLabel">&stats.channels;</td> <td class="statValue"><span class="statChannels"/></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>
|
||||
<td class="statLabel">&stats.sampleRate;</td> <td class="statValue"><span class="statSampleRate"/></td>
|
||||
</tr>
|
||||
<tr style="height: 1em;"/>
|
||||
|
||||
<tr>
|
||||
<td class="statLabel">&stats.framesParsed;</td> <td class="statValue"><span class="statFramesParsed"/></td>
|
||||
<td class="statLabel"></td> <td class="statValue"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="statLabel">&stats.framesDecoded;</td> <td class="statValue"><span class="statFramesDecoded"/></td>
|
||||
<td class="statLabel"></td> <td class="statValue"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="statLabel">&stats.framesPresented;</td> <td class="statValue"><span class="statFramesPresented"/></td>
|
||||
<td class="statLabel"></td> <td class="statValue"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="statLabel">&stats.framesPainted;</td> <td class="statValue"><span class="statFramesPainted"/></td>
|
||||
<td class="statLabel"></td> <td class="statValue"></td>
|
||||
</tr>
|
||||
</html:table>
|
||||
</vbox>
|
||||
|
||||
<vbox class="controlsOverlay">
|
||||
<spacer class="controlsSpacer" flex="1"/>
|
||||
<hbox class="controlBar" hidden="true">
|
||||
|
@ -255,6 +310,7 @@
|
|||
bufferBar : null,
|
||||
statusOverlay : null,
|
||||
controlsSpacer : null,
|
||||
stats : {},
|
||||
|
||||
randomID : 0,
|
||||
videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
|
||||
|
@ -354,6 +410,15 @@
|
|||
if (controlBarWasHidden)
|
||||
this.controlBar.setAttribute("hidden", "true");
|
||||
this.adjustControlSize();
|
||||
|
||||
this._handleCustomEventsBound = this.handleCustomEvents.bind(this);
|
||||
this.video.addEventListener("media-showStatistics", this._handleCustomEventsBound, false, true);
|
||||
},
|
||||
|
||||
handleCustomEvents : function (e) {
|
||||
if (!e.isTrusted)
|
||||
return;
|
||||
this.showStatistics(e.detail);
|
||||
},
|
||||
|
||||
get dynamicControls() {
|
||||
|
@ -513,8 +578,14 @@
|
|||
},
|
||||
|
||||
terminateEventListeners : function () {
|
||||
if (this.statsInterval) {
|
||||
clearInterval(this.statsInterval);
|
||||
this.statsInterval = null;
|
||||
}
|
||||
for each (var event in this.videoEvents)
|
||||
this.video.removeEventListener(event, this, false);
|
||||
this.video.removeEventListener("media-showStatistics", this._handleCustomEventsBound, false);
|
||||
delete this._handleCustomEventsBound;
|
||||
this.log("--- videocontrols terminated ---");
|
||||
},
|
||||
|
||||
|
@ -756,6 +827,100 @@
|
|||
this.muteButton.setAttribute("aria-label", value);
|
||||
},
|
||||
|
||||
_getComputedPropertyValueAsInt : function(element, property) {
|
||||
let value = window.getComputedStyle(element, null).getPropertyValue(property);
|
||||
return parseInt(value, 10);
|
||||
},
|
||||
|
||||
STATS_INTERVAL_MS : 500,
|
||||
statsInterval : null,
|
||||
|
||||
showStatistics : function(shouldShow) {
|
||||
if (this.statsInterval) {
|
||||
clearInterval(this.statsInterval);
|
||||
this.statsInterval = null;
|
||||
}
|
||||
|
||||
if (shouldShow) {
|
||||
this.video.mozMediaStatisticsShowing = true;
|
||||
|
||||
this.statsOverlay.hidden = false;
|
||||
this.statsInterval = setInterval(this.updateStats.bind(this), this.STATS_INTERVAL_MS);
|
||||
this.updateStats();
|
||||
} else {
|
||||
delete this.video.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 = 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;
|
||||
|
||||
s.channels.textContent = v.mozChannels;
|
||||
s.sampRate.textContent = (v.mozSampleRate / 1000).toFixed(3) + " kHz";
|
||||
},
|
||||
|
||||
keyHandler : function(event) {
|
||||
// Ignore keys when content might be providing its own.
|
||||
if (!this.video.hasAttribute("controls"))
|
||||
|
@ -925,8 +1090,23 @@
|
|||
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.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.channels = document.getAnonymousElementByAttribute(binding, "class", "statChannels");
|
||||
this.stats.sampRate = document.getAnonymousElementByAttribute(binding, "class", "statSampleRate");
|
||||
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();
|
||||
|
||||
// videocontrols.css hides the control bar by default, because if script
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<!ENTITY muteButton.muteLabel "Mute">
|
||||
<!ENTITY muteButton.unmuteLabel "Unmute">
|
||||
|
||||
<!ENTITY stats.media "Media">
|
||||
<!ENTITY stats.size "Size">
|
||||
<!ENTITY stats.activity "Activity">
|
||||
<!ENTITY stats.activityPaused "Paused">
|
||||
<!ENTITY stats.activityPlaying "Playing">
|
||||
<!ENTITY stats.activityEnded "Ended">
|
||||
<!ENTITY stats.activitySeeking "(seeking)">
|
||||
<!ENTITY stats.volume "Volume">
|
||||
<!ENTITY stats.channels "Channels">
|
||||
<!ENTITY stats.sampleRate "Sample Rate">
|
||||
<!ENTITY stats.framesParsed "Frames parsed">
|
||||
<!ENTITY stats.framesDecoded "Frames decoded">
|
||||
<!ENTITY stats.framesPresented "Frames presented">
|
||||
<!ENTITY stats.framesPainted "Frames painted">
|
||||
|
||||
<!-- LOCALIZATION NOTE (scrubberScale.nameFormat): the #1 string is the current
|
||||
media position, and the #2 string is the total duration. For example, when at
|
||||
the 5 minute mark in a 6 hour long video, #1 would be "5:00" and #2 would be
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
@namespace html url("http://www.w3.org/1999/xhtml");
|
||||
|
||||
.controlBar {
|
||||
height: 28px;
|
||||
|
@ -180,6 +181,24 @@
|
|||
background: url(chrome://global/skin/media/error.png) no-repeat center;
|
||||
}
|
||||
|
||||
/* Statistics formatting */
|
||||
html|td {
|
||||
padding: 2px;
|
||||
}
|
||||
html|table {
|
||||
font-family: Helvetica, Ariel, sans-serif;
|
||||
font-size: 11px;
|
||||
color: white;
|
||||
text-shadow:
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;
|
||||
width: 100%;
|
||||
background: rgba(68,68,68,.7);
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/* CSS Transitions */
|
||||
.controlBar:not([immediate]) {
|
||||
-moz-transition-property: opacity;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
@namespace html url("http://www.w3.org/1999/xhtml");
|
||||
|
||||
.controlBar {
|
||||
height: 28px;
|
||||
|
@ -189,6 +190,24 @@
|
|||
background: url(chrome://global/skin/media/error.png) no-repeat center;
|
||||
}
|
||||
|
||||
/* Statistics formatting */
|
||||
html|td {
|
||||
padding: 2px;
|
||||
}
|
||||
html|table {
|
||||
font-family: Helvetica, Ariel, sans-serif;
|
||||
font-size: 11px;
|
||||
color: white;
|
||||
text-shadow:
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;
|
||||
width: 100%;
|
||||
background: rgba(68,68,68,.7);
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/* CSS Transitions */
|
||||
.controlBar:not([immediate]) {
|
||||
-moz-transition-property: opacity;
|
||||
|
|
Загрузка…
Ссылка в новой задаче