220 lines
8.2 KiB
JavaScript
220 lines
8.2 KiB
JavaScript
|
// Derived from Chromium WebRTC Internals Dashboard - see Acknowledgements for full license details
|
||
|
|
||
|
/**
|
||
|
* Creates a ICE candidate grid.
|
||
|
* @param {Element} peerConnectionElement
|
||
|
*/
|
||
|
|
||
|
import {$} from './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);
|
||
|
});
|
||
|
}
|
||
|
|