chromium/content/browser/webrtc/resources/candidate_grid.js

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * Creates a ICE candidate grid.
 * @param {Element} peerConnectionElement
 */

import {$} from 'chrome://resources/js/util.js';
/**
 * A helper function for appending a child element to |parent|.
 * Copied from webrtc_internals.js
 *
 * @param {!Element} parent The parent element.
 * @param {string} tag The child element tag.
 * @param {string} text The textContent of the new DIV.
 * @return {!Element} the new DIV element.
 */
function appendChildWithText(parent, tag, text) {
  const child = document.createElement(tag);
  child.textContent = text;
  parent.appendChild(child);
  return child;
}

export function createIceCandidateGrid(peerConnectionElement) {
  const container = document.createElement('details');
  appendChildWithText(container, 'summary', 'ICE candidate grid');

  const table = document.createElement('table');
  table.id = 'grid-' + peerConnectionElement.id;
  table.className = 'candidategrid';
  container.appendChild(table);

  const tableHeader = document.createElement('tr');
  table.append(tableHeader);

  // For candidate pairs.
  appendChildWithText(tableHeader, 'th', 'Candidate (pair) id');
  // [1] is used for both candidate pairs and individual candidates.
  appendChildWithText(tableHeader, 'th', 'State / Candidate type');
  // For individual candidates.
  appendChildWithText(tableHeader, 'th', 'Network type / address');
  appendChildWithText(tableHeader, 'th', 'Port');
  appendChildWithText(tableHeader, 'th', 'Protocol / candidate type');
  appendChildWithText(tableHeader, 'th', '(Pair) Priority');

  // For candidate pairs.
  appendChildWithText(tableHeader, 'th', 'Bytes sent / received');
  appendChildWithText(tableHeader, 'th',
      'STUN requests sent / responses received');
  appendChildWithText(tableHeader, 'th',
      'STUN requests received / responses sent');
  appendChildWithText(tableHeader, 'th', 'RTT');
  appendChildWithText(tableHeader, 'th', 'Last data sent / received');
  appendChildWithText(tableHeader, 'th', 'Last update');

  peerConnectionElement.appendChild(container);
}

/**
 * Creates or returns a table row in the ICE candidate grid.
 * @param {string} peerConnectionElement id
 * @param {string} stat object id
 * @param {type} type of the row
 */
function findOrCreateGridRow(peerConnectionElementId, statId, type) {
  const elementId = 'grid-' + peerConnectionElementId
      + '-' + statId + '-' + type;
  let row = document.getElementById(elementId);
  if (!row) {
    row = document.createElement('tr');
    row.id = elementId;
    for (let i = 0; i < 12; i++) {
      row.appendChild(document.createElement('td'));
    }
    $('grid-' + peerConnectionElementId).appendChild(row);
  }
  return row;
}

/**
 * Updates a table row in the ICE candidate grid.
 * @param {string} peerConnectionElement id
 * @param {boolean} whether the pair is the selected pair of a transport
 *   (displayed bold)
 * @param {object} candidate pair stats report
 * @param {Map} full map of stats
 */
function appendRow(peerConnectionElement, active, candidatePair, stats) {
  const pairRow = findOrCreateGridRow(peerConnectionElement.id,
      candidatePair.id, 'candidatepair');
  pairRow.classList.add('candidategrid-candidatepair')
  if (active) {
    pairRow.classList.add('candidategrid-active');
  }
  // Set transport-specific fields.
  pairRow.children[0].innerText = candidatePair.id;
  pairRow.children[1].innerText = candidatePair.state;
  // Show (pair) priority as hex.
  pairRow.children[5].innerText =
      '0x' + parseInt(candidatePair.priority, 10).toString(16);
  pairRow.children[6].innerText =
      candidatePair.bytesSent + ' / ' + candidatePair.bytesReceived;
  pairRow.children[7].innerText = candidatePair.requestsSent + ' / ' +
    candidatePair.responsesReceived;
  pairRow.children[8].innerText = candidatePair.requestsReceived + ' / ' +
    candidatePair.responsesSent;
  pairRow.children[9].innerText =
    candidatePair.currentRoundTripTime !== undefined ?
        candidatePair.currentRoundTripTime + 's' : '';
  if (candidatePair.lastPacketSentTimestamp) {
    pairRow.children[10].innerText =
      (new Date(candidatePair.lastPacketSentTimestamp))
        .toLocaleTimeString() + ' / ' +
      (new Date(candidatePair.lastPacketReceivedTimestamp))
        .toLocaleTimeString();
  }
  pairRow.children[11].innerText = (new Date()).toLocaleTimeString();

  // Local candidate.
  const localRow = findOrCreateGridRow(peerConnectionElement.id,
      candidatePair.id, 'local');
  localRow.className = 'candidategrid-candidate'
  const localCandidate = stats.get(candidatePair.localCandidateId);
  ['id', 'type', 'address', 'port', 'candidateType',
      'priority'].forEach((stat, index) => {
    // `relayProtocol` is only set for local relay candidates.
    if (stat == 'candidateType' && localCandidate.relayProtocol) {
      localRow.children[index].innerText = localCandidate[stat] +
          '(' + localCandidate.relayProtocol + ')';
      if (localCandidate.url) {
        localRow.children[index].innerText += '\n' + localCandidate.url;
      }
    } else if (stat === 'priority') {
      const priority = parseInt(localCandidate[stat], 10) & 0xFFFFFFFF;
      localRow.children[index].innerText = '0x' + priority.toString(16) +
          // RFC 5245 - 4.1.2.1.
          // priority = (2^24)*(type preference) +
          //            (2^8)*(local preference) +
          //            (2^0)*(256 - component ID)
          '\n' + (priority >> 24) +
          ' | ' + ((priority >> 8) & 0xFFFF) +
          ' | ' + (priority & 0xFF);
    } else if (stat === 'address') {
      localRow.children[index].innerText = localCandidate[stat] || '(not set)';
    } else {
      localRow.children[index].innerText = localCandidate[stat];
    }
  });
  // `networkType` is only known for the local candidate so put it into the
  // pair row above the address. Also highlight VPN adapters.
  pairRow.children[2].innerText = localCandidate.networkType;
  if (localCandidate['vpn'] === true) {
    pairRow.children[2].innerText += ' (VPN)';
  }
  // `protocol` must always be the same for the pair
  // so put it into the pair row above the candidate type.
  // Add `tcpType` for local candidates.
  pairRow.children[4].innerText = localCandidate.protocol;
  if (localCandidate.tcpType) {
    pairRow.children[4].innerText += ' ' + localCandidate.tcpType;
  }

  // Remote candidate.
  const remoteRow = findOrCreateGridRow(peerConnectionElement.id,
      candidatePair.id, 'remote');
  remoteRow.className = 'candidategrid-candidate'
  const remoteCandidate = stats.get(candidatePair.remoteCandidateId);
  ['id', 'type', 'address', 'port', 'candidateType',
      'priority'].forEach((stat, index) => {
    if (stat === 'priority') {
      remoteRow.children[index].innerText = '0x' +
          parseInt(remoteCandidate[stat], 10).toString(16);
    } else if (stat === 'address') {
      remoteRow.children[index].innerText = remoteCandidate[stat] ||
          '(not set)';
    } else {
      remoteRow.children[index].innerText = remoteCandidate[stat];
    }
  });
  return pairRow;
}

/**
 * Updates the (spec) ICE candidate grid.
 * @param {Element} peerConnectionElement
 * @param {Map} stats reconstructed stats object.
 */
export function updateIceCandidateGrid(peerConnectionElement, stats) {
  const container = $('grid-' + peerConnectionElement.id);
  // Remove the active/bold marker from all rows.
  container.childNodes.forEach(row => {
    row.classList.remove('candidategrid-active');
  });
  let activePairIds = [];
  // Find the active transport(s), then find its candidate pair
  // and display it first. Note that previously selected pairs continue to be
  // shown since rows are not removed.
  stats.forEach(transportReport => {
    if (transportReport.type !== 'transport') {
      return;
    }
    if (!transportReport.selectedCandidatePairId) {
      return;
    }
    activePairIds.push(transportReport.selectedCandidatePairId);
    appendRow(peerConnectionElement, true,
        stats.get(transportReport.selectedCandidatePairId), stats);
  });

  // Then iterate over the other candidate pairs.
  stats.forEach(report => {
    if (report.type !== 'candidate-pair' || activePairIds.includes(report.id)) {
      return;
    }
    appendRow(peerConnectionElement, false, report, stats);
  });
}