Bug 1100508: Easily capture about:webrtc page data for a bug reports. r=jib, r=mikedeboer
authorPaul Kerr [:pkerr] <pkerr@mozilla.com>
Mon, 23 Mar 2015 12:37:30 -0700
changeset 265447 6f9fee1b03d79402749e701798063828432b2281
parent 265446 8e5d8f34c53eecd8838576830403ef2ec7322245
child 265448 bcebc84e7238d5dea0f615f49e3f7ddbfc5a7277
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjib, mikedeboer
bugs1100508
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1100508: Easily capture about:webrtc page data for a bug reports. r=jib, r=mikedeboer
b2g/locales/jar.mn
media/webrtc/signaling/src/common/browser_logging/WebRtcLog.cpp
mobile/android/locales/jar.mn
toolkit/content/aboutwebrtc/README.txt
toolkit/content/aboutwebrtc/aboutWebrtc.css
toolkit/content/aboutwebrtc/aboutWebrtc.js
toolkit/content/aboutwebrtc/aboutWebrtc.jsx
toolkit/content/aboutwebrtc/aboutWebrtc.xhtml
toolkit/locales/en-US/chrome/global/aboutWebrtc.properties
toolkit/locales/jar.mn
--- a/b2g/locales/jar.mn
+++ b/b2g/locales/jar.mn
@@ -31,16 +31,18 @@ relativesrcdir toolkit/locales:
 #about:crashes
   locale/@AB_CD@/b2g-l10n/overrides/crashreporter/crashes.dtd         (%crashreporter/crashes.dtd)
   locale/@AB_CD@/b2g-l10n/overrides/crashreporter/crashes.properties  (%crashreporter/crashes.properties)
 #about:mozilla
   locale/@AB_CD@/b2g-l10n/overrides/global/mozilla.dtd                (%chrome/global/mozilla.dtd)
 #about:telemetry
   locale/@AB_CD@/b2g-l10n/overrides/global/aboutTelemetry.dtd         (%chrome/global/aboutTelemetry.dtd)
   locale/@AB_CD@/b2g-l10n/overrides/global/aboutTelemetry.properties  (%chrome/global/aboutTelemetry.properties)
+#about:webrtc
+  locale/@AB_CD@/b2g-l10n/overrides/global/aboutWebrtc.properties  (%chrome/global/aboutWebrtc.properties)
 
 % override chrome://global/locale/about.dtd chrome://b2g-l10n/locale/overrides/about.dtd
 % override chrome://global/locale/aboutAbout.dtd chrome://b2g-l10n/locale/overrides/aboutAbout.dtd
 % override chrome://global/locale/aboutRights.dtd chrome://b2g-l10n/locale/overrides/aboutRights.dtd
 % override chrome://global/locale/commonDialogs.properties chrome://b2g-l10n/locale/overrides/commonDialogs.properties
 % override chrome://mozapps/locale/handling/handling.properties chrome://b2g-l10n/locale/overrides/handling/handling.properties
 % override chrome://global/locale/intl.properties chrome://b2g-l10n/locale/overrides/intl.properties
 % override chrome://global/locale/intl.css chrome://b2g-l10n/locale/overrides/intl.css
@@ -49,16 +51,17 @@ relativesrcdir toolkit/locales:
 % override chrome://mozapps/locale/update/updates.properties chrome://b2g-l10n/locale/overrides/update/updates.properties
 % override chrome://global/locale/aboutSupport.dtd chrome://b2g-l10n/locale/overrides/global/aboutSupport.dtd
 % override chrome://global/locale/aboutSupport.properties chrome://b2g-l10n/locale/overrides/global/aboutSupport.properties
 % override chrome://global/locale/crashes.dtd chrome://b2g-l10n/locale/overrides/crashreporter/crashes.dtd
 % override chrome://global/locale/crashes.properties chrome://b2g-l10n/locale/overrides/crashreporter/crashes.properties
 % override chrome://global/locale/mozilla.dtd chrome://b2g-l10n/locale/overrides/global/mozilla.dtd
 % override chrome://global/locale/aboutTelemetry.dtd chrome://b2g-l10n/locale/overrides/global/aboutTelemetry.dtd
 % override chrome://global/locale/aboutTelemetry.properties chrome://b2g-l10n/locale/overrides/global/aboutTelemetry.properties
+% override chrome://global/locale/aboutWebrtc.properties chrome://b2g-l10n/locale/overrides/global/aboutWebrtc.properties
 
 # overrides for dom l10n, also for en-US
 relativesrcdir dom/locales:
   locale/@AB_CD@/b2g-l10n/overrides/global.dtd                  (%chrome/global.dtd)
   locale/@AB_CD@/b2g-l10n/overrides/AccessFu.properties         (%chrome/accessibility/AccessFu.properties)
   locale/@AB_CD@/b2g-l10n/overrides/dom/dom.properties          (%chrome/dom/dom.properties)
 #about:plugins
   locale/@AB_CD@/b2g-l10n/overrides/plugins.properties          (%chrome/plugins.properties)
--- a/media/webrtc/signaling/src/common/browser_logging/WebRtcLog.cpp
+++ b/media/webrtc/signaling/src/common/browser_logging/WebRtcLog.cpp
@@ -136,16 +136,22 @@ void ConfigWebRtcLog(uint32_t trace_mask
   webrtc::Trace::set_aec_debug_filename(aAECLogDir.get());
   if (trace_mask != 0) {
     if (aLogFile.EqualsLiteral("nspr")) {
       webrtc::Trace::SetTraceCallback(&gWebRtcCallback);
     } else {
       webrtc::Trace::SetTraceFile(aLogFile.get(), multi_log);
     }
   }
+#ifdef MOZILLA_INTERNAL_API
+  // Capture the final choices for the trace settings.
+  mozilla::Preferences::SetCString("media.webrtc.debug.log_file", aLogFile);
+  mozilla::Preferences::SetUint("media.webrtc.debug.trace_mask", trace_mask);
+  mozilla::Preferences::SetCString("media.webrtc.debug.aec_log_dir", aAECLogDir);
+#endif
   return;
 }
 
 void StartWebRtcLog(uint32_t log_level)
 {
   if (gWebRtcTraceLoggingOn && log_level != 0) {
     return;
   }
--- a/mobile/android/locales/jar.mn
+++ b/mobile/android/locales/jar.mn
@@ -64,16 +64,18 @@ relativesrcdir toolkit/locales:
 #about:crashes
   locale/@AB_CD@/browser/overrides/crashreporter/crashes.dtd         (%crashreporter/crashes.dtd)
   locale/@AB_CD@/browser/overrides/crashreporter/crashes.properties  (%crashreporter/crashes.properties)
 #about:mozilla
   locale/@AB_CD@/browser/overrides/global/mozilla.dtd                (%chrome/global/mozilla.dtd)
 #about:telemetry
   locale/@AB_CD@/browser/overrides/global/aboutTelemetry.dtd         (%chrome/global/aboutTelemetry.dtd)
   locale/@AB_CD@/browser/overrides/global/aboutTelemetry.properties  (%chrome/global/aboutTelemetry.properties)
+#about:webrtc
+  locale/@AB_CD@/browser/overrides/global/aboutWebrtc.properties  (%chrome/global/aboutWebrtc.properties)
 
 % override chrome://global/locale/about.dtd chrome://browser/locale/overrides/about.dtd
 % override chrome://global/locale/aboutAbout.dtd chrome://browser/locale/overrides/aboutAbout.dtd
 % override chrome://global/locale/aboutRights.dtd chrome://browser/locale/overrides/aboutRights.dtd
 % override chrome://global/locale/charsetMenu.properties chrome://browser/locale/overrides/charsetMenu.properties
 % override chrome://global/locale/commonDialogs.properties chrome://browser/locale/overrides/commonDialogs.properties
 % override chrome://mozapps/locale/handling/handling.properties chrome://browser/locale/handling.properties
 % override chrome://global/locale/intl.properties chrome://browser/locale/overrides/intl.properties
@@ -84,16 +86,17 @@ relativesrcdir toolkit/locales:
 % override chrome://mozapps/locale/plugins/plugins.dtd chrome://browser/locale/overrides/plugins/plugins.dtd
 % override chrome://global/locale/aboutSupport.dtd chrome://browser/locale/overrides/global/aboutSupport.dtd
 % override chrome://global/locale/aboutSupport.properties chrome://browser/locale/overrides/global/aboutSupport.properties
 % override chrome://global/locale/crashes.dtd chrome://browser/locale/overrides/crashreporter/crashes.dtd
 % override chrome://global/locale/crashes.properties chrome://browser/locale/overrides/crashreporter/crashes.properties
 % override chrome://global/locale/mozilla.dtd chrome://browser/locale/overrides/global/mozilla.dtd
 % override chrome://global/locale/aboutTelemetry.dtd chrome://browser/locale/overrides/global/aboutTelemetry.dtd
 % override chrome://global/locale/aboutTelemetry.properties chrome://browser/locale/overrides/global/aboutTelemetry.properties
+% override chrome://global/locale/aboutWebrtc.properties chrome://browser/locale/overrides/global/aboutWebrtc.properties
 
 # overrides for dom l10n, also for en-US
 relativesrcdir dom/locales:
   locale/@AB_CD@/browser/overrides/global.dtd                  (%chrome/global.dtd)
   locale/@AB_CD@/browser/overrides/AccessFu.properties         (%chrome/accessibility/AccessFu.properties)
   locale/@AB_CD@/browser/overrides/dom/dom.properties          (%chrome/dom/dom.properties)
 #about:plugins
   locale/@AB_CD@/browser/overrides/plugins.properties          (%chrome/plugins.properties)
deleted file mode 100644
--- a/toolkit/content/aboutwebrtc/README.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-Working with React JSX files
-============================
-
-The about:webrtc page uses [React](http://facebook.github.io/react/).
-The UI is written in JSX files and transpiled to JS before we
-commit. You need to install the JSX compiler using npm in order to
-compile the .jsx files into regular .js ones:
-
-    npm install -g react-tools
-
-Run the following command:
-
-   jsx -w -x jsx . .
-
-jsx can also be do a one-time compile pass instead of watching if the
--w argument is omitted. Be sure to commit any transpiled files at the
-same time as changes to their sources.
-
-IMPORTANT: do not modify the generated files, only their JSX
-counterpart.
-
--- a/toolkit/content/aboutwebrtc/aboutWebrtc.css
+++ b/toolkit/content/aboutwebrtc/aboutWebrtc.css
@@ -1,78 +1,92 @@
+/* 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/. */
+
 html {
-    background-color: #EDECEB;
-    font: message-box;
+  background-color: #edeceb;
+  font: message-box;
 }
 
-#logs {
-    font-family: monospace;
+.controls {
+  font-size: 1.1em;
+  display: inline-block;
+  margin: 0 0.5em;
+}
+
+.control {
+  margin: 0.5em 0;
+}
+
+.control > button {
+  margin: 0 0.25em;
 }
 
-.peer-connections {
-    padding: 2em;
-    margin: 1em 0em;
-    border: 1px solid #AFACA9;
-    border-radius: 10px;
-    background: none repeat scroll 0% 0% #FFF;
+.message > p {
+  margin: 0.5em 0.5em;
+}
+
+.log p {
+  font-family: monospace;
+  padding-left: 2em;
+  text-indent: -2em;
+  margin-top: 2px;
+  margin-bottom: 2px;
 }
 
-.peer-connection h3 {
-    background-color: #DDD;
-    cursor: pointer;
+#content {
+  padding: 1em 2em;
+  margin: 1em 0;
+  border: 1px solid #afaca9;
+  border-radius: 10px;
+  background: none repeat scroll 0% 0% #fff;
 }
 
-.peer-connection h3 span:last-child {
-    float: right;
+.peer-connection > h3,
+.log > h3 {
+  background-color: #ddd;
 }
 
 .peer-connection section {
-    border: 1px solid #AFACA9;
-    padding: 10px;
+  border: 1px solid #afaca9;
+  padding: 10px;
 }
 
 .peer-connection table {
-    width: 100%;
-    text-align: center;
-    border: none;
+  width: 100%;
+  text-align: center;
+  border: none;
 }
 
 .peer-connection table th,
 .peer-connection table td {
-    padding: 0.4em;
+  padding: 0.4em;
 }
 
 .peer-connection table tr:nth-child(even) {
-    background-color: #DDD;
+  background-color: #ddd;
 }
 
-.pcid span:first-child {
-    font-weight: bold;
+.info-label {
+  font-weight: bold;
 }
 
-.tabs > ul {
-  list-style-type: none;
-  margin: 0;
-  padding: 3px 10px;
+.section-ctrl {
+  margin: 1em 1.5em;
 }
 
-.tabs li {
-  display: inline;
-  position: relative;
-  top: 1px;
-  padding: 3px 10px;
+div.fold-trigger {
+  color: blue;
+  cursor: pointer;
 }
 
-.tabs li > a {
-  text-decoration: none;
-  padding: 0 2em;
+@media screen {
+  .fold-closed {
+    display: none !important;
+  }
 }
 
-.tabs li.active {
-  background-color: #FFF;
-  border: 1px solid #AFACA9;
-  border-bottom: none;
+@media print {
+  .no-print {
+    display: none !important;
+  }
 }
-
-.tabs section {
-  clear: both;
-}
-
--- a/toolkit/content/aboutwebrtc/aboutWebrtc.js
+++ b/toolkit/content/aboutwebrtc/aboutWebrtc.js
@@ -1,532 +1,762 @@
-/** @jsx React.DOM */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* jshint newcap:false */
-/* global React, WebrtcGlobalInformation, document */
-
 "use strict";
 
-var Tabs = React.createClass({displayName: "Tabs",
-  getDefaultProps: function() {
-    return {selectedIndex: 0};
+/* global WebrtcGlobalInformation, document */
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(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;
+
+let reportsRetrieved = new Promise(resolve =>
+  WebrtcGlobalInformation.getAllStats(stats => resolve(stats))
+);
+
+let logRetrieved = new Promise(resolve =>
+  WebrtcGlobalInformation.getLogging("", log => resolve(log))
+);
+
+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 content = document.querySelector("#content");
+  if (!content) {
+    return;
+  }
+
+  Promise.all([reportsRetrieved, logRetrieved])
+    .then(([stats, log]) => {
+      AboutWebRTC.init(stats.reports, log);
+      content.appendChild(AboutWebRTC.render());
+    }).catch(error => {
+      let msg = document.createElement("h3");
+      msg.textContent = getString("cannot_retieve_log");
+      content.appendChild(msg);
+      msg = document.createElement("p");
+      msg.innerHTML = `${error.name}: ${error.message}`;
+      content.appendChild(msg);
+    });
+}
+
+let ControlSet = {
+  render: function() {
+    let controls = document.createElement("div");
+    let control = document.createElement("div");
+    let message = document.createElement("div");
+
+    controls.className = "controls";
+    control.className = "control";
+    message.className = "message";
+    controls.appendChild(control);
+    controls.appendChild(message);
+
+    this.controlSection = control;
+    this.messageSection = message;
+    return controls;
+  },
+
+  add: function(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: function () {
+    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: function() {
+    this.ctrl.textContent = this._label;
+
+    if (this._message) {
+      this.msg.innerHTML =
+        `<span class="info-label">${this._messageHeader}:</span> ${this._message}`;
+    } else {
+      this.msg.innerHTML = null;
+    }
   },
 
-  getInitialState: function() {
-    return {selectedIndex: this.props.selectedIndex};
-  },
+  onClick: function(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;
+  let rv = FilePicker.show();
+
+  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;
+  }
+};
 
-  selectTab: function(index) {
-    return function(event) {
-      event.preventDefault();
-      this.setState({selectedIndex: index});
-    }.bind(this);
-  },
+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;
 
-  _findSelectedTabContent: function() {
-    // Using map() to filter children…
-    // https://github.com/facebook/react/issues/1644#issuecomment-45138113
-    return React.Children.map(this.props.children, function(tab, i) {
-      return i === this.state.selectedIndex ? tab : null;
-    }.bind(this));
+AecLogging.prototype.offState = function () {
+  this._label = getString("aec_logging_off_state_label");
+  try {
+    let file = Services.prefs.getCharPref("media.webrtc.debug.aec_log_dir");
+    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 {
+    let file = Services.prefs.getCharPref("media.webrtc.debug.aec_log_dir");
+    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();
+};
+
+let AboutWebRTC = {
+  _reports: [],
+  _log: [],
+
+  init: function(reports, log) {
+    this._reports = reports || [];
+    this._log = log || [];
   },
 
   render: function() {
-    var cx = React.addons.classSet;
-    return (
-      React.createElement("div", {className: "tabs"}, 
-        React.createElement("ul", null, 
-          React.Children.map(this.props.children, function(tab, i) {
-            return (
-              React.createElement("li", {className: cx({active: i === this.state.selectedIndex})}, 
-                React.createElement("a", {href: "#", key: i, onClick: this.selectTab(i)}, 
-                  tab.props.title
-                )
-              )
-            );
-          }.bind(this))
-        ), 
-        this._findSelectedTabContent()
-      )
-    );
-  }
-});
-
-var Tab = React.createClass({displayName: "Tab",
-  render: function() {
-    return React.createElement("section", null, this.props.children);
-  }
-});
-
-var AboutWebRTC = React.createClass({displayName: "AboutWebRTC",
-  getInitialState: function() {
-    return {logs: null, reports: this.props.reports};
+    let content = document.createDocumentFragment();
+    content.appendChild(this.renderPeerConnections());
+    content.appendChild(this.renderConnectionLog());
+    return content;
   },
 
-  displayLogs: function() {
-    WebrtcGlobalInformation.getLogging('', function(logs) {
-      this.setState({logs: logs, reports: this.state.reports});
-    }.bind(this));
+  renderPeerConnections: function() {
+    let connections = document.createDocumentFragment();
+
+    let reports = [...this._reports];
+    reports.sort((a, b) => a.timestamp - b.timestamp);
+    for (let report of reports) {
+      let peerConnection = new PeerConnection(report);
+      connections.appendChild(peerConnection.render());
+    };
+
+    return connections;
   },
 
+  renderConnectionLog: function() {
+    let content = document.createElement("div");
+    content.className = "log";
+
+    let elem = document.createElement("h3");
+    elem.textContent = getString("log_heading");
+    content.appendChild(elem);
+
+    let div = document.createElement("div");
+    let sectionCtrl = document.createElement("div");
+    sectionCtrl.className = "section-ctrl no-print";
+    let foldEffect = new FoldEffect(div, {
+      showMsg: getString("log_show_msg"),
+      hideMsg: getString("log_hide_msg")
+    });
+    sectionCtrl.appendChild(foldEffect.render());
+    content.appendChild(sectionCtrl);
+
+    for (let line of this._log) {
+      elem = document.createElement("p");
+      elem.textContent = line;
+      div.appendChild(elem);
+    };
+
+    content.appendChild(div);
+    return content;
+  }
+};
+
+function PeerConnection(report) {
+  this._report = report;
+}
+
+PeerConnection.prototype = {
   render: function() {
-    return (
-      React.createElement("div", null, 
-        React.createElement("div", {id: "stats"}, 
-          React.createElement(PeerConnections, {reports: this.state.reports})
-        ), 
-        React.createElement("div", {id: "buttons"}, 
-          React.createElement(LogsButton, {handler: this.displayLogs}), 
-          React.createElement(DebugButton, null), 
-          React.createElement(AECButton, null)
-        ), 
-        React.createElement("div", {id: "logs"}, 
-          this.state.logs ? React.createElement(Logs, {logs: this.state.logs}) : null
-        )
-      )
-    );
-  }
-});
+    let pc = document.createElement("div");
+    pc.className = "peer-connection";
+    pc.appendChild(this.renderHeading());
 
-var PeerConnections = React.createClass({displayName: "PeerConnections",
-  getInitialState: function() {
-    // Sort the reports to have the more recent at the top
-    var reports = this.props.reports.slice().sort(function(r1, r2) {
-      return r2.timestamp - r1.timestamp;
-    });
+    let div = document.createElement("div");
+    let sectionCtrl = document.createElement("div");
+    sectionCtrl.className = "section-ctrl no-print";
+    let foldEffect = new FoldEffect(div);
+    sectionCtrl.appendChild(foldEffect.render());
+    pc.appendChild(sectionCtrl);
 
-    return {reports: reports};
+    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;
   },
 
-  render: function() {
-    return (
-      React.createElement("div", {className: "peer-connections"}, 
-        
-          this.state.reports.map(function(report, i) {
-            return React.createElement(PeerConnection, {key: i, report: report});
-          })
-        
-      )
-    );
-  }
-});
+  renderHeading: function () {
+    let pcInfo = this.getPCInfo(this._report);
+    let heading = document.createElement("h3");
+    let now = new Date(this._report.timestamp).toTimeString();
+    heading.textContent =
+      `[ ${pcInfo.id} ] ${pcInfo.url} ${pcInfo.closed ? `(${getString("connection_closed")})` : ""} ${now}`;
+    return heading;
+  },
 
-var PeerConnection = React.createClass({displayName: "PeerConnection",
+  renderDesc: function() {
+    let info = document.createElement("div");
+    let label = document.createElement("span");
+    let body = document.createElement("span");
+
+    label.className = "info-label";
+    label.textContent = `${getString("peer_connection_id_label")}: `;
+    info.appendChild(label);
+
+    body.className = "info-body";
+    body.textContent = this._report.pcid;
+    info.appendChild(body);
+
+    return info;
+  },
+
   getPCInfo: function(report) {
     return {
       id: report.pcid.match(/id=(\S+)/)[1],
       url: report.pcid.match(/url=([^)]+)/)[1],
       closed: report.closed
     };
-  },
-
-  getIceCandidatePairs: function(report) {
-    var candidates =
-      report.iceCandidateStats.reduce(function(candidates, candidate) {
-        candidates[candidate.id] = candidate;
+  }
+};
 
-        return candidates;
-      }, {});
+function SDPStats(report) {
+  this._report = report;
+}
 
-    var pairs = report.iceCandidatePairStats.map(function(pair) {
-      var localCandidate = candidates[pair.localCandidateId];
-      var remoteCandidate = candidates[pair.remoteCandidateId];
+SDPStats.prototype = {
+  render: function() {
+    let div = document.createElement("div");
+    let elem = document.createElement("h4");
 
-      return {
-        localCandidate: candidateToString(localCandidate),
-        remoteCandidate: candidateToString(remoteCandidate),
-        state: pair.state,
-        priority: pair.mozPriority,
-        nominated: pair.nominated,
-        selected: pair.selected
-      };
-    });
+    elem.textContent = getString("sdp_heading");
+    div.appendChild(elem);
+
+    elem = document.createElement("h5");
+    elem.textContent = getString("local_sdp_heading");
+    div.appendChild(elem);
 
-    var pairedCandidates = pairs.reduce(function(paired, pair) {
-      paired.add(pair.localCandidate.id);
-      paired.add(pair.remoteCandidate.id);
-
-      return paired;
-    }, new Set());
+    elem = document.createElement("pre");
+    elem.textContent = this._report.localSdp;
+    div.appendChild(elem);
 
-    var unifiedPairs =
-      report.iceCandidateStats.reduce(function(pairs, candidate) {
-        if (pairedCandidates.has(candidate)) {
-            return pairs;
-        }
-
-        var isLocal = candidate.type === "localcandidate";
+    elem = document.createElement("h5");
+    elem.textContent = getString("remote_sdp_heading");
+    div.appendChild(elem);
 
-        pairs.push({
-          localCandidate:  isLocal ? candidateToString(candidate) : null,
-          remoteCandidate: isLocal ? null : candidateToString(candidate),
-          state: null,
-          priority: null,
-          nominated: null,
-          selected: null
-        });
+    elem = document.createElement("pre");
+    elem.textContent = this._report.remoteSdp;
+    div.appendChild(elem);
 
-        return pairs;
-      }, pairs);
-
-    return unifiedPairs;
-  },
+    return div;
+  }
+};
 
-  getInitialState: function() {
-    return {
-      unfolded: false
-    };
-  },
+function RTPStats(report) {
+  this._report = report;
+  this._stats = [];
+}
 
-  onFold: function() {
-    this.setState({unfolded: !this.state.unfolded});
-  },
+RTPStats.prototype = {
+  render: function() {
+    let div = document.createElement("div");
+    let heading = document.createElement("h4");
+
+    heading.textContent = getString("rtp_stats_heading");
+    div.appendChild(heading);
 
-  body: function(report, pairs) {
-    return (
-      React.createElement("div", null, 
-        React.createElement("p", {className: "pcid"}, "PeerConnection ID: ", report.pcid), 
-        React.createElement(Tabs, null, 
-          React.createElement(Tab, {title: "Ice Stats"}, 
-            React.createElement(IceStats, {pairs: pairs})
-          ), 
-          React.createElement(Tab, {title: "SDP"}, 
-            React.createElement(SDP, {type: "local", sdp: report.localSdp}), 
-            React.createElement(SDP, {type: "remote", sdp: report.remoteSdp})
-          ), 
-          React.createElement(Tab, {title: "RTP Stats"}, 
-            React.createElement(RTPStats, {report: report})
-          )
-        )
-      )
-    );
+    this.generateRTPStats();
+
+    for (let statSet of this._stats) {
+      div.appendChild(this.renderRTPStatSet(statSet));
+    };
+
+    return div;
   },
 
-  render: function() {
-    var report = this.props.report;
-    var pcInfo = this.getPCInfo(report);
-    var pairs  = this.getIceCandidatePairs(report);
-
-    return (
-      React.createElement("div", {className: "peer-connection"}, 
-        React.createElement("h3", {onClick: this.onFold}, 
-          "[", pcInfo.id, "] ", pcInfo.url, " ", pcInfo.closed ? "(closed)" : null, 
-          new Date(report.timestamp).toTimeString()
-        ), 
-        this.state.unfolded ? this.body(report, pairs) : undefined
-      )
-    );
-  }
-});
-
-var IceStats = React.createClass({displayName: "IceStats",
-  sortHeadings: {
-    "Local candidate":  "localCandidate",
-    "Remote candidate": "remoteCandidate",
-    "Ice State":        "state",
-    "Priority":         "priority",
-    "Nominated":        "nominated",
-    "Selected":         "selected"
-  },
-
-  sort: function(key) {
-    var sorting = this.state.sorting;
-    var pairs = this.state.pairs.slice().sort(function(pair1, pair2) {
-      var value1 = pair1[key] ? pair1[key].toString() : "";
-      var value2 = pair2[key] ? pair2[key].toString() : "";
-
-      // Reverse sorting
-      if (key === sorting) {
-        return value2.localeCompare(value1);
-      }
-
-      return value1.localeCompare(value2);
-    }.bind(this));
-
-    sorting = (key === sorting) ? null : key;
-    this.setState({pairs: pairs, sorting: sorting});
-  },
-
-  generateSortHeadings: function() {
-    return Object.keys(this.sortHeadings).map(function(heading, i) {
-      var sortKey = this.sortHeadings[heading];
-      return (
-        React.createElement("th", null, 
-          React.createElement("a", {href: "#", onClick: this.sort.bind(this, sortKey)}, 
-            heading
-          )
-        )
-      );
-    }.bind(this));
-  },
+  generateRTPStats: function() {
+    let remoteRtpStats = {};
+    let rtpStats = [].concat((this._report.inboundRTPStreamStats  || []),
+                             (this._report.outboundRTPStreamStats || []));
 
-  getInitialState: function() {
-    return {pairs: this.props.pairs, sorting: null};
-  },
-
-  render: function() {
-    return (
-      React.createElement("table", null, 
-        React.createElement("tbody", null, 
-          React.createElement("tr", null, this.generateSortHeadings()), 
-          this.state.pairs.map(function(pair, i) {
-            return React.createElement(IceCandidatePair, {key: i, pair: pair});
-          })
-        )
-      )
-    );
-  }
-});
-
-var IceCandidatePair = React.createClass({displayName: "IceCandidatePair",
-  render: function() {
-    var pair = this.props.pair;
-    return (
-      React.createElement("tr", null, 
-        React.createElement("td", null, pair.localCandidate), 
-        React.createElement("td", null, pair.remoteCandidate), 
-        React.createElement("td", null, pair.state), 
-        React.createElement("td", null, pair.priority), 
-        React.createElement("td", null, pair.nominated ? '✔' : null), 
-        React.createElement("td", null, pair.selected ? '✔' : null)
-      )
-    );
-  }
-});
-
-var SDP = React.createClass({displayName: "SDP",
-  render: function() {
-    var type = labelize(this.props.type);
-
-    return (
-      React.createElement("div", null, 
-        React.createElement("h4", null, type, " SDP"), 
-        React.createElement("pre", null, 
-          this.props.sdp
-        )
-      )
-    );
-  }
-});
-
-var RTPStats = React.createClass({displayName: "RTPStats",
-  getRtpStats: function(report) {
-    var remoteRtpStats = {};
-    var rtpStats = [];
-
-    rtpStats = rtpStats.concat(report.inboundRTPStreamStats  || []);
-    rtpStats = rtpStats.concat(report.outboundRTPStreamStats || []);
-
-    rtpStats.forEach(function(stats) {
+    // 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;
       }
-    });
+    };
 
-    rtpStats.forEach(function(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];
       }
-    });
+    };
 
-    return rtpStats;
+    this._stats = rtpStats;
   },
 
-  dumpAvStats: function(stats) {
-    var statsString = "";
+  renderAvStats: function(stats) {
+    let statsString = "";
+
     if (stats.mozAvSyncDelay) {
-      statsString += `A/V sync: ${stats.mozAvSyncDelay} ms `;
+      statsString += `${getString("av_sync_label")}: ${stats.mozAvSyncDelay} ms `;
     }
     if (stats.mozJitterBufferDelay) {
-      statsString += `Jitter-buffer delay: ${stats.mozJitterBufferDelay} ms`;
+      statsString += `${getString("jitter_buffer_delay_label")}: ${stats.mozJitterBufferDelay} ms`;
     }
 
-    return React.createElement("div", null, statsString);
+    let line = document.createElement("p");
+    line.textContent = statsString;
+    return line;
   },
 
-  dumpCoderStats: function(stats) {
-    var statsString = "";
-    var label;
+  renderCoderStats: function(stats) {
+    let statsString = "";
+    let label;
 
     if (stats.bitrateMean) {
-      statsString += ` Avg. bitrate: ${(stats.bitrateMean/1000000).toFixed(2)} Mbps`;
+      statsString += ` ${getString("avg_bitrate_label")}: ${(stats.bitrateMean / 1000000).toFixed(2)} Mbps`;
       if (stats.bitrateStdDev) {
-        statsString += ` (${(stats.bitrateStdDev/1000000).toFixed(2)} SD)`;
+        statsString += ` (${(stats.bitrateStdDev / 1000000).toFixed(2)} SD)`;
       }
     }
 
     if (stats.framerateMean) {
-      statsString += ` Avg. framerate: ${(stats.framerateMean).toFixed(2)} fps`;
+      statsString += ` ${getString("avg_framerate_label")}: ${(stats.framerateMean).toFixed(2)} fps`;
       if (stats.framerateStdDev) {
         statsString += ` (${stats.framerateStdDev.toFixed(2)} SD)`;
       }
     }
 
     if (stats.droppedFrames) {
-      statsString += ` Dropped frames: ${stats.droppedFrames}`;
+      statsString += ` ${getString("dropped_frames_label")}: ${stats.droppedFrames}`;
     }
     if (stats.discardedPackets) {
-      statsString += ` Discarded packets: ${stats.discardedPackets}`;
+      statsString += ` ${getString("discarded_packets_label")}: ${stats.discardedPackets}`;
     }
 
     if (statsString) {
-      label = (stats.packetsReceived)? " Decoder:" : " Encoder:";
+      label = (stats.packetsReceived ? ` ${getString("decoder_label")}:` : ` ${getString("encoder_label")}:`);
       statsString = label + statsString;
     }
 
-    return React.createElement("div", null, statsString);
+    let line = document.createElement("p");
+    line.textContent = statsString;
+    return line;
   },
 
-  dumpRtpStats: function(stats, type) {
-    var label = labelize(type);
-    var time  = new Date(stats.timestamp).toTimeString();
-
-    var statsString = `${label}: ${time} ${stats.type} SSRC: ${stats.ssrc}`;
+  renderTransportStats: function(stats, type) {
+    let label = this.labelize(type);
+    let time  = new Date(stats.timestamp).toTimeString();
+    let statsString = `${label}: ${time} ${stats.type} SSRC: ${stats.ssrc}`;
 
     if (stats.packetsReceived) {
-      statsString += ` Received: ${stats.packetsReceived} packets`;
+      statsString += ` ${getString("received_label")}: ${stats.packetsReceived} ${getString("packets")}`;
 
       if (stats.bytesReceived) {
-        statsString += ` (${round00(stats.bytesReceived/1024)} Kb)`;
+        statsString += ` (${(stats.bytesReceived / 1024).toFixed(2)} Kb)`;
       }
 
-      statsString += ` Lost: ${stats.packetsLost} Jitter: ${stats.jitter}`;
+      statsString += ` ${getString("lost_label")}: ${stats.packetsLost} ${getString("jitter_label")}: ${stats.jitter}`;
 
       if (stats.mozRtt) {
         statsString += ` RTT: ${stats.mozRtt} ms`;
       }
     } else if (stats.packetsSent) {
-      statsString += ` Sent: ${stats.packetsSent} packets`;
+      statsString += ` ${getString("sent_label")}: ${stats.packetsSent} ${getString("packets")}`;
       if (stats.bytesSent) {
-        statsString += ` (${round00(stats.bytesSent/1024)} Kb)`;
+        statsString += ` (${(stats.bytesSent / 1024).toFixed(2)} Kb)`;
       }
     }
 
-    return React.createElement("div", null, statsString);
+    let line = document.createElement("p");
+    line.textContent = statsString;
+    return line;
+  },
+
+  renderRTPStatSet: function(stats) {
+    let div = document.createElement("div");
+    let heading = document.createElement("h5");
+
+    heading.textContent = stats.id;
+    div.appendChild(heading);
+
+    if (stats.MozAvSyncDelay || stats.mozJitterBufferDelay) {
+      div.appendChild(this.renderAvStats(stats));
+    }
+
+    div.appendChild(this.renderCoderStats(stats));
+    div.appendChild(this.renderTransportStats(stats, getString("local")));
+
+    if (stats.remoteId && stats.remoteRtpStats) {
+      div.appendChild(this.renderTransportStats(stats.remoteRtpStats, getString("remote")));
+    }
+
+    return div;
+  },
+
+  labelize: function(label) {
+    return `${label.charAt(0).toUpperCase()}${label.slice(1)}`;
+  }
+};
+
+function ICEStats(report) {
+  this._report = report;
+}
+
+ICEStats.prototype = {
+  render: function() {
+    let tbody = [];
+    for (let stat of this.generateICEStats()) {
+      tbody.push([
+        stat.localcandidate || "",
+        stat.remotecandidate || "",
+        stat.state || "",
+        stat.priority || "",
+        stat.nominated || "",
+        stat.selected || ""
+      ]);
+    };
+
+    let statsTable = new SimpleTable(
+      [getString("local_candidate"), getString("remote_candidate"), getString("ice_state"),
+       getString("priority"), getString("nominated"), getString("selected")],
+      tbody);
+
+    let div = document.createElement("div");
+    let heading = document.createElement("h4");
+
+    heading.textContent = getString("ice_stats_heading");
+    div.appendChild(heading);
+    div.appendChild(statsTable.render());
+
+    return div;
   },
 
-  render: function() {
-    var rtpStats = this.getRtpStats(this.props.report);
+  generateICEStats: function() {
+    // 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);
+    }
 
-    return (
-      React.createElement("div", null, 
-        rtpStats.map(function(stats) {
-          var isAvStats = (stats.mozAvSyncDelay || stats.mozJitterBufferDelay);
-          var remoteRtpStats = stats.remoteId ? stats.remoteRtpStats : null;
+    // 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);
 
-          return [
-            React.createElement("h5", null, stats.id),
-            isAvStats ? this.dumpAvStats(stats) : null,
-            this.dumpCoderStats(stats),
-            this.dumpRtpStats(stats, "local"),
-            remoteRtpStats ? this.dumpRtpStats(remoteRtpStats, "remote") : null
-          ];
-        }.bind(this))
-      )
-    );
-  }
-});
+      if (local) {
+        stat = {
+          localcandidate: this.candidateToString(local),
+          state: pair.state,
+          priority: pair.mozPriority,
+          nominated: pair.nominated,
+          selected: pair.selected
+        };
+        matched[local.id] = true;
 
-var LogsButton = React.createClass({displayName: "LogsButton",
-  render: function() {
-    return (
-      React.createElement("button", {onClick: this.props.handler}, "Connection log")
-    );
-  }
-});
+        if (remote) {
+          stat.remotecandidate = this.candidateToString(remote);
+          matched[remote.id] = true;
+        }
+        stats.push(stat);
+      }
+    };
 
-var DebugButton = React.createClass({displayName: "DebugButton",
-  getInitialState: function() {
-    var on = (WebrtcGlobalInformation.debugLevel > 0);
-    return {on: on};
+    for (let c of candidates.values()) {
+      if (matched[c.id])
+        continue;
+
+      stat = {};
+      stat[c.type] = this.candidateToString(c);
+      stats.push(stat);
+    };
+
+    return stats.sort((a, b) => (b.priority || 0) - (a.priority || 0));
   },
 
-  onClick: function() {
-    if (this.state.on)
-      WebrtcGlobalInformation.debugLevel = 0;
-    else
-      WebrtcGlobalInformation.debugLevel = 65535;
+  candidateToString: function(c) {
+    var type = c.candidateType;
+
+    if (c.type == "localcandidate" && c.candidateType == "relayed") {
+      type = `${c.candidateType}-${c.mozLocalTransport}`;
+    }
+
+    return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`;
+  }
+};
 
-    this.setState({on: !this.state.on});
+function SimpleTable(heading, data) {
+  this._heading = heading || [];
+  this._data = data;
+}
+
+SimpleTable.prototype = {
+  renderRow: function(list) {
+    let row = document.createElement("tr");
+
+    for (let elem of list) {
+      let cell = document.createElement("td");
+      cell.textContent = elem;
+      row.appendChild(cell);
+    };
+
+    return row;
   },
 
   render: function() {
-    return (
-      React.createElement("button", {onClick: this.onClick}, 
-        this.state.on ? "Stop debug mode" : "Start debug mode"
-      )
-    );
+    let table = document.createElement("table");
+
+    if (this._heading) {
+      table.appendChild(this.renderRow(this._heading));
+    }
+
+    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;
+  }
+};
 
-var AECButton = React.createClass({displayName: "AECButton",
-  getInitialState: function() {
-    return {on: WebrtcGlobalInformation.aecDebug};
+FoldEffect.prototype = {
+  render: function() {
+    this._target.classList.add("fold-target");
+
+    let ctrl = document.createElement("div");
+    this._trigger = ctrl;
+    ctrl.className = "fold-trigger";
+    ctrl.addEventListener("click", this.onClick.bind(this));
+    this.close();
+
+    FoldEffect._sections.push(this);
+    return ctrl;
   },
 
   onClick: function() {
-    WebrtcGlobalInformation.aecDebug = !this.state.on;
-    this.setState({on: WebrtcGlobalInformation.aecDebug});
+    if (this._target.classList.contains("fold-closed")) {
+      this.open();
+    } else {
+      this.close();
+    }
+    return true;
+  },
+
+  open: function() {
+    this._target.classList.remove("fold-closed");
+    this._trigger.setAttribute("title", this._hideHint);
+    this._trigger.textContent = this._hideMsg;
   },
 
-  render: function() {
-    return (
-      React.createElement("button", {onClick: this.onClick}, 
-        this.state.on ? "Stop AEC logging" : "Start AEC logging"
-      )
-    );
+  close: function() {
+    this._target.classList.add("fold-closed");
+    this._trigger.setAttribute("title", this._showHint);
+    this._trigger.textContent = this._showMsg;
   }
-});
+};
 
-var Logs = React.createClass({displayName: "Logs",
-  render: function() {
-    return (
-      React.createElement("div", null, 
-        React.createElement("h3", null, "Logging:"), 
-        React.createElement("div", null, 
-          this.props.logs.map(function(line, i) {
-            return React.createElement("div", {key: i}, line);
-          })
-        )
-      )
-    );
-  }
-});
-
-function iceCandidateMapping(iceCandidates) {
-  var candidates = {};
-  iceCandidates = iceCandidates || [];
+FoldEffect._sections = [];
 
-  iceCandidates.forEach(function(candidate) {
-    candidates[candidate.id] = candidate;
-  });
-
-  return candidates;
-}
-
-function round00(num) {
-  return Math.round(num * 100) / 100;
-}
-
-function labelize(label) {
-  return `${label.charAt(0).toUpperCase()}${label.slice(1)}`;
-}
+FoldEffect.expandAll = function() {
+  for (let section of this._sections) {
+    section.open();
+  };
+};
 
-function candidateToString(c) {
-  var type = c.candidateType;
-
-  if (c.type == "localcandidate" && c.candidateType == "relayed") {
-    type = `${c.candidateType}-${c.mozLocalTransport}`;
-  }
-
-  return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`;
-}
-
-function onLoad() {
-  WebrtcGlobalInformation.getAllStats(function(globalReport) {
-    var reports = globalReport.reports;
-    React.render(React.createElement(AboutWebRTC, {reports: reports}),
-                 document.querySelector("#body"));
-  });
-}
+FoldEffect.collapseAll = function() {
+  for (let section of this._sections) {
+    section.close();
+  };
+};
deleted file mode 100644
--- a/toolkit/content/aboutwebrtc/aboutWebrtc.jsx
+++ /dev/null
@@ -1,532 +0,0 @@
-/** @jsx React.DOM */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* jshint newcap:false */
-/* global React, WebrtcGlobalInformation, document */
-
-"use strict";
-
-var Tabs = React.createClass({
-  getDefaultProps: function() {
-    return {selectedIndex: 0};
-  },
-
-  getInitialState: function() {
-    return {selectedIndex: this.props.selectedIndex};
-  },
-
-  selectTab: function(index) {
-    return function(event) {
-      event.preventDefault();
-      this.setState({selectedIndex: index});
-    }.bind(this);
-  },
-
-  _findSelectedTabContent: function() {
-    // Using map() to filter children…
-    // https://github.com/facebook/react/issues/1644#issuecomment-45138113
-    return React.Children.map(this.props.children, function(tab, i) {
-      return i === this.state.selectedIndex ? tab : null;
-    }.bind(this));
-  },
-
-  render: function() {
-    var cx = React.addons.classSet;
-    return (
-      <div className="tabs">
-        <ul>{
-          React.Children.map(this.props.children, function(tab, i) {
-            return (
-              <li className={cx({active: i === this.state.selectedIndex})}>
-                <a href="#" key={i} onClick={this.selectTab(i)}>
-                  {tab.props.title}
-                </a>
-              </li>
-            );
-          }.bind(this))
-        }</ul>
-        {this._findSelectedTabContent()}
-      </div>
-    );
-  }
-});
-
-var Tab = React.createClass({
-  render: function() {
-    return <section>{this.props.children}</section>;
-  }
-});
-
-var AboutWebRTC = React.createClass({
-  getInitialState: function() {
-    return {logs: null, reports: this.props.reports};
-  },
-
-  displayLogs: function() {
-    WebrtcGlobalInformation.getLogging('', function(logs) {
-      this.setState({logs: logs, reports: this.state.reports});
-    }.bind(this));
-  },
-
-  render: function() {
-    return (
-      <div>
-        <div id="stats">
-          <PeerConnections reports={this.state.reports}/>
-        </div>
-        <div id="buttons">
-          <LogsButton handler={this.displayLogs}/>
-          <DebugButton/>
-          <AECButton/>
-        </div>
-        <div id="logs">{
-          this.state.logs ? <Logs logs={this.state.logs} /> : null
-        }</div>
-      </div>
-    );
-  }
-});
-
-var PeerConnections = React.createClass({
-  getInitialState: function() {
-    // Sort the reports to have the more recent at the top
-    var reports = this.props.reports.slice().sort(function(r1, r2) {
-      return r2.timestamp - r1.timestamp;
-    });
-
-    return {reports: reports};
-  },
-
-  render: function() {
-    return (
-      <div className="peer-connections">
-        {
-          this.state.reports.map(function(report, i) {
-            return <PeerConnection key={i} report={report}/>;
-          })
-        }
-      </div>
-    );
-  }
-});
-
-var PeerConnection = React.createClass({
-  getPCInfo: function(report) {
-    return {
-      id: report.pcid.match(/id=(\S+)/)[1],
-      url: report.pcid.match(/url=([^)]+)/)[1],
-      closed: report.closed
-    };
-  },
-
-  getIceCandidatePairs: function(report) {
-    var candidates =
-      report.iceCandidateStats.reduce(function(candidates, candidate) {
-        candidates[candidate.id] = candidate;
-
-        return candidates;
-      }, {});
-
-    var pairs = report.iceCandidatePairStats.map(function(pair) {
-      var localCandidate = candidates[pair.localCandidateId];
-      var remoteCandidate = candidates[pair.remoteCandidateId];
-
-      return {
-        localCandidate: candidateToString(localCandidate),
-        remoteCandidate: candidateToString(remoteCandidate),
-        state: pair.state,
-        priority: pair.mozPriority,
-        nominated: pair.nominated,
-        selected: pair.selected
-      };
-    });
-
-    var pairedCandidates = pairs.reduce(function(paired, pair) {
-      paired.add(pair.localCandidate.id);
-      paired.add(pair.remoteCandidate.id);
-
-      return paired;
-    }, new Set());
-
-    var unifiedPairs =
-      report.iceCandidateStats.reduce(function(pairs, candidate) {
-        if (pairedCandidates.has(candidate)) {
-            return pairs;
-        }
-
-        var isLocal = candidate.type === "localcandidate";
-
-        pairs.push({
-          localCandidate:  isLocal ? candidateToString(candidate) : null,
-          remoteCandidate: isLocal ? null : candidateToString(candidate),
-          state: null,
-          priority: null,
-          nominated: null,
-          selected: null
-        });
-
-        return pairs;
-      }, pairs);
-
-    return unifiedPairs;
-  },
-
-  getInitialState: function() {
-    return {
-      unfolded: false
-    };
-  },
-
-  onFold: function() {
-    this.setState({unfolded: !this.state.unfolded});
-  },
-
-  body: function(report, pairs) {
-    return (
-      <div>
-        <p className="pcid">PeerConnection ID: {report.pcid}</p>
-        <Tabs>
-          <Tab title="Ice Stats">
-            <IceStats pairs={pairs} />
-          </Tab>
-          <Tab title="SDP">
-            <SDP type="local" sdp={report.localSdp} />
-            <SDP type="remote" sdp={report.remoteSdp} />
-          </Tab>
-          <Tab title="RTP Stats">
-            <RTPStats report={report} />
-          </Tab>
-        </Tabs>
-      </div>
-    );
-  },
-
-  render: function() {
-    var report = this.props.report;
-    var pcInfo = this.getPCInfo(report);
-    var pairs  = this.getIceCandidatePairs(report);
-
-    return (
-      <div className="peer-connection">
-        <h3 onClick={this.onFold}>
-          [{pcInfo.id}] {pcInfo.url} {pcInfo.closed ? "(closed)" : null}
-          {new Date(report.timestamp).toTimeString()}
-        </h3>
-        {this.state.unfolded ? this.body(report, pairs) : undefined}
-      </div>
-    );
-  }
-});
-
-var IceStats = React.createClass({
-  sortHeadings: {
-    "Local candidate":  "localCandidate",
-    "Remote candidate": "remoteCandidate",
-    "Ice State":        "state",
-    "Priority":         "priority",
-    "Nominated":        "nominated",
-    "Selected":         "selected"
-  },
-
-  sort: function(key) {
-    var sorting = this.state.sorting;
-    var pairs = this.state.pairs.slice().sort(function(pair1, pair2) {
-      var value1 = pair1[key] ? pair1[key].toString() : "";
-      var value2 = pair2[key] ? pair2[key].toString() : "";
-
-      // Reverse sorting
-      if (key === sorting) {
-        return value2.localeCompare(value1);
-      }
-
-      return value1.localeCompare(value2);
-    }.bind(this));
-
-    sorting = (key === sorting) ? null : key;
-    this.setState({pairs: pairs, sorting: sorting});
-  },
-
-  generateSortHeadings: function() {
-    return Object.keys(this.sortHeadings).map(function(heading, i) {
-      var sortKey = this.sortHeadings[heading];
-      return (
-        <th>
-          <a href="#" onClick={this.sort.bind(this, sortKey)}>
-            {heading}
-          </a>
-        </th>
-      );
-    }.bind(this));
-  },
-
-  getInitialState: function() {
-    return {pairs: this.props.pairs, sorting: null};
-  },
-
-  render: function() {
-    return (
-      <table>
-        <tbody>
-          <tr>{this.generateSortHeadings()}</tr>
-          {this.state.pairs.map(function(pair, i) {
-            return <IceCandidatePair key={i} pair={pair} />;
-          })}
-        </tbody>
-      </table>
-    );
-  }
-});
-
-var IceCandidatePair = React.createClass({
-  render: function() {
-    var pair = this.props.pair;
-    return (
-      <tr>
-        <td>{pair.localCandidate}</td>
-        <td>{pair.remoteCandidate}</td>
-        <td>{pair.state}</td>
-        <td>{pair.priority}</td>
-        <td>{pair.nominated ? '✔' : null}</td>
-        <td>{pair.selected ? '✔' : null}</td>
-      </tr>
-    );
-  }
-});
-
-var SDP = React.createClass({
-  render: function() {
-    var type = labelize(this.props.type);
-
-    return (
-      <div>
-        <h4>{type} SDP</h4>
-        <pre>
-          {this.props.sdp}
-        </pre>
-      </div>
-    );
-  }
-});
-
-var RTPStats = React.createClass({
-  getRtpStats: function(report) {
-    var remoteRtpStats = {};
-    var rtpStats = [];
-
-    rtpStats = rtpStats.concat(report.inboundRTPStreamStats  || []);
-    rtpStats = rtpStats.concat(report.outboundRTPStreamStats || []);
-
-    rtpStats.forEach(function(stats) {
-      if (stats.isRemote) {
-        remoteRtpStats[stats.id] = stats;
-      }
-    });
-
-    rtpStats.forEach(function(stats) {
-      if (stats.remoteId) {
-        stats.remoteRtpStats = remoteRtpStats[stats.remoteId];
-      }
-    });
-
-    return rtpStats;
-  },
-
-  dumpAvStats: function(stats) {
-    var statsString = "";
-    if (stats.mozAvSyncDelay) {
-      statsString += `A/V sync: ${stats.mozAvSyncDelay} ms `;
-    }
-    if (stats.mozJitterBufferDelay) {
-      statsString += `Jitter-buffer delay: ${stats.mozJitterBufferDelay} ms`;
-    }
-
-    return <div>{statsString}</div>;
-  },
-
-  dumpCoderStats: function(stats) {
-    var statsString = "";
-    var label;
-
-    if (stats.bitrateMean) {
-      statsString += ` Avg. bitrate: ${(stats.bitrateMean/1000000).toFixed(2)} Mbps`;
-      if (stats.bitrateStdDev) {
-        statsString += ` (${(stats.bitrateStdDev/1000000).toFixed(2)} SD)`;
-      }
-    }
-
-    if (stats.framerateMean) {
-      statsString += ` Avg. framerate: ${(stats.framerateMean).toFixed(2)} fps`;
-      if (stats.framerateStdDev) {
-        statsString += ` (${stats.framerateStdDev.toFixed(2)} SD)`;
-      }
-    }
-
-    if (stats.droppedFrames) {
-      statsString += ` Dropped frames: ${stats.droppedFrames}`;
-    }
-    if (stats.discardedPackets) {
-      statsString += ` Discarded packets: ${stats.discardedPackets}`;
-    }
-
-    if (statsString) {
-      label = (stats.packetsReceived)? " Decoder:" : " Encoder:";
-      statsString = label + statsString;
-    }
-
-    return <div>{statsString}</div>;
-  },
-
-  dumpRtpStats: function(stats, type) {
-    var label = labelize(type);
-    var time  = new Date(stats.timestamp).toTimeString();
-
-    var statsString = `${label}: ${time} ${stats.type} SSRC: ${stats.ssrc}`;
-
-    if (stats.packetsReceived) {
-      statsString += ` Received: ${stats.packetsReceived} packets`;
-
-      if (stats.bytesReceived) {
-        statsString += ` (${round00(stats.bytesReceived/1024)} Kb)`;
-      }
-
-      statsString += ` Lost: ${stats.packetsLost} Jitter: ${stats.jitter}`;
-
-      if (stats.mozRtt) {
-        statsString += ` RTT: ${stats.mozRtt} ms`;
-      }
-    } else if (stats.packetsSent) {
-      statsString += ` Sent: ${stats.packetsSent} packets`;
-      if (stats.bytesSent) {
-        statsString += ` (${round00(stats.bytesSent/1024)} Kb)`;
-      }
-    }
-
-    return <div>{statsString}</div>;
-  },
-
-  render: function() {
-    var rtpStats = this.getRtpStats(this.props.report);
-
-    return (
-      <div>{
-        rtpStats.map(function(stats) {
-          var isAvStats = (stats.mozAvSyncDelay || stats.mozJitterBufferDelay);
-          var remoteRtpStats = stats.remoteId ? stats.remoteRtpStats : null;
-
-          return [
-            <h5>{stats.id}</h5>,
-            isAvStats ? this.dumpAvStats(stats) : null,
-            this.dumpCoderStats(stats),
-            this.dumpRtpStats(stats, "local"),
-            remoteRtpStats ? this.dumpRtpStats(remoteRtpStats, "remote") : null
-          ];
-        }.bind(this))
-      }</div>
-    );
-  }
-});
-
-var LogsButton = React.createClass({
-  render: function() {
-    return (
-      <button onClick={this.props.handler}>Connection log</button>
-    );
-  }
-});
-
-var DebugButton = React.createClass({
-  getInitialState: function() {
-    var on = (WebrtcGlobalInformation.debugLevel > 0);
-    return {on: on};
-  },
-
-  onClick: function() {
-    if (this.state.on)
-      WebrtcGlobalInformation.debugLevel = 0;
-    else
-      WebrtcGlobalInformation.debugLevel = 65535;
-
-    this.setState({on: !this.state.on});
-  },
-
-  render: function() {
-    return (
-      <button onClick={this.onClick}>{
-        this.state.on ? "Stop debug mode" : "Start debug mode"
-      }</button>
-    );
-  }
-});
-
-var AECButton = React.createClass({
-  getInitialState: function() {
-    return {on: WebrtcGlobalInformation.aecDebug};
-  },
-
-  onClick: function() {
-    WebrtcGlobalInformation.aecDebug = !this.state.on;
-    this.setState({on: WebrtcGlobalInformation.aecDebug});
-  },
-
-  render: function() {
-    return (
-      <button onClick={this.onClick}>{
-        this.state.on ? "Stop AEC logging" : "Start AEC logging"
-      }</button>
-    );
-  }
-});
-
-var Logs = React.createClass({
-  render: function() {
-    return (
-      <div>
-        <h3>Logging:</h3>
-        <div>{
-          this.props.logs.map(function(line, i) {
-            return <div key={i}>{line}</div>;
-          })
-        }</div>
-      </div>
-    );
-  }
-});
-
-function iceCandidateMapping(iceCandidates) {
-  var candidates = {};
-  iceCandidates = iceCandidates || [];
-
-  iceCandidates.forEach(function(candidate) {
-    candidates[candidate.id] = candidate;
-  });
-
-  return candidates;
-}
-
-function round00(num) {
-  return Math.round(num * 100) / 100;
-}
-
-function labelize(label) {
-  return `${label.charAt(0).toUpperCase()}${label.slice(1)}`;
-}
-
-function candidateToString(c) {
-  var type = c.candidateType;
-
-  if (c.type == "localcandidate" && c.candidateType == "relayed") {
-    type = `${c.candidateType}-${c.mozLocalTransport}`;
-  }
-
-  return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`;
-}
-
-function onLoad() {
-  WebrtcGlobalInformation.getAllStats(function(globalReport) {
-    var reports = globalReport.reports;
-    React.render(<AboutWebRTC reports={reports}/>,
-                 document.querySelector("#body"));
-  });
-}
--- a/toolkit/content/aboutwebrtc/aboutWebrtc.xhtml
+++ b/toolkit/content/aboutwebrtc/aboutWebrtc.xhtml
@@ -1,22 +1,24 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
-
 <!DOCTYPE html [
 <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD;
 ]>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
-    <title>Webrtc Internals</title>
-    <link rel="stylesheet" type="text/css" media="all" href="chrome://global/content/aboutwebrtc/aboutWebrtc.css"/>
+    <title>about:webrtc</title>
+    <link rel="stylesheet" type="text/css" media="all"
+      href="chrome://global/content/aboutwebrtc/aboutWebrtc.css"/>
+
+    <script type="text/javascript;version=1.8"
+            src="chrome://global/content/aboutwebrtc/aboutWebrtc.js"
+            defer="defer"></script>
   </head>
   <body id="body" onload="onLoad()">
+    <div id="controls" class="no-print"></div>
+    <div id="content"></div>
   </body>
-
-  <script type="text/javascript" src="chrome://browser/content/loop/shared/libs/react-0.12.2.js"></script>
-  <script type="text/javascript;version=1.8" src="chrome://global/content/aboutwebrtc/aboutWebrtc.js"/>
 </html>
-
new file mode 100644
--- /dev/null
+++ b/toolkit/locales/en-US/chrome/global/aboutWebrtc.properties
@@ -0,0 +1,62 @@
+# 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/.
+
+#Note to translators:
+#The text "WebRTC" should not be translated. It is the general label for the standards based technology.
+document_title = WebRTC Internals
+cannot_retieve_log = Cannot retrieve WebRTC log data
+save_page_label = Save Page
+# Note to translators:
+# - %1$S will be replaced by saved page file name.
+save_page_msg = page saved to: %1$S
+save_page_dialog_title = save about:webrtc as
+debug_mode_msg_label = Debug Mode
+debug_mode_off_state_label = Start Debug Mode
+# Note to translators:
+# - %1$S will be replaced by the trace log file name
+debug_mode_off_state_msg = trace log can be found at: %1$S
+debug_mode_on_state_msg = debug mode active, writing trace messages to: %1$S
+debug_mode_on_state_label = Stop Debug Mode
+aec_logging_msg_label = AEC Logging
+aec_logging_off_state_label = Start AEC Logging
+# Note to translators:
+# - %1$S will be replaced by the path to the AEC capture sample files
+aec_logging_off_state_msg = captured log files can be found in: %1$S
+aec_logging_on_state_label = Stop AEC Logging
+aec_logging_on_state_msg = AEC logging active (speak with the caller for a few minutes and then stop the capture)
+log_heading = Connection Log
+log_show_msg = show log
+log_hide_msg = hide log
+connection_closed = closed
+peer_connection_id_label = PeerConnection ID
+sdp_heading = SDP
+local_sdp_heading = Local SDP
+remote_sdp_heading = Remote SDP
+rtp_stats_heading = RTPStats
+local = local
+remote = remote
+local_candidate = Local Candidate
+remote_candidate = Remote Candidate
+ice_state = ICE State
+priority = Priority
+nominated = Nominated
+selected = Selected
+ice_stats_heading = ICE Stats
+fold_show_msg = show details
+fold_show_hint = click to expand this section
+fold_hide_msg = hide details
+fold_hide_hint = click to collapse this section
+av_sync_label = A/V sync
+jitter_buffer_delay_label = Jitter-buffer delay
+avg_bitrate_label = Avg. bitrate
+avg_framerate_label = Avg. framerate
+dropped_frames_label = Dropped frames
+discarded_packets_label = Discarded packets
+decoder_label = Decoder
+encoder_label = Encoder
+received_label = Received
+packets = packets
+lost_label = Lost
+jitter_label = Jitter
+sent_label = Sent"
--- a/toolkit/locales/jar.mn
+++ b/toolkit/locales/jar.mn
@@ -10,16 +10,17 @@
   locale/@AB_CD@/global/aboutAbout.dtd                  (%chrome/global/aboutAbout.dtd)
   locale/@AB_CD@/global/aboutReader.properties          (%chrome/global/aboutReader.properties)
   locale/@AB_CD@/global/aboutRights.dtd                 (%chrome/global/aboutRights.dtd)
   locale/@AB_CD@/global/aboutNetworking.dtd             (%chrome/global/aboutNetworking.dtd)
   locale/@AB_CD@/global/aboutSupport.dtd                (%chrome/global/aboutSupport.dtd)
   locale/@AB_CD@/global/aboutSupport.properties         (%chrome/global/aboutSupport.properties)
   locale/@AB_CD@/global/aboutTelemetry.dtd              (%chrome/global/aboutTelemetry.dtd)
   locale/@AB_CD@/global/aboutTelemetry.properties       (%chrome/global/aboutTelemetry.properties)
+  locale/@AB_CD@/global/aboutWebrtc.properties          (%chrome/global/aboutWebrtc.properties)
   locale/@AB_CD@/global/autocomplete.properties         (%chrome/global/autocomplete.properties)
   locale/@AB_CD@/global/appPicker.dtd                   (%chrome/global/appPicker.dtd)
   locale/@AB_CD@/global/brand.dtd                       (generic/chrome/global/brand.dtd)
   locale/@AB_CD@/global/browser.properties              (%chrome/global/browser.properties)
   locale/@AB_CD@/global/charsetMenu.dtd                 (%chrome/global/charsetMenu.dtd)
   locale/@AB_CD@/global/charsetMenu.properties          (%chrome/global/charsetMenu.properties)
   locale/@AB_CD@/global/commonDialog.dtd                (%chrome/global/commonDialog.dtd)
   locale/@AB_CD@/global/commonDialogs.properties        (%chrome/global/commonDialogs.properties)