Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  aboutWebrtc.mjs   Sprache: unbekannt

 
/* 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/. */

import { GraphImpl } from "chrome://global/content/aboutwebrtc/graph.mjs";
import { GraphDb } from "chrome://global/content/aboutwebrtc/graphdb.mjs";
import { Disclosure } from "chrome://global/content/aboutwebrtc/disclosure.mjs";
import { ConfigurationList } from "chrome://global/content/aboutwebrtc/configurationList.mjs";
import { CopyButton } from "./copyButton.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
});

function makeFilePickerService() {
  const fpContractID = "@mozilla.org/filepicker;1";
  const fpIID = Ci.nsIFilePicker;
  return Cc[fpContractID].createInstance(fpIID);
}

const WGI = WebrtcGlobalInformation;

const LOGFILE_NAME_DEFAULT = "aboutWebrtc.html";

class Renderer {
  // Long function names preserved until code can be uniformly moved to new names
  renderElement(eleName, options, l10n_id, l10n_args) {
    let elem = Object.assign(document.createElement(eleName), options);
    if (l10n_id) {
      document.l10n.setAttributes(elem, l10n_id, l10n_args);
    }
    return elem;
  }
  elem() {
    return this.renderElement(...arguments);
  }
  text(eleName, textContent, options) {
    return this.renderElement(eleName, { textContent, ...options });
  }
  renderElements(eleName, options, list) {
    const element = renderElement(eleName, options);
    element.append(...list);
    return element;
  }
  elems() {
    return this.renderElements(...arguments);
  }
}

// Proxies a Renderer instance to provide some meta programming methods to make
// adding elements more readable, e.g. elemRenderer.elem_h4(...) instead of
// elemRenderer.elem("h4", ...).
const elemRenderer = new Proxy(new Renderer(), {
  get(target, prop) {
    // Function prefixes to proxy.
    const proxied = {
      elem_: (...args) => target.elem(...args),
      elems_: (...args) => target.elems(...args),
      text_: (...args) => target.text(...args),
    };
    for (let [prefix, func] of Object.entries(proxied)) {
      if (prop.startsWith(prefix) && prop.length > prefix.length) {
        return (...args) => func(prop.substring(prefix.length), ...args);
      }
    }
    // Pass non-matches to the base object
    return Reflect.get(...arguments);
  },
});

let graphData = [];
let mostRecentReports = {};
let sdpHistories = [];
let historyTsMemoForPcid = {};
let sdpHistoryTsMemoForPcid = {};

function clearStatsHistory() {
  graphData = [];
  mostRecentReports = {};
  sdpHistories = [];
  historyTsMemoForPcid = {};
  sdpHistoryTsMemoForPcid = {};
}

function appendReportToHistory(report) {
  appendSdpHistory(report);
  mostRecentReports[report.pcid] = report;
  if (graphData[report.pcid] === undefined) {
    graphData[report.pcid] ??= new GraphDb(report);
  } else {
    graphData[report.pcid].insertReportData(report);
  }
}

function appendSdpHistory({ pcid, sdpHistory: newHistory }) {
  sdpHistories[pcid] ??= [];
  let storedHistory = sdpHistories[pcid];
  newHistory.forEach(entry => {
    const { timestamp } = entry;
    if (!storedHistory.length || storedHistory.at(-1).timestamp < timestamp) {
      storedHistory.push(entry);
      sdpHistoryTsMemoForPcid[pcid] = timestamp;
    }
  });
}

function recentStats() {
  return Object.values(mostRecentReports);
}

// Returns the sdpHistory for a given stats report
function getSdpHistory({ pcid, timestamp: a }) {
  sdpHistories[pcid] ??= [];
  return sdpHistories[pcid].filter(({ timestamp: b }) => a >= b);
}

function appendStats(allStats) {
  allStats.forEach(appendReportToHistory);
}

function getAndUpdateStatsTsMemoForPcid(pcid) {
  historyTsMemoForPcid[pcid] = mostRecentReports[pcid]?.timestamp;
  return historyTsMemoForPcid[pcid] || null;
}

function getSdpTsMemoForPcid(pcid) {
  return sdpHistoryTsMemoForPcid[pcid] || null;
}

const REQUEST_FULL_REFRESH = true;
const REQUEST_UPDATE_ONLY_REFRESH = false;

async function getStats(requestFullRefresh) {
  if (
    requestFullRefresh ||
    !Services.prefs.getBoolPref("media.aboutwebrtc.hist.enabled")
  ) {
    // Upon clearing the history we need to get all the stats to rebuild what
    // will become the skeleton of the page.hg wip
    const { reports } = await new Promise(r => WGI.getAllStats(r));
    appendStats(reports);
    return reports.sort((a, b) => b.timestamp - a.timestamp);
  }
  const pcids = await new Promise(r => WGI.getStatsHistoryPcIds(r));
  await Promise.all(
    [...pcids].map(pcid =>
      new Promise(r =>
        WGI.getStatsHistorySince(
          r,
          pcid,
          getAndUpdateStatsTsMemoForPcid(pcid),
          getSdpTsMemoForPcid(pcid)
        )
      ).then(r => {
        appendStats(r.reports);
        r.sdpHistories.forEach(hist => appendSdpHistory(hist));
      })
    )
  );
  let recent = recentStats();
  return recent.sort((a, b) => b.timestamp - a.timestamp);
}

const renderElement = (eleName, options, l10n_id, l10n_args) =>
  elemRenderer.elem(eleName, options, l10n_id, l10n_args);

const renderText = (eleName, textContent, options) =>
  elemRenderer.text(eleName, textContent, options);

const renderElements = (eleName, options, list) =>
  elemRenderer.elems(eleName, options, list);

// Button control classes

class Control {
  label = null;
  message = null;
  messageArgs = null;
  messageHeader = null;

  render() {
    this.ctrl = renderElement(
      "button",
      { onclick: () => this.onClick() },
      this.label
    );
    this.msg = renderElement("p");
    this.update();
    return [this.ctrl, this.msg];
  }

  update() {
    document.l10n.setAttributes(this.ctrl, this.label);
    this.msg.textContent = "";
    if (this.message) {
      this.msg.append(
        renderElement(
          "span",
          {
            className: "info-label",
          },
          this.messageHeader
        ),
        renderElement(
          "span",
          {
            className: "info-body",
          },
          this.message,
          this.messageArgs
        )
      );
    }
  }
}

class SavePage extends Control {
  constructor() {
    super();
    this.messageHeader = "about-webrtc-save-page-label";
    this.label = "about-webrtc-save-page-label";
  }

  async onClick() {
    FoldEffect.expandAll();
    let [dialogTitle] = await document.l10n.formatValues([
      { id: "about-webrtc-save-page-dialog-title" },
    ]);
    let FilePicker = makeFilePickerService();
    const lazyFileUtils = lazy.FileUtils;
    FilePicker.init(window.browsingContext, dialogTitle, FilePicker.modeSave);
    FilePicker.defaultString = LOGFILE_NAME_DEFAULT;
    const rv = await new Promise(r => FilePicker.open(r));
    if (rv != FilePicker.returnOK && rv != FilePicker.returnReplace) {
      return;
    }
    const fout = lazyFileUtils.openAtomicFileOutputStream(
      FilePicker.file,
      lazyFileUtils.MODE_WRONLY | lazyFileUtils.MODE_CREATE
    );
    const content = document.querySelector("#content");
    const noPrintList = [...content.querySelectorAll(".no-print")];
    for (const node of noPrintList) {
      node.style.setProperty("display", "none");
    }
    try {
      fout.write(content.outerHTML, content.outerHTML.length);
    } finally {
      lazyFileUtils.closeAtomicFileOutputStream(fout);
      for (const node of noPrintList) {
        node.style.removeProperty("display");
      }
    }
    this.message = "about-webrtc-save-page-complete-msg";
    this.messageArgs = { path: FilePicker.file.path };
    this.update();
  }
}

class EnableLogging extends Control {
  constructor() {
    super();
    this.label = "about-webrtc-enable-logging-label";
    this.message = null;
  }

  onClick() {
    this.update();
    window.open("about:logging?preset=webrtc");
  }
}

class AecLogging extends Control {
  constructor() {
    super();
    this.messageHeader = "about-webrtc-aec-logging-msg-label";

    if (WGI.aecDebug) {
      this.setState(true);
    } else {
      this.label = "about-webrtc-aec-logging-off-state-label";
      this.message = null;
    }
  }

  setState(state) {
    this.label = state
      ? "about-webrtc-aec-logging-on-state-label"
      : "about-webrtc-aec-logging-off-state-label";
    try {
      if (!state) {
        const file = WGI.aecDebugLogDir;
        this.message = "about-webrtc-aec-logging-toggled-off-state-msg";
        this.messageArgs = { path: file };
      } else {
        this.message = "about-webrtc-aec-logging-toggled-on-state-msg";
      }
    } catch (e) {
      this.message = null;
    }
  }

  onClick() {
    if (Services.env.get("MOZ_DISABLE_CONTENT_SANDBOX") != "1") {
      this.message = "about-webrtc-aec-logging-unavailable-sandbox";
    } else {
      this.setState((WGI.aecDebug = !WGI.aecDebug));
    }
    this.update();
  }
}

class ShowTab extends Control {
  constructor(browserId) {
    super();
    this.label = "about-webrtc-show-tab-label";
    this.message = null;
    this.browserId = browserId;
  }

  onClick() {
    const globalBrowser =
      window.ownerGlobal.browsingContext.topChromeWindow.gBrowser;
    for (const tab of globalBrowser.visibleTabs) {
      if (tab.linkedBrowser && tab.linkedBrowser.browserId == this.browserId) {
        globalBrowser.selectedTab = tab;
        return;
      }
    }
    this.ctrl.disabled = true;
  }
}

(async () => {
  // Setup. Retrieve reports & log while page loads.

  const primarySections = [];
  let peerConnections = renderElement("div");
  let connectionLog = renderElement("div");
  let userModifiedConfigView = renderElement("div");

  const content = document.querySelector("#content");
  content.append(peerConnections, connectionLog, userModifiedConfigView);
  await new Promise(r => (window.onload = r));
  {
    const ctrl = renderElement("div", { className: "control" });
    const msg = renderElement("div", { className: "message" });
    const add = ([control, message]) => {
      ctrl.appendChild(control);
      msg.appendChild(message);
    };
    add(new SavePage().render());
    add(new EnableLogging().render());
    add(new AecLogging().render());

    const ctrls = document.querySelector("#controls");
    ctrls.append(renderElements("div", { className: "controls" }, [ctrl, msg]));

    const mediactx = document.querySelector("#mediactx");
    const mediaCtxSection = await renderMediaCtx(elemRenderer);
    primarySections.push(mediaCtxSection);
    mediactx.append(mediaCtxSection.view());
  }

  // This does not handle the auto-refresh, only the manual refreshes needed
  // for certain user actions, and the initial population of the data
  async function refresh() {
    const pcSection = await renderPeerConnectionSection();
    primarySections.push(pcSection);
    const pcDiv = pcSection.view();
    const connectionLogSection = await renderConnectionLog();
    primarySections.push(connectionLogSection);
    const logDiv = connectionLogSection.view();

    // Replace previous info
    peerConnections.replaceWith(pcDiv);
    connectionLog.replaceWith(logDiv);
    const userModifiedConfigSection = await renderUserPrefSection();
    primarySections.push(userModifiedConfigSection);
    userModifiedConfigView.replaceWith(userModifiedConfigSection.view());
    peerConnections = pcDiv;
    connectionLog = logDiv;
  }
  refresh();

  const INTERVAL_MS = 250;
  const HALF_INTERVAL_MS = INTERVAL_MS / 2;
  // This handles autorefresh and forced refresh, not initial document loading
  async function autorefresh() {
    const startTime = performance.now();
    await Promise.all(primarySections.map(s => s.autoUpdate()));
    const elapsed = performance.now() - startTime;
    // Using half the refresh interval as
    const timeout =
      elapsed > HALF_INTERVAL_MS ? INTERVAL_MS : INTERVAL_MS - elapsed;
    return timeout;
  }
  let timeout = INTERVAL_MS;
  while (true) {
    timeout = await autorefresh();
    await new Promise(r => setTimeout(r, timeout));
  }
})();

const peerConnectionAutoRefreshState = {
  /** @type HTMLInputElement */
  primaryCheckbox: undefined,
  /** @type [HTMLInputElement] */
  secondaryCheckboxes: [],

  secondaryClicked() {
    const { checkedBoxes, uncheckedBoxes } = this.secondaryCheckboxes
      .filter(cb => !cb.hidden)
      .reduce(
        (sums, { checked }) => {
          if (checked) {
            sums.checkedBoxes += 1;
          } else {
            sums.uncheckedBoxes += 1;
          }
          return sums;
        },
        {
          checkedBoxes: 0,
          uncheckedBoxes: 0,
        }
      );
    // Stay checked unless all secondary boxes are unchecked
    this.primaryCheckbox.checked = checkedBoxes > 0;
    // Display an indeterminate state when there are both checked and unchecked boxes
    this.primaryCheckbox.indeterminate = checkedBoxes && uncheckedBoxes;
  },
  primaryClicked() {
    for (const cb of this.secondaryCheckboxes.filter(c => !c.hidden)) {
      cb.checked = this.primaryCheckbox.checked;
    }
    this.primaryCheckbox.indeterminate = false;
  },
};

function renderCopyTextToClipboardButton(rndr, id, l10n_id, getTextFn) {
  return rndr.elem_button(
    {
      id: `copytextbutton-${id}`,
      onclick() {
        navigator.clipboard.writeText(getTextFn());
      },
    },
    l10n_id
  );
}

async function renderPeerConnectionSection() {
  // Render pcs and log
  let reports = await getStats();
  let needsFullUpdate = REQUEST_UPDATE_ONLY_REFRESH;
  reports.sort((a, b) => a.browserId - b.browserId);

  // Used by the renderTransportStats function to calculate stat deltas
  const hist = {};

  // Adding a pcid to this list will cause the stats for that list to be refreshed
  // on the next update interval. This is useful for one time refreshes like the
  // "Refresh" button. The list is cleared at the end of each refresh interval.
  const forceRefreshList = [];

  const openPeerConnectionReports = reports.filter(r => !r.closed);
  const closedPeerConnectionReports = reports.filter(r => r.closed);
  const closedPCSection = document.createElement("div");
  if (closedPeerConnectionReports.length) {
    const closedPeerConnectionDisclosure = renderFoldableSection(
      closedPCSection,
      {
        showMsg: "about-webrtc-closed-peerconnection-disclosure-show-msg",
        hideMsg: "about-webrtc-closed-peerconnection-disclosure-hide-msg",
        startsCollapsed: [...openPeerConnectionReports].size,
      }
    );
    closedPCSection.append(closedPeerConnectionDisclosure);
    closedPeerConnectionDisclosure.append(
      ...closedPeerConnectionReports.map(r =>
        renderPeerConnection(r, () => forceRefreshList.push(r.pcid))
      )
    );
  }

  const primarySection = await PrimarySection.make({
    headingL10nId: "about-webrtc-peerconnections-section-heading",
    disclosureShowL10nId: "about-webrtc-peerconnections-section-show-msg",
    disclosureHideL10nId: "about-webrtc-peerconnections-section-hide-msg",
    autoRefreshPref: "media.aboutwebrtc.auto_refresh.peerconnection_section",
    renderFn: async () => {
      const body = document.createElement("div");
      body.append(
        ...openPeerConnectionReports.map(r =>
          renderPeerConnection(r, () => forceRefreshList.push(r.pcid))
        ),
        closedPCSection
      );
      return body;
    },
    // Creates the filling for the disclosure
    updateFn: async () => {
      let statsReports = await getStats(needsFullUpdate);
      needsFullUpdate = REQUEST_UPDATE_ONLY_REFRESH;

      async function translate(element) {
        const frag = document.createDocumentFragment();
        frag.append(element);
        await document.l10n.translateFragment(frag);
        return frag;
      }

      const translateSection = async (report, id, renderFunc) => {
        const element = document.getElementById(`${id}: ${report.pcid}`);
        const result =
          element && (await translate(renderFunc(elemRenderer, report, hist)));
        return { element, translated: result };
      };

      const sections = (
        await Promise.all(
          // Add filter to check the refreshEnabledPcids
          statsReports
            .filter(
              ({ pcid }) =>
                document.getElementById(`autorefresh-${pcid}`)?.checked ||
                forceRefreshList.includes(pcid)
            )
            .flatMap(report => [
              translateSection(
                report,
                "pc-heading",
                renderPeerConnectionHeading
              ),
              translateSection(report, "ice-stats", renderICEStats),
              translateSection(
                report,
                "ice-raw-stats-fold",
                renderRawICEStatsFold
              ),
              translateSection(report, "rtp-stats", renderRTPStats),
              translateSection(report, "sdp-stats", renderSDPStats),
              translateSection(report, "bandwidth-stats", renderBandwidthStats),
              translateSection(report, "frame-stats", renderFrameRateStats),
            ])
        )
      ).filter(({ element }) => element);
      document.l10n.pauseObserving();
      for (const { element, translated } of sections) {
        element.replaceWith(translated);
      }
      document.l10n.resumeObserving();
      while (forceRefreshList.length) {
        forceRefreshList.pop();
      }
    },
    // Updates the contents.
    headerElementsFn: async () => {
      const clearStatsButton = document.createElement("button");
      Object.assign(clearStatsButton, {
        className: "no-print",
        onclick: async () => {
          WGI.clearAllStats();
          clearStatsHistory();
          needsFullUpdate = REQUEST_FULL_REFRESH;
          primarySection.updateFn();
        },
      });
      document.l10n.setAttributes(clearStatsButton, "about-webrtc-stats-clear");
      return [clearStatsButton];
    },
  });
  peerConnectionAutoRefreshState.primaryCheckbox = primarySection.autorefresh;
  let originalOnChange = primarySection.autorefresh.onchange;
  primarySection.autorefresh.onchange = () => {
    originalOnChange();
    peerConnectionAutoRefreshState.primaryClicked();
  };
  return primarySection;
}

function renderSubsectionHeading(l10n_id, copyFunc) {
  const heading = document.createElement("div");
  heading.className = "subsection-heading";
  const h4 = document.createElement("h4");
  if (copyFunc != undefined) {
    const copyButton = new CopyButton(copyFunc);
    h4.appendChild(copyButton.element);
  }
  const text = document.createElement("span");
  document.l10n.setAttributes(text, l10n_id);
  h4.appendChild(text);
  heading.appendChild(h4);
  return heading;
}

function renderPeerConnection(report, forceRefreshFn) {
  const rndr = elemRenderer;
  const { pcid, configuration } = report;
  const pcStats = report.peerConnectionStats[0];

  const pcDiv = renderElement("div", { className: "peer-connection" });
  pcDiv.append(renderPeerConnectionTools(rndr, report, forceRefreshFn));
  {
    const section = renderFoldableSection(pcDiv);
    section.append(
      renderElements("div", {}, [
        renderElement(
          "span",
          {
            className: "info-label",
          },
          "about-webrtc-peerconnection-id-label"
        ),
        renderText("span", pcid, { className: "info-body" }),
        rndr.elems_p({}, [
          rndr.elem_span(
            { className: "info-label" },
            "about-webrtc-data-channels-opened-label"
          ),
          rndr.text_span(pcStats.dataChannelsOpened, {
            className: "info-body",
          }),
        ]),
        rndr.elems_p({}, [
          rndr.elem_span(
            { className: "info-label" },
            "about-webrtc-data-channels-closed-label"
          ),
          rndr.text_span(pcStats.dataChannelsClosed, {
            className: "info-body",
          }),
        ]),
        renderConfiguration(rndr, configuration),
      ]),
      renderRTPStats(rndr, report),
      renderICEStats(rndr, report),
      renderRawICEStats(rndr, report),
      renderSDPStats(rndr, report),
      renderBandwidthStats(rndr, report),
      renderFrameRateStats(rndr, report)
    );
    pcDiv.append(section);
  }
  return pcDiv;
}

function renderPeerConnectionMediaSummary(rndr, report) {
  // Takes a codecId value and returns a corresponding codec stats object
  const getCodecById = aId => report.codecStats.find(({ id }) => id == aId);

  // Find all the codecs used by send streams
  const sendCodecs = new Set(
    [...report.outboundRtpStreamStats]
      .filter(({ codecId }) => codecId)
      .map(({ codecId }) => getCodecById(codecId).mimeType)
      .sort()
  );

  // Find all the codecs used by receive streams
  const recvCodecs = new Set(
    [...report.inboundRtpStreamStats]
      .filter(({ codecId }) => codecId)
      .map(({ codecId }) => getCodecById(codecId).mimeType)
      .sort()
  );

  // Take all the codecs that appear in both the send and receive codec lists
  const sendRecvCodecs = new Set(
    [...sendCodecs, ...recvCodecs].filter(
      c => sendCodecs.has(c) && recvCodecs.has(c)
    )
  );

  // Remove the common codecs from the send and receive codec lists.
  // sendCodecs will now contain send only codecs
  // receiveCodecs will now contain receive only codecs
  sendRecvCodecs.forEach(c => {
    sendCodecs.delete(c);
    recvCodecs.delete(c);
  });

  const formatter = new Intl.ListFormat("en", {
    style: "short",
    type: "conjunction",
  });

  // Create a label with the codecs common to send and receive streams
  const sendRecvSpan = sendRecvCodecs.size
    ? [
        rndr.elem_span({}, "about-webrtc-short-send-receive-direction", {
          codecs: formatter.format(sendRecvCodecs),
        }),
      ]
    : [];

  // Do the same for send only codecs
  const sendSpan = sendCodecs.size
    ? [
        rndr.elem_span({}, "about-webrtc-short-send-direction", {
          codecs: formatter.format(sendCodecs),
        }),
      ]
    : [];

  // Do the same for receive only codecs
  const recvSpan = recvCodecs.size
    ? [
        rndr.elem_span({}, "about-webrtc-short-receive-direction", {
          codecs: formatter.format(recvCodecs),
        }),
      ]
    : [];

  return [...sendRecvSpan, ...sendSpan, ...recvSpan];
}

function renderPeerConnectionHeading(rndr, report) {
  const { pcid, timestamp, closed: isClosed, browserId } = report;
  const id = pcid.match(/id=(\S+)/)[1];
  const url = pcid.match(/url=([^)]+)/)[1];
  const now = new Date(timestamp);
  return isClosed
    ? rndr.elems_div(
        {
          id: `pc-heading: ${pcid}`,
          class: "pc-heading",
        },
        [
          rndr.elems_h3({}, [
            rndr.elem_span({}, "about-webrtc-connection-closed", {
              "browser-id": browserId,
              id,
              url,
              now,
            }),
            ...renderPeerConnectionMediaSummary(rndr, report),
          ]),
        ]
      )
    : rndr.elems_div(
        {
          id: `pc-heading: ${pcid}`,
          class: "pc-heading",
        },
        [
          rndr.elems_h3({}, [
            rndr.elem_span({}, "about-webrtc-connection-open", {
              "browser-id": browserId,
              id,
              url,
              now,
            }),
            ...renderPeerConnectionMediaSummary(rndr, report),
          ]),
        ]
      );
}

function renderPeerConnectionTools(rndr, report, forceRefreshFn) {
  const { pcid, browserId } = report;
  const id = pcid.match(/id=(\S+)/)[1];
  const copyHistButton = !Services.prefs.getBoolPref(
    "media.aboutwebrtc.hist.enabled"
  )
    ? []
    : [
        rndr.elem_button(
          {
            id: `copytextbutton-hist-${id}`,
            onclick() {
              WGI.getStatsHistorySince(
                hist =>
                  navigator.clipboard.writeText(JSON.stringify(hist, null, 2)),
                pcid
              );
            },
          },
          "about-webrtc-copy-report-history-button"
        ),
      ];
  const autorefreshButton = rndr.elem_input({
    id: `autorefresh-${pcid}`,
    className: "autorefresh",
    type: "checkbox",
    hidden: report.closed,
    checked: Services.prefs.getBoolPref(
      "media.aboutwebrtc.auto_refresh.peerconnection_section"
    ),
    onchange: () => peerConnectionAutoRefreshState.secondaryClicked(),
  });
  peerConnectionAutoRefreshState.secondaryCheckboxes.push(autorefreshButton);
  const forceRefreshButton = rndr.elem_button(
    {
      id: `force-refresh-pc-${id}`,
      onclick() {
        forceRefreshFn();
      },
    },
    "about-webrtc-force-refresh-button"
  );
  const autorefreshLabel = rndr.elem_label(
    {
      className: "autorefresh",
      hidden: autorefreshButton.hidden,
    },
    "about-webrtc-auto-refresh-label"
  );
  return renderElements("div", { id: "pc-tools: " + pcid }, [
    renderPeerConnectionHeading(rndr, report),
    new ShowTab(browserId).render()[0],
    renderCopyTextToClipboardButton(
      rndr,
      report.pcid,
      "about-webrtc-copy-report-button",
      () => JSON.stringify({ ...report }, null, 2)
    ),
    ...copyHistButton,
    forceRefreshButton,
    autorefreshButton,
    autorefreshLabel,
  ]);
}

const trimNewlines = sdp => sdp.replaceAll("\r\n", "\n");

const tabElementProps = (element, elemSubId, pcid) => ({
  className:
    elemSubId != "answer"
      ? `tab-${element}`
      : `tab-${element} active-tab-${element}`,
  id: `tab_${element}_${elemSubId}_${pcid}`,
});

const renderSDPTab = (rndr, sdp, props) =>
  rndr.elems("div", props, [rndr.text("pre", trimNewlines(sdp))]);

const renderSDPHistoryTab = (rndr, hist, props) => {
  // All SDP in sequential order. Add onclick handler to scroll the associated
  // SDP into view below.
  let first = Math.min(...hist.map(({ timestamp }) => timestamp));
  const parts = hist.map(({ isLocal, timestamp, sdp, errors: errs }) => {
    let errorsSubSect = () => [
      rndr.elem_h5({}, "about-webrtc-sdp-parsing-errors-heading"),
      ...errs.map(({ lineNumber: n, error: e }) => rndr.text_br(`${n}: ${e}`)),
    ];

    let sdpSection = [
      rndr.elem_h5({}, "about-webrtc-sdp-set-timestamp", {
        timestamp,
        "relative-timestamp": timestamp - first,
      }),
      ...(errs && errs.length ? errorsSubSect() : []),
      rndr.text_pre(trimNewlines(sdp)),
    ];

    return {
      link: rndr.elems_div({}, [
        rndr.elem_h5(
          {
            className: "sdp-history-link",
            onclick: () => sdpSection[0].scrollIntoView(),
          },
          isLocal
            ? "about-webrtc-sdp-set-at-timestamp-local"
            : "about-webrtc-sdp-set-at-timestamp-remote",
          { timestamp }
        ),
      ]),
      ...(isLocal ? { local: sdpSection } : { remote: sdpSection }),
    };
  });

  return rndr.elems_div(props, [
    // Render the links
    rndr.elems_div(
      {},
      parts.map(({ link }) => link)
    ),
    rndr.elems_div({ className: "sdp-history" }, [
      // Render the SDP into separate columns for local and remote.
      rndr.elems_div({}, [
        rndr.elem_h4({}, "about-webrtc-local-sdp-heading"),
        ...parts.filter(({ local }) => local).flatMap(({ local }) => local),
      ]),
      rndr.elems_div({}, [
        rndr.elem_h4({}, "about-webrtc-remote-sdp-heading"),
        ...parts.filter(({ remote }) => remote).flatMap(({ remote }) => remote),
      ]),
    ]),
  ]);
};

function renderSDPStats(rndr, { offerer, pcid, timestamp }) {
  // Get the most recent (as of timestamp) local and remote SDPs from the
  // history
  const sdpEntries = getSdpHistory({ pcid, timestamp });
  const localSdp = sdpEntries.findLast(({ isLocal }) => isLocal)?.sdp || "";
  const remoteSdp = sdpEntries.findLast(({ isLocal }) => !isLocal)?.sdp || "";

  const sdps = offerer
    ? { offer: localSdp, answer: remoteSdp }
    : { offer: remoteSdp, answer: localSdp };

  const sdpLabels = offerer
    ? { offer: "local", answer: "remote" }
    : { offer: "remote", answer: "local" };

  sdpLabels.l10n = {
    offer: offerer
      ? "about-webrtc-local-sdp-heading-offer"
      : "about-webrtc-remote-sdp-heading-offer",
    answer: offerer
      ? "about-webrtc-remote-sdp-heading-answer"
      : "about-webrtc-local-sdp-heading-answer",
    history: "about-webrtc-sdp-history-heading",
  };

  const tabPaneProps = elemSubId => tabElementProps("pane", elemSubId, pcid);

  const panes = {
    answer: renderSDPTab(rndr, sdps.answer, tabPaneProps("answer")),
    offer: renderSDPTab(rndr, sdps.offer, tabPaneProps("offer")),
    history: renderSDPHistoryTab(
      rndr,
      getSdpHistory({ pcid, timestamp }),
      tabPaneProps("history")
    ),
  };

  // Creates the properties and l10n label for tab buttons
  const tabButtonProps = (elemSubId, pane) => [
    {
      ...tabElementProps("button", elemSubId, pcid),
      onclick({ currentTarget: t }) {
        const flipPane = c => c.classList.toggle("active-tab-pane", c == pane);
        Object.values(panes).forEach(flipPane);
        const selButton = c => c.classList.toggle("active-tab-button", c == t);
        [...t.parentElement.children].forEach(selButton);
      },
    },
    sdpLabels.l10n[elemSubId],
  ];

  const sdpDiv = renderSubsectionHeading("about-webrtc-sdp-heading", () =>
    JSON.stringify(
      {
        offer: {
          side: sdpLabels.offer,
          sdp: sdps.offer.split("\r\n"),
        },
        answer: {
          side: sdpLabels.answer,
          sdp: sdps.answer.split("\r\n"),
        },
      },
      null,
      2
    )
  );
  const outer = document.createElement("div", { id: "sdp-stats" + pcid });
  outer.appendChild(sdpDiv);
  let foldSection = renderFoldableSection(outer, {
    showMsg: "about-webrtc-show-msg-sdp",
    hideMsg: "about-webrtc-hide-msg-sdp",
  });
  foldSection.append(
    rndr.elems_div({ className: "tab-buttons" }, [
      ...Object.entries(panes).map(([elemSubId, pane]) =>
        rndr.elem_button(...tabButtonProps(elemSubId, pane))
      ),
      ...Object.values(panes),
    ])
  );
  outer.append(foldSection);
  return outer;
}

function renderBandwidthStats(rndr, report) {
  const statsDiv = renderElement("div", {
    id: "bandwidth-stats: " + report.pcid,
  });
  const table = renderSimpleTable(
    "",
    [
      "about-webrtc-track-identifier",
      "about-webrtc-send-bandwidth-bytes-sec",
      "about-webrtc-receive-bandwidth-bytes-sec",
      "about-webrtc-max-padding-bytes-sec",
      "about-webrtc-pacer-delay-ms",
      "about-webrtc-round-trip-time-ms",
    ],
    report.bandwidthEstimations.map(stat => [
      stat.trackIdentifier,
      stat.sendBandwidthBps,
      stat.receiveBandwidthBps,
      stat.maxPaddingBps,
      stat.pacerDelayMs,
      stat.rttMs,
    ])
  );
  statsDiv.append(
    renderElement("h4", {}, "about-webrtc-bandwidth-stats-heading"),
    table
  );
  return statsDiv;
}

function renderFrameRateStats(rndr, report) {
  const statsDiv = renderElement("div", { id: "frame-stats: " + report.pcid });
  report.videoFrameHistories.forEach(hist => {
    const stats = hist.entries.map(stat => {
      stat.elapsed = stat.lastFrameTimestamp - stat.firstFrameTimestamp;
      if (stat.elapsed < 1) {
        stat.elapsed = "0.00";
      }
      stat.elapsed = (stat.elapsed / 1_000).toFixed(3);
      if (stat.elapsed && stat.consecutiveFrames) {
        stat.avgFramerate = (stat.consecutiveFrames / stat.elapsed).toFixed(2);
      } else {
        stat.avgFramerate = "0.00";
      }
      return stat;
    });

    const table = renderSimpleTable(
      "",
      [
        "about-webrtc-width-px",
        "about-webrtc-height-px",
        "about-webrtc-consecutive-frames",
        "about-webrtc-time-elapsed",
        "about-webrtc-estimated-framerate",
        "about-webrtc-rotation-degrees",
        "about-webrtc-first-frame-timestamp",
        "about-webrtc-last-frame-timestamp",
        "about-webrtc-local-receive-ssrc",
        "about-webrtc-remote-send-ssrc",
      ],
      stats.map(stat =>
        [
          stat.width,
          stat.height,
          stat.consecutiveFrames,
          stat.elapsed,
          stat.avgFramerate,
          stat.rotationAngle,
          stat.firstFrameTimestamp,
          stat.lastFrameTimestamp,
          stat.localSsrc,
          stat.remoteSsrc || "?",
        ].map(entry => (Object.is(entry, undefined) ? "<<undefined>>" : entry))
      )
    );

    statsDiv.append(
      renderElement("h4", {}, "about-webrtc-frame-stats-heading", {
        "track-identifier": hist.trackIdentifier,
      }),
      table
    );
  });

  return statsDiv;
}

function renderRTPStats(rndr, report, hist) {
  const rtpStats = [
    ...(report.inboundRtpStreamStats || []),
    ...(report.outboundRtpStreamStats || []),
  ];
  const remoteRtpStats = [
    ...(report.remoteInboundRtpStreamStats || []),
    ...(report.remoteOutboundRtpStreamStats || []),
  ];

  // Generate an id-to-streamStat index for each remote streamStat. This will
  // be used next to link the remote to its local side.
  const remoteRtpStatsMap = {};
  for (const stat of remoteRtpStats) {
    remoteRtpStatsMap[stat.id] = stat;
  }

  // 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 (const stat of rtpStats.filter(s => "remoteId" in s)) {
    stat.remoteRtpStats = remoteRtpStatsMap[stat.remoteId];
  }
  for (const stat of rtpStats.filter(s => "codecId" in s)) {
    stat.codecStat = report.codecStats.find(({ id }) => id == stat.codecId);
  }
  const graphsByStat = stat =>
    (graphData[report.pcid]?.getGraphDataById(stat.id) || []).map(gd => {
      // For some (remote) graphs data comes in slowly.
      // Those graphs can be larger to show trends.
      const histSecs = gd.getConfig().histSecs;
      const width = (histSecs > 30 ? histSecs / 3 : 15) * 20;
      const height = 100;
      const graph = new GraphImpl(width, height);
      graph.startTime = () => stat.timestamp - histSecs * 1000;
      graph.stopTime = () => stat.timestamp;
      if (gd.subKey == "packetsLost") {
        const oldMaxColor = graph.maxColor;
        graph.maxColor = data => (data.value == 0 ? "red" : oldMaxColor(data));
      }
      // Get a bit more history for averages (20%)
      const dataSet = gd.getDataSetSince(
        graph.startTime() - histSecs * 0.2 * 1000
      );
      return graph.drawSparseValues(dataSet, gd.subKey, gd.getConfig());
    });
  // Render stats set
  return renderElements(
    "div",
    { id: "rtp-stats: " + report.pcid, className: "rtp-stats" },
    [
      renderSubsectionHeading("about-webrtc-rtp-stats-heading", () =>
        JSON.stringify([...rtpStats, ...remoteRtpStats], null, 2)
      ),
      ...rtpStats.map(stat => {
        const { ssrc, remoteId, remoteRtpStats: rtcpStats } = stat;
        const remoteGraphs = rtcpStats
          ? [
              rndr.elems_div({}, [
                rndr.text_h6(rtcpStats.type),
                ...graphsByStat(rtcpStats),
              ]),
            ]
          : [];
        const mime = stat?.codecStat?.mimeType?.concat(" - ") || "";
        const div = renderElements("div", {}, [
          rndr.text_h5(`${mime}SSRC ${ssrc}`),
          rndr.elems_div({}, [rndr.text_h6(stat.type), ...graphsByStat(stat)]),
          ...remoteGraphs,
          renderCodecStats(stat),
          renderTransportStats(stat, true, hist),
        ]);
        if (remoteId && rtcpStats) {
          div.append(renderTransportStats(rtcpStats, false));
        }
        return div;
      }),
    ]
  );
}

function renderCodecStats({
  codecStat,
  framesEncoded,
  framesDecoded,
  framesDropped,
  discardedPackets,
  packetsReceived,
}) {
  let elements = [];

  if (codecStat) {
    elements.push(
      renderText("span", `${codecStat.payloadType} ${codecStat.mimeType}`, {})
    );
    if (framesEncoded !== undefined || framesDecoded !== undefined) {
      elements.push(
        renderElement(
          "span",
          { className: "stat-label" },
          "about-webrtc-frames",
          {
            frames: framesEncoded || framesDecoded || 0,
          }
        )
      );
    }
    if (codecStat.channels !== undefined) {
      elements.push(
        renderElement(
          "span",
          { className: "stat-label" },
          "about-webrtc-channels",
          {
            channels: codecStat.channels,
          }
        )
      );
    }
    elements.push(
      renderText(
        "span",
        ` ${codecStat.clockRate} ${codecStat.sdpFmtpLine || ""}`,
        {}
      )
    );
  }
  if (framesDropped !== undefined) {
    elements.push(
      renderElement(
        "span",
        { className: "stat-label" },
        "about-webrtc-dropped-frames-label"
      )
    );
    elements.push(renderText("span", ` ${framesDropped}`, {}));
  }
  if (discardedPackets !== undefined) {
    elements.push(
      renderElement(
        "span",
        { className: "stat-label" },
        "about-webrtc-discarded-packets-label"
      )
    );
    elements.push(renderText("span", ` ${discardedPackets}`, {}));
  }
  if (elements.length) {
    if (packetsReceived !== undefined) {
      elements.unshift(
        renderElement("span", {}, "about-webrtc-decoder-label"),
        renderText("span", ": ")
      );
    } else {
      elements.unshift(
        renderElement("span", {}, "about-webrtc-encoder-label"),
        renderText("span", ": ")
      );
    }
  }
  return renderElements("div", {}, elements);
}

function renderTransportStats(
  {
    id,
    timestamp,
    type,
    packetsReceived,
    bytesReceived,
    packetsLost,
    jitter,
    roundTripTime,
    packetsSent,
    bytesSent,
  },
  local,
  hist
) {
  if (hist) {
    if (hist[id] === undefined) {
      hist[id] = {};
    }
  }

  const estimateKBps = (curTimestamp, lastTimestamp, bytes, lastBytes) => {
    if (!curTimestamp || !lastTimestamp || !bytes || !lastBytes) {
      return "0.0";
    }
    const elapsedTime = curTimestamp - lastTimestamp;
    if (elapsedTime <= 0) {
      return "0.0";
    }
    return ((bytes - lastBytes) / elapsedTime).toFixed(1);
  };

  let elements = [];

  if (local) {
    elements.push(
      renderElement("span", {}, "about-webrtc-type-local"),
      renderText("span", ": ")
    );
  } else {
    elements.push(
      renderElement("span", {}, "about-webrtc-type-remote"),
      renderText("span", ": ")
    );
  }

  const time = new Date(timestamp).toTimeString();
  elements.push(renderText("span", `${time} ${type}`));

  if (packetsReceived) {
    elements.push(
      renderElement(
        "span",
        { className: "stat-label" },
        "about-webrtc-received-label",
        {
          packets: packetsReceived,
        }
      )
    );

    if (bytesReceived) {
      let s = ` (${(bytesReceived / 1024).toFixed(2)} Kb`;
      if (local && hist) {
        s += ` , ${estimateKBps(
          timestamp,
          hist[id].lastTimestamp,
          bytesReceived,
          hist[id].lastBytesReceived
        )} KBps`;
      }
      s += ")";
      elements.push(renderText("span", s));
    }

    elements.push(
      renderElement(
        "span",
        { className: "stat-label" },
        "about-webrtc-lost-label",
        {
          packets: packetsLost,
        }
      )
    );
    elements.push(
      renderElement(
        "span",
        { className: "stat-label" },
        "about-webrtc-jitter-label",
        {
          jitter,
        }
      )
    );

    if (roundTripTime !== undefined) {
      elements.push(renderText("span", ` RTT: ${roundTripTime * 1000} ms`));
    }
  } else if (packetsSent) {
    elements.push(
      renderElement(
        "span",
        { className: "stat-label" },
        "about-webrtc-sent-label",
        {
          packets: packetsSent,
        }
      )
    );
    if (bytesSent) {
      let s = ` (${(bytesSent / 1024).toFixed(2)} Kb`;
      if (local && hist) {
        s += `, ${estimateKBps(
          timestamp,
          hist[id].lastTimestamp,
          bytesSent,
          hist[id].lastBytesSent
        )} KBps`;
      }
      s += ")";
      elements.push(renderText("span", s));
    }
  }

  // Update history
  if (hist) {
    hist[id].lastBytesReceived = bytesReceived;
    hist[id].lastBytesSent = bytesSent;
    hist[id].lastTimestamp = timestamp;
  }

  return renderElements("div", {}, elements);
}

function renderRawIceTable(caption, candidates) {
  const table = renderSimpleTable(
    "",
    [caption],
    [...new Set(candidates.sort())].filter(i => i).map(i => [i])
  );
  table.className = "raw-candidate";
  return table;
}

function renderConfiguration(rndr, c) {
  const provided = "about-webrtc-configuration-element-provided";
  const notProvided = "about-webrtc-configuration-element-not-provided";

  // Create the text for a configuration field
  const cfg = (obj, key) => [
    renderElement("br"),
    `${key}: `,
    key in obj ? obj[key] : renderElement("i", {}, notProvided),
  ];

  // Create the text for a fooProvided configuration field
  const pro = (obj, key) => [
    renderElement("br"),
    `${key}(`,
    renderElement("i", {}, provided),
    `/`,
    renderElement("i", {}, notProvided),
    `): `,
    renderElement("i", {}, obj[`${key}Provided`] ? provided : notProvided),
  ];

  const confDiv = rndr.elem_div({ display: "contents" });
  let disclosure = renderFoldableSection(confDiv, {
    showMsg: "about-webrtc-pc-configuration-show-msg",
    hideMsg: "about-webrtc-pc-configuration-hide-msg",
  });
  disclosure.append(
    rndr.elems_div({ classList: "peer-connection-config" }, [
      "RTCConfiguration",
      ...cfg(c, "bundlePolicy"),
      ...cfg(c, "iceTransportPolicy"),
      ...pro(c, "peerIdentity"),
      ...cfg(c, "sdpSemantics"),
      renderElement("br"),
      "iceServers: ",
      ...(!c.iceServers
        ? [renderElement("i", {}, notProvided)]
        : c.iceServers.map(i =>
            renderElements("div", {}, [
              `urls: ${JSON.stringify(i.urls)}`,
              ...pro(i, "credential"),
              ...pro(i, "userName"),
            ])
          )),
    ])
  );
  confDiv.append(disclosure);
  return confDiv;
}

function renderICEStats(rndr, report) {
  const iceDiv = renderElements("div", { id: "ice-stats: " + report.pcid }, [
    renderSubsectionHeading("about-webrtc-ice-stats-heading", () =>
      JSON.stringify(
        [...report.iceCandidateStats, ...report.iceCandidatePairStats],
        null,
        2
      )
    ),
  ]);

  // Render ICECandidate table
  {
    const caption = renderElement(
      "caption",
      { className: "no-print" },
      "about-webrtc-trickle-caption-msg"
    );

    // Generate ICE stats
    const stats = [];
    {
      // Create an index based on candidate ID for each element in the
      // iceCandidateStats array.
      const candidates = {};
      for (const candidate of report.iceCandidateStats) {
        candidates[candidate.id] = candidate;
      }

      // a method to see if a given candidate id is in the array of tickled
      // candidates.
      const isTrickled = candidateId =>
        report.trickledIceCandidateStats.some(({ id }) => id == candidateId);

      // A component may have a remote or local candidate address or both.
      // Combine those with both; these will be the peer candidates.
      const matched = {};

      for (const {
        localCandidateId,
        remoteCandidateId,
        componentId,
        state,
        priority,
        nominated,
        selected,
        bytesSent,
        bytesReceived,
      } of report.iceCandidatePairStats) {
        const local = candidates[localCandidateId];
        if (local) {
          const stat = {
            ["local-candidate"]: candidateToString(local),
            componentId,
            state,
            priority,
            nominated,
            selected,
            bytesSent,
            bytesReceived,
          };
          matched[local.id] = true;
          if (isTrickled(local.id)) {
            stat["local-trickled"] = true;
          }

          const remote = candidates[remoteCandidateId];
          if (remote) {
            stat["remote-candidate"] = candidateToString(remote);
            matched[remote.id] = true;
            if (isTrickled(remote.id)) {
              stat["remote-trickled"] = true;
            }
          }
          stats.push(stat);
        }
      }

      // sort (group by) componentId first, then bytesSent if available, else by
      // priority
      stats.sort((a, b) => {
        if (a.componentId != b.componentId) {
          return a.componentId - b.componentId;
        }
        return b.bytesSent
          ? b.bytesSent - (a.bytesSent || 0)
          : (b.priority || 0) - (a.priority || 0);
      });
    }
    // Render ICE stats
    // don't use |stat.x || ""| here because it hides 0 values
    const statsTable = renderSimpleTable(
      caption,
      [
        "about-webrtc-ice-state",
        "about-webrtc-nominated",
        "about-webrtc-selected",
        "about-webrtc-local-candidate",
        "about-webrtc-remote-candidate",
        "about-webrtc-ice-component-id",
        "about-webrtc-priority",
        "about-webrtc-ice-pair-bytes-sent",
        "about-webrtc-ice-pair-bytes-received",
      ],
      stats.map(stat =>
        [
          stat.state,
          stat.nominated,
          stat.selected,
          stat["local-candidate"],
          stat["remote-candidate"],
          stat.componentId,
          stat.priority,
          stat.bytesSent,
          stat.bytesReceived,
        ].map(entry => (Object.is(entry, undefined) ? "" : entry))
      )
    );

    // after rendering the table, we need to change the class name for each
    // candidate pair's local or remote candidate if it was trickled.
    let index = 0;
    for (const {
      state,
      nominated,
      selected,
      "local-trickled": localTrickled,
      "remote-trickled": remoteTrickled,
    } of stats) {
      // look at statsTable row index + 1 to skip column headers
      const { cells } = statsTable.rows[++index];
      cells[0].className = `ice-${state}`;
      if (nominated) {
        cells[1].className = "ice-succeeded";
      }
      if (selected) {
        cells[2].className = "ice-succeeded";
      }
      if (localTrickled) {
        cells[3].className = "ice-trickled";
      }
      if (remoteTrickled) {
        cells[4].className = "ice-trickled";
      }
    }

    // if the current row's component id changes, mark the bottom of the
    // previous row with a thin, black border to differentiate the
    // component id grouping.
    let previousRow;
    for (const row of statsTable.rows) {
      if (previousRow) {
        if (previousRow.cells[5].innerHTML != row.cells[5].innerHTML) {
          previousRow.className = "bottom-border";
        }
      }
      previousRow = row;
    }
    iceDiv.append(statsTable);
  }
  // restart/rollback counts.
  iceDiv.append(
    renderIceMetric("about-webrtc-ice-restart-count-label", report.iceRestarts),
    renderIceMetric(
      "about-webrtc-ice-rollback-count-label",
      report.iceRollbacks
    )
  );
  return iceDiv;
}

function renderRawICEStats(rndr, report) {
  const iceDiv = renderElements("div", { id: "ice-stats: " + report.pcid }, [
    renderSubsectionHeading("about-webrtc-raw-candidates-heading", () =>
      JSON.stringify(
        [...report.rawLocalCandidates, ...report.rawRemoteCandidates],
        null,
        2
      )
    ),
  ]);
  // Render raw ICECandidate section
  {
    const foldSection = renderFoldableSection(iceDiv, {
      showMsg: "about-webrtc-raw-cand-section-show-msg",
      hideMsg: "about-webrtc-raw-cand-section-hide-msg",
    });

    // render raw candidates
    foldSection.append(renderRawICEStatsFold(rndr, report));
    iceDiv.append(foldSection);
  }
  return iceDiv;
}

function renderRawICEStatsFold(rndr, report) {
  return renderElements("div", { id: "ice-raw-stats-fold: " + report.pcid }, [
    renderRawIceTable(
      "about-webrtc-raw-local-candidate",
      report.rawLocalCandidates
    ),
    renderRawIceTable(
      "about-webrtc-raw-remote-candidate",
      report.rawRemoteCandidates
    ),
  ]);
}

function renderIceMetric(label, value) {
  return renderElements("div", {}, [
    renderElement("span", { className: "info-label" }, label),
    renderText("span", value, { className: "info-body" }),
  ]);
}

function candidateToString({
  type,
  address,
  port,
  protocol,
  candidateType,
  relayProtocol,
  proxied,
} = {}) {
  if (!type) {
    return "*";
  }
  if (relayProtocol) {
    candidateType = `${candidateType}-${relayProtocol}`;
  }
  proxied = type == "local-candidate" ? ` [${proxied}]` : "";
  return `${address}:${port}/${protocol}(${candidateType})${proxied}`;
}

async function renderConnectionLog() {
  const getLog = () => new Promise(r => WGI.getLogging("", r));
  const logView = document.createElement("div");
  const displayLogs = logLines => {
    logView.replaceChildren();
    logView.append(
      ...logLines.map(line => {
        const e = document.createElement("p");
        e.textContent = line;
        return e;
      })
    );
  };
  const clearLogsButton = document.createElement("button");

  Object.assign(clearLogsButton, {
    className: "no-print",
    onclick: async () => {
      await WGI.clearLogging();
      displayLogs(await getLog());
    },
  });
  document.l10n.setAttributes(clearLogsButton, "about-webrtc-log-clear");
  return PrimarySection.make({
    headingL10nId: "about-webrtc-log-heading",
    disclosureShowL10nId: "about-webrtc-log-section-show-msg",
    disclosureHideL10nId: "about-webrtc-log-section-hide-msg",
    autoRefreshPref: "media.aboutwebrtc.auto_refresh.connection_log_section",
    renderFn: async () => {
      displayLogs(await getLog());
      return logView;
    },
    updateFn: async () => {
      displayLogs(await getLog());
    },
    headerElementsFn: async () => [clearLogsButton],
  });
}

const PREFERENCES = {
  branches: [
    "media.aboutwebrtc",
    "media.peerconnection",
    "media.navigator",
    "media.getusermedia",
    "media.gmp-gmpopenh264.enabled",
  ],
  hidden: [
    "media.aboutwebrtc.auto_refresh.peerconnection_section",
    "media.aboutwebrtc.auto_refresh.connection_log_section",
    "media.aboutwebrtc.auto_refresh.user_modified_config_section",
    "media.aboutwebrtc.auto_refresh.media_ctx_section",
  ],
};

async function renderUserPrefSection() {
  const getConfigPaths = () => {
    return PREFERENCES.branches
      .flatMap(Services.prefs.getChildList)
      .filter(Services.prefs.prefHasUserValue)
      .filter(p => !PREFERENCES.hidden.includes(p));
  };
  const prefList = new ConfigurationList(getConfigPaths());
  return PrimarySection.make({
    headingL10nId: "about-webrtc-user-modified-configuration-heading",
    disclosureShowL10nId: "about-webrtc-user-modified-configuration-show-msg",
    disclosureHideL10nId: "about-webrtc-user-modified-configuration-hide-msg",
    autoRefreshPref:
      "media.aboutwebrtc.auto_refresh.user_modified_config_section",
    renderFn: () => prefList.view(),
    updateFn: () => {
      prefList.setPrefPaths(getConfigPaths());
      prefList.update();
    },
  });
}

function renderFoldableSection(parentElem, options = {}) {
  const section = renderElement("div");
  if (parentElem) {
    const ctrl = renderElements("div", { className: "section-ctrl no-print" }, [
      new FoldEffect(section, options).render(),
    ]);
    parentElem.append(ctrl);
  }
  return section;
}

function renderSimpleTable(caption, headings, data) {
  const heads = headings.map(text => renderElement("th", {}, text));
  const renderCell = text => renderText("td", text);

  return renderElements("table", {}, [
    caption,
    renderElements("tr", {}, heads),
    ...data.map(line => renderElements("tr", {}, line.map(renderCell))),
  ]);
}

class FoldEffect {
  constructor(
    target,
    {
      showMsg = "about-webrtc-fold-default-show-msg",
      hideMsg = "about-webrtc-fold-default-hide-msg",
      startsCollapsed = true,
    } = {}
  ) {
    Object.assign(this, { target, showMsg, hideMsg, startsCollapsed });
  }

  render() {
    this.target.classList.add("fold-target");
    this.trigger = renderElement("div", { className: "fold-trigger" });
    this.trigger.classList.add("heading-medium", this.showMsg, this.hideMsg);
    if (this.startsCollapsed) {
      this.collapse();
    }
    this.trigger.onclick = () => {
      if (this.target.classList.contains("fold-closed")) {
        this.expand();
      } else {
        this.collapse();
      }
    };
    return this.trigger;
  }

  expand() {
    this.target.classList.remove("fold-closed");
    document.l10n.setAttributes(this.trigger, this.hideMsg);
  }

  collapse() {
    this.target.classList.add("fold-closed");
    document.l10n.setAttributes(this.trigger, this.showMsg);
  }

  static expandAll() {
    for (const target of document.getElementsByClassName("fold-closed")) {
      target.classList.remove("fold-closed");
    }
    for (const trigger of document.getElementsByClassName("fold-trigger")) {
      const hideMsg = trigger.classList[2];
      document.l10n.setAttributes(trigger, hideMsg);
    }
  }

  static collapseAll() {
    for (const target of document.getElementsByClassName("fold-target")) {
      target.classList.add("fold-closed");
    }
    for (const trigger of document.getElementsByClassName("fold-trigger")) {
      const showMsg = trigger.classList[1];
      document.l10n.setAttributes(trigger, showMsg);
    }
  }
}

class PrimarySection {
  /** @returns {Promise<PrimarySection>} */
  static async make({
    headingL10nId,
    disclosureShowL10nId,
    disclosureHideL10nId,
    autoRefreshPref,
    renderFn = async () => {}, // Creates the filling for the disclosure
    updateFn = async () => {}, // Updates the contents.
    headerElementsFn = async () => [], // Accessory elements for the heading
  }) {
    const newSect = new PrimarySection();
    Object.assign(newSect, {
      autoRefreshPref,
      renderFn,
      updateFn,
      headerElementsFn,
    });

    // Top level of the section
    const sectionContainer = document.createElement("div");
    // Section heading is always visible and contains the disclosure control,
    // the section title, the autorefresh button, and any accessory elements.
    const sectionHeading = document.createElement("div");
    sectionHeading.className = "section-heading";
    sectionContainer.appendChild(sectionHeading);
    // The section body is the portion that contains the disclosure body
    // container.
    const sectionBody = document.createElement("div");
    sectionBody.className = "section-body";
    sectionContainer.appendChild(sectionBody);

    const disclosure = new Disclosure({
      showMsg: disclosureShowL10nId,
      hideMsg: disclosureHideL10nId,
    });
    sectionHeading.appendChild(disclosure.control());

    const heading = document.createElement("h3");
    document.l10n.setAttributes(heading, headingL10nId);
    sectionHeading.append(heading);

    const autorefresh = document.createElement("input");
    Object.assign(autorefresh, {
      type: "checkbox",
      class: "autorefresh",
      id: autoRefreshPref,
      checked: Services.prefs.getBoolPref(autoRefreshPref),
      onchange: () =>
        Services.prefs.setBoolPref(autoRefreshPref, autorefresh.checked),
    });
    newSect.autorefresh = autorefresh;
    newSect.autorefreshPrefState = newSect.autorefresh.checked;
    const autorefreshLabel = document.createElement("label");
    autorefreshLabel.className = "autorefresh";
    autorefreshLabel.htmlFor = autorefresh.id;
    document.l10n.setAttributes(
      autorefreshLabel,
      "about-webrtc-auto-refresh-label"
    );
    sectionHeading.append(autorefresh, autorefreshLabel);

    let rendered = await renderFn();
    if (rendered) {
      disclosure.view().appendChild(rendered);
    }
    sectionBody.append(disclosure.view());

    let headerElements = (await newSect.headerElementsFn(newSect)) || [];
    sectionHeading.append(...headerElements);

    newSect.section = sectionContainer;
    return newSect;
  }
  view() {
    return this.section;
  }
  async update() {
    return this.updateFn(this);
  }
  async autoUpdate() {
    let prefState = Services.prefs.getBoolPref(this.autoRefreshPref);
    if (prefState != this.autorefreshPrefState) {
      this.autorefreshPrefState = prefState;
      this.autorefresh.checked = prefState;
    }
    if (this.autorefresh.checked || this.autorefresh.indeterminate) {
      return this.updateFn(this);
    }
    return null;
  }
}

async function renderMediaCtx(rndr) {
  const ctx = WGI.getMediaContext();
  const prefs = [
    "media.peerconnection.video.vp9_enabled",
    "media.peerconnection.video.vp9_preferred",
    "media.navigator.video.h264.level",
    "media.navigator.video.h264.max_mbps",
    "media.navigator.video.h264.max_mbps",
    "media.navigator.video.max_fs",
    "media.navigator.video.max_fr",
    "media.navigator.video.use_tmmbr",
    "media.navigator.video.use_remb",
    "media.navigator.video.use_transport_cc",
    "media.navigator.audio.use_fec",
    "media.navigator.video.red_ulpfec_enabled",
    "media.webrtc.codec.video.av1.enabled",
    "media.webrtc.codec.video.av1.experimental_preferred",
  ];

  const confList = new ConfigurationList(prefs);
  const hasH264Hardware = rndr.text_p(
    `hasH264Hardware: ${ctx.hasH264Hardware}`
  );
  hasH264Hardware.dataset.value = ctx.hasH264Hardware;
  const hasAv1 = rndr.text_p(`hasAv1: ${ctx.hasAv1}`);
  hasAv1.dataset.value = ctx.hasAv1;
  const renderFn = async () =>
    rndr.elems_div({}, [
      hasH264Hardware,
      hasAv1,
      rndr.elem_hr(),
      confList.view(),
    ]);
  const updateFn = async () => {
    const newCtx = WGI.getMediaContext();
    if (hasH264Hardware.dataset.value != newCtx.hasH264Hardware) {
      hasH264Hardware.dataset.value = newCtx.hasH264Hardware;
      hasH264Hardware.textContent = `hasH264Hardware: ${newCtx.hasH264Hardware}`;
    }
    if (hasAv1.dataset.value != newCtx.hasAv1) {
      hasAv1.dataset.value = newCtx.hasAv1;
      hasAv1.textContent = `hasAv1: ${newCtx.hasAv1}`;
    }
    confList.update();
  };

  return PrimarySection.make({
    headingL10nId: "about-webrtc-media-context-heading",
    disclosureShowL10nId: "about-webrtc-media-context-show-msg",
    disclosureHideL10nId: "about-webrtc-media-context-hide-msg",
    autoRefreshPref: "media.aboutwebrtc.auto_refresh.media_ctx_section",
    renderFn,
    updateFn,
  });
}

[ Dauer der Verarbeitung: 0.8 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge