1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
content / browser / webrtc / resources / candidate_grid.js [blame]
// 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);
});
}