1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 const { XPCOMUtils } = ChromeUtils.import(
7 "resource://gre/modules/XPCOMUtils.jsm"
9 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
11 ChromeUtils.defineModuleGetter(
14 "resource://gre/modules/FileUtils.jsm"
16 XPCOMUtils.defineLazyServiceGetter(
19 "@mozilla.org/filepicker;1",
22 XPCOMUtils.defineLazyGetter(this, "strings", () =>
23 Services.strings.createBundle("chrome://global/locale/aboutWebrtc.properties")
26 const string = strings.GetStringFromName;
27 const format = strings.formatStringFromName;
28 const WGI = WebrtcGlobalInformation;
30 const LOGFILE_NAME_DEFAULT = "aboutWebrtc.html";
31 const WEBRTC_TRACE_ALL = 65535;
33 async function getStats() {
34 const { reports } = await new Promise(r => WGI.getAllStats(r));
35 return [...reports].sort((a, b) => b.timestamp - a.timestamp);
38 const getLog = () => new Promise(r => WGI.getLogging("", r));
40 const renderElement = (name, options) =>
41 Object.assign(document.createElement(name), options);
43 const renderText = (name, textContent, options) =>
44 renderElement(name, Object.assign({ textContent }, options));
46 const renderElements = (name, options, list) => {
47 const element = renderElement(name, options);
48 element.append(...list);
52 // Button control classes
60 this.ctrl = renderElement("button", { onclick: () => this.onClick() });
61 this.msg = renderElement("p");
63 return [this.ctrl, this.msg];
67 this.ctrl.textContent = this.label;
68 this.msg.textContent = "";
71 renderText("span", `${this.messageHeader}: `, {
72 className: "info-label",
80 class SavePage extends Control {
83 this.messageHeader = string("save_page_label");
84 this.label = string("save_page_label");
88 FoldEffect.expandAll();
91 string("save_page_dialog_title"),
94 FilePicker.defaultString = LOGFILE_NAME_DEFAULT;
95 const rv = await new Promise(r => FilePicker.open(r));
96 if (rv != FilePicker.returnOK && rv != FilePicker.returnReplace) {
99 const fout = FileUtils.openAtomicFileOutputStream(
101 FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE
103 const content = document.querySelector("#content");
104 const noPrintList = [...content.querySelectorAll(".no-print")];
105 for (const node of noPrintList) {
106 node.style.setProperty("display", "none");
109 fout.write(content.outerHTML, content.outerHTML.length);
111 FileUtils.closeAtomicFileOutputStream(fout);
112 for (const node of noPrintList) {
113 node.style.removeProperty("display");
116 this.message = format("save_page_msg", [FilePicker.file.path]);
121 class DebugMode extends Control {
124 this.messageHeader = string("debug_mode_msg_label");
126 if (WGI.debugLevel > 0) {
129 this.label = string("debug_mode_off_state_label");
134 const stateString = state ? "on" : "off";
135 this.label = string(`debug_mode_${stateString}_state_label`);
137 const file = Services.prefs.getCharPref("media.webrtc.debug.log_file");
138 this.message = format(`debug_mode_${stateString}_state_msg`, [file]);
146 this.setState((WGI.debugLevel = WGI.debugLevel ? 0 : WEBRTC_TRACE_ALL));
151 class AecLogging extends Control {
154 this.messageHeader = string("aec_logging_msg_label");
159 this.label = string("aec_logging_off_state_label");
165 this.label = string(`aec_logging_${state ? "on" : "off"}_state_label`);
168 const file = WGI.aecDebugLogDir;
169 this.message = format("aec_logging_off_state_msg", [file]);
171 this.message = string("aec_logging_on_state_msg");
179 this.setState((WGI.aecDebug = !WGI.aecDebug));
184 class ShowTab extends Control {
185 constructor(browserId) {
187 this.label = string("show_tab_label");
189 this.browserId = browserId;
194 window.ownerGlobal.browsingContext.topChromeWindow.gBrowser;
195 for (const tab of gBrowser.visibleTabs) {
196 if (tab.linkedBrowser && tab.linkedBrowser.browserId == this.browserId) {
197 gBrowser.selectedTab = tab;
201 this.ctrl.disabled = true;
206 // Setup. Retrieve reports & log while page loads.
207 const haveReports = getStats();
208 const haveLog = getLog();
209 await new Promise(r => (window.onload = r));
211 document.title = string("document_title");
213 const ctrl = renderElement("div", { className: "control" });
214 const msg = renderElement("div", { className: "message" });
215 const add = ([control, message]) => {
216 ctrl.appendChild(control);
217 msg.appendChild(message);
219 add(new SavePage().render());
220 add(new DebugMode().render());
221 add(new AecLogging().render());
223 const ctrls = document.querySelector("#controls");
224 ctrls.append(renderElements("div", { className: "controls" }, [ctrl, msg]));
227 // Render pcs and log
228 let reports = await haveReports;
229 let log = await haveLog;
231 reports.sort((a, b) => a.browserId - b.browserId);
233 let peerConnections = renderElement("div");
234 let connectionLog = renderElement("div");
235 let userPrefs = renderElement("div");
237 const content = document.querySelector("#content");
238 content.append(peerConnections, connectionLog, userPrefs);
241 const pcDiv = renderElements("div", { className: "stats" }, [
242 renderElements("span", { className: "section-heading" }, [
243 renderText("h3", string("stats_heading")),
244 renderText("button", string("stats_clear"), {
245 className: "no-print",
246 onclick: async () => {
248 reports = await getStats();
253 ...reports.map(renderPeerConnection),
255 const logDiv = renderElements("div", { className: "log" }, [
256 renderElements("span", { className: "section-heading" }, [
257 renderText("h3", string("log_heading")),
258 renderElement("button", {
259 textContent: string("log_clear"),
260 className: "no-print",
261 onclick: async () => {
263 log = await getLog();
270 const div = renderFoldableSection(logDiv, {
271 showMsg: string("log_show_msg"),
272 hideMsg: string("log_hide_msg"),
274 div.append(...log.map(line => renderText("p", line)));
277 // Replace previous info
278 peerConnections.replaceWith(pcDiv);
279 connectionLog.replaceWith(logDiv);
280 userPrefs.replaceWith((userPrefs = renderUserPrefs()));
282 peerConnections = pcDiv;
283 connectionLog = logDiv;
289 userPrefs.replaceWith((userPrefs = renderUserPrefs()));
290 const reports = await getStats();
291 reports.forEach(report => {
292 const replace = (id, renderFunc) => {
293 const elem = document.getElementById(`${id}: ${report.pcid}`);
295 elem.replaceWith(renderFunc(report, history));
298 replace("ice-stats", renderICEStats);
299 replace("rtp-stats", renderRTPStats);
300 replace("bandwidth-stats", renderBandwidthStats);
301 replace("frame-stats", renderFrameRateStats);
309 function renderPeerConnection(report) {
310 const { pcid, browserId, closed, timestamp, configuration } = report;
312 const pcDiv = renderElement("div", { className: "peer-connection" });
314 const id = pcid.match(/id=(\S+)/)[1];
315 const url = pcid.match(/url=([^)]+)/)[1];
316 const closedStr = closed ? `(${string("connection_closed")})` : "";
317 const now = new Date(timestamp).toString();
320 renderText("h3", `[ ${browserId} | ${id} ] ${url} ${closedStr} ${now}`)
322 pcDiv.append(new ShowTab(browserId).render()[0]);
325 const section = renderFoldableSection(pcDiv);
327 renderElements("div", {}, [
328 renderText("span", `${string("peer_connection_id_label")}: `, {
329 className: "info-label",
331 renderText("span", pcid, { className: "info-body" }),
333 renderConfiguration(configuration),
334 renderICEStats(report),
335 renderSDPStats(report),
336 renderBandwidthStats(report),
337 renderFrameRateStats(report),
338 renderRTPStats(report)
340 pcDiv.append(section);
345 function renderSDPStats({ offerer, localSdp, remoteSdp, sdpHistory }) {
346 const trimNewlines = sdp => sdp.replaceAll("\r\n", "\n");
348 const statsDiv = renderElements("div", {}, [
349 renderText("h4", string("sdp_heading")),
352 `${string("local_sdp_heading")} (${string(offerer ? "offer" : "answer")})`
354 renderText("pre", trimNewlines(localSdp)),
357 `${string("remote_sdp_heading")} (${string(
358 offerer ? "answer" : "offer"
361 renderText("pre", trimNewlines(remoteSdp)),
362 renderText("h4", string("sdp_history_heading")),
365 // All SDP in sequential order. Add onclick handler to scroll the associated
366 // SDP into view below.
367 for (const { isLocal, timestamp } of sdpHistory) {
368 const histDiv = renderElement("div", {});
369 const text = renderText(
371 format("sdp_set_at_timestamp", [
372 string(`${isLocal ? "local" : "remote"}_sdp_heading`),
375 { className: "sdp-history-link" }
377 text.onclick = () => {
378 const elem = document.getElementById("sdp-history: " + timestamp);
380 elem.scrollIntoView();
383 histDiv.append(text);
384 statsDiv.append(histDiv);
387 // Render the SDP into separate columns for local and remote.
388 const section = renderElement("div", { className: "sdp-history" });
389 const localDiv = renderElements("div", {}, [
390 renderText("h4", `${string("local_sdp_heading")}`),
392 const remoteDiv = renderElements("div", {}, [
393 renderText("h4", `${string("remote_sdp_heading")}`),
397 for (const { isLocal, timestamp, sdp, errors } of sdpHistory) {
401 const histDiv = isLocal ? localDiv : remoteDiv;
405 format("sdp_set_timestamp", [timestamp, timestamp - first]),
406 { id: "sdp-history: " + timestamp }
410 histDiv.append(renderElement("h5", string("sdp_parsing_errors_heading")));
412 for (const { lineNumber, error } of errors) {
413 histDiv.append(renderElement("br"), `${lineNumber}: ${error}`);
415 histDiv.append(renderText("pre", trimNewlines(sdp)));
417 section.append(localDiv, remoteDiv);
418 statsDiv.append(section);
422 function renderBandwidthStats(report) {
423 const statsDiv = renderElement("div", {
424 id: "bandwidth-stats: " + report.pcid,
426 const table = renderSimpleTable(
430 "send_bandwidth_bytes_sec",
431 "receive_bandwidth_bytes_sec",
432 "max_padding_bytes_sec",
434 "round_trip_time_ms",
435 ].map(columnName => string(columnName)),
436 report.bandwidthEstimations.map(stat => [
437 stat.trackIdentifier,
438 stat.sendBandwidthBps,
439 stat.receiveBandwidthBps,
441 stat.receiveBandwidthBps,
445 statsDiv.append(renderText("h4", string("bandwidth_stats_heading")), table);
449 function renderFrameRateStats(report) {
450 const statsDiv = renderElement("div", { id: "frame-stats: " + report.pcid });
451 report.videoFrameHistories.forEach(history => {
452 const stats = history.entries.map(stat => {
453 stat.elapsed = stat.lastFrameTimestamp - stat.firstFrameTimestamp;
454 if (stat.elapsed < 1) {
457 stat.elapsed = (stat.elapsed / 1_000).toFixed(3);
458 if (stat.elapsed && stat.consecutiveFrames) {
459 stat.avgFramerate = (stat.consecutiveFrames / stat.elapsed).toFixed(2);
461 stat.avgFramerate = string("n_a");
466 const table = renderSimpleTable(
471 "consecutive_frames",
473 "estimated_framerate",
475 "first_frame_timestamp",
476 "last_frame_timestamp",
477 "local_receive_ssrc",
479 ].map(columnName => string(columnName)),
484 stat.consecutiveFrames,
488 stat.firstFrameTimestamp,
489 stat.lastFrameTimestamp,
491 stat.remoteSsrc || "?",
492 ].map(entry => (Object.is(entry, undefined) ? "<<undefined>>" : entry))
499 `${string("frame_stats_heading")} - MediaStreamTrack Id: ${
500 history.trackIdentifier
510 function renderRTPStats(report, history) {
512 ...(report.inboundRtpStreamStats || []),
513 ...(report.outboundRtpStreamStats || []),
515 const remoteRtpStats = [
516 ...(report.remoteInboundRtpStreamStats || []),
517 ...(report.remoteOutboundRtpStreamStats || []),
520 // Generate an id-to-streamStat index for each remote streamStat. This will
521 // be used next to link the remote to its local side.
522 const remoteRtpStatsMap = {};
523 for (const stat of remoteRtpStats) {
524 remoteRtpStatsMap[stat.id] = stat;
527 // If a streamStat has a remoteId attribute, create a remoteRtpStats
528 // attribute that references the remote streamStat entry directly.
529 // That is, the index generated above is merged into the returned list.
530 for (const stat of rtpStats.filter(s => "remoteId" in s)) {
531 stat.remoteRtpStats = remoteRtpStatsMap[stat.remoteId];
533 const stats = [...rtpStats, ...remoteRtpStats];
536 return renderElements("div", { id: "rtp-stats: " + report.pcid }, [
537 renderText("h4", string("rtp_stats_heading")),
538 ...stats.map(stat => {
539 const { id, remoteId, remoteRtpStats } = stat;
540 const div = renderElements("div", {}, [
541 renderText("h5", id),
542 renderCoderStats(stat),
543 renderTransportStats(stat, true, history),
545 if (remoteId && remoteRtpStats) {
546 div.append(renderTransportStats(remoteRtpStats, false));
553 function renderCoderStats({
565 s += ` ${string("avg_bitrate_label")}: ${(bitrateMean / 1000000).toFixed(
569 s += ` (${(bitrateStdDev / 1000000).toFixed(2)} SD)`;
573 s += ` ${string("avg_framerate_label")}: ${framerateMean.toFixed(2)} fps`;
574 if (framerateStdDev) {
575 s += ` (${framerateStdDev.toFixed(2)} SD)`;
579 s += ` ${string("dropped_frames_label")}: ${droppedFrames}`;
581 if (discardedPackets) {
582 s += ` ${string("discarded_packets_label")}: ${discardedPackets}`;
585 s = ` ${string(`${packetsReceived ? "de" : "en"}coder_label`)}:${s}`;
587 return renderText("p", s);
590 function renderTransportStats(
607 const typeLabel = local ? string("typeLocal") : string("typeRemote");
610 if (history[id] === undefined) {
615 const estimateKbps = (timestamp, lastTimestamp, bytes, lastBytes) => {
616 if (!timestamp || !lastTimestamp || !bytes || !lastBytes) {
617 return string("n_a");
619 const elapsedTime = timestamp - lastTimestamp;
620 if (elapsedTime <= 0) {
621 return string("n_a");
623 return ((bytes - lastBytes) / elapsedTime).toFixed(1);
626 const time = new Date(timestamp).toTimeString();
627 let s = `${typeLabel}: ${time} ${type} SSRC: ${ssrc}`;
629 const packets = string("packets");
630 if (packetsReceived) {
631 s += ` ${string("received_label")}: ${packetsReceived} ${packets}`;
634 s += ` (${(bytesReceived / 1024).toFixed(2)} Kb`;
635 if (local && history) {
636 s += ` , ~${estimateKbps(
638 history[id].lastTimestamp,
640 history[id].lastBytesReceived
646 s += ` ${string("lost_label")}: ${packetsLost} ${string(
651 s += ` RTT: ${roundTripTime * 1000} ms`;
653 } else if (packetsSent) {
654 s += ` ${string("sent_label")}: ${packetsSent} ${packets}`;
656 s += ` (${(bytesSent / 1024).toFixed(2)} Kb`;
657 if (local && history) {
658 s += `, ~${estimateKbps(
660 history[id].lastTimestamp,
662 history[id].lastBytesSent
671 history[id].lastBytesReceived = bytesReceived;
672 history[id].lastBytesSent = bytesSent;
673 history[id].lastTimestamp = timestamp;
676 return renderText("p", s);
679 function renderRawIceTable(caption, candidates) {
680 const table = renderSimpleTable(
683 [...new Set(candidates.sort())].filter(i => i).map(i => [i])
685 table.className = "raw-candidate";
689 function renderConfiguration(c) {
690 const provided = string("configuration_element_provided");
691 const notProvided = string("configuration_element_not_provided");
693 // Create the text for a configuration field
694 const cfg = (obj, key) => [
697 key in obj ? obj[key] : renderText("i", notProvided),
700 // Create the text for a fooProvided configuration field
701 const pro = (obj, key) => [
704 renderText("i", provided),
706 renderText("i", notProvided),
708 renderText("i", obj[`${key}Provided`] ? provided : notProvided),
711 return renderElements("div", { classList: "peer-connection-config" }, [
713 ...cfg(c, "bundlePolicy"),
714 ...cfg(c, "iceTransportPolicy"),
715 ...pro(c, "peerIdentity"),
716 ...cfg(c, "sdpSemantics"),
720 ? [renderText("i", notProvided)]
721 : c.iceServers.map(i =>
722 renderElements("div", {}, [
723 `urls: ${JSON.stringify(i.urls)}`,
724 ...pro(i, "credential"),
725 ...pro(i, "userName"),
731 function renderICEStats(report) {
732 const iceDiv = renderElements("div", { id: "ice-stats: " + report.pcid }, [
733 renderText("h4", string("ice_stats_heading")),
736 // Render ICECandidate table
738 const caption = renderElement("caption", { className: "no-print" });
740 // This takes the caption message with the replacement token, breaks
741 // it around the token, and builds the spans for each portion of the
742 // caption. This is to allow localization to put the color name for
743 // the highlight wherever it is appropriate in the translated string
744 // while avoiding innerHTML warnings from eslint.
745 const [start, end] = string("trickle_caption_msg2").split(/%(?:1\$)?S/);
747 // only append span if non-whitespace chars present
748 if (/\S/.test(start)) {
749 caption.append(renderText("span", start));
752 renderText("span", string("trickle_highlight_color_name2"), {
753 className: "ice-trickled",
756 // only append span if non-whitespace chars present
757 if (/\S/.test(end)) {
758 caption.append(renderText("span", end));
761 // Generate ICE stats
764 // Create an index based on candidate ID for each element in the
765 // iceCandidateStats array.
766 const candidates = {};
767 for (const candidate of report.iceCandidateStats) {
768 candidates[candidate.id] = candidate;
771 // a method to see if a given candidate id is in the array of tickled
773 const isTrickled = candidateId =>
774 report.trickledIceCandidateStats.some(({ id }) => id == candidateId);
776 // A component may have a remote or local candidate address or both.
777 // Combine those with both; these will be the peer candidates.
790 } of report.iceCandidatePairStats) {
791 const local = candidates[localCandidateId];
794 ["local-candidate"]: candidateToString(local),
803 matched[local.id] = true;
804 if (isTrickled(local.id)) {
805 stat["local-trickled"] = true;
808 const remote = candidates[remoteCandidateId];
810 stat["remote-candidate"] = candidateToString(remote);
811 matched[remote.id] = true;
812 if (isTrickled(remote.id)) {
813 stat["remote-trickled"] = true;
820 // sort (group by) componentId first, then bytesSent if available, else by
822 stats.sort((a, b) => {
823 if (a.componentId != b.componentId) {
824 return a.componentId - b.componentId;
827 ? b.bytesSent - (a.bytesSent || 0)
828 : (b.priority || 0) - (a.priority || 0);
832 // don't use |stat.x || ""| here because it hides 0 values
833 const statsTable = renderSimpleTable(
843 "ice_pair_bytes_sent",
844 "ice_pair_bytes_received",
845 ].map(columnName => string(columnName)),
851 stat["local-candidate"],
852 stat["remote-candidate"],
857 ].map(entry => (Object.is(entry, undefined) ? "" : entry))
861 // after rendering the table, we need to change the class name for each
862 // candidate pair's local or remote candidate if it was trickled.
868 "local-trickled": localTrickled,
869 "remote-trickled": remoteTrickled,
871 // look at statsTable row index + 1 to skip column headers
872 const { cells } = statsTable.rows[++index];
873 cells[0].className = `ice-${state}`;
875 cells[1].className = "ice-succeeded";
878 cells[2].className = "ice-succeeded";
881 cells[3].className = "ice-trickled";
883 if (remoteTrickled) {
884 cells[4].className = "ice-trickled";
888 // if the current row's component id changes, mark the bottom of the
889 // previous row with a thin, black border to differentiate the
890 // component id grouping.
892 for (const row of statsTable.rows) {
894 if (previousRow.cells[5].innerHTML != row.cells[5].innerHTML) {
895 previousRow.className = "bottom-border";
900 iceDiv.append(statsTable);
902 // add just a bit of vertical space between the restart/rollback
903 // counts and the ICE candidate pair table above.
906 renderIceMetric("ice_restart_count_label", report.iceRestarts),
907 renderIceMetric("ice_rollback_count_label", report.iceRollbacks)
910 // Render raw ICECandidate section
912 const section = renderElements("div", {}, [
913 renderText("h4", string("raw_candidates_heading")),
915 const foldSection = renderFoldableSection(section, {
916 showMsg: string("raw_cand_show_msg"),
917 hideMsg: string("raw_cand_hide_msg"),
920 // render raw candidates
922 renderElements("div", {}, [
923 renderRawIceTable("raw_local_candidate", report.rawLocalCandidates),
924 renderRawIceTable("raw_remote_candidate", report.rawRemoteCandidates),
927 section.append(foldSection);
928 iceDiv.append(section);
933 function renderIceMetric(label, value) {
934 return renderElements("div", {}, [
935 renderText("span", `${string(label)}: `, { className: "info-label" }),
936 renderText("span", value, { className: "info-body" }),
940 function candidateToString({
953 candidateType = `${candidateType}-${relayProtocol}`;
955 proxied = type == "local-candidate" ? ` [${proxied}]` : "";
956 return `${address}:${port}/${protocol}(${candidateType})${proxied}`;
959 function renderUserPrefs() {
960 const getPref = key => {
961 switch (Services.prefs.getPrefType(key)) {
962 case Services.prefs.PREF_BOOL:
963 return Services.prefs.getBoolPref(key);
964 case Services.prefs.PREF_INT:
965 return Services.prefs.getIntPref(key);
966 case Services.prefs.PREF_STRING:
967 return Services.prefs.getStringPref(key);
972 "media.peerconnection",
974 "media.getusermedia",
976 const renderPref = p => renderText("p", `${p}: ${getPref(p)}`);
977 const display = prefs
978 .flatMap(Services.prefs.getChildList)
979 .filter(Services.prefs.prefHasUserValue)
981 return renderElements(
986 style: display.length ? "" : "visibility:hidden",
989 renderElements("span", { className: "section-heading" }, [
990 renderText("h3", string("custom_webrtc_configuration_heading")),
997 function renderFoldableSection(parent, options = {}) {
998 const section = renderElement("div");
1000 const ctrl = renderElements("div", { className: "section-ctrl no-print" }, [
1001 new FoldEffect(section, options).render(),
1003 parent.append(ctrl);
1008 function renderSimpleTable(caption, headings, data) {
1009 const heads = headings.map(text => renderText("th", text));
1010 const renderCell = text => renderText("td", text);
1012 return renderElements("table", {}, [
1014 renderElements("tr", {}, heads),
1015 ...data.map(line => renderElements("tr", {}, line.map(renderCell))),
1020 static allSections = [];
1025 showMsg = string("fold_show_msg"),
1026 showHint = string("fold_show_hint"),
1027 hideMsg = string("fold_hide_msg"),
1028 hideHint = string("fold_hide_hint"),
1031 showMsg = `\u25BC ${showMsg}`;
1032 hideMsg = `\u25B2 ${hideMsg}`;
1033 Object.assign(this, { target, showMsg, showHint, hideMsg, hideHint });
1037 this.target.classList.add("fold-target");
1038 this.trigger = renderElement("div", { className: "fold-trigger" });
1040 this.trigger.onclick = () => {
1041 if (this.target.classList.contains("fold-closed")) {
1047 FoldEffect.allSections.push(this);
1048 return this.trigger;
1052 this.target.classList.remove("fold-closed");
1053 this.trigger.setAttribute("title", this.hideHint);
1054 this.trigger.textContent = this.hideMsg;
1058 this.target.classList.add("fold-closed");
1059 this.trigger.setAttribute("title", this.showHint);
1060 this.trigger.textContent = this.showMsg;
1063 static expandAll() {
1064 for (const section of FoldEffect.allSections) {
1069 static collapseAll() {
1070 for (const section of FoldEffect.allSections) {