| // 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); |
| }); |
| } |
| |