aboutWebrtc.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  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";
  5. /* global WebrtcGlobalInformation, document */
  6. var Cu = Components.utils;
  7. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  8. Cu.import("resource://gre/modules/Services.jsm");
  9. XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
  10. "resource://gre/modules/FileUtils.jsm");
  11. XPCOMUtils.defineLazyServiceGetter(this, "FilePicker",
  12. "@mozilla.org/filepicker;1", "nsIFilePicker");
  13. XPCOMUtils.defineLazyGetter(this, "strings", () => {
  14. return Services.strings.createBundle("chrome://global/locale/aboutWebrtc.properties");
  15. });
  16. const getString = strings.GetStringFromName;
  17. const formatString = strings.formatStringFromName;
  18. const LOGFILE_NAME_DEFAULT = "aboutWebrtc.html";
  19. const WEBRTC_TRACE_ALL = 65535;
  20. function getStats() {
  21. return new Promise(resolve =>
  22. WebrtcGlobalInformation.getAllStats(stats => resolve(stats)));
  23. }
  24. function getLog() {
  25. return new Promise(resolve =>
  26. WebrtcGlobalInformation.getLogging("", log => resolve(log)));
  27. }
  28. // Begin initial data queries as page loads. Store returned Promises for
  29. // later use.
  30. var reportsRetrieved = getStats();
  31. var logRetrieved = getLog();
  32. function onLoad() {
  33. document.title = getString("document_title");
  34. let controls = document.querySelector("#controls");
  35. if (controls) {
  36. let set = ControlSet.render();
  37. ControlSet.add(new SavePage());
  38. ControlSet.add(new DebugMode());
  39. ControlSet.add(new AecLogging());
  40. controls.appendChild(set);
  41. }
  42. let contentElem = document.querySelector("#content");
  43. if (!contentElem) {
  44. return;
  45. }
  46. let contentInit = function(data) {
  47. AboutWebRTC.init(onClearStats, onClearLog);
  48. AboutWebRTC.render(contentElem, data);
  49. };
  50. Promise.all([reportsRetrieved, logRetrieved])
  51. .then(([stats, log]) => contentInit({reports: stats.reports, log: log}))
  52. .catch(error => contentInit({error: error}));
  53. }
  54. function onClearLog() {
  55. WebrtcGlobalInformation.clearLogging();
  56. getLog()
  57. .then(log => AboutWebRTC.refresh({log: log}))
  58. .catch(error => AboutWebRTC.refresh({logError: error}));
  59. }
  60. function onClearStats() {
  61. WebrtcGlobalInformation.clearAllStats();
  62. getStats()
  63. .then(stats => AboutWebRTC.refresh({reports: stats.reports}))
  64. .catch(error => AboutWebRTC.refresh({reportError: error}));
  65. }
  66. var ControlSet = {
  67. render: function() {
  68. let controls = document.createElement("div");
  69. let control = document.createElement("div");
  70. let message = document.createElement("div");
  71. controls.className = "controls";
  72. control.className = "control";
  73. message.className = "message";
  74. controls.appendChild(control);
  75. controls.appendChild(message);
  76. this.controlSection = control;
  77. this.messageSection = message;
  78. return controls;
  79. },
  80. add: function(controlObj) {
  81. let [controlElem, messageElem] = controlObj.render();
  82. this.controlSection.appendChild(controlElem);
  83. this.messageSection.appendChild(messageElem);
  84. }
  85. };
  86. function Control() {
  87. this._label = null;
  88. this._message = null;
  89. this._messageHeader = null;
  90. }
  91. Control.prototype = {
  92. render: function () {
  93. let controlElem = document.createElement("button");
  94. let messageElem = document.createElement("p");
  95. this.ctrl = controlElem;
  96. controlElem.onclick = this.onClick.bind(this);
  97. this.msg = messageElem;
  98. this.update();
  99. return [controlElem, messageElem];
  100. },
  101. set label(val) {
  102. return this._labelVal = val || "\xA0";
  103. },
  104. get label() {
  105. return this._labelVal;
  106. },
  107. set message(val) {
  108. return this._messageVal = val;
  109. },
  110. get message() {
  111. return this._messageVal;
  112. },
  113. update: function() {
  114. this.ctrl.textContent = this._label;
  115. this.msg.textContent = "";
  116. if (this._message) {
  117. this.msg.appendChild(Object.assign(document.createElement("span"), {
  118. className: "info-label",
  119. textContent: `${this._messageHeader}: `,
  120. }));
  121. this.msg.appendChild(document.createTextNode(this._message));
  122. }
  123. },
  124. onClick: function(event) {
  125. return true;
  126. }
  127. };
  128. function SavePage() {
  129. Control.call(this);
  130. this._messageHeader = getString("save_page_label");
  131. this._label = getString("save_page_label");
  132. }
  133. SavePage.prototype = Object.create(Control.prototype);
  134. SavePage.prototype.constructor = SavePage;
  135. SavePage.prototype.onClick = function() {
  136. let content = document.querySelector("#content");
  137. if (!content)
  138. return;
  139. FoldEffect.expandAll();
  140. FilePicker.init(window, getString("save_page_dialog_title"), FilePicker.modeSave);
  141. FilePicker.defaultString = LOGFILE_NAME_DEFAULT;
  142. let rv = FilePicker.show();
  143. if (rv == FilePicker.returnOK || rv == FilePicker.returnReplace) {
  144. let fout = FileUtils.openAtomicFileOutputStream(
  145. FilePicker.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE);
  146. let nodes = content.querySelectorAll(".no-print");
  147. let noPrintList = [];
  148. for (let node of nodes) {
  149. noPrintList.push(node);
  150. node.style.setProperty("display", "none");
  151. }
  152. fout.write(content.outerHTML, content.outerHTML.length);
  153. FileUtils.closeAtomicFileOutputStream(fout);
  154. for (let node of noPrintList) {
  155. node.style.removeProperty("display");
  156. }
  157. this._message = formatString("save_page_msg", [FilePicker.file.path], 1);
  158. this.update();
  159. }
  160. };
  161. function DebugMode() {
  162. Control.call(this);
  163. this._messageHeader = getString("debug_mode_msg_label");
  164. if (WebrtcGlobalInformation.debugLevel > 0) {
  165. this.onState();
  166. } else {
  167. this._label = getString("debug_mode_off_state_label");
  168. this._message = null;
  169. }
  170. }
  171. DebugMode.prototype = Object.create(Control.prototype);
  172. DebugMode.prototype.constructor = DebugMode;
  173. DebugMode.prototype.onState = function() {
  174. this._label = getString("debug_mode_on_state_label");
  175. try {
  176. let file = Services.prefs.getCharPref("media.webrtc.debug.log_file");
  177. this._message = formatString("debug_mode_on_state_msg", [file], 1);
  178. } catch (e) {
  179. this._message = null;
  180. }
  181. };
  182. DebugMode.prototype.offState = function() {
  183. this._label = getString("debug_mode_off_state_label");
  184. try {
  185. let file = Services.prefs.getCharPref("media.webrtc.debug.log_file");
  186. this._message = formatString("debug_mode_off_state_msg", [file], 1);
  187. } catch (e) {
  188. this._message = null;
  189. }
  190. };
  191. DebugMode.prototype.onClick = function() {
  192. if (WebrtcGlobalInformation.debugLevel > 0) {
  193. WebrtcGlobalInformation.debugLevel = 0;
  194. this.offState();
  195. } else {
  196. WebrtcGlobalInformation.debugLevel = WEBRTC_TRACE_ALL;
  197. this.onState();
  198. }
  199. this.update();
  200. };
  201. function AecLogging() {
  202. Control.call(this);
  203. this._messageHeader = getString("aec_logging_msg_label");
  204. if (WebrtcGlobalInformation.aecDebug) {
  205. this.onState();
  206. } else {
  207. this._label = getString("aec_logging_off_state_label");
  208. this._message = null;
  209. }
  210. }
  211. AecLogging.prototype = Object.create(Control.prototype);
  212. AecLogging.prototype.constructor = AecLogging;
  213. AecLogging.prototype.offState = function () {
  214. this._label = getString("aec_logging_off_state_label");
  215. try {
  216. let file = Services.prefs.getCharPref("media.webrtc.debug.aec_log_dir");
  217. this._message = formatString("aec_logging_off_state_msg", [file], 1);
  218. } catch (e) {
  219. this._message = null;
  220. }
  221. };
  222. AecLogging.prototype.onState = function () {
  223. this._label = getString("aec_logging_on_state_label");
  224. try {
  225. let file = Services.prefs.getCharPref("media.webrtc.debug.aec_log_dir");
  226. this._message = getString("aec_logging_on_state_msg");
  227. } catch (e) {
  228. this._message = null;
  229. }
  230. };
  231. AecLogging.prototype.onClick = function () {
  232. if (WebrtcGlobalInformation.aecDebug) {
  233. WebrtcGlobalInformation.aecDebug = false;
  234. this.offState();
  235. } else {
  236. WebrtcGlobalInformation.aecDebug = true;
  237. this.onState();
  238. }
  239. this.update();
  240. };
  241. var AboutWebRTC = {
  242. _reports: [],
  243. _log: [],
  244. init: function(onClearStats, onClearLog) {
  245. this._onClearStats = onClearStats;
  246. this._onClearLog = onClearLog;
  247. },
  248. render: function(parent, data) {
  249. this._content = parent;
  250. this._setData(data);
  251. if (data.error) {
  252. let msg = document.createElement("h3");
  253. msg.textContent = getString("cannot_retrieve_log");
  254. parent.appendChild(msg);
  255. msg = document.createElement("p");
  256. msg.textContent = `${data.error.name}: ${data.error.message}`;
  257. parent.appendChild(msg);
  258. return;
  259. }
  260. this._peerConnections = this.renderPeerConnections();
  261. this._connectionLog = this.renderConnectionLog();
  262. this._content.appendChild(this._peerConnections);
  263. this._content.appendChild(this._connectionLog);
  264. },
  265. _setData: function(data) {
  266. if (data.reports) {
  267. this._reports = data.reports;
  268. }
  269. if (data.log) {
  270. this._log = data.log;
  271. }
  272. },
  273. refresh: function(data) {
  274. this._setData(data);
  275. let pc = this._peerConnections;
  276. this._peerConnections = this.renderPeerConnections();
  277. let log = this._connectionLog;
  278. this._connectionLog = this.renderConnectionLog();
  279. this._content.replaceChild(this._peerConnections, pc);
  280. this._content.replaceChild(this._connectionLog, log);
  281. },
  282. renderPeerConnections: function() {
  283. let connections = document.createElement("div");
  284. connections.className = "stats";
  285. let heading = document.createElement("span");
  286. heading.className = "section-heading";
  287. let elem = document.createElement("h3");
  288. elem.textContent = getString("stats_heading");
  289. heading.appendChild(elem);
  290. elem = document.createElement("button");
  291. elem.textContent = "Clear History";
  292. elem.className = "no-print";
  293. elem.onclick = this._onClearStats;
  294. heading.appendChild(elem);
  295. connections.appendChild(heading);
  296. if (!this._reports || !this._reports.length) {
  297. return connections;
  298. }
  299. let reports = [...this._reports];
  300. reports.sort((a, b) => b.timestamp - a.timestamp);
  301. for (let report of reports) {
  302. let peerConnection = new PeerConnection(report);
  303. connections.appendChild(peerConnection.render());
  304. }
  305. return connections;
  306. },
  307. renderConnectionLog: function() {
  308. let content = document.createElement("div");
  309. content.className = "log";
  310. let heading = document.createElement("span");
  311. heading.className = "section-heading";
  312. let elem = document.createElement("h3");
  313. elem.textContent = getString("log_heading");
  314. heading.appendChild(elem);
  315. elem = document.createElement("button");
  316. elem.textContent = "Clear Log";
  317. elem.className = "no-print";
  318. elem.onclick = this._onClearLog;
  319. heading.appendChild(elem);
  320. content.appendChild(heading);
  321. if (!this._log || !this._log.length) {
  322. return content;
  323. }
  324. let div = document.createElement("div");
  325. let sectionCtrl = document.createElement("div");
  326. sectionCtrl.className = "section-ctrl no-print";
  327. let foldEffect = new FoldEffect(div, {
  328. showMsg: getString("log_show_msg"),
  329. hideMsg: getString("log_hide_msg")
  330. });
  331. sectionCtrl.appendChild(foldEffect.render());
  332. content.appendChild(sectionCtrl);
  333. for (let line of this._log) {
  334. elem = document.createElement("p");
  335. elem.textContent = line;
  336. div.appendChild(elem);
  337. }
  338. content.appendChild(div);
  339. return content;
  340. }
  341. };
  342. function PeerConnection(report) {
  343. this._report = report;
  344. }
  345. PeerConnection.prototype = {
  346. render: function() {
  347. let pc = document.createElement("div");
  348. pc.className = "peer-connection";
  349. pc.appendChild(this.renderHeading());
  350. let div = document.createElement("div");
  351. let sectionCtrl = document.createElement("div");
  352. sectionCtrl.className = "section-ctrl no-print";
  353. let foldEffect = new FoldEffect(div);
  354. sectionCtrl.appendChild(foldEffect.render());
  355. pc.appendChild(sectionCtrl);
  356. div.appendChild(this.renderDesc());
  357. div.appendChild(new ICEStats(this._report).render());
  358. div.appendChild(new SDPStats(this._report).render());
  359. div.appendChild(new RTPStats(this._report).render());
  360. pc.appendChild(div);
  361. return pc;
  362. },
  363. renderHeading: function () {
  364. let pcInfo = this.getPCInfo(this._report);
  365. let heading = document.createElement("h3");
  366. let now = new Date(this._report.timestamp).toTimeString();
  367. heading.textContent =
  368. `[ ${pcInfo.id} ] ${pcInfo.url} ${pcInfo.closed ? `(${getString("connection_closed")})` : ""} ${now}`;
  369. return heading;
  370. },
  371. renderDesc: function() {
  372. let info = document.createElement("div");
  373. let label = document.createElement("span");
  374. let body = document.createElement("span");
  375. label.className = "info-label";
  376. label.textContent = `${getString("peer_connection_id_label")}: `;
  377. info.appendChild(label);
  378. body.className = "info-body";
  379. body.textContent = this._report.pcid;
  380. info.appendChild(body);
  381. return info;
  382. },
  383. getPCInfo: function(report) {
  384. return {
  385. id: report.pcid.match(/id=(\S+)/)[1],
  386. url: report.pcid.match(/url=([^)]+)/)[1],
  387. closed: report.closed
  388. };
  389. }
  390. };
  391. function SDPStats(report) {
  392. this._report = report;
  393. }
  394. SDPStats.prototype = {
  395. render: function() {
  396. let div = document.createElement("div");
  397. let elem = document.createElement("h4");
  398. elem.textContent = getString("sdp_heading");
  399. div.appendChild(elem);
  400. elem = document.createElement("h5");
  401. elem.textContent = getString("local_sdp_heading");
  402. div.appendChild(elem);
  403. elem = document.createElement("pre");
  404. elem.textContent = this._report.localSdp;
  405. div.appendChild(elem);
  406. elem = document.createElement("h5");
  407. elem.textContent = getString("remote_sdp_heading");
  408. div.appendChild(elem);
  409. elem = document.createElement("pre");
  410. elem.textContent = this._report.remoteSdp;
  411. div.appendChild(elem);
  412. return div;
  413. }
  414. };
  415. function RTPStats(report) {
  416. this._report = report;
  417. this._stats = [];
  418. }
  419. RTPStats.prototype = {
  420. render: function() {
  421. let div = document.createElement("div");
  422. let heading = document.createElement("h4");
  423. heading.textContent = getString("rtp_stats_heading");
  424. div.appendChild(heading);
  425. this.generateRTPStats();
  426. for (let statSet of this._stats) {
  427. div.appendChild(this.renderRTPStatSet(statSet));
  428. }
  429. return div;
  430. },
  431. generateRTPStats: function() {
  432. let remoteRtpStats = {};
  433. let rtpStats = [].concat((this._report.inboundRTPStreamStats || []),
  434. (this._report.outboundRTPStreamStats || []));
  435. // Generate an id-to-streamStat index for each streamStat that is marked
  436. // as a remote. This will be used next to link the remote to its local side.
  437. for (let stats of rtpStats) {
  438. if (stats.isRemote) {
  439. remoteRtpStats[stats.id] = stats;
  440. }
  441. }
  442. // If a streamStat has a remoteId attribute, create a remoteRtpStats
  443. // attribute that references the remote streamStat entry directly.
  444. // That is, the index generated above is merged into the returned list.
  445. for (let stats of rtpStats) {
  446. if (stats.remoteId) {
  447. stats.remoteRtpStats = remoteRtpStats[stats.remoteId];
  448. }
  449. }
  450. this._stats = rtpStats;
  451. },
  452. renderAvStats: function(stats) {
  453. let statsString = "";
  454. if (stats.mozAvSyncDelay) {
  455. statsString += `${getString("av_sync_label")}: ${stats.mozAvSyncDelay} ms `;
  456. }
  457. if (stats.mozJitterBufferDelay) {
  458. statsString += `${getString("jitter_buffer_delay_label")}: ${stats.mozJitterBufferDelay} ms`;
  459. }
  460. let line = document.createElement("p");
  461. line.textContent = statsString;
  462. return line;
  463. },
  464. renderCoderStats: function(stats) {
  465. let statsString = "";
  466. let label;
  467. if (stats.bitrateMean) {
  468. statsString += ` ${getString("avg_bitrate_label")}: ${(stats.bitrateMean / 1000000).toFixed(2)} Mbps`;
  469. if (stats.bitrateStdDev) {
  470. statsString += ` (${(stats.bitrateStdDev / 1000000).toFixed(2)} SD)`;
  471. }
  472. }
  473. if (stats.framerateMean) {
  474. statsString += ` ${getString("avg_framerate_label")}: ${(stats.framerateMean).toFixed(2)} fps`;
  475. if (stats.framerateStdDev) {
  476. statsString += ` (${stats.framerateStdDev.toFixed(2)} SD)`;
  477. }
  478. }
  479. if (stats.droppedFrames) {
  480. statsString += ` ${getString("dropped_frames_label")}: ${stats.droppedFrames}`;
  481. }
  482. if (stats.discardedPackets) {
  483. statsString += ` ${getString("discarded_packets_label")}: ${stats.discardedPackets}`;
  484. }
  485. if (statsString) {
  486. label = (stats.packetsReceived ? ` ${getString("decoder_label")}:` : ` ${getString("encoder_label")}:`);
  487. statsString = label + statsString;
  488. }
  489. let line = document.createElement("p");
  490. line.textContent = statsString;
  491. return line;
  492. },
  493. renderTransportStats: function(stats, typeLabel) {
  494. let time = new Date(stats.timestamp).toTimeString();
  495. let statsString = `${typeLabel}: ${time} ${stats.type} SSRC: ${stats.ssrc}`;
  496. if (stats.packetsReceived) {
  497. statsString += ` ${getString("received_label")}: ${stats.packetsReceived} ${getString("packets")}`;
  498. if (stats.bytesReceived) {
  499. statsString += ` (${(stats.bytesReceived / 1024).toFixed(2)} Kb)`;
  500. }
  501. statsString += ` ${getString("lost_label")}: ${stats.packetsLost} ${getString("jitter_label")}: ${stats.jitter}`;
  502. if (stats.mozRtt) {
  503. statsString += ` RTT: ${stats.mozRtt} ms`;
  504. }
  505. } else if (stats.packetsSent) {
  506. statsString += ` ${getString("sent_label")}: ${stats.packetsSent} ${getString("packets")}`;
  507. if (stats.bytesSent) {
  508. statsString += ` (${(stats.bytesSent / 1024).toFixed(2)} Kb)`;
  509. }
  510. }
  511. let line = document.createElement("p");
  512. line.textContent = statsString;
  513. return line;
  514. },
  515. renderRTPStatSet: function(stats) {
  516. let div = document.createElement("div");
  517. let heading = document.createElement("h5");
  518. heading.textContent = stats.id;
  519. div.appendChild(heading);
  520. if (stats.MozAvSyncDelay || stats.mozJitterBufferDelay) {
  521. div.appendChild(this.renderAvStats(stats));
  522. }
  523. div.appendChild(this.renderCoderStats(stats));
  524. div.appendChild(this.renderTransportStats(stats, getString("typeLocal")));
  525. if (stats.remoteId && stats.remoteRtpStats) {
  526. div.appendChild(this.renderTransportStats(stats.remoteRtpStats, getString("typeRemote")));
  527. }
  528. return div;
  529. },
  530. };
  531. function ICEStats(report) {
  532. this._report = report;
  533. }
  534. ICEStats.prototype = {
  535. render: function() {
  536. let tbody = [];
  537. for (let stat of this.generateICEStats()) {
  538. tbody.push([
  539. stat.localcandidate || "",
  540. stat.remotecandidate || "",
  541. stat.state || "",
  542. stat.priority || "",
  543. stat.nominated || "",
  544. stat.selected || ""
  545. ]);
  546. }
  547. let statsTable = new SimpleTable(
  548. [getString("local_candidate"), getString("remote_candidate"), getString("ice_state"),
  549. getString("priority"), getString("nominated"), getString("selected")],
  550. tbody);
  551. let div = document.createElement("div");
  552. let heading = document.createElement("h4");
  553. heading.textContent = getString("ice_stats_heading");
  554. div.appendChild(heading);
  555. div.appendChild(statsTable.render());
  556. return div;
  557. },
  558. generateICEStats: function() {
  559. // Create an index based on candidate ID for each element in the
  560. // iceCandidateStats array.
  561. let candidates = new Map();
  562. for (let candidate of this._report.iceCandidateStats) {
  563. candidates.set(candidate.id, candidate);
  564. }
  565. // A component may have a remote or local candidate address or both.
  566. // Combine those with both; these will be the peer candidates.
  567. let matched = {};
  568. let stats = [];
  569. let stat;
  570. for (let pair of this._report.iceCandidatePairStats) {
  571. let local = candidates.get(pair.localCandidateId);
  572. let remote = candidates.get(pair.remoteCandidateId);
  573. if (local) {
  574. stat = {
  575. localcandidate: this.candidateToString(local),
  576. state: pair.state,
  577. priority: pair.priority,
  578. nominated: pair.nominated,
  579. selected: pair.selected
  580. };
  581. matched[local.id] = true;
  582. if (remote) {
  583. stat.remotecandidate = this.candidateToString(remote);
  584. matched[remote.id] = true;
  585. }
  586. stats.push(stat);
  587. }
  588. }
  589. for (let c of candidates.values()) {
  590. if (matched[c.id])
  591. continue;
  592. stat = {};
  593. stat[c.type] = this.candidateToString(c);
  594. stats.push(stat);
  595. }
  596. return stats.sort((a, b) => (b.priority || 0) - (a.priority || 0));
  597. },
  598. candidateToString: function(c) {
  599. if (!c) {
  600. return "*";
  601. }
  602. var type = c.candidateType;
  603. if (c.type == "localcandidate" && c.candidateType == "relayed") {
  604. type = `${c.candidateType}-${c.mozLocalTransport}`;
  605. }
  606. return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`;
  607. }
  608. };
  609. function SimpleTable(heading, data) {
  610. this._heading = heading || [];
  611. this._data = data;
  612. }
  613. SimpleTable.prototype = {
  614. renderRow: function(list) {
  615. let row = document.createElement("tr");
  616. for (let elem of list) {
  617. let cell = document.createElement("td");
  618. cell.textContent = elem;
  619. row.appendChild(cell);
  620. }
  621. return row;
  622. },
  623. render: function() {
  624. let table = document.createElement("table");
  625. if (this._heading) {
  626. table.appendChild(this.renderRow(this._heading));
  627. }
  628. for (let row of this._data) {
  629. table.appendChild(this.renderRow(row));
  630. }
  631. return table;
  632. }
  633. };
  634. function FoldEffect(targetElem, options = {}) {
  635. if (targetElem) {
  636. this._showMsg = "\u25BC " + (options.showMsg || getString("fold_show_msg"));
  637. this._showHint = options.showHint || getString("fold_show_hint");
  638. this._hideMsg = "\u25B2 " + (options.hideMsg || getString("fold_hide_msg"));
  639. this._hideHint = options.hideHint || getString("fold_hide_hint");
  640. this._target = targetElem;
  641. }
  642. }
  643. FoldEffect.prototype = {
  644. render: function() {
  645. this._target.classList.add("fold-target");
  646. let ctrl = document.createElement("div");
  647. this._trigger = ctrl;
  648. ctrl.className = "fold-trigger";
  649. ctrl.addEventListener("click", this.onClick.bind(this));
  650. this.close();
  651. FoldEffect._sections.push(this);
  652. return ctrl;
  653. },
  654. onClick: function() {
  655. if (this._target.classList.contains("fold-closed")) {
  656. this.open();
  657. } else {
  658. this.close();
  659. }
  660. return true;
  661. },
  662. open: function() {
  663. this._target.classList.remove("fold-closed");
  664. this._trigger.setAttribute("title", this._hideHint);
  665. this._trigger.textContent = this._hideMsg;
  666. },
  667. close: function() {
  668. this._target.classList.add("fold-closed");
  669. this._trigger.setAttribute("title", this._showHint);
  670. this._trigger.textContent = this._showMsg;
  671. }
  672. };
  673. FoldEffect._sections = [];
  674. FoldEffect.expandAll = function() {
  675. for (let section of this._sections) {
  676. section.open();
  677. }
  678. };
  679. FoldEffect.collapseAll = function() {
  680. for (let section of this._sections) {
  681. section.close();
  682. }
  683. };