Bug 1669129 - [devtools] Enable devtools.overflow.debugging.enabled. r=jdescottes
[gecko.git] / toolkit / content / aboutwebrtc / aboutWebrtc.js
blobac63f4d838292ca666d71614fb6cdfc38a5fb469
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/. */
4 "use strict";
6 const { XPCOMUtils } = ChromeUtils.import(
7   "resource://gre/modules/XPCOMUtils.jsm"
8 );
9 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
11 ChromeUtils.defineModuleGetter(
12   this,
13   "FileUtils",
14   "resource://gre/modules/FileUtils.jsm"
16 XPCOMUtils.defineLazyServiceGetter(
17   this,
18   "FilePicker",
19   "@mozilla.org/filepicker;1",
20   "nsIFilePicker"
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);
49   return element;
52 // Button control classes
54 class Control {
55   label = null;
56   message = null;
57   messageHeader = null;
59   render() {
60     this.ctrl = renderElement("button", { onclick: () => this.onClick() });
61     this.msg = renderElement("p");
62     this.update();
63     return [this.ctrl, this.msg];
64   }
66   update() {
67     this.ctrl.textContent = this.label;
68     this.msg.textContent = "";
69     if (this.message) {
70       this.msg.append(
71         renderText("span", `${this.messageHeader}: `, {
72           className: "info-label",
73         }),
74         this.message
75       );
76     }
77   }
80 class SavePage extends Control {
81   constructor() {
82     super();
83     this.messageHeader = string("save_page_label");
84     this.label = string("save_page_label");
85   }
87   async onClick() {
88     FoldEffect.expandAll();
89     FilePicker.init(
90       window,
91       string("save_page_dialog_title"),
92       FilePicker.modeSave
93     );
94     FilePicker.defaultString = LOGFILE_NAME_DEFAULT;
95     const rv = await new Promise(r => FilePicker.open(r));
96     if (rv != FilePicker.returnOK && rv != FilePicker.returnReplace) {
97       return;
98     }
99     const fout = FileUtils.openAtomicFileOutputStream(
100       FilePicker.file,
101       FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE
102     );
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");
107     }
108     try {
109       fout.write(content.outerHTML, content.outerHTML.length);
110     } finally {
111       FileUtils.closeAtomicFileOutputStream(fout);
112       for (const node of noPrintList) {
113         node.style.removeProperty("display");
114       }
115     }
116     this.message = format("save_page_msg", [FilePicker.file.path]);
117     this.update();
118   }
121 class DebugMode extends Control {
122   constructor() {
123     super();
124     this.messageHeader = string("debug_mode_msg_label");
126     if (WGI.debugLevel > 0) {
127       this.setState(true);
128     } else {
129       this.label = string("debug_mode_off_state_label");
130     }
131   }
133   setState(state) {
134     const stateString = state ? "on" : "off";
135     this.label = string(`debug_mode_${stateString}_state_label`);
136     try {
137       const file = Services.prefs.getCharPref("media.webrtc.debug.log_file");
138       this.message = format(`debug_mode_${stateString}_state_msg`, [file]);
139     } catch (e) {
140       this.message = null;
141     }
142     return state;
143   }
145   onClick() {
146     this.setState((WGI.debugLevel = WGI.debugLevel ? 0 : WEBRTC_TRACE_ALL));
147     this.update();
148   }
151 class AecLogging extends Control {
152   constructor() {
153     super();
154     this.messageHeader = string("aec_logging_msg_label");
156     if (WGI.aecDebug) {
157       this.setState(true);
158     } else {
159       this.label = string("aec_logging_off_state_label");
160       this.message = null;
161     }
162   }
164   setState(state) {
165     this.label = string(`aec_logging_${state ? "on" : "off"}_state_label`);
166     try {
167       if (!state) {
168         const file = WGI.aecDebugLogDir;
169         this.message = format("aec_logging_off_state_msg", [file]);
170       } else {
171         this.message = string("aec_logging_on_state_msg");
172       }
173     } catch (e) {
174       this.message = null;
175     }
176   }
178   onClick() {
179     this.setState((WGI.aecDebug = !WGI.aecDebug));
180     this.update();
181   }
184 class ShowTab extends Control {
185   constructor(browserId) {
186     super();
187     this.label = string("show_tab_label");
188     this.message = null;
189     this.browserId = browserId;
190   }
192   onClick() {
193     const gBrowser =
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;
198         return;
199       }
200     }
201     this.ctrl.disabled = true;
202   }
205 (async () => {
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");
212   {
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);
218     };
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]));
225   }
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);
240   function refresh() {
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 () => {
247             WGI.clearAllStats();
248             reports = await getStats();
249             refresh();
250           },
251         }),
252       ]),
253       ...reports.map(renderPeerConnection),
254     ]);
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 () => {
262             WGI.clearLogging();
263             log = await getLog();
264             refresh();
265           },
266         }),
267       ]),
268     ]);
269     if (log.length) {
270       const div = renderFoldableSection(logDiv, {
271         showMsg: string("log_show_msg"),
272         hideMsg: string("log_hide_msg"),
273       });
274       div.append(...log.map(line => renderText("p", line)));
275       logDiv.append(div);
276     }
277     // Replace previous info
278     peerConnections.replaceWith(pcDiv);
279     connectionLog.replaceWith(logDiv);
280     userPrefs.replaceWith((userPrefs = renderUserPrefs()));
282     peerConnections = pcDiv;
283     connectionLog = logDiv;
284   }
285   refresh();
287   window.setInterval(
288     async history => {
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}`);
294           if (elem) {
295             elem.replaceWith(renderFunc(report, history));
296           }
297         };
298         replace("ice-stats", renderICEStats);
299         replace("rtp-stats", renderRTPStats);
300         replace("bandwidth-stats", renderBandwidthStats);
301         replace("frame-stats", renderFrameRateStats);
302       });
303     },
304     500,
305     {}
306   );
307 })();
309 function renderPeerConnection(report) {
310   const { pcid, browserId, closed, timestamp, configuration } = report;
312   const pcDiv = renderElement("div", { className: "peer-connection" });
313   {
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();
319     pcDiv.append(
320       renderText("h3", `[ ${browserId} | ${id} ] ${url} ${closedStr} ${now}`)
321     );
322     pcDiv.append(new ShowTab(browserId).render()[0]);
323   }
324   {
325     const section = renderFoldableSection(pcDiv);
326     section.append(
327       renderElements("div", {}, [
328         renderText("span", `${string("peer_connection_id_label")}: `, {
329           className: "info-label",
330         }),
331         renderText("span", pcid, { className: "info-body" }),
332       ]),
333       renderConfiguration(configuration),
334       renderICEStats(report),
335       renderSDPStats(report),
336       renderBandwidthStats(report),
337       renderFrameRateStats(report),
338       renderRTPStats(report)
339     );
340     pcDiv.append(section);
341   }
342   return pcDiv;
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")),
350     renderText(
351       "h5",
352       `${string("local_sdp_heading")} (${string(offerer ? "offer" : "answer")})`
353     ),
354     renderText("pre", trimNewlines(localSdp)),
355     renderText(
356       "h5",
357       `${string("remote_sdp_heading")} (${string(
358         offerer ? "answer" : "offer"
359       )})`
360     ),
361     renderText("pre", trimNewlines(remoteSdp)),
362     renderText("h4", string("sdp_history_heading")),
363   ]);
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(
370       "h5",
371       format("sdp_set_at_timestamp", [
372         string(`${isLocal ? "local" : "remote"}_sdp_heading`),
373         timestamp,
374       ]),
375       { className: "sdp-history-link" }
376     );
377     text.onclick = () => {
378       const elem = document.getElementById("sdp-history: " + timestamp);
379       if (elem) {
380         elem.scrollIntoView();
381       }
382     };
383     histDiv.append(text);
384     statsDiv.append(histDiv);
385   }
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")}`),
391   ]);
392   const remoteDiv = renderElements("div", {}, [
393     renderText("h4", `${string("remote_sdp_heading")}`),
394   ]);
396   let first = NaN;
397   for (const { isLocal, timestamp, sdp, errors } of sdpHistory) {
398     if (isNaN(first)) {
399       first = timestamp;
400     }
401     const histDiv = isLocal ? localDiv : remoteDiv;
402     histDiv.append(
403       renderText(
404         "h5",
405         format("sdp_set_timestamp", [timestamp, timestamp - first]),
406         { id: "sdp-history: " + timestamp }
407       )
408     );
409     if (errors.length) {
410       histDiv.append(renderElement("h5", string("sdp_parsing_errors_heading")));
411     }
412     for (const { lineNumber, error } of errors) {
413       histDiv.append(renderElement("br"), `${lineNumber}: ${error}`);
414     }
415     histDiv.append(renderText("pre", trimNewlines(sdp)));
416   }
417   section.append(localDiv, remoteDiv);
418   statsDiv.append(section);
419   return statsDiv;
422 function renderBandwidthStats(report) {
423   const statsDiv = renderElement("div", {
424     id: "bandwidth-stats: " + report.pcid,
425   });
426   const table = renderSimpleTable(
427     "",
428     [
429       "track_identifier",
430       "send_bandwidth_bytes_sec",
431       "receive_bandwidth_bytes_sec",
432       "max_padding_bytes_sec",
433       "pacer_delay_ms",
434       "round_trip_time_ms",
435     ].map(columnName => string(columnName)),
436     report.bandwidthEstimations.map(stat => [
437       stat.trackIdentifier,
438       stat.sendBandwidthBps,
439       stat.receiveBandwidthBps,
440       stat.maxPaddingBps,
441       stat.receiveBandwidthBps,
442       stat.rttMs,
443     ])
444   );
445   statsDiv.append(renderText("h4", string("bandwidth_stats_heading")), table);
446   return statsDiv;
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) {
455         stat.elapsed = 0;
456       }
457       stat.elapsed = (stat.elapsed / 1_000).toFixed(3);
458       if (stat.elapsed && stat.consecutiveFrames) {
459         stat.avgFramerate = (stat.consecutiveFrames / stat.elapsed).toFixed(2);
460       } else {
461         stat.avgFramerate = string("n_a");
462       }
463       return stat;
464     });
466     const table = renderSimpleTable(
467       "",
468       [
469         "width_px",
470         "height_px",
471         "consecutive_frames",
472         "time_elapsed",
473         "estimated_framerate",
474         "rotation_degrees",
475         "first_frame_timestamp",
476         "last_frame_timestamp",
477         "local_receive_ssrc",
478         "remote_send_ssrc",
479       ].map(columnName => string(columnName)),
480       stats.map(stat =>
481         [
482           stat.width,
483           stat.height,
484           stat.consecutiveFrames,
485           stat.elapsed,
486           stat.avgFramerate,
487           stat.rotationAngle,
488           stat.firstFrameTimestamp,
489           stat.lastFrameTimestamp,
490           stat.localSsrc,
491           stat.remoteSsrc || "?",
492         ].map(entry => (Object.is(entry, undefined) ? "<<undefined>>" : entry))
493       )
494     );
496     statsDiv.append(
497       renderText(
498         "h4",
499         `${string("frame_stats_heading")} - MediaStreamTrack Id: ${
500           history.trackIdentifier
501         }`
502       ),
503       table
504     );
505   });
507   return statsDiv;
510 function renderRTPStats(report, history) {
511   const rtpStats = [
512     ...(report.inboundRtpStreamStats || []),
513     ...(report.outboundRtpStreamStats || []),
514   ];
515   const remoteRtpStats = [
516     ...(report.remoteInboundRtpStreamStats || []),
517     ...(report.remoteOutboundRtpStreamStats || []),
518   ];
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;
525   }
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];
532   }
533   const stats = [...rtpStats, ...remoteRtpStats];
535   // Render stats set
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),
544       ]);
545       if (remoteId && remoteRtpStats) {
546         div.append(renderTransportStats(remoteRtpStats, false));
547       }
548       return div;
549     }),
550   ]);
553 function renderCoderStats({
554   bitrateMean,
555   bitrateStdDev,
556   framerateMean,
557   framerateStdDev,
558   droppedFrames,
559   discardedPackets,
560   packetsReceived,
561 }) {
562   let s = "";
564   if (bitrateMean) {
565     s += ` ${string("avg_bitrate_label")}: ${(bitrateMean / 1000000).toFixed(
566       2
567     )} Mbps`;
568     if (bitrateStdDev) {
569       s += ` (${(bitrateStdDev / 1000000).toFixed(2)} SD)`;
570     }
571   }
572   if (framerateMean) {
573     s += ` ${string("avg_framerate_label")}: ${framerateMean.toFixed(2)} fps`;
574     if (framerateStdDev) {
575       s += ` (${framerateStdDev.toFixed(2)} SD)`;
576     }
577   }
578   if (droppedFrames) {
579     s += ` ${string("dropped_frames_label")}: ${droppedFrames}`;
580   }
581   if (discardedPackets) {
582     s += ` ${string("discarded_packets_label")}: ${discardedPackets}`;
583   }
584   if (s.length) {
585     s = ` ${string(`${packetsReceived ? "de" : "en"}coder_label`)}:${s}`;
586   }
587   return renderText("p", s);
590 function renderTransportStats(
591   {
592     id,
593     timestamp,
594     type,
595     ssrc,
596     packetsReceived,
597     bytesReceived,
598     packetsLost,
599     jitter,
600     roundTripTime,
601     packetsSent,
602     bytesSent,
603   },
604   local,
605   history
606 ) {
607   const typeLabel = local ? string("typeLocal") : string("typeRemote");
609   if (history) {
610     if (history[id] === undefined) {
611       history[id] = {};
612     }
613   }
615   const estimateKbps = (timestamp, lastTimestamp, bytes, lastBytes) => {
616     if (!timestamp || !lastTimestamp || !bytes || !lastBytes) {
617       return string("n_a");
618     }
619     const elapsedTime = timestamp - lastTimestamp;
620     if (elapsedTime <= 0) {
621       return string("n_a");
622     }
623     return ((bytes - lastBytes) / elapsedTime).toFixed(1);
624   };
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}`;
633     if (bytesReceived) {
634       s += ` (${(bytesReceived / 1024).toFixed(2)} Kb`;
635       if (local && history) {
636         s += ` , ~${estimateKbps(
637           timestamp,
638           history[id].lastTimestamp,
639           bytesReceived,
640           history[id].lastBytesReceived
641         )} Kbps`;
642       }
643       s += ")";
644     }
646     s += ` ${string("lost_label")}: ${packetsLost} ${string(
647       "jitter_label"
648     )}: ${jitter}`;
650     if (roundTripTime) {
651       s += ` RTT: ${roundTripTime * 1000} ms`;
652     }
653   } else if (packetsSent) {
654     s += ` ${string("sent_label")}: ${packetsSent} ${packets}`;
655     if (bytesSent) {
656       s += ` (${(bytesSent / 1024).toFixed(2)} Kb`;
657       if (local && history) {
658         s += `, ~${estimateKbps(
659           timestamp,
660           history[id].lastTimestamp,
661           bytesSent,
662           history[id].lastBytesSent
663         )} Kbps`;
664       }
665       s += ")";
666     }
667   }
669   // Update history
670   if (history) {
671     history[id].lastBytesReceived = bytesReceived;
672     history[id].lastBytesSent = bytesSent;
673     history[id].lastTimestamp = timestamp;
674   }
676   return renderText("p", s);
679 function renderRawIceTable(caption, candidates) {
680   const table = renderSimpleTable(
681     "",
682     [string(caption)],
683     [...new Set(candidates.sort())].filter(i => i).map(i => [i])
684   );
685   table.className = "raw-candidate";
686   return table;
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) => [
695     renderElement("br"),
696     `${key}: `,
697     key in obj ? obj[key] : renderText("i", notProvided),
698   ];
700   // Create the text for a fooProvided configuration field
701   const pro = (obj, key) => [
702     renderElement("br"),
703     `${key}(`,
704     renderText("i", provided),
705     `/`,
706     renderText("i", notProvided),
707     `): `,
708     renderText("i", obj[`${key}Provided`] ? provided : notProvided),
709   ];
711   return renderElements("div", { classList: "peer-connection-config" }, [
712     "RTCConfiguration",
713     ...cfg(c, "bundlePolicy"),
714     ...cfg(c, "iceTransportPolicy"),
715     ...pro(c, "peerIdentity"),
716     ...cfg(c, "sdpSemantics"),
717     renderElement("br"),
718     "iceServers: ",
719     ...(!c.iceServers
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"),
726           ])
727         )),
728   ]);
731 function renderICEStats(report) {
732   const iceDiv = renderElements("div", { id: "ice-stats: " + report.pcid }, [
733     renderText("h4", string("ice_stats_heading")),
734   ]);
736   // Render ICECandidate table
737   {
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));
750     }
751     caption.append(
752       renderText("span", string("trickle_highlight_color_name2"), {
753         className: "ice-trickled",
754       })
755     );
756     // only append span if non-whitespace chars present
757     if (/\S/.test(end)) {
758       caption.append(renderText("span", end));
759     }
761     // Generate ICE stats
762     const stats = [];
763     {
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;
769       }
771       // a method to see if a given candidate id is in the array of tickled
772       // candidates.
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.
778       const matched = {};
780       for (const {
781         localCandidateId,
782         remoteCandidateId,
783         componentId,
784         state,
785         priority,
786         nominated,
787         selected,
788         bytesSent,
789         bytesReceived,
790       } of report.iceCandidatePairStats) {
791         const local = candidates[localCandidateId];
792         if (local) {
793           const stat = {
794             ["local-candidate"]: candidateToString(local),
795             componentId,
796             state,
797             priority,
798             nominated,
799             selected,
800             bytesSent,
801             bytesReceived,
802           };
803           matched[local.id] = true;
804           if (isTrickled(local.id)) {
805             stat["local-trickled"] = true;
806           }
808           const remote = candidates[remoteCandidateId];
809           if (remote) {
810             stat["remote-candidate"] = candidateToString(remote);
811             matched[remote.id] = true;
812             if (isTrickled(remote.id)) {
813               stat["remote-trickled"] = true;
814             }
815           }
816           stats.push(stat);
817         }
818       }
820       // sort (group by) componentId first, then bytesSent if available, else by
821       // priority
822       stats.sort((a, b) => {
823         if (a.componentId != b.componentId) {
824           return a.componentId - b.componentId;
825         }
826         return b.bytesSent
827           ? b.bytesSent - (a.bytesSent || 0)
828           : (b.priority || 0) - (a.priority || 0);
829       });
830     }
831     // Render ICE stats
832     // don't use |stat.x || ""| here because it hides 0 values
833     const statsTable = renderSimpleTable(
834       caption,
835       [
836         "ice_state",
837         "nominated",
838         "selected",
839         "local_candidate",
840         "remote_candidate",
841         "ice_component_id",
842         "priority",
843         "ice_pair_bytes_sent",
844         "ice_pair_bytes_received",
845       ].map(columnName => string(columnName)),
846       stats.map(stat =>
847         [
848           stat.state,
849           stat.nominated,
850           stat.selected,
851           stat["local-candidate"],
852           stat["remote-candidate"],
853           stat.componentId,
854           stat.priority,
855           stat.bytesSent,
856           stat.bytesReceived,
857         ].map(entry => (Object.is(entry, undefined) ? "" : entry))
858       )
859     );
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.
863     let index = 0;
864     for (const {
865       state,
866       nominated,
867       selected,
868       "local-trickled": localTrickled,
869       "remote-trickled": remoteTrickled,
870     } of stats) {
871       // look at statsTable row index + 1 to skip column headers
872       const { cells } = statsTable.rows[++index];
873       cells[0].className = `ice-${state}`;
874       if (nominated) {
875         cells[1].className = "ice-succeeded";
876       }
877       if (selected) {
878         cells[2].className = "ice-succeeded";
879       }
880       if (localTrickled) {
881         cells[3].className = "ice-trickled";
882       }
883       if (remoteTrickled) {
884         cells[4].className = "ice-trickled";
885       }
886     }
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.
891     let previousRow;
892     for (const row of statsTable.rows) {
893       if (previousRow) {
894         if (previousRow.cells[5].innerHTML != row.cells[5].innerHTML) {
895           previousRow.className = "bottom-border";
896         }
897       }
898       previousRow = row;
899     }
900     iceDiv.append(statsTable);
901   }
902   // add just a bit of vertical space between the restart/rollback
903   // counts and the ICE candidate pair table above.
904   iceDiv.append(
905     renderElement("br"),
906     renderIceMetric("ice_restart_count_label", report.iceRestarts),
907     renderIceMetric("ice_rollback_count_label", report.iceRollbacks)
908   );
910   // Render raw ICECandidate section
911   {
912     const section = renderElements("div", {}, [
913       renderText("h4", string("raw_candidates_heading")),
914     ]);
915     const foldSection = renderFoldableSection(section, {
916       showMsg: string("raw_cand_show_msg"),
917       hideMsg: string("raw_cand_hide_msg"),
918     });
920     // render raw candidates
921     foldSection.append(
922       renderElements("div", {}, [
923         renderRawIceTable("raw_local_candidate", report.rawLocalCandidates),
924         renderRawIceTable("raw_remote_candidate", report.rawRemoteCandidates),
925       ])
926     );
927     section.append(foldSection);
928     iceDiv.append(section);
929   }
930   return iceDiv;
933 function renderIceMetric(label, value) {
934   return renderElements("div", {}, [
935     renderText("span", `${string(label)}: `, { className: "info-label" }),
936     renderText("span", value, { className: "info-body" }),
937   ]);
940 function candidateToString({
941   type,
942   address,
943   port,
944   protocol,
945   candidateType,
946   relayProtocol,
947   proxied,
948 } = {}) {
949   if (!type) {
950     return "*";
951   }
952   if (relayProtocol) {
953     candidateType = `${candidateType}-${relayProtocol}`;
954   }
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);
968     }
969     return "";
970   };
971   const prefs = [
972     "media.peerconnection",
973     "media.navigator",
974     "media.getusermedia",
975   ];
976   const renderPref = p => renderText("p", `${p}: ${getPref(p)}`);
977   const display = prefs
978     .flatMap(Services.prefs.getChildList)
979     .filter(Services.prefs.prefHasUserValue)
980     .map(renderPref);
981   return renderElements(
982     "div",
983     {
984       id: "prefs",
985       className: "prefs",
986       style: display.length ? "" : "visibility:hidden",
987     },
988     [
989       renderElements("span", { className: "section-heading" }, [
990         renderText("h3", string("custom_webrtc_configuration_heading")),
991       ]),
992       ...display,
993     ]
994   );
997 function renderFoldableSection(parent, options = {}) {
998   const section = renderElement("div");
999   if (parent) {
1000     const ctrl = renderElements("div", { className: "section-ctrl no-print" }, [
1001       new FoldEffect(section, options).render(),
1002     ]);
1003     parent.append(ctrl);
1004   }
1005   return section;
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", {}, [
1013     caption,
1014     renderElements("tr", {}, heads),
1015     ...data.map(line => renderElements("tr", {}, line.map(renderCell))),
1016   ]);
1019 class FoldEffect {
1020   static allSections = [];
1022   constructor(
1023     target,
1024     {
1025       showMsg = string("fold_show_msg"),
1026       showHint = string("fold_show_hint"),
1027       hideMsg = string("fold_hide_msg"),
1028       hideHint = string("fold_hide_hint"),
1029     } = {}
1030   ) {
1031     showMsg = `\u25BC ${showMsg}`;
1032     hideMsg = `\u25B2 ${hideMsg}`;
1033     Object.assign(this, { target, showMsg, showHint, hideMsg, hideHint });
1034   }
1036   render() {
1037     this.target.classList.add("fold-target");
1038     this.trigger = renderElement("div", { className: "fold-trigger" });
1039     this.collapse();
1040     this.trigger.onclick = () => {
1041       if (this.target.classList.contains("fold-closed")) {
1042         this.expand();
1043       } else {
1044         this.collapse();
1045       }
1046     };
1047     FoldEffect.allSections.push(this);
1048     return this.trigger;
1049   }
1051   expand() {
1052     this.target.classList.remove("fold-closed");
1053     this.trigger.setAttribute("title", this.hideHint);
1054     this.trigger.textContent = this.hideMsg;
1055   }
1057   collapse() {
1058     this.target.classList.add("fold-closed");
1059     this.trigger.setAttribute("title", this.showHint);
1060     this.trigger.textContent = this.showMsg;
1061   }
1063   static expandAll() {
1064     for (const section of FoldEffect.allSections) {
1065       section.expand();
1066     }
1067   }
1069   static collapseAll() {
1070     for (const section of FoldEffect.allSections) {
1071       section.collapse();
1072     }
1073   }