Bug 971110: Prettify about:webrtc r=niko,pkerr

This commit is contained in:
Romain Gauthier 2014-09-19 19:31:47 +02:00
parent 39350f9a37
commit 830662648f
8 changed files with 1183 additions and 442 deletions

View File

@ -67,7 +67,7 @@ static RedirEntry kRedirMap[] = {
nsIAboutModule::ALLOW_SCRIPT },
{ "networking", "chrome://global/content/aboutNetworking.xhtml",
nsIAboutModule::ALLOW_SCRIPT },
{ "webrtc", "chrome://global/content/aboutWebrtc.xhtml",
{ "webrtc", "chrome://global/content/aboutwebrtc/aboutWebrtc.xhtml",
nsIAboutModule::ALLOW_SCRIPT },
// about:srcdoc is unresolvable by specification. It is included here
// because the security manager would disallow srcdoc iframes otherwise.

View File

@ -1,440 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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 html [
<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Webrtc Internals</title>
</head>
<script>
function displayLogs(logs) {
var logsDiv = document.getElementById('logs');
while (logsDiv.lastChild) {
logsDiv.removeChild(logsDiv.lastChild);
}
logsDiv.appendChild(document.createElement('h3'))
.appendChild(document.createTextNode('Logging:'));
logs.forEach(function(logLine){
logsDiv.appendChild(document.createElement('div'))
.appendChild(document.createTextNode(logLine));
});
}
function candidateTypeString(cand) {
if (cand.type == "localcandidate") {
if (cand.candidateType == "relayed") {
return cand.candidateType + '-' + cand.mozLocalTransport;
}
}
return cand.candidateType;
}
function candidateAddrString(cand) {
return cand.ipAddress + ':' +
cand.portNumber + '/' +
cand.transport + '(' +
candidateTypeString(cand) + ')';
}
function buildCandPairTableRow(candPair, localCand, remoteCand) {
var row = document.createElement('tr');
row.onclick = function() {
WebrtcGlobalInformation.getLogging("CAND-PAIR(" + row.id, displayLogs);
}
if (localCand) {
row.appendChild(document.createElement('td'))
.appendChild(document.createTextNode(candidateAddrString(localCand)));
} else {
row.appendChild(document.createElement('td'))
.appendChild(document.createTextNode(candPair.localCandidateId));
}
if (remoteCand) {
row.appendChild(document.createElement('td'))
.appendChild(document.createTextNode(candidateAddrString(remoteCand)));
} else {
row.appendChild(document.createElement('td'))
.appendChild(document.createTextNode(candPair.remoteCandidateId));
}
row.appendChild(document.createElement('td'))
.appendChild(document.createTextNode(candPair.state));
row.appendChild(document.createElement('td'))
.appendChild(document.createTextNode(candPair.mozPriority));
row.appendChild(document.createElement('td'))
.appendChild(document.createTextNode(candPair.nominated ? '*' : ''));
row.appendChild(document.createElement('td'))
.appendChild(document.createTextNode(candPair.selected ? '*' : ''));
return row;
}
function buildCandTableRow(cand) {
var row = document.createElement('tr');
row.appendChild(document.createElement('td'))
.appendChild(document.createTextNode(cand.ipAddress + ':' +
cand.portNumber + '/' +
cand.transport));
row.appendChild(document.createElement('td'))
.appendChild(document.createTextNode(candidateTypeString(cand)));
return row;
}
function buildCandPairTableHeader() {
var headerRow = document.createElement('tr');
headerRow.appendChild(document.createElement('th'))
.appendChild(document.createTextNode('Local candidate'));
headerRow.appendChild(document.createElement('th'))
.appendChild(document.createTextNode('Remote candidate'));
headerRow.appendChild(document.createElement('th'))
.appendChild(document.createTextNode('ICE State'));
headerRow.appendChild(document.createElement('th'))
.appendChild(document.createTextNode('Priority'));
headerRow.appendChild(document.createElement('th'))
.appendChild(document.createTextNode('Nominated'));
headerRow.appendChild(document.createElement('th'))
.appendChild(document.createTextNode('Selected'));
return headerRow;
}
function buildCandTableHeader(isLocal) {
var headerRow = document.createElement('tr');
headerRow.appendChild(document.createElement('th'))
.appendChild(document.createTextNode(isLocal ?
'Local candidate addr' :
'Remote candidate addr'));
headerRow.appendChild(document.createElement('th'))
.appendChild(document.createTextNode('Type'));
return headerRow;
}
function buildEmptyCandPairTable() {
var candPairTable = document.createElement('table');
candPairTable.appendChild(buildCandPairTableHeader());
return candPairTable;
}
function buildEmptyCandTable(local) {
var candTable = document.createElement('table');
candTable.appendChild(buildCandTableHeader(local));
return candTable;
}
function round00(num) {
return Math.round(num * 100) / 100;
}
function dumpAvStat(stat) {
var div = document.createElement('div');
var statsString = "";
if (stat.mozAvSyncDelay !== undefined) {
statsString += "A/V sync: " + stat.mozAvSyncDelay + " ms ";
}
if (stat.mozJitterBufferDelay !== undefined) {
statsString += "Jitter-buffer delay: " + stat.mozJitterBufferDelay + " ms";
}
div.appendChild(document.createTextNode(statsString));
return div;
}
function dumpRtpStat(stat, label) {
var div = document.createElement('div');
var statsString = " " + label + new Date(stat.timestamp).toTimeString() +
" " + stat.type + " SSRC: " + stat.ssrc;
if (stat.packetsReceived !== undefined) {
statsString += " Received: " + stat.packetsReceived + " packets";
if (stat.bytesReceived !== undefined) {
statsString += " (" + round00(stat.bytesReceived/1024) + " Kb)";
}
statsString += " Lost: " + stat.packetsLost + " Jitter: " + stat.jitter;
if (stat.mozRtt !== undefined) {
statsString += " RTT: " + stat.mozRtt + " ms";
}
} else if (stat.packetsSent !== undefined) {
statsString += " Sent: " + stat.packetsSent + " packets";
if (stat.bytesSent !== undefined) {
statsString += " (" + round00(stat.bytesSent/1024) + " Kb)";
}
}
div.appendChild(document.createTextNode(statsString));
return div;
}
function dumpCoderStat(stat) {
var div = document.createElement('div');
if (stat.bitrateMean !== undefined ||
stat.framerateMean !== undefined ||
stat.droppedFrames !== undefined ||
stat.discardedPackets !== undefined) {
var statsString = (stat.packetsReceived !== undefined)? " Decoder:" : " Encoder:";
if (stat.bitrateMean !== undefined) {
statsString += " Avg. bitrate: " + (stat.bitrateMean/1000000).toFixed(2) + " Mbps";
if (stat.bitrateStdDev !== undefined) {
statsString += " (" + (stat.bitrateStdDev/1000000).toFixed(2) + " SD)";
}
}
if (stat.framerateMean !== undefined) {
statsString += " Avg. framerate: " + (stat.framerateMean).toFixed(2) + " fps";
if (stat.framerateStdDev !== undefined) {
statsString += " (" + stat.framerateStdDev.toFixed(2) + " SD)";
}
}
if (stat.droppedFrames !== undefined) {
statsString += " Dropped frames: " + stat.droppedFrames;
}
if (stat.discardedPackets !== undefined) {
statsString += " Discarded packets: " + stat.discardedPackets;
}
div.appendChild(document.createTextNode(statsString));
}
return div;
}
function buildPcDiv(stats, pcDivHeading) {
var newPcDiv = document.createElement('div');
var heading = document.createElement('h3');
if (stats.closed) {
heading.appendChild(document.createTextNode("Closed "));
}
heading.appendChild(document.createTextNode(pcDivHeading));
heading.appendChild(document.createTextNode(" " +
new Date(stats.timestamp).toTimeString()));
newPcDiv.appendChild(heading);
// First, ICE stats
var iceHeading = document.createElement('h4');
iceHeading.appendChild(document.createTextNode("ICE statistics"));
newPcDiv.appendChild(iceHeading);
var iceTablesByComponent = {};
function getIceTables(componentId) {
if (!iceTablesByComponent[componentId]) {
iceTablesByComponent[componentId] = {
candidatePairTable: buildEmptyCandPairTable(),
localCandidateTable: buildEmptyCandTable(true),
remoteCandidateTable: buildEmptyCandTable(false)
};
}
return iceTablesByComponent[componentId];
}
// Candidates
var candidateMap = {}; // Used later to speed up recording of candidate pairs
if (stats.iceCandidateStats) {
stats.iceCandidateStats.forEach(function(cand) {
var tables = getIceTables(cand.componentId);
candidateMap[cand.id] = cand;
if (cand.type == "localcandidate") {
tables.localCandidateTable.appendChild(buildCandTableRow(cand));
} else {
tables.remoteCandidateTable.appendChild(buildCandTableRow(cand));
}
});
}
// Candidate pairs
if (stats.iceCandidatePairStats) {
stats.iceCandidatePairStats.forEach(function(candPair) {
var candPairTable =
getIceTables(candPair.componentId).candidatePairTable;
candPairTable.appendChild(
buildCandPairTableRow(candPair,
candidateMap[candPair.localCandidateId],
candidateMap[candPair.remoteCandidateId]));
});
}
// Now that tables are completely built, put them on the page.
for (var cid in iceTablesByComponent) {
if (iceTablesByComponent.hasOwnProperty(cid)) {
var tables = iceTablesByComponent[cid];
newPcDiv.appendChild(document.createElement('h4'))
.appendChild(document.createTextNode(cid));
newPcDiv.appendChild(tables.candidatePairTable);
newPcDiv.appendChild(tables.localCandidateTable);
newPcDiv.appendChild(tables.remoteCandidateTable);
}
}
// end of ICE stats
// Now, SDP
var localSdpHeading = document.createElement('h4');
localSdpHeading.appendChild(document.createTextNode("Local SDP"));
newPcDiv.appendChild(localSdpHeading);
var localSdpDiv = document.createElement('pre');
localSdpDiv.appendChild(document.createTextNode(stats.localSdp));
newPcDiv.appendChild(localSdpDiv);
var remoteSdpHeading = document.createElement('h4');
remoteSdpHeading.appendChild(document.createTextNode("Remote SDP"));
newPcDiv.appendChild(remoteSdpHeading);
var remoteSdpDiv = document.createElement('pre');
remoteSdpDiv.appendChild(document.createTextNode(stats.remoteSdp));
newPcDiv.appendChild(remoteSdpDiv);
// End of SDP
// Now, RTP stats
var rtpHeading = document.createElement('h4');
rtpHeading.appendChild(document.createTextNode("RTP statistics"));
newPcDiv.appendChild(rtpHeading);
// Build map from id -> remote RTP stats (ie; stats obtained from RTCP
// from the other end). This allows us to pair up local/remote stats for
// the same stream more easily.
var remoteRtpStatsMap = {};
var addRemoteStatToMap = function (rtpStat) {
if (rtpStat.isRemote) {
remoteRtpStatsMap[rtpStat.id] = rtpStat;
}
}
if (stats.inboundRTPStreamStats) {
stats.inboundRTPStreamStats.forEach(addRemoteStatToMap);
}
if (stats.outboundRTPStreamStats) {
stats.outboundRTPStreamStats.forEach(addRemoteStatToMap);
}
var addRtpStatPairToDocument = function (rtpStat) {
if (!rtpStat.isRemote) {
newPcDiv.appendChild(document.createElement('h5'))
.appendChild(document.createTextNode(rtpStat.id));
if (rtpStat.mozAvSyncDelay !== undefined ||
rtpStat.mozJitterBufferDelay !== undefined) {
newPcDiv.appendChild(dumpAvStat(rtpStat));
}
newPcDiv.appendChild(dumpCoderStat(rtpStat));
newPcDiv.appendChild(dumpRtpStat(rtpStat, "Local: "));
// Might not be receiving RTCP, so we have no idea what the
// statistics look like from the perspective of the other end.
if (rtpStat.remoteId) {
var remoteRtpStat = remoteRtpStatsMap[rtpStat.remoteId];
newPcDiv.appendChild(dumpRtpStat(remoteRtpStat, "Remote: "));
}
}
}
if (stats.outboundRTPStreamStats) {
stats.outboundRTPStreamStats.forEach(addRtpStatPairToDocument);
}
if (stats.inboundRTPStreamStats) {
stats.inboundRTPStreamStats.forEach(addRtpStatPairToDocument);
}
return newPcDiv;
}
function displayStats(globalReport) {
console.log("Got stats callback.");
globalReport.reports.forEach(function (report) {
var pcDivHeading = 'PeerConnection:' + report.pcid;
var pcDiv = document.getElementById(pcDivHeading);
var newPcDiv = buildPcDiv(report, pcDivHeading);
newPcDiv.id = pcDivHeading;
if (!pcDiv) {
document.getElementById('stats').appendChild(newPcDiv);
} else {
document.getElementById('stats').replaceChild(newPcDiv, pcDiv);
}
});
}
function onLoad() {
WebrtcGlobalInformation.getAllStats(displayStats);
if (WebrtcGlobalInformation.debugLevel) {
setDebugButton(true);
} else {
setDebugButton(false);
}
if (WebrtcGlobalInformation.aecDebug) {
setAECDebugButton(true);
} else {
setAECDebugButton(false);
}
}
function startDebugMode() {
WebrtcGlobalInformation.debugLevel = 65535;
setDebugButton(true);
}
function stopDebugMode() {
WebrtcGlobalInformation.debugLevel = 0;
setDebugButton(false);
}
function setDebugButton(on) {
var button = document.getElementById("debug-toggle-button");
button.innerHTML = on ? "Stop debug mode" : "Start debug mode";
button.onclick = on ? stopDebugMode : startDebugMode;
}
function startAECDebugMode() {
WebrtcGlobalInformation.aecDebug = true;
setAECDebugButton(true);
}
function stopAECDebugMode() {
WebrtcGlobalInformation.aecDebug = false;
setAECDebugButton(false);
}
function setAECDebugButton(on) {
var button = document.getElementById("aec-debug-toggle-button");
button.innerHTML = on ? "Stop AEC logging" : "Start AEC logging";
button.onclick = on ? stopAECDebugMode : startAECDebugMode;
}
</script>
<body id="body" onload="onLoad()">
<div id="stats">
</div>
<button onclick="WebrtcGlobalInformation.getLogging('', displayLogs)">
Connection log
</button>
<button id="debug-toggle-button" onclick="startDebugMode()">
Start debug mode
</button>
<button id="aec-debug-toggle-button" onclick="startAECDebugMode()">
Start AEC logging
</button>
<div id="logs">
</div>
</body>
</html>
<!-- vim: softtabstop=2:shiftwidth=2:expandtab
-->

View File

@ -0,0 +1,21 @@
Working with React JSX files
============================
The about:webrtc page uses [React](http://facebook.github.io/react/).
The UI is written in JSX files and transpiled to JS before we
commit. You need to install the JSX compiler using npm in order to
compile the .jsx files into regular .js ones:
npm install -g react-tools
Run the following command:
jsx -w -x jsx . .
jsx can also be do a one-time compile pass instead of watching if the
-w argument is omitted. Be sure to commit any transpiled files at the
same time as changes to their sources.
IMPORTANT: do not modify the generated files, only their JSX
counterpart.

View File

@ -0,0 +1,78 @@
html {
background-color: #EDECEB;
font: message-box;
}
#logs {
font-family: monospace;
}
.peer-connections {
padding: 2em;
margin: 1em 0em;
border: 1px solid #AFACA9;
border-radius: 10px;
background: none repeat scroll 0% 0% #FFF;
}
.peer-connection h3 {
background-color: #DDD;
cursor: pointer;
}
.peer-connection h3 span:last-child {
float: right;
}
.peer-connection section {
border: 1px solid #AFACA9;
padding: 10px;
}
.peer-connection table {
width: 100%;
text-align: center;
border: none;
}
.peer-connection table th,
.peer-connection table td {
padding: 0.4em;
}
.peer-connection table tr:nth-child(even) {
background-color: #DDD;
}
.pcid span:first-child {
font-weight: bold;
}
.tabs > ul {
list-style-type: none;
margin: 0;
padding: 3px 10px;
}
.tabs li {
display: inline;
position: relative;
top: 1px;
padding: 3px 10px;
}
.tabs li > a {
text-decoration: none;
padding: 0 2em;
}
.tabs li.active {
background-color: #FFF;
border: 1px solid #AFACA9;
border-bottom: none;
}
.tabs section {
clear: both;
}

View File

@ -0,0 +1,529 @@
/** @jsx React.DOM */
/* 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/. */
"use strict";
var Tabs = React.createClass({displayName: 'Tabs',
getDefaultProps: function() {
return {selectedIndex: 0};
},
getInitialState: function() {
return {selectedIndex: this.props.selectedIndex};
},
selectTab: function(index) {
return function(event) {
event.preventDefault();
this.setState({selectedIndex: index});
}.bind(this);
},
_findSelectedTabContent: function() {
// Using map() to filter children…
// https://github.com/facebook/react/issues/1644#issuecomment-45138113
return React.Children.map(this.props.children, function(tab, i) {
return i === this.state.selectedIndex ? tab : null;
}.bind(this))
},
render: function() {
var cx = React.addons.classSet;
return (
React.DOM.div({className: "tabs"},
React.DOM.ul(null,
React.Children.map(this.props.children, function(tab, i) {
return (
React.DOM.li({className: cx({active: i === this.state.selectedIndex})},
React.DOM.a({href: "#", key: i, onClick: this.selectTab(i)},
tab.props.title
)
)
);
}.bind(this))
),
this._findSelectedTabContent()
)
);
}
});
var Tab = React.createClass({displayName: 'Tab',
render: function() {
return React.DOM.section(null, this.props.children);
}
});
var AboutWebRTC = React.createClass({displayName: 'AboutWebRTC',
getInitialState: function() {
return {logs: null, reports: this.props.reports};
},
displayLogs: function() {
WebrtcGlobalInformation.getLogging('', function(logs) {
this.setState({logs: logs, reports: this.state.reports});
}.bind(this))
},
render: function() {
return (
React.DOM.div(null,
React.DOM.div({id: "stats"},
PeerConnections({reports: this.state.reports})
),
React.DOM.div({id: "buttons"},
LogsButton({handler: this.displayLogs}),
DebugButton(null),
AECButton(null)
),
React.DOM.div({id: "logs"},
this.state.logs ? Logs({logs: this.state.logs}) : null
)
)
);
}
});
var PeerConnections = React.createClass({displayName: 'PeerConnections',
getInitialState: function() {
// Sort the reports to have the more recent at the top
var reports = this.props.reports.slice().sort(function(r1, r2) {
return r2.timestamp - r1.timestamp;
});
return {reports: reports};
},
render: function() {
return (
React.DOM.div({className: "peer-connections"},
this.state.reports.map(function(report, i) {
return PeerConnection({key: i, report: report});
})
)
);
}
});
var PeerConnection = React.createClass({displayName: 'PeerConnection',
getPCInfo: function(report) {
return {
id: report.pcid.match(/id=(\S+)/)[1],
url: report.pcid.match(/http[^)]+/)[0],
closed: report.closed
};
},
getIceCandidatePairs: function(report) {
var candidates =
report.iceCandidateStats.reduce(function(candidates, candidate) {
candidates[candidate.id] = candidate;
return candidates;
}, {});
var pairs = report.iceCandidatePairStats.map(function(pair) {
var localCandidate = candidates[pair.localCandidateId];
var remoteCandidate = candidates[pair.remoteCandidateId];
return {
localCandidate: candidateToString(localCandidate),
remoteCandidate: candidateToString(remoteCandidate),
state: pair.state,
priority: pair.mozPriority,
nominated: pair.nominated,
selected: pair.selected
};
});
var pairedCandidates = pairs.reduce(function(paired, pair) {
paired.add(pair.localCandidate.id);
paired.add(pair.remoteCandidate.id);
return paired
}, new Set());
var unifiedPairs =
report.iceCandidateStats.reduce(function(pairs, candidate) {
if (pairedCandidates.has(candidate)) {
return pairs;
}
var isLocal = candidate.type === "localcandidate";
pairs.push({
localCandidate: isLocal ? candidateToString(candidate) : null,
remoteCandidate: isLocal ? null : candidateToString(candidate),
state: null,
priority: null,
nominated: null,
selected: null
});
return pairs;
}, pairs);
return unifiedPairs;
},
getInitialState: function() {
return {
unfolded: false
};
},
onFold: function() {
this.setState({unfolded: !this.state.unfolded});
},
body: function(report, pairs) {
return (
React.DOM.div(null,
React.DOM.p({className: "pcid"}, "PeerConnection ID: ", report.pcid),
Tabs(null,
Tab({title: "Ice Stats"},
IceStats({pairs: pairs})
),
Tab({title: "SDP"},
SDP({type: "local", sdp: report.localSdp}),
SDP({type: "remote", sdp: report.remoteSdp})
),
Tab({title: "RTP Stats"},
RTPStats({report: report})
)
)
)
);
},
render: function() {
var report = this.props.report;
var pcInfo = this.getPCInfo(report);
var pairs = this.getIceCandidatePairs(report);
return (
React.DOM.div({className: "peer-connection"},
React.DOM.h3({onClick: this.onFold},
"[", pcInfo.id, "] ", pcInfo.url, " ", pcInfo.closed ? "(closed)" : null,
new Date(report.timestamp).toTimeString()
),
this.state.unfolded ? this.body(report, pairs) : undefined
)
);
}
});
var IceStats = React.createClass({displayName: 'IceStats',
sortHeadings: {
"Local candidate": "localCandidate",
"Remote candidate": "remoteCandidate",
"Ice State": "state",
"Priority": "priority",
"Nominated": "nominated",
"Selected": "selected"
},
sort: function(key) {
var sorting = this.state.sorting;
var pairs = this.state.pairs.slice().sort(function(pair1, pair2) {
var value1 = pair1[key] ? pair1[key].toString() : "";
var value2 = pair2[key] ? pair2[key].toString() : "";
// Reverse sorting
if (key === sorting) {
return value2.localeCompare(value1);
}
return value1.localeCompare(value2);
}.bind(this));
sorting = (key === sorting) ? null : key;
this.setState({pairs: pairs, sorting: sorting});
},
generateSortHeadings: function() {
return Object.keys(this.sortHeadings).map(function(heading, i) {
var sortKey = this.sortHeadings[heading];
return (
React.DOM.th(null,
React.DOM.a({href: "#", onClick: this.sort.bind(this, sortKey)},
heading
)
)
);
}.bind(this));
},
getInitialState: function() {
return {pairs: this.props.pairs, sorting: null};
},
render: function() {
return (
React.DOM.table(null,
React.DOM.tbody(null,
React.DOM.tr(null, this.generateSortHeadings()),
this.state.pairs.map(function(pair, i) {
return IceCandidatePair({key: i, pair: pair});
})
)
)
);
}
});
var IceCandidatePair = React.createClass({displayName: 'IceCandidatePair',
render: function() {
var pair = this.props.pair;
return (
React.DOM.tr(null,
React.DOM.td(null, pair.localCandidate),
React.DOM.td(null, pair.remoteCandidate),
React.DOM.td(null, pair.state),
React.DOM.td(null, pair.priority),
React.DOM.td(null, pair.nominated ? '✔' : null),
React.DOM.td(null, pair.selected ? '✔' : null)
)
);
}
});
var SDP = React.createClass({displayName: 'SDP',
render: function() {
var type = labelize(this.props.type);
return (
React.DOM.div(null,
React.DOM.h4(null, type, " SDP"),
React.DOM.pre(null,
this.props.sdp
)
)
);
}
});
var RTPStats = React.createClass({displayName: 'RTPStats',
getRtpStats: function(report) {
var remoteRtpStats = {};
var rtpStats = [];
rtpStats = rtpStats.concat(report.inboundRTPStreamStats || []);
rtpStats = rtpStats.concat(report.outboundRTPStreamStats || []);
rtpStats.forEach(function(stats) {
if (stats.isRemote) {
remoteRtpStats[stats.id] = stats;
}
});
rtpStats.forEach(function(stats) {
if (stats.remoteId) {
stats.remoteRtpStats = remoteRtpStats[stats.remoteId];
}
});
return rtpStats;
},
dumpAvStats: function(stats) {
var statsString = "";
if (stats.mozAvSyncDelay) {
statsString += `A/V sync: ${stats.mozAvSyncDelay} ms `;
}
if (stats.mozJitterBufferDelay) {
statsString += `Jitter-buffer delay: ${stats.mozJitterBufferDelay} ms`;
}
return React.DOM.div(null, statsString);
},
dumpCoderStats: function(stats) {
var statsString = "";
var label;
if (stats.bitrateMean) {
statsString += ` Avg. bitrate: ${(stats.bitrateMean/1000000).toFixed(2)} Mbps`;
if (stats.bitrateStdDev) {
statsString += ` (${(stats.bitrateStdDev/1000000).toFixed(2)} SD)`;
}
}
if (stats.framerateMean) {
statsString += ` Avg. framerate: ${(stats.framerateMean).toFixed(2)} fps`;
if (stats.framerateStdDev) {
statsString += ` (${stats.framerateStdDev.toFixed(2)} SD)`;
}
}
if (stats.droppedFrames) {
statsString += ` Dropped frames: ${stats.droppedFrames}`;
}
if (stats.discardedPackets) {
statsString += ` Discarded packets: ${stats.discardedPackets}`;
}
if (statsString) {
label = (stats.packetsReceived)? " Decoder:" : " Encoder:";
statsString = label + statsString;
}
return React.DOM.div(null, statsString);
},
dumpRtpStats: function(stats, type) {
var label = labelize(type);
var time = new Date(stats.timestamp).toTimeString();
var statsString = `${label}: ${time} ${stats.type} SSRC: ${stats.ssrc}`;
if (stats.packetsReceived) {
statsString += ` Received: ${stats.packetsReceived} packets`;
if (stats.bytesReceived) {
statsString += ` (${round00(stats.bytesReceived/1024)} Kb)`;
}
statsString += ` Lost: ${stats.packetsLost} Jitter: ${stats.jitter}`;
if (stats.mozRtt) {
statsString += ` RTT: ${stats.mozRtt} ms`;
}
} else if (stats.packetsSent) {
statsString += ` Sent: ${stats.packetsSent} packets`;
if (stats.bytesSent) {
statsString += ` (${round00(stats.bytesSent/1024)} Kb)`;
}
}
return React.DOM.div(null, statsString);
},
render: function() {
var rtpStats = this.getRtpStats(this.props.report);
return (
React.DOM.div(null,
rtpStats.map(function(stats) {
var isAvStats = (stats.mozAvSyncDelay || stats.mozJitterBufferDelay);
var remoteRtpStats = stats.remoteId ? stats.remoteRtpStats : null;
return [
React.DOM.h5(null, stats.id),
isAvStats ? this.dumpAvStats(stats) : null,
this.dumpCoderStats(stats),
this.dumpRtpStats(stats, "local"),
remoteRtpStats ? this.dumpRtpStats(remoteRtpStats, "remote") : null
]
}.bind(this))
)
);
}
});
var LogsButton = React.createClass({displayName: 'LogsButton',
render: function() {
return (
React.DOM.button({onClick: this.props.handler}, "Connection log")
);
}
});
var DebugButton = React.createClass({displayName: 'DebugButton',
getInitialState: function() {
var on = (WebrtcGlobalInformation.debugLevel > 0);
return {on: on};
},
onClick: function() {
if (this.state.on)
WebrtcGlobalInformation.debugLevel = 0;
else
WebrtcGlobalInformation.debugLevel = 65535;
this.setState({on: !this.state.on});
},
render: function() {
return (
React.DOM.button({onClick: this.onClick},
this.state.on ? "Stop debug mode" : "Start debug mode"
)
);
}
});
var AECButton = React.createClass({displayName: 'AECButton',
getInitialState: function() {
return {on: WebrtcGlobalInformation.aecDebug};
},
onClick: function() {
WebrtcGlobalInformation.aecDebug = !this.state.on;
this.setState({on: WebrtcGlobalInformation.aecDebug});
},
render: function() {
return (
React.DOM.button({onClick: this.onClick},
this.state.on ? "Stop AEC logging" : "Start AEC logging"
)
);
}
});
var Logs = React.createClass({displayName: 'Logs',
render: function() {
return (
React.DOM.div(null,
React.DOM.h3(null, "Logging:"),
React.DOM.div(null,
this.props.logs.map(function(line, i) {
return React.DOM.div({key: i}, line);
})
)
)
);
}
});
function iceCandidateMapping(iceCandidates) {
var candidates = {};
iceCandidates = iceCandidates || [];
iceCandidates.forEach(function(candidate) {
candidates[candidate.id] = candidate;
});
return candidates;
}
function round00(num) {
return Math.round(num * 100) / 100;
}
function labelize(label) {
return `${label.charAt(0).toUpperCase()}${label.slice(1)}`;
}
function candidateToString(c) {
var type = c.candidateType;
if (c.type == "localcandidate" && c.candidateType == "relayed") {
type = `${c.candidateType}-${c.mozLocalTransport}`;
}
return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`
}
function onLoad() {
WebrtcGlobalInformation.getAllStats(function(globalReport) {
var reports = globalReport.reports;
React.renderComponent(AboutWebRTC({reports: reports}),
document.querySelector("#body"));
});
}

View File

@ -0,0 +1,529 @@
/** @jsx React.DOM */
/* 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/. */
"use strict";
var Tabs = React.createClass({
getDefaultProps: function() {
return {selectedIndex: 0};
},
getInitialState: function() {
return {selectedIndex: this.props.selectedIndex};
},
selectTab: function(index) {
return function(event) {
event.preventDefault();
this.setState({selectedIndex: index});
}.bind(this);
},
_findSelectedTabContent: function() {
// Using map() to filter children
// https://github.com/facebook/react/issues/1644#issuecomment-45138113
return React.Children.map(this.props.children, function(tab, i) {
return i === this.state.selectedIndex ? tab : null;
}.bind(this))
},
render: function() {
var cx = React.addons.classSet;
return (
<div className="tabs">
<ul>{
React.Children.map(this.props.children, function(tab, i) {
return (
<li className={cx({active: i === this.state.selectedIndex})}>
<a href="#" key={i} onClick={this.selectTab(i)}>
{tab.props.title}
</a>
</li>
);
}.bind(this))
}</ul>
{this._findSelectedTabContent()}
</div>
);
}
});
var Tab = React.createClass({
render: function() {
return <section>{this.props.children}</section>;
}
});
var AboutWebRTC = React.createClass({
getInitialState: function() {
return {logs: null, reports: this.props.reports};
},
displayLogs: function() {
WebrtcGlobalInformation.getLogging('', function(logs) {
this.setState({logs: logs, reports: this.state.reports});
}.bind(this))
},
render: function() {
return (
<div>
<div id="stats">
<PeerConnections reports={this.state.reports}/>
</div>
<div id="buttons">
<LogsButton handler={this.displayLogs}/>
<DebugButton/>
<AECButton/>
</div>
<div id="logs">{
this.state.logs ? <Logs logs={this.state.logs} /> : null
}</div>
</div>
);
}
});
var PeerConnections = React.createClass({
getInitialState: function() {
// Sort the reports to have the more recent at the top
var reports = this.props.reports.slice().sort(function(r1, r2) {
return r2.timestamp - r1.timestamp;
});
return {reports: reports};
},
render: function() {
return (
<div className="peer-connections">
{
this.state.reports.map(function(report, i) {
return <PeerConnection key={i} report={report}/>;
})
}
</div>
);
}
});
var PeerConnection = React.createClass({
getPCInfo: function(report) {
return {
id: report.pcid.match(/id=(\S+)/)[1],
url: report.pcid.match(/http[^)]+/)[0],
closed: report.closed
};
},
getIceCandidatePairs: function(report) {
var candidates =
report.iceCandidateStats.reduce(function(candidates, candidate) {
candidates[candidate.id] = candidate;
return candidates;
}, {});
var pairs = report.iceCandidatePairStats.map(function(pair) {
var localCandidate = candidates[pair.localCandidateId];
var remoteCandidate = candidates[pair.remoteCandidateId];
return {
localCandidate: candidateToString(localCandidate),
remoteCandidate: candidateToString(remoteCandidate),
state: pair.state,
priority: pair.mozPriority,
nominated: pair.nominated,
selected: pair.selected
};
});
var pairedCandidates = pairs.reduce(function(paired, pair) {
paired.add(pair.localCandidate.id);
paired.add(pair.remoteCandidate.id);
return paired
}, new Set());
var unifiedPairs =
report.iceCandidateStats.reduce(function(pairs, candidate) {
if (pairedCandidates.has(candidate)) {
return pairs;
}
var isLocal = candidate.type === "localcandidate";
pairs.push({
localCandidate: isLocal ? candidateToString(candidate) : null,
remoteCandidate: isLocal ? null : candidateToString(candidate),
state: null,
priority: null,
nominated: null,
selected: null
});
return pairs;
}, pairs);
return unifiedPairs;
},
getInitialState: function() {
return {
unfolded: false
};
},
onFold: function() {
this.setState({unfolded: !this.state.unfolded});
},
body: function(report, pairs) {
return (
<div>
<p className="pcid">PeerConnection ID: {report.pcid}</p>
<Tabs>
<Tab title="Ice Stats">
<IceStats pairs={pairs} />
</Tab>
<Tab title="SDP">
<SDP type="local" sdp={report.localSdp} />
<SDP type="remote" sdp={report.remoteSdp} />
</Tab>
<Tab title="RTP Stats">
<RTPStats report={report} />
</Tab>
</Tabs>
</div>
);
},
render: function() {
var report = this.props.report;
var pcInfo = this.getPCInfo(report);
var pairs = this.getIceCandidatePairs(report);
return (
<div className="peer-connection">
<h3 onClick={this.onFold}>
[{pcInfo.id}] {pcInfo.url} {pcInfo.closed ? "(closed)" : null}
{new Date(report.timestamp).toTimeString()}
</h3>
{this.state.unfolded ? this.body(report, pairs) : undefined}
</div>
);
}
});
var IceStats = React.createClass({
sortHeadings: {
"Local candidate": "localCandidate",
"Remote candidate": "remoteCandidate",
"Ice State": "state",
"Priority": "priority",
"Nominated": "nominated",
"Selected": "selected"
},
sort: function(key) {
var sorting = this.state.sorting;
var pairs = this.state.pairs.slice().sort(function(pair1, pair2) {
var value1 = pair1[key] ? pair1[key].toString() : "";
var value2 = pair2[key] ? pair2[key].toString() : "";
// Reverse sorting
if (key === sorting) {
return value2.localeCompare(value1);
}
return value1.localeCompare(value2);
}.bind(this));
sorting = (key === sorting) ? null : key;
this.setState({pairs: pairs, sorting: sorting});
},
generateSortHeadings: function() {
return Object.keys(this.sortHeadings).map(function(heading, i) {
var sortKey = this.sortHeadings[heading];
return (
<th>
<a href="#" onClick={this.sort.bind(this, sortKey)}>
{heading}
</a>
</th>
);
}.bind(this));
},
getInitialState: function() {
return {pairs: this.props.pairs, sorting: null};
},
render: function() {
return (
<table>
<tbody>
<tr>{this.generateSortHeadings()}</tr>
{this.state.pairs.map(function(pair, i) {
return <IceCandidatePair key={i} pair={pair} />;
})}
</tbody>
</table>
);
}
});
var IceCandidatePair = React.createClass({
render: function() {
var pair = this.props.pair;
return (
<tr>
<td>{pair.localCandidate}</td>
<td>{pair.remoteCandidate}</td>
<td>{pair.state}</td>
<td>{pair.priority}</td>
<td>{pair.nominated ? '✔' : null}</td>
<td>{pair.selected ? '✔' : null}</td>
</tr>
);
}
});
var SDP = React.createClass({
render: function() {
var type = labelize(this.props.type);
return (
<div>
<h4>{type} SDP</h4>
<pre>
{this.props.sdp}
</pre>
</div>
);
}
});
var RTPStats = React.createClass({
getRtpStats: function(report) {
var remoteRtpStats = {};
var rtpStats = [];
rtpStats = rtpStats.concat(report.inboundRTPStreamStats || []);
rtpStats = rtpStats.concat(report.outboundRTPStreamStats || []);
rtpStats.forEach(function(stats) {
if (stats.isRemote) {
remoteRtpStats[stats.id] = stats;
}
});
rtpStats.forEach(function(stats) {
if (stats.remoteId) {
stats.remoteRtpStats = remoteRtpStats[stats.remoteId];
}
});
return rtpStats;
},
dumpAvStats: function(stats) {
var statsString = "";
if (stats.mozAvSyncDelay) {
statsString += `A/V sync: ${stats.mozAvSyncDelay} ms `;
}
if (stats.mozJitterBufferDelay) {
statsString += `Jitter-buffer delay: ${stats.mozJitterBufferDelay} ms`;
}
return <div>{statsString}</div>;
},
dumpCoderStats: function(stats) {
var statsString = "";
var label;
if (stats.bitrateMean) {
statsString += ` Avg. bitrate: ${(stats.bitrateMean/1000000).toFixed(2)} Mbps`;
if (stats.bitrateStdDev) {
statsString += ` (${(stats.bitrateStdDev/1000000).toFixed(2)} SD)`;
}
}
if (stats.framerateMean) {
statsString += ` Avg. framerate: ${(stats.framerateMean).toFixed(2)} fps`;
if (stats.framerateStdDev) {
statsString += ` (${stats.framerateStdDev.toFixed(2)} SD)`;
}
}
if (stats.droppedFrames) {
statsString += ` Dropped frames: ${stats.droppedFrames}`;
}
if (stats.discardedPackets) {
statsString += ` Discarded packets: ${stats.discardedPackets}`;
}
if (statsString) {
label = (stats.packetsReceived)? " Decoder:" : " Encoder:";
statsString = label + statsString;
}
return <div>{statsString}</div>;
},
dumpRtpStats: function(stats, type) {
var label = labelize(type);
var time = new Date(stats.timestamp).toTimeString();
var statsString = `${label}: ${time} ${stats.type} SSRC: ${stats.ssrc}`;
if (stats.packetsReceived) {
statsString += ` Received: ${stats.packetsReceived} packets`;
if (stats.bytesReceived) {
statsString += ` (${round00(stats.bytesReceived/1024)} Kb)`;
}
statsString += ` Lost: ${stats.packetsLost} Jitter: ${stats.jitter}`;
if (stats.mozRtt) {
statsString += ` RTT: ${stats.mozRtt} ms`;
}
} else if (stats.packetsSent) {
statsString += ` Sent: ${stats.packetsSent} packets`;
if (stats.bytesSent) {
statsString += ` (${round00(stats.bytesSent/1024)} Kb)`;
}
}
return <div>{statsString}</div>;
},
render: function() {
var rtpStats = this.getRtpStats(this.props.report);
return (
<div>{
rtpStats.map(function(stats) {
var isAvStats = (stats.mozAvSyncDelay || stats.mozJitterBufferDelay);
var remoteRtpStats = stats.remoteId ? stats.remoteRtpStats : null;
return [
<h5>{stats.id}</h5>,
isAvStats ? this.dumpAvStats(stats) : null,
this.dumpCoderStats(stats),
this.dumpRtpStats(stats, "local"),
remoteRtpStats ? this.dumpRtpStats(remoteRtpStats, "remote") : null
]
}.bind(this))
}</div>
);
}
});
var LogsButton = React.createClass({
render: function() {
return (
<button onClick={this.props.handler}>Connection log</button>
);
}
});
var DebugButton = React.createClass({
getInitialState: function() {
var on = (WebrtcGlobalInformation.debugLevel > 0);
return {on: on};
},
onClick: function() {
if (this.state.on)
WebrtcGlobalInformation.debugLevel = 0;
else
WebrtcGlobalInformation.debugLevel = 65535;
this.setState({on: !this.state.on});
},
render: function() {
return (
<button onClick={this.onClick}>{
this.state.on ? "Stop debug mode" : "Start debug mode"
}</button>
);
}
});
var AECButton = React.createClass({
getInitialState: function() {
return {on: WebrtcGlobalInformation.aecDebug};
},
onClick: function() {
WebrtcGlobalInformation.aecDebug = !this.state.on;
this.setState({on: WebrtcGlobalInformation.aecDebug});
},
render: function() {
return (
<button onClick={this.onClick}>{
this.state.on ? "Stop AEC logging" : "Start AEC logging"
}</button>
);
}
});
var Logs = React.createClass({
render: function() {
return (
<div>
<h3>Logging:</h3>
<div>{
this.props.logs.map(function(line, i) {
return <div key={i}>{line}</div>;
})
}</div>
</div>
);
}
});
function iceCandidateMapping(iceCandidates) {
var candidates = {};
iceCandidates = iceCandidates || [];
iceCandidates.forEach(function(candidate) {
candidates[candidate.id] = candidate;
});
return candidates;
}
function round00(num) {
return Math.round(num * 100) / 100;
}
function labelize(label) {
return `${label.charAt(0).toUpperCase()}${label.slice(1)}`;
}
function candidateToString(c) {
var type = c.candidateType;
if (c.type == "localcandidate" && c.candidateType == "relayed") {
type = `${c.candidateType}-${c.mozLocalTransport}`;
}
return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`
}
function onLoad() {
WebrtcGlobalInformation.getAllStats(function(globalReport) {
var reports = globalReport.reports;
React.renderComponent(<AboutWebRTC reports={reports}/>,
document.querySelector("#body"));
});
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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 html [
<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Webrtc Internals</title>
<link rel="stylesheet" type="text/css" media="all" href="chrome://global/content/aboutwebrtc/aboutWebrtc.css"/>
</head>
<body id="body" onload="onLoad()">
</body>
<script type="text/javascript" src="chrome://browser/content/loop/shared/libs/react-0.11.2.js"></script>
<script type="text/javascript;version=1.8" src="chrome://global/content/aboutwebrtc/aboutWebrtc.js"/>
</html>

View File

@ -20,7 +20,9 @@ toolkit.jar:
content/global/aboutRights-unbranded.xhtml (aboutRights-unbranded.xhtml)
content/global/aboutNetworking.js
content/global/aboutNetworking.xhtml
content/global/aboutWebrtc.xhtml
content/global/aboutwebrtc/aboutWebrtc.css (aboutwebrtc/aboutWebrtc.css)
content/global/aboutwebrtc/aboutWebrtc.js (aboutwebrtc/aboutWebrtc.js)
content/global/aboutwebrtc/aboutWebrtc.xhtml (aboutwebrtc/aboutWebrtc.xhtml)
* content/global/aboutSupport.js
* content/global/aboutSupport.xhtml
* content/global/aboutTelemetry.js