forked from mirrors/gecko-dev
Changing the units roundTripTime is reported in from milliseconds to seconds Differential Revision: https://phabricator.services.mozilla.com/D19544 --HG-- extra : moz-landing-system : lando
955 lines
27 KiB
JavaScript
955 lines
27 KiB
JavaScript
/* 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";
|
|
|
|
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
ChromeUtils.defineModuleGetter(this, "FileUtils",
|
|
"resource://gre/modules/FileUtils.jsm");
|
|
XPCOMUtils.defineLazyServiceGetter(this, "FilePicker",
|
|
"@mozilla.org/filepicker;1", "nsIFilePicker");
|
|
XPCOMUtils.defineLazyGetter(this, "strings", () => {
|
|
return Services.strings.createBundle("chrome://global/locale/aboutWebrtc.properties");
|
|
});
|
|
|
|
const getString = strings.GetStringFromName;
|
|
const formatString = strings.formatStringFromName;
|
|
|
|
const LOGFILE_NAME_DEFAULT = "aboutWebrtc.html";
|
|
const WEBRTC_TRACE_ALL = 65535;
|
|
|
|
function getStats() {
|
|
return new Promise(resolve =>
|
|
WebrtcGlobalInformation.getAllStats(stats => resolve(stats)));
|
|
}
|
|
|
|
function getLog() {
|
|
return new Promise(resolve =>
|
|
WebrtcGlobalInformation.getLogging("", log => resolve(log)));
|
|
}
|
|
|
|
// Begin initial data queries as page loads. Store returned Promises for
|
|
// later use.
|
|
var reportsRetrieved = getStats();
|
|
var logRetrieved = getLog();
|
|
|
|
function onLoad() {
|
|
document.title = getString("document_title");
|
|
let controls = document.querySelector("#controls");
|
|
if (controls) {
|
|
let set = ControlSet.render();
|
|
ControlSet.add(new SavePage());
|
|
ControlSet.add(new DebugMode());
|
|
ControlSet.add(new AecLogging());
|
|
controls.appendChild(set);
|
|
}
|
|
|
|
let contentElem = document.querySelector("#content");
|
|
if (!contentElem) {
|
|
return;
|
|
}
|
|
|
|
let contentInit = function(data) {
|
|
AboutWebRTC.init(onClearStats, onClearLog);
|
|
AboutWebRTC.render(contentElem, data);
|
|
};
|
|
|
|
Promise.all([reportsRetrieved, logRetrieved])
|
|
.then(([stats, log]) => contentInit({reports: stats.reports, log}))
|
|
.catch(error => contentInit({error}));
|
|
}
|
|
|
|
function onClearLog() {
|
|
WebrtcGlobalInformation.clearLogging();
|
|
getLog()
|
|
.then(log => AboutWebRTC.refresh({log}))
|
|
.catch(error => AboutWebRTC.refresh({logError: error}));
|
|
}
|
|
|
|
function onClearStats() {
|
|
WebrtcGlobalInformation.clearAllStats();
|
|
getStats()
|
|
.then(stats => AboutWebRTC.refresh({reports: stats.reports}))
|
|
.catch(error => AboutWebRTC.refresh({reportError: error}));
|
|
}
|
|
|
|
var ControlSet = {
|
|
render() {
|
|
let controls = renderElement("div", null, {className: "controls"});
|
|
this.controlSection = renderElement("div", null, {className: "control"});
|
|
this.messageSection = renderElement("div", null, {className: "message"});
|
|
|
|
controls.appendChild(this.controlSection);
|
|
controls.appendChild(this.messageSection);
|
|
|
|
return controls;
|
|
},
|
|
|
|
add(controlObj) {
|
|
let [controlElem, messageElem] = controlObj.render();
|
|
this.controlSection.appendChild(controlElem);
|
|
this.messageSection.appendChild(messageElem);
|
|
},
|
|
};
|
|
|
|
function Control() {
|
|
this._label = null;
|
|
this._message = null;
|
|
this._messageHeader = null;
|
|
}
|
|
|
|
Control.prototype = {
|
|
render() {
|
|
let controlElem = document.createElement("button");
|
|
let messageElem = document.createElement("p");
|
|
|
|
this.ctrl = controlElem;
|
|
controlElem.onclick = this.onClick.bind(this);
|
|
this.msg = messageElem;
|
|
this.update();
|
|
|
|
return [controlElem, messageElem];
|
|
},
|
|
|
|
set label(val) {
|
|
return this._labelVal = val || "\xA0";
|
|
},
|
|
|
|
get label() {
|
|
return this._labelVal;
|
|
},
|
|
|
|
set message(val) {
|
|
return this._messageVal = val;
|
|
},
|
|
|
|
get message() {
|
|
return this._messageVal;
|
|
},
|
|
|
|
update() {
|
|
this.ctrl.textContent = this._label;
|
|
|
|
this.msg.textContent = "";
|
|
if (this._message) {
|
|
this.msg.appendChild(Object.assign(document.createElement("span"), {
|
|
className: "info-label",
|
|
textContent: `${this._messageHeader}: `,
|
|
}));
|
|
this.msg.appendChild(document.createTextNode(this._message));
|
|
}
|
|
},
|
|
|
|
onClick(event) {
|
|
return true;
|
|
},
|
|
};
|
|
|
|
function SavePage() {
|
|
Control.call(this);
|
|
this._messageHeader = getString("save_page_label");
|
|
this._label = getString("save_page_label");
|
|
}
|
|
|
|
SavePage.prototype = Object.create(Control.prototype);
|
|
SavePage.prototype.constructor = SavePage;
|
|
|
|
SavePage.prototype.onClick = function() {
|
|
let content = document.querySelector("#content");
|
|
|
|
if (!content)
|
|
return;
|
|
|
|
FoldEffect.expandAll();
|
|
FilePicker.init(window, getString("save_page_dialog_title"), FilePicker.modeSave);
|
|
FilePicker.defaultString = LOGFILE_NAME_DEFAULT;
|
|
FilePicker.open(rv => {
|
|
if (rv == FilePicker.returnOK || rv == FilePicker.returnReplace) {
|
|
let fout = FileUtils.openAtomicFileOutputStream(
|
|
FilePicker.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE);
|
|
|
|
let nodes = content.querySelectorAll(".no-print");
|
|
let noPrintList = [];
|
|
for (let node of nodes) {
|
|
noPrintList.push(node);
|
|
node.style.setProperty("display", "none");
|
|
}
|
|
|
|
fout.write(content.outerHTML, content.outerHTML.length);
|
|
FileUtils.closeAtomicFileOutputStream(fout);
|
|
|
|
for (let node of noPrintList) {
|
|
node.style.removeProperty("display");
|
|
}
|
|
|
|
this._message = formatString("save_page_msg", [FilePicker.file.path], 1);
|
|
this.update();
|
|
}
|
|
});
|
|
};
|
|
|
|
function DebugMode() {
|
|
Control.call(this);
|
|
this._messageHeader = getString("debug_mode_msg_label");
|
|
|
|
if (WebrtcGlobalInformation.debugLevel > 0) {
|
|
this.onState();
|
|
} else {
|
|
this._label = getString("debug_mode_off_state_label");
|
|
this._message = null;
|
|
}
|
|
}
|
|
|
|
DebugMode.prototype = Object.create(Control.prototype);
|
|
DebugMode.prototype.constructor = DebugMode;
|
|
|
|
DebugMode.prototype.onState = function() {
|
|
this._label = getString("debug_mode_on_state_label");
|
|
try {
|
|
let file = Services.prefs.getCharPref("media.webrtc.debug.log_file");
|
|
this._message = formatString("debug_mode_on_state_msg", [file], 1);
|
|
} catch (e) {
|
|
this._message = null;
|
|
}
|
|
};
|
|
|
|
DebugMode.prototype.offState = function() {
|
|
this._label = getString("debug_mode_off_state_label");
|
|
try {
|
|
let file = Services.prefs.getCharPref("media.webrtc.debug.log_file");
|
|
this._message = formatString("debug_mode_off_state_msg", [file], 1);
|
|
} catch (e) {
|
|
this._message = null;
|
|
}
|
|
};
|
|
|
|
DebugMode.prototype.onClick = function() {
|
|
if (WebrtcGlobalInformation.debugLevel > 0) {
|
|
WebrtcGlobalInformation.debugLevel = 0;
|
|
this.offState();
|
|
} else {
|
|
WebrtcGlobalInformation.debugLevel = WEBRTC_TRACE_ALL;
|
|
this.onState();
|
|
}
|
|
|
|
this.update();
|
|
};
|
|
|
|
function AecLogging() {
|
|
Control.call(this);
|
|
this._messageHeader = getString("aec_logging_msg_label");
|
|
|
|
if (WebrtcGlobalInformation.aecDebug) {
|
|
this.onState();
|
|
} else {
|
|
this._label = getString("aec_logging_off_state_label");
|
|
this._message = null;
|
|
}
|
|
}
|
|
|
|
AecLogging.prototype = Object.create(Control.prototype);
|
|
AecLogging.prototype.constructor = AecLogging;
|
|
|
|
AecLogging.prototype.offState = function() {
|
|
this._label = getString("aec_logging_off_state_label");
|
|
try {
|
|
let file = WebrtcGlobalInformation.aecDebugLogDir;
|
|
this._message = formatString("aec_logging_off_state_msg", [file], 1);
|
|
} catch (e) {
|
|
this._message = null;
|
|
}
|
|
};
|
|
|
|
AecLogging.prototype.onState = function() {
|
|
this._label = getString("aec_logging_on_state_label");
|
|
try {
|
|
this._message = getString("aec_logging_on_state_msg");
|
|
} catch (e) {
|
|
this._message = null;
|
|
}
|
|
};
|
|
|
|
AecLogging.prototype.onClick = function() {
|
|
if (WebrtcGlobalInformation.aecDebug) {
|
|
WebrtcGlobalInformation.aecDebug = false;
|
|
this.offState();
|
|
} else {
|
|
WebrtcGlobalInformation.aecDebug = true;
|
|
this.onState();
|
|
}
|
|
this.update();
|
|
};
|
|
|
|
var AboutWebRTC = {
|
|
_reports: [],
|
|
_log: [],
|
|
|
|
init(onClearStats, onClearLog) {
|
|
this._onClearStats = onClearStats;
|
|
this._onClearLog = onClearLog;
|
|
},
|
|
|
|
render(parent, data) {
|
|
this._content = parent;
|
|
this._setData(data);
|
|
|
|
if (data.error) {
|
|
parent.appendChild(renderElement("h3", getString("cannot_retrieve_log")));
|
|
parent.appendChild(
|
|
renderElement("p", `${data.error.name}: ${data.error.message}`));
|
|
return;
|
|
}
|
|
|
|
this._peerConnections = this.renderPeerConnections();
|
|
this._connectionLog = this.renderConnectionLog();
|
|
this._content.appendChild(this._peerConnections);
|
|
this._content.appendChild(this._connectionLog);
|
|
},
|
|
|
|
_setData(data) {
|
|
if (data.reports) {
|
|
this._reports = data.reports;
|
|
}
|
|
|
|
if (data.log) {
|
|
this._log = data.log;
|
|
}
|
|
},
|
|
|
|
refresh(data) {
|
|
this._setData(data);
|
|
let pc = this._peerConnections;
|
|
this._peerConnections = this.renderPeerConnections();
|
|
let log = this._connectionLog;
|
|
this._connectionLog = this.renderConnectionLog();
|
|
this._content.replaceChild(this._peerConnections, pc);
|
|
this._content.replaceChild(this._connectionLog, log);
|
|
},
|
|
|
|
renderPeerConnections() {
|
|
let connections = renderElement("div", null, {className: "stats"});
|
|
|
|
let heading = renderElement("span", null, {className: "section-heading"});
|
|
heading.appendChild(renderElement("h3", getString("stats_heading")));
|
|
|
|
heading.appendChild(renderElement("button", getString("stats_clear"), {
|
|
className: "no-print",
|
|
onclick: this._onClearStats,
|
|
}));
|
|
connections.appendChild(heading);
|
|
|
|
if (!this._reports || !this._reports.length) {
|
|
return connections;
|
|
}
|
|
|
|
let reports = [...this._reports];
|
|
reports.sort((a, b) => b.timestamp - a.timestamp);
|
|
for (let report of reports) {
|
|
let peerConnection = new PeerConnection(report);
|
|
connections.appendChild(peerConnection.render());
|
|
}
|
|
|
|
return connections;
|
|
},
|
|
|
|
renderConnectionLog() {
|
|
let content = renderElement("div", null, {className: "log"});
|
|
|
|
let heading = renderElement("span", null, {className: "section-heading"});
|
|
heading.appendChild(renderElement("h3", getString("log_heading")));
|
|
heading.appendChild(renderElement("button", getString("log_clear"), {
|
|
className: "no-print",
|
|
onclick: this._onClearLog,
|
|
}));
|
|
content.appendChild(heading);
|
|
|
|
if (!this._log || !this._log.length) {
|
|
return content;
|
|
}
|
|
|
|
let div = new FoldableSection(content, {
|
|
showMsg: getString("log_show_msg"),
|
|
hideMsg: getString("log_hide_msg"),
|
|
}).render();
|
|
|
|
for (let line of this._log) {
|
|
div.appendChild(renderElement("p", line));
|
|
}
|
|
|
|
content.appendChild(div);
|
|
return content;
|
|
},
|
|
};
|
|
|
|
function PeerConnection(report) {
|
|
this._report = report;
|
|
}
|
|
|
|
PeerConnection.prototype = {
|
|
render() {
|
|
let pc = renderElement("div", null, {className: "peer-connection"});
|
|
pc.appendChild(this.renderHeading());
|
|
|
|
let div = new FoldableSection(pc).render();
|
|
|
|
div.appendChild(this.renderDesc());
|
|
div.appendChild(new ICEStats(this._report).render());
|
|
div.appendChild(new SDPStats(this._report).render());
|
|
div.appendChild(new RTPStats(this._report).render());
|
|
|
|
pc.appendChild(div);
|
|
return pc;
|
|
},
|
|
|
|
renderHeading() {
|
|
let pcInfo = this.getPCInfo(this._report);
|
|
let heading = document.createElement("h3");
|
|
let now = new Date(this._report.timestamp).toString();
|
|
heading.textContent =
|
|
`[ ${pcInfo.id} ] ${pcInfo.url} ${pcInfo.closed ? `(${getString("connection_closed")})` : ""} ${now}`;
|
|
return heading;
|
|
},
|
|
|
|
renderDesc() {
|
|
let info = document.createElement("div");
|
|
|
|
info.appendChild(
|
|
renderElement("span", `${getString("peer_connection_id_label")}: `), {
|
|
className: "info-label",
|
|
});
|
|
|
|
info.appendChild(renderElement("span", this._report.pcid, {
|
|
className: "info-body",
|
|
}));
|
|
|
|
return info;
|
|
},
|
|
|
|
getPCInfo(report) {
|
|
return {
|
|
id: report.pcid.match(/id=(\S+)/)[1],
|
|
url: report.pcid.match(/url=([^)]+)/)[1],
|
|
closed: report.closed,
|
|
};
|
|
},
|
|
};
|
|
|
|
function renderElement(elemName, elemText, options = {}) {
|
|
let elem = document.createElement(elemName);
|
|
// check for null instead of using elemText || "" so we don't hide
|
|
// elements with 0 values
|
|
if (elemText != null) {
|
|
elem.textContent = elemText;
|
|
}
|
|
Object.assign(elem, options);
|
|
return elem;
|
|
}
|
|
|
|
function SDPStats(report) {
|
|
this._report = report;
|
|
}
|
|
|
|
SDPStats.prototype = {
|
|
render() {
|
|
let div = document.createElement("div");
|
|
div.appendChild(renderElement("h4", getString("sdp_heading")));
|
|
|
|
let offerLabel = `(${getString("offer")})`;
|
|
let answerLabel = `(${getString("answer")})`;
|
|
let localSdpHeading =
|
|
`${getString("local_sdp_heading")} ${this._report.offerer ? offerLabel : answerLabel}`;
|
|
let remoteSdpHeading =
|
|
`${getString("remote_sdp_heading")} ${this._report.offerer ? answerLabel : offerLabel}`;
|
|
|
|
div.appendChild(renderElement("h5", localSdpHeading));
|
|
div.appendChild(renderElement("pre", this._report.localSdp));
|
|
|
|
div.appendChild(renderElement("h5", remoteSdpHeading));
|
|
div.appendChild(renderElement("pre", this._report.remoteSdp));
|
|
|
|
return div;
|
|
},
|
|
};
|
|
|
|
function RTPStats(report) {
|
|
this._report = report;
|
|
this._stats = [];
|
|
}
|
|
|
|
RTPStats.prototype = {
|
|
render() {
|
|
let div = document.createElement("div");
|
|
div.appendChild(renderElement("h4", getString("rtp_stats_heading")));
|
|
|
|
this.generateRTPStats();
|
|
|
|
for (let statSet of this._stats) {
|
|
div.appendChild(this.renderRTPStatSet(statSet));
|
|
}
|
|
|
|
return div;
|
|
},
|
|
|
|
generateRTPStats() {
|
|
let remoteRtpStats = {};
|
|
let rtpStats = [].concat((this._report.inboundRTPStreamStats || []),
|
|
(this._report.outboundRTPStreamStats || []));
|
|
|
|
// Generate an id-to-streamStat index for each streamStat that is marked
|
|
// as a remote. This will be used next to link the remote to its local side.
|
|
for (let stats of rtpStats) {
|
|
if (stats.isRemote) {
|
|
remoteRtpStats[stats.id] = stats;
|
|
}
|
|
}
|
|
|
|
// If a streamStat has a remoteId attribute, create a remoteRtpStats
|
|
// attribute that references the remote streamStat entry directly.
|
|
// That is, the index generated above is merged into the returned list.
|
|
for (let stats of rtpStats) {
|
|
if (stats.remoteId) {
|
|
stats.remoteRtpStats = remoteRtpStats[stats.remoteId];
|
|
}
|
|
}
|
|
|
|
this._stats = rtpStats;
|
|
},
|
|
|
|
renderCoderStats(stats) {
|
|
let statsString = "";
|
|
let label;
|
|
|
|
if (stats.bitrateMean) {
|
|
statsString += ` ${getString("avg_bitrate_label")}: ${(stats.bitrateMean / 1000000).toFixed(2)} Mbps`;
|
|
if (stats.bitrateStdDev) {
|
|
statsString += ` (${(stats.bitrateStdDev / 1000000).toFixed(2)} SD)`;
|
|
}
|
|
}
|
|
|
|
if (stats.framerateMean) {
|
|
statsString += ` ${getString("avg_framerate_label")}: ${(stats.framerateMean).toFixed(2)} fps`;
|
|
if (stats.framerateStdDev) {
|
|
statsString += ` (${stats.framerateStdDev.toFixed(2)} SD)`;
|
|
}
|
|
}
|
|
|
|
if (stats.droppedFrames) {
|
|
statsString += ` ${getString("dropped_frames_label")}: ${stats.droppedFrames}`;
|
|
}
|
|
if (stats.discardedPackets) {
|
|
statsString += ` ${getString("discarded_packets_label")}: ${stats.discardedPackets}`;
|
|
}
|
|
|
|
if (statsString) {
|
|
label = (stats.packetsReceived ? ` ${getString("decoder_label")}:` : ` ${getString("encoder_label")}:`);
|
|
statsString = label + statsString;
|
|
}
|
|
|
|
return renderElement("p", statsString);
|
|
},
|
|
|
|
renderTransportStats(stats, typeLabel) {
|
|
let time = new Date(stats.timestamp).toTimeString();
|
|
let statsString = `${typeLabel}: ${time} ${stats.type} SSRC: ${stats.ssrc}`;
|
|
|
|
if (stats.packetsReceived) {
|
|
statsString += ` ${getString("received_label")}: ${stats.packetsReceived} ${getString("packets")}`;
|
|
|
|
if (stats.bytesReceived) {
|
|
statsString += ` (${(stats.bytesReceived / 1024).toFixed(2)} Kb)`;
|
|
}
|
|
|
|
statsString += ` ${getString("lost_label")}: ${stats.packetsLost} ${getString("jitter_label")}: ${stats.jitter}`;
|
|
|
|
if (stats.roundTripTime) {
|
|
statsString += ` RTT: ${stats.roundTripTime * 1000} ms`;
|
|
}
|
|
} else if (stats.packetsSent) {
|
|
statsString += ` ${getString("sent_label")}: ${stats.packetsSent} ${getString("packets")}`;
|
|
if (stats.bytesSent) {
|
|
statsString += ` (${(stats.bytesSent / 1024).toFixed(2)} Kb)`;
|
|
}
|
|
}
|
|
|
|
return renderElement("p", statsString);
|
|
},
|
|
|
|
renderRTPStatSet(stats) {
|
|
let div = document.createElement("div");
|
|
div.appendChild(renderElement("h5", stats.id));
|
|
|
|
div.appendChild(this.renderCoderStats(stats));
|
|
div.appendChild(this.renderTransportStats(stats, getString("typeLocal")));
|
|
|
|
if (stats.remoteId && stats.remoteRtpStats) {
|
|
div.appendChild(this.renderTransportStats(stats.remoteRtpStats, getString("typeRemote")));
|
|
}
|
|
|
|
return div;
|
|
},
|
|
};
|
|
|
|
function ICEStats(report) {
|
|
this._report = report;
|
|
}
|
|
|
|
ICEStats.prototype = {
|
|
render() {
|
|
let div = document.createElement("div");
|
|
div.appendChild(renderElement("h4", getString("ice_stats_heading")));
|
|
|
|
div.appendChild(this.renderICECandidateTable());
|
|
// add just a bit of vertical space between the restart/rollback
|
|
// counts and the ICE candidate pair table above.
|
|
div.appendChild(document.createElement("br"));
|
|
div.appendChild(this.renderIceMetric("ice_restart_count_label",
|
|
this._report.iceRestarts));
|
|
div.appendChild(this.renderIceMetric("ice_rollback_count_label",
|
|
this._report.iceRollbacks));
|
|
|
|
div.appendChild(this.renderRawICECandidateSection());
|
|
|
|
return div;
|
|
},
|
|
|
|
renderICECandidateTable() {
|
|
let caption = renderElement("caption", null, {className: "no-print"});
|
|
|
|
// This takes the caption message with the replacement token, breaks
|
|
// it around the token, and builds the spans for each portion of the
|
|
// caption. This is to allow localization to put the color name for
|
|
// the highlight wherever it is appropriate in the translated string
|
|
// while avoiding innerHTML warnings from eslint.
|
|
let captionTemplate = getString("trickle_caption_msg2");
|
|
let [start, end] = captionTemplate.split(/%(?:1\$)?S/);
|
|
|
|
// only append span if non-whitespace chars present
|
|
if (/\S/.test(start)) {
|
|
caption.appendChild(
|
|
renderElement("span", `${start}`));
|
|
}
|
|
caption.appendChild(
|
|
renderElement("span", getString("trickle_highlight_color_name2"), {
|
|
className: "trickled",
|
|
}));
|
|
// only append span if non-whitespace chars present
|
|
if (/\S/.test(end)) {
|
|
caption.appendChild(
|
|
renderElement("span", `${end}`));
|
|
}
|
|
|
|
let stats = this.generateICEStats();
|
|
// don't use |stat.x || ""| here because it hides 0 values
|
|
let tbody = stats.map(stat => [
|
|
stat["local-candidate"],
|
|
stat["remote-candidate"],
|
|
stat.componentId,
|
|
stat.state,
|
|
stat.priority,
|
|
stat.nominated,
|
|
stat.selected,
|
|
stat.bytesSent,
|
|
stat.bytesReceived,
|
|
].map(entry => Object.is(entry, undefined) ? "" : entry));
|
|
|
|
let statsTable = new SimpleTable(
|
|
["local_candidate", "remote_candidate",
|
|
"ice_component_id", "ice_state",
|
|
"priority", "nominated", "selected",
|
|
"ice_pair_bytes_sent", "ice_pair_bytes_received",
|
|
].map(columnName => getString(columnName)),
|
|
tbody, caption).render();
|
|
|
|
// after rendering the table, we need to change the class name for each
|
|
// candidate pair's local or remote candidate if it was trickled.
|
|
stats.forEach((stat, index) => {
|
|
// look at statsTable row index + 1 to skip column headers
|
|
let rowIndex = index + 1;
|
|
if (stat["remote-trickled"]) {
|
|
statsTable.rows[rowIndex].cells[1].className = "trickled";
|
|
}
|
|
if (stat["local-trickled"]) {
|
|
statsTable.rows[rowIndex].cells[0].className = "trickled";
|
|
}
|
|
});
|
|
|
|
// if the next row's component id changes, mark the bottom of the
|
|
// current row with a thin, black border to differentiate the
|
|
// component id grouping.
|
|
let rowCount = statsTable.rows.length - 1;
|
|
for (var i = 0; i < rowCount; i++) {
|
|
if (statsTable.rows[i].cells[2].innerHTML !==
|
|
statsTable.rows[i + 1].cells[2].innerHTML) {
|
|
statsTable.rows[i].className = "bottom-border";
|
|
}
|
|
}
|
|
|
|
return statsTable;
|
|
},
|
|
|
|
renderRawICECandidates() {
|
|
let div = document.createElement("div");
|
|
|
|
let tbody = [];
|
|
let rows = this.generateRawICECandidates();
|
|
for (let row of rows) {
|
|
tbody.push([row.local, row.remote]);
|
|
}
|
|
|
|
let statsTable = new SimpleTable(
|
|
[getString("raw_local_candidate"), getString("raw_remote_candidate")],
|
|
tbody).render();
|
|
|
|
// we want different formatting on the raw stats table (namely, left-align)
|
|
statsTable.className = "raw-candidate";
|
|
div.appendChild(statsTable);
|
|
|
|
return div;
|
|
},
|
|
|
|
renderRawICECandidateSection() {
|
|
let section = document.createElement("div");
|
|
section.appendChild(
|
|
renderElement("h4", getString("raw_candidates_heading")));
|
|
|
|
let div = new FoldableSection(section, {
|
|
showMsg: getString("raw_cand_show_msg"),
|
|
hideMsg: getString("raw_cand_hide_msg"),
|
|
}).render();
|
|
|
|
div.appendChild(this.renderRawICECandidates());
|
|
|
|
section.appendChild(div);
|
|
|
|
return section;
|
|
},
|
|
|
|
generateRawICECandidates() {
|
|
let rows = [];
|
|
let row;
|
|
|
|
let rawLocals = this._report.rawLocalCandidates.sort();
|
|
// add to a Set (to remove duplicates) because some of these come from
|
|
// candidates in use and some come from the raw trickled candidates
|
|
// received that may have been dropped because no stream was found or
|
|
// they were for a component id that was too high.
|
|
let rawRemotes = [...new Set(this._report.rawRemoteCandidates)].sort();
|
|
let rowCount = Math.max(rawLocals.length, rawRemotes.length);
|
|
for (var i = 0; i < rowCount; i++) {
|
|
let rawLocal = rawLocals[i];
|
|
let rawRemote = rawRemotes[i];
|
|
row = {
|
|
local: rawLocal || "",
|
|
remote: rawRemote || "",
|
|
};
|
|
rows.push(row);
|
|
}
|
|
return rows;
|
|
},
|
|
|
|
renderIceMetric(labelName, value) {
|
|
let info = document.createElement("div");
|
|
|
|
info.appendChild(
|
|
renderElement("span", `${getString(labelName)}: `, {
|
|
className: "info-label",
|
|
}));
|
|
info.appendChild(
|
|
renderElement("span", value, {className: "info-body"}));
|
|
|
|
return info;
|
|
},
|
|
|
|
generateICEStats() {
|
|
// Create an index based on candidate ID for each element in the
|
|
// iceCandidateStats array.
|
|
let candidates = new Map();
|
|
|
|
for (let candidate of this._report.iceCandidateStats) {
|
|
candidates.set(candidate.id, candidate);
|
|
}
|
|
|
|
// a method to see if a given candidate id is in the array of tickled
|
|
// candidates.
|
|
let isTrickled = id => [...this._report.trickledIceCandidateStats].some(
|
|
candidate => candidate.id == id);
|
|
|
|
// A component may have a remote or local candidate address or both.
|
|
// Combine those with both; these will be the peer candidates.
|
|
let matched = {};
|
|
let stats = [];
|
|
let stat;
|
|
|
|
for (let pair of this._report.iceCandidatePairStats) {
|
|
let local = candidates.get(pair.localCandidateId);
|
|
let remote = candidates.get(pair.remoteCandidateId);
|
|
if (local) {
|
|
stat = {
|
|
["local-candidate"]: this.candidateToString(local),
|
|
componentId: pair.componentId,
|
|
state: pair.state,
|
|
priority: pair.priority,
|
|
nominated: pair.nominated,
|
|
selected: pair.selected,
|
|
bytesSent: pair.bytesSent,
|
|
bytesReceived: pair.bytesReceived,
|
|
};
|
|
matched[local.id] = true;
|
|
if (isTrickled(local.id)) {
|
|
stat["local-trickled"] = true;
|
|
}
|
|
|
|
if (remote) {
|
|
stat["remote-candidate"] = this.candidateToString(remote);
|
|
matched[remote.id] = true;
|
|
if (isTrickled(remote.id)) {
|
|
stat["remote-trickled"] = true;
|
|
}
|
|
}
|
|
stats.push(stat);
|
|
}
|
|
}
|
|
|
|
// sort (group by) componentId first, then bytesSent if available, else by
|
|
// priority
|
|
return stats.sort((a, b) => {
|
|
if (a.componentId != b.componentId) {
|
|
return a.componentId - b.componentId;
|
|
}
|
|
return (b.bytesSent ?
|
|
(b.bytesSent || 0) - (a.bytesSent || 0) :
|
|
(b.priority || 0) - (a.priority || 0)
|
|
);
|
|
});
|
|
},
|
|
|
|
candidateToString(c) {
|
|
if (!c) {
|
|
return "*";
|
|
}
|
|
|
|
var type = c.candidateType;
|
|
|
|
if (c.type == "local-candidate" && c.candidateType == "relayed") {
|
|
type = `${c.candidateType}-${c.relayProtocol}`;
|
|
}
|
|
|
|
return `${c.address}:${c.port}/${c.transport}(${type})`;
|
|
},
|
|
};
|
|
|
|
function FoldableSection(parentElement, options = {}) {
|
|
this._foldableElement = document.createElement("div");
|
|
if (parentElement) {
|
|
let sectionCtrl = renderElement("div", null, {
|
|
className: "section-ctrl no-print",
|
|
});
|
|
let foldEffect = new FoldEffect(this._foldableElement, options);
|
|
sectionCtrl.appendChild(foldEffect.render());
|
|
parentElement.appendChild(sectionCtrl);
|
|
}
|
|
}
|
|
|
|
FoldableSection.prototype = {
|
|
render() {
|
|
return this._foldableElement;
|
|
},
|
|
};
|
|
|
|
function SimpleTable(heading, data, caption) {
|
|
this._heading = heading || [];
|
|
this._data = data;
|
|
this._caption = caption;
|
|
}
|
|
|
|
SimpleTable.prototype = {
|
|
renderRow(list, header) {
|
|
let row = document.createElement("tr");
|
|
let elemType = (header ? "th" : "td");
|
|
|
|
for (let elem of list) {
|
|
row.appendChild(renderElement(elemType, elem));
|
|
}
|
|
|
|
return row;
|
|
},
|
|
|
|
render() {
|
|
let table = document.createElement("table");
|
|
|
|
if (this._caption) {
|
|
table.appendChild(this._caption);
|
|
}
|
|
|
|
if (this._heading) {
|
|
table.appendChild(this.renderRow(this._heading, true));
|
|
}
|
|
|
|
for (let row of this._data) {
|
|
table.appendChild(this.renderRow(row));
|
|
}
|
|
|
|
return table;
|
|
},
|
|
};
|
|
|
|
function FoldEffect(targetElem, options = {}) {
|
|
if (targetElem) {
|
|
this._showMsg = "\u25BC " + (options.showMsg || getString("fold_show_msg"));
|
|
this._showHint = options.showHint || getString("fold_show_hint");
|
|
this._hideMsg = "\u25B2 " + (options.hideMsg || getString("fold_hide_msg"));
|
|
this._hideHint = options.hideHint || getString("fold_hide_hint");
|
|
this._target = targetElem;
|
|
}
|
|
}
|
|
|
|
FoldEffect.prototype = {
|
|
render() {
|
|
this._target.classList.add("fold-target");
|
|
|
|
let ctrl = renderElement("div", null, {className: "fold-trigger"});
|
|
this._trigger = ctrl;
|
|
ctrl.addEventListener("click", this.onClick.bind(this));
|
|
this.close();
|
|
|
|
FoldEffect._sections.push(this);
|
|
return ctrl;
|
|
},
|
|
|
|
onClick() {
|
|
if (this._target.classList.contains("fold-closed")) {
|
|
this.open();
|
|
} else {
|
|
this.close();
|
|
}
|
|
return true;
|
|
},
|
|
|
|
open() {
|
|
this._target.classList.remove("fold-closed");
|
|
this._trigger.setAttribute("title", this._hideHint);
|
|
this._trigger.textContent = this._hideMsg;
|
|
},
|
|
|
|
close() {
|
|
this._target.classList.add("fold-closed");
|
|
this._trigger.setAttribute("title", this._showHint);
|
|
this._trigger.textContent = this._showMsg;
|
|
},
|
|
};
|
|
|
|
FoldEffect._sections = [];
|
|
|
|
FoldEffect.expandAll = function() {
|
|
for (let section of this._sections) {
|
|
section.open();
|
|
}
|
|
};
|
|
|
|
FoldEffect.collapseAll = function() {
|
|
for (let section of this._sections) {
|
|
section.close();
|
|
}
|
|
};
|