├── .gitignore ├── README.md ├── eslint.config.js ├── import-common.js ├── import-rtcstats.js ├── import.js ├── index.html ├── main.js ├── package.json ├── rtcstats-utils.js ├── rtcstats.html └── videoreplay-config.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Import webrtc-internal dumps 2 | Chrome webrtc-internals page is tremendously useful but lacks the ability to reimport the exported dumps. 3 | This web page provides that functionality and is co-developed with the Chromium page. 4 | It also uses a [better library for graphs](http://www.highcharts.com/) that adds the ability to zoom into regions of interest. 5 | 6 | ## What do all these parameters mean? 7 | 8 | I teamed up with [Tsahi Levent-Levi](https://bloggeek.me/) to describe the parameters from webrtc-internals as a series of blog posts: 9 | * [Everything you wanted to know about webrtc-internals and getStats](https://bloggeek.me/webrtc-internals/) 10 | 11 | [See also the 2017 version of that](http://testrtc.com/webrtc-internals-parameters/). 12 | 13 | ## License 14 | MIT 15 | 16 | Note that the (awesome) Highcharts library used for plots may need a license. See http://shop.highsoft.com/faq/non-commercial 17 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import stylistic from '@stylistic/eslint-plugin' 2 | 3 | export default [ 4 | { 5 | plugins: { 6 | '@stylistic': stylistic 7 | }, 8 | rules: { 9 | '@stylistic/indent': ['error', 4], 10 | } 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /import-common.js: -------------------------------------------------------------------------------- 1 | const SDPUtils = window.adapter.sdp; 2 | 3 | function filterStatsGraphs(event, container) { 4 | const filter = event.target.value; 5 | const filters = filter.split(','); 6 | container.childNodes.forEach(node => { 7 | if (node.nodeName !== 'DETAILS') { 8 | return; 9 | } 10 | const statsType = node.attributes['data-statsType']; 11 | if (!filter || filters.includes(statsType) || 12 | filters.find(f => statsType.includes(f))) { 13 | node.style.display = 'block'; 14 | } else { 15 | node.style.display = 'none'; 16 | } 17 | }); 18 | } 19 | 20 | export function processDescriptionEvent(container, eventType, description, last_sections, remote_sections) { 21 | const {type, sdp} = description; 22 | const sections = SDPUtils.splitSections(sdp); 23 | container.innerText += ' (type: "' + type + '", ' + sections.length + ' sections)'; 24 | if (last_sections) { 25 | container.innerText += ' munged'; 26 | container.style.backgroundColor = '#FBCEB1'; 27 | } 28 | const copyBtn = document.createElement('button'); 29 | copyBtn.innerText = '\uD83D\uDCCB'; // clipboard 30 | copyBtn.className = 'copyBtn'; 31 | copyBtn.onclick = () => { 32 | navigator.clipboard.writeText(JSON.stringify({type, sdp})); 33 | }; 34 | container.appendChild(copyBtn); 35 | 36 | const el = document.createElement('pre'); 37 | sections.forEach((section, index) => { 38 | const lines = SDPUtils.splitLines(section); 39 | const mid = SDPUtils.getMid(section); 40 | const direction = SDPUtils.getDirection(section, sections[0]); 41 | 42 | const details = document.createElement('details'); 43 | // Fold by default for large SDP. 44 | details.open = sections.length < 10 && direction !== 'inactive'; 45 | details.innerText = section; 46 | 47 | const summary = document.createElement('summary'); 48 | summary.innerText = lines[0] + 49 | ' (' + (lines.length - 1) + ' more lines)' + 50 | (mid ? ' mid=' + mid : ''); 51 | if (lines[0].startsWith('m=')) { 52 | summary.innerText += ' direction=' + direction; 53 | const is_rejected = SDPUtils.parseMLine(lines[0]).port === 0; 54 | if (is_rejected) { 55 | summary.innerText += ' rejected'; 56 | const was_rejected = remote_sections && remote_sections[index] && 57 | SDPUtils.parseMLine(remote_sections[index]).port === 0; 58 | if (['createOffer', 'createAnswer', 'setLocalDescription'].includes(eventType)) { 59 | summary.style.backgroundColor = '#ddd'; 60 | } 61 | details.open = false; 62 | } 63 | if (last_sections && last_sections[index] !== sections[index]) { 64 | // Ignore triggering from simple reordering which is ok-ish. 65 | const last_lines = SDPUtils.splitLines(last_sections[index]).sort(); 66 | const current_lines = SDPUtils.splitLines(sections[index]).sort(); 67 | if (last_lines.findIndex((line, index) => line !== current_lines[index]) !== -1) { 68 | summary.innerText += ' munged'; 69 | summary.style.backgroundColor = '#FBCEB1'; 70 | details.open = true; 71 | } else { 72 | summary.innerText += ' reordered'; 73 | } 74 | } 75 | } 76 | details.appendChild(summary); 77 | el.appendChild(details); 78 | }); 79 | container.appendChild(el); 80 | } 81 | 82 | export function createContainers(connid, url, containers) { 83 | let el; 84 | const container = document.createElement('details'); 85 | container.open = true; 86 | container.style.margin = '10px'; 87 | 88 | const summary = document.createElement('summary'); 89 | summary.innerText = 'Connection:' + connid + ' URL: ' + url; 90 | container.appendChild(summary); 91 | 92 | const configuration = document.createElement('div'); 93 | container.appendChild(configuration); 94 | 95 | // show state transitions, like in https://webrtc.github.io/samples/src/content/peerconnection/states 96 | const signalingState = document.createElement('div'); 97 | signalingState.id = 'signalingstate_' + connid; 98 | signalingState.textContent = 'Signaling state:'; 99 | container.appendChild(signalingState); 100 | const iceConnectionState = document.createElement('div'); 101 | iceConnectionState.id = 'iceconnectionstate_' + connid; 102 | iceConnectionState.textContent = 'ICE connection state:'; 103 | container.appendChild(iceConnectionState); 104 | 105 | const connectionState = document.createElement('div'); 106 | connectionState.id = 'connectionstate_' + connid; 107 | connectionState.textContent = 'Connection state:'; 108 | container.appendChild(connectionState); 109 | 110 | const candidates = document.createElement('table'); 111 | candidates.className = 'candidatepairtable'; 112 | container.appendChild(candidates); 113 | 114 | const updateLog = document.createElement('table'); 115 | const head = document.createElement('tr'); 116 | updateLog.appendChild(head); 117 | 118 | el = document.createElement('th'); 119 | el.innerText = 'connection ' + connid; 120 | head.appendChild(el); 121 | 122 | el = document.createElement('th'); 123 | head.appendChild(el); 124 | 125 | container.appendChild(updateLog); 126 | 127 | const graphHeader = document.createElement('div'); 128 | const graphs = document.createElement('div'); 129 | 130 | const label = document.createElement('label'); 131 | label.innerText = 'Filter graphs by type including '; 132 | graphHeader.appendChild(label); 133 | const input = document.createElement('input'); 134 | input.placeholder = 'separate multiple values by `,`'; 135 | input.size = 25; 136 | input.oninput = (e) => filterStatsGraphs(e, graphs); 137 | graphHeader.appendChild(input); 138 | 139 | container.appendChild(graphHeader); 140 | container.appendChild(graphs); 141 | 142 | containers[connid] = { 143 | updateLog, 144 | iceConnectionState, 145 | connectionState, 146 | signalingState, 147 | candidates, 148 | url: summary, 149 | configuration, 150 | graphs, 151 | }; 152 | 153 | return container; 154 | } 155 | 156 | export function createCandidateTable(lastStats, parentElement) { 157 | const head = document.createElement('tr'); 158 | [ 159 | 'Transport id', 160 | 'Candidate pair id', 161 | 'Candidate id', 162 | '', // local/remote, leave empty 163 | 'type', 164 | 'address', 165 | 'port', 166 | 'protocol', 167 | 'priority / relayProtocol', 168 | 'interface', 169 | 'requestsSent / responsesReceived', 170 | 'requestsReceived / responsesSent', 171 | ].forEach((text) => { 172 | const el = document.createElement('td'); 173 | el.innerText = text; 174 | head.appendChild(el); 175 | }); 176 | parentElement.appendChild(head); 177 | 178 | for (let transportId in lastStats) { 179 | if (lastStats[transportId].type !== 'transport') continue; 180 | const transport = lastStats[transportId]; 181 | 182 | let row = document.createElement('tr'); 183 | let el = document.createElement('td'); 184 | el.innerText = transportId; 185 | row.appendChild(el); 186 | 187 | el = document.createElement('td'); 188 | el.innerText = transport.selectedCandidatePairId || '(none)'; 189 | row.appendChild(el); 190 | 191 | for (let i = 2; i < head.childElementCount; i++) { 192 | el = document.createElement('td'); 193 | row.appendChild(el); 194 | } 195 | 196 | parentElement.appendChild(row); 197 | 198 | for (let pairId in lastStats) { 199 | if (lastStats[pairId].type !== 'candidate-pair') continue; 200 | const pair = lastStats[pairId]; 201 | if (pair.transportId !== transportId) continue; 202 | row = document.createElement('tr'); 203 | 204 | row.appendChild(document.createElement('td')); 205 | 206 | el = document.createElement('td'); 207 | el.innerText = pairId; 208 | row.appendChild(el); 209 | 210 | parentElement.appendChild(row); 211 | for (let i = 2; i < head.childElementCount; i++) { 212 | el = document.createElement('td'); 213 | if (i === 8) { 214 | el.innerText = pair.priority; 215 | } else if (i === 10) { 216 | el.innerText = (pair.requestsSent + pair.consentRequestsSent) + ' / ' + pair.responsesReceived; 217 | if (pair.bytesSent) el.innerText += '\nPayload bytesSent=' + pair.bytesSent; 218 | } else if (i === 11) { 219 | el.innerText = pair.requestsReceived + ' / ' + pair.responsesSent; 220 | if (pair.bytesReceived) el.innerText += '\nPayload bytesReceived=' + pair.bytesReceived; 221 | } 222 | row.appendChild(el); 223 | } 224 | 225 | for (let candidateId in lastStats) { 226 | if (!['local-candidate', 'remote-candidate'].includes(lastStats[candidateId].type)) continue; 227 | if (!(candidateId === pair.localCandidateId || candidateId === pair.remoteCandidateId)) continue; 228 | const candidate = lastStats[candidateId]; 229 | row = document.createElement('tr'); 230 | 231 | row.appendChild(document.createElement('td')); 232 | row.appendChild(document.createElement('td')); 233 | el = document.createElement('td'); 234 | el.innerText = candidateId; 235 | row.appendChild(el); 236 | 237 | el = document.createElement('td'); 238 | el.innerText = candidate.isRemote ? 'remote' : 'local'; 239 | row.appendChild(el); 240 | 241 | el = document.createElement('td'); 242 | el.innerText = candidate.candidateType; 243 | row.appendChild(el); 244 | 245 | el = document.createElement('td'); 246 | el.innerText = candidate.address || candidate.ip; 247 | row.appendChild(el); 248 | 249 | el = document.createElement('td'); 250 | el.innerText = candidate.port; 251 | row.appendChild(el); 252 | 253 | el = document.createElement('td'); 254 | el.innerText = candidate.protocol; 255 | row.appendChild(el); 256 | 257 | el = document.createElement('td'); 258 | el.innerText = candidate.priority; 259 | if (candidate.relayProtocol) { 260 | el.innerText += ' ' + candidate.relayProtocol; 261 | } 262 | row.appendChild(el); 263 | 264 | el = document.createElement('td'); 265 | el.innerText = candidate.networkType || 'unknown'; 266 | row.appendChild(el); 267 | 268 | row.appendChild(document.createElement('td')); 269 | row.appendChild(document.createElement('td')); 270 | 271 | parentElement.appendChild(row); 272 | } 273 | } 274 | } 275 | } 276 | 277 | export function processGetUserMedia(data, parentElement) { 278 | const container = document.createElement('details'); 279 | container.open = true; 280 | container.style.margin = '10px'; 281 | 282 | const summary = document.createElement('summary'); 283 | summary.innerText = 'getUserMedia calls (' + (data.length / 2)+ ')'; 284 | container.appendChild(summary); 285 | 286 | const table = document.createElement('table'); 287 | const head = document.createElement('tr'); 288 | table.appendChild(head); 289 | 290 | container.appendChild(table); 291 | 292 | const columns = ['request_type', 'origin', 'pid', 'rid', 293 | 'audio', 'video', 'audio_track_info', 'video_track_info', 294 | 'error', 'error_message']; 295 | const displayNames = { 296 | request_id: 'id', 297 | reqest_type: 'type', 298 | audio: 'audio constraints', 299 | video: 'video constraints', 300 | audio_track_info: 'audio track', 301 | video_track_info: 'video track', 302 | error_message: 'error message', 303 | }; 304 | columns.forEach(name => { 305 | let el; 306 | el = document.createElement('th'); 307 | el.innerText = displayNames[name] || name; 308 | head.appendChild(el); 309 | }); 310 | 311 | parentElement.appendChild(container); 312 | data.forEach(event => { 313 | const id = ['gum-row', event.pid, event.rid, event.request_id].join('-'); 314 | if (!event.origin) { 315 | // Not a getUserMedia call but a response, update the row with the request. 316 | const existingRow = document.getElementById(id); 317 | if (event.error) { 318 | existingRow.childNodes[8].innerText = event.error; 319 | existingRow.childNodes[9].innerText = event.error_message; 320 | return; 321 | } 322 | if (event.audio_track_info) { 323 | existingRow.childNodes[6].innerText = event.audio_track_info; 324 | } 325 | if (event.video_track_info) { 326 | existingRow.childNodes[7].innerText = event.video_track_info; 327 | } 328 | return; 329 | } 330 | // Add a new row for the getUserMedia request. 331 | const row = document.createElement('tr'); 332 | row.id = id; 333 | columns.forEach(attribute => { 334 | const cell = document.createElement('td'); 335 | const el = document.createElement('pre'); 336 | if (['audio', 'video'].includes(attribute)) { 337 | el.innerText = event.hasOwnProperty(attribute) ? (event[attribute] || 'true') : 'not set'; 338 | } else { 339 | el.innerText = event.hasOwnProperty(attribute) ? event[attribute] : ''; 340 | } 341 | cell.appendChild(el); 342 | row.appendChild(cell); 343 | }); 344 | table.appendChild(row); 345 | }); 346 | } 347 | 348 | export function createGraphOptions(statsId, statsType, reports, referenceTime) { 349 | const series = []; 350 | series.statsType = statsType; 351 | const plotBands = []; 352 | const labels = { 353 | type: statsType, 354 | id: statsId, 355 | }; 356 | reports.sort().forEach(report => { 357 | const [name, data, statsType] = report; 358 | if (name === 'active' && statsType === 'outbound-rtp') { 359 | // set up a x-axis plotbands: 360 | // https://www.highcharts.com/docs/chart-concepts/plot-bands-and-plot-lines 361 | data.filter((el, index, values) => { 362 | return !(index > 0 && index < values.length - 1 && values[index - 1][1] == el[1]); 363 | }).forEach((item, index, values) => { 364 | if (item[1] === true) { 365 | return; 366 | } 367 | plotBands.push({ 368 | from: item[0], 369 | to: (values[index + 1] || [])[0], 370 | label: { 371 | align: 'center', 372 | text: 'sender disabled', 373 | }, 374 | }); 375 | }); 376 | return; 377 | } 378 | if (name === 'qualityLimitationReason' && statsType === 'outbound-rtp') { 379 | // set up a x-axis plotbands: 380 | // https://www.highcharts.com/docs/chart-concepts/plot-bands-and-plot-lines 381 | data.filter((el, index, values) => { 382 | return !(index > 0 && index < values.length - 1 && values[index - 1][1] == el[1]); 383 | }).forEach((item, index, values) => { 384 | if (item[1] === 'none') { 385 | return; 386 | } 387 | plotBands.push({ 388 | from: item[0], 389 | to: (values[index + 1] || [])[0], 390 | label: { 391 | align: 'center', 392 | text: item[1] + '-limited', 393 | }, 394 | }); 395 | }); 396 | return; 397 | } 398 | if (['encoderImplementation', 'decoderImplementation'].includes(name) && ['inbound-rtp', 'outbound-rtp'].includes(statsType)) { 399 | // set up a x-axis plotbands: 400 | // https://www.highcharts.com/docs/chart-concepts/plot-bands-and-plot-lines 401 | data.filter((el, index, values) => { 402 | return !(index > 0 && index < values.length - 1 && values[index - 1][1] == el[1]); 403 | }).forEach((item, index, values) => { 404 | plotBands.push({ 405 | from: item[0], 406 | to: (values[index + 1] || [])[0], 407 | label: { 408 | align: 'left', 409 | text: name + ': ' + item[1], 410 | }, 411 | color: index % 2 === 0 ? 'white' : 'rgba(253, 253, 222, 0.3)', 412 | }); 413 | }); 414 | return; 415 | } 416 | if (name === 'scalabilityMode' && statsType === 'outbound-rtp') { 417 | // set up a x-axis plotbands: 418 | // https://www.highcharts.com/docs/chart-concepts/plot-bands-and-plot-lines 419 | data.filter((el, index, values) => { 420 | return !(index > 0 && index < values.length - 1 && values[index - 1][1] == el[1]); 421 | }).forEach((item, index, values) => { 422 | plotBands.push({ 423 | from: item[0], 424 | to: (values[index + 1] || [])[0], 425 | label: { 426 | align: 'right', 427 | text: name + ': ' + item[1], 428 | y: 30, 429 | }, 430 | // This one is fully transparent (white with 100% alpha) since it overlaps with encoderImplementation. 431 | color: (255, 255, 255, 1), 432 | // But has a 1px border so it is possible to see changes unrelated to codec switches. 433 | borderWidth: 1, 434 | borderColor: 'rgba(189, 189, 189, 0.3)', 435 | }); 436 | }); 437 | return; 438 | } 439 | 440 | const statsForLabels = [ 441 | 'kind', 'mid', 'rid', 442 | 'ssrc', 'rtxSsrc', 'fecSsrc', 443 | 'encoderImplementation', 'decoderImplementation', 'scalabilityMode', 444 | 'scalabilityMode', '[codec]', 445 | 'label', // for datachannels 446 | ]; 447 | if (statsForLabels.includes(name)) { 448 | labels[name] = data[0][1]; 449 | } 450 | series.id = statsId; 451 | 452 | if (typeof(data[0][1]) !== 'number') return; 453 | const ignoredSeries = [ 454 | 'timestamp', 455 | 'protocol', 'dataChannelIdentifier', 456 | 'streamIdentifier', 'trackIdentifier', 457 | 'priority', 'port', 458 | 'ssrc', 'rtxSsrc', 'fecSsrc', 459 | 'mid', 'rid', 460 | ]; 461 | if (ignoredSeries.includes(name)) { 462 | return; 463 | } 464 | 465 | const hiddenSeries = [ 466 | 'bytesReceived', 'bytesSent', 467 | 'headerBytesReceived', 'headerBytesSent', 468 | 'packetsReceived', 'packetsSent', 469 | 'qpSum', 470 | 'framesEncoded', 'framesDecoded', 'totalEncodeTime', 471 | 'lastPacketReceivedTimestamp', 'lastPacketSentTimestamp', 472 | 'remoteTimestamp', 'estimatedPlayoutTimestamp', 473 | 'audioInputLevel', 'audioOutputLevel', 474 | 'totalSamplesDuration', 'totalSamplesReceived', 475 | 'jitterBufferEmittedCount', 476 | ]; 477 | const secondYAxis = [ 478 | // candidate-pair 479 | 'consentRequestsSent', 'requestsSent', 'requestsReceived', 'responsesSent', 'responsesReceived', 480 | // data-channel 481 | '[messagesReceived/s]', '[messagesSent/s]', 482 | // inbound-rtp 483 | '[framesReceived/s]', '[framesDecoded/s]', '[keyFramesDecoded/s]', 'frameWidth', 'frameHeight', 484 | // outbound-rtp' 485 | '[framesSent/s]', '[framesEncoded/s]', '[keyFramesEncoded/s]', 'frameWidth', 'frameHeight', 486 | ]; 487 | 488 | series.push({ 489 | name, 490 | data, 491 | visible: !hiddenSeries.includes(name), 492 | yAxis: secondYAxis.includes(name) ? 1 : 0, 493 | }); 494 | }); 495 | 496 | // Optionally start all graphs at the same point in time. 497 | if (referenceTime) { 498 | series 499 | .filter(s => s.data[0].length) 500 | .map(s => { 501 | if (s.data[0] !== referenceTime) { 502 | s.data.unshift([referenceTime, undefined]); 503 | } 504 | }); 505 | } 506 | 507 | // TODO: it would be nice to sort the graphs such that same mids go together. 508 | if (series.length === 0) { 509 | return; 510 | } 511 | return { 512 | title: { 513 | text: null 514 | }, 515 | xAxis: { 516 | type: 'datetime', 517 | plotBands, 518 | }, 519 | yAxis: [{ 520 | min: series.kind ? 0 : undefined 521 | }, 522 | { 523 | min: series.kind ? 0 : undefined 524 | }, 525 | ], 526 | chart: { 527 | zoomType: 'x', 528 | }, 529 | series, 530 | labels, 531 | }; 532 | } 533 | -------------------------------------------------------------------------------- /import-rtcstats.js: -------------------------------------------------------------------------------- 1 | let fileFormat; 2 | function doImport(evt) { 3 | evt.target.disabled = 'disabled'; 4 | const files = evt.target.files; 5 | const file = files[0]; 6 | const reader = new FileReader(); 7 | reader.onload = (function(file) { 8 | return function(e) { 9 | let result = e.target.result; 10 | if (typeof result === 'object') { 11 | result = pako.inflate(result, {to: 'string'}); 12 | } 13 | if (result.indexOf('\n') === -1) { 14 | // old format v0 15 | console.error('Not a supported format, maybe webrtc-internals?'); 16 | return; 17 | } 18 | 19 | const baseStats = {}; 20 | const lines = result.split('\n'); 21 | // The first line must be a JSON object with metadata. 22 | console.log(lines[0]); 23 | const theLog = JSON.parse(lines.shift()); 24 | fileFormat = theLog.fileFormat; 25 | theLog.peerConnections = {}; 26 | theLog.getUserMedia = []; 27 | lines.forEach(line => { 28 | if (!line.length) { 29 | return; // Ignore empty lines. 30 | } 31 | const data = JSON.parse(line); 32 | if (!Array.isArray(data) || data.length !== 4) { 33 | console.log('Unsupported line', line); 34 | return; 35 | } 36 | let [method, connection_id, value, time] = data; 37 | time = new Date(time); 38 | switch(method) { 39 | case 'getUserMedia': 40 | case 'getUserMediaOnSuccess': 41 | case 'getUserMediaOnFailure': 42 | case 'navigator.mediaDevices.getUserMedia': 43 | case 'navigator.mediaDevices.getUserMediaOnSuccess': 44 | case 'navigator.mediaDevices.getUserMediaOnFailure': 45 | case 'navigator.mediaDevices.getDisplayMedia': 46 | case 'navigator.mediaDevices.getDisplayMediaOnSuccess': 47 | case 'navigator.mediaDevices.getDisplayMediaOnFailure': 48 | theLog.getUserMedia.push({ 49 | time, 50 | type: method, 51 | value, 52 | }); 53 | break; 54 | default: 55 | if (!theLog.peerConnections[connection_id]) { 56 | theLog.peerConnections[connection_id] = []; 57 | baseStats[connection_id] = {}; 58 | } 59 | if (method === 'getstats') { // delta-compressed stats 60 | value = decompress(baseStats[connection_id], value); 61 | baseStats[connection_id] = JSON.parse(JSON.stringify(value)); 62 | } 63 | theLog.peerConnections[connection_id].push({ 64 | time, 65 | type: method, 66 | value, 67 | }); 68 | break; 69 | } 70 | }); 71 | importUpdatesAndStats(theLog); 72 | }; 73 | })(file); 74 | if (file.type === 'application/gzip') { 75 | reader.readAsArrayBuffer(files[0]); 76 | } else { 77 | reader.readAsText(files[0]); 78 | } 79 | } 80 | 81 | function createContainers(connid, url) { 82 | let el; 83 | const container = document.createElement('details'); 84 | container.open = true; 85 | container.style.margin = '10px'; 86 | 87 | let summary = document.createElement('summary'); 88 | summary.innerText = 'Connection:' + connid + ' URL: ' + url; 89 | container.appendChild(summary); 90 | 91 | let signalingState; 92 | let iceConnectionState; 93 | let connectionState; 94 | if (connid !== 'null') { 95 | // show state transitions, like in https://webrtc.github.io/samples/src/content/peerconnection/states 96 | signalingState = document.createElement('div'); 97 | signalingState.id = 'signalingstate_' + connid; 98 | signalingState.textContent = 'Signaling state:'; 99 | container.appendChild(signalingState); 100 | 101 | iceConnectionState = document.createElement('div'); 102 | iceConnectionState.id = 'iceconnectionstate_' + connid; 103 | iceConnectionState.textContent = 'ICE connection state:'; 104 | container.appendChild(iceConnectionState); 105 | 106 | connectionState = document.createElement('div'); 107 | connectionState.id = 'connectionstate_' + connid; 108 | connectionState.textContent = 'Connection state:'; 109 | container.appendChild(connectionState); 110 | } 111 | 112 | let candidates; 113 | if (connid !== 'null') { 114 | // for ice candidates 115 | const iceContainer = document.createElement('details'); 116 | iceContainer.open = true; 117 | summary = document.createElement('summary'); 118 | summary.innerText = 'ICE candidate grid'; 119 | iceContainer.appendChild(summary); 120 | 121 | candidates = document.createElement('table'); 122 | candidates.className = 'candidatepairtable'; 123 | const head = document.createElement('tr'); 124 | candidates.appendChild(head); 125 | 126 | el = document.createElement('td'); 127 | el.innerText = 'Local address'; 128 | head.appendChild(el); 129 | 130 | el = document.createElement('td'); 131 | el.innerText = 'Local type'; 132 | head.appendChild(el); 133 | 134 | el = document.createElement('td'); 135 | el.innerText = 'Remote address'; 136 | head.appendChild(el); 137 | 138 | el = document.createElement('td'); 139 | el.innerText = 'Remote type'; 140 | head.appendChild(el); 141 | 142 | el = document.createElement('td'); 143 | el.innerText = 'Requests sent'; 144 | head.appendChild(el); 145 | 146 | el = document.createElement('td'); 147 | el.innerText = 'Responses received'; 148 | head.appendChild(el); 149 | 150 | el = document.createElement('td'); 151 | el.innerText = 'Requests received'; 152 | head.appendChild(el); 153 | 154 | el = document.createElement('td'); 155 | el.innerText = 'Responses sent'; 156 | head.appendChild(el); 157 | 158 | el = document.createElement('td'); 159 | el.innerText = 'Active Connection'; 160 | head.appendChild(el); 161 | 162 | iceContainer.appendChild(candidates); 163 | container.appendChild(iceContainer); 164 | } 165 | 166 | const updateLogContainer = document.createElement('details'); 167 | updateLogContainer.open = true; 168 | container.appendChild(updateLogContainer); 169 | 170 | summary = document.createElement('summary'); 171 | summary.innerText = 'PeerConnection updates:'; 172 | updateLogContainer.appendChild(summary); 173 | 174 | const updateLog = document.createElement('table'); 175 | updateLogContainer.appendChild(updateLog); 176 | 177 | const graphs = document.createElement('div'); 178 | container.appendChild(graphs); 179 | 180 | containers[connid] = { 181 | updateLog, 182 | iceConnectionState, 183 | connectionState, 184 | signalingState, 185 | candidates, 186 | graphs, 187 | }; 188 | 189 | return container; 190 | } 191 | 192 | function processGUM(data) { 193 | const container = document.createElement('details'); 194 | container.open = true; 195 | container.style.margin = '10px'; 196 | 197 | const summary = document.createElement('summary'); 198 | summary.innerText = 'getUserMedia calls'; 199 | container.appendChild(summary); 200 | 201 | const table = document.createElement('table'); 202 | const head = document.createElement('tr'); 203 | table.appendChild(head); 204 | 205 | let el; 206 | el = document.createElement('th'); 207 | el.innerText = 'getUserMedia'; 208 | head.appendChild(el); 209 | 210 | container.appendChild(table); 211 | 212 | document.getElementById('tables').appendChild(container); 213 | data.forEach(event => { 214 | processTraceEvent(table, event); // abusing the peerconnection trace event processor... 215 | }); 216 | } 217 | 218 | function processTraceEvent(table, event) { 219 | const row = document.createElement('tr'); 220 | let el = document.createElement('td'); 221 | el.setAttribute('nowrap', ''); 222 | el.innerText = event.time; 223 | row.appendChild(el); 224 | 225 | // recreate the HTML of webrtc-internals 226 | const details = document.createElement('details'); 227 | el = document.createElement('summary'); 228 | el.innerText = event.type; 229 | details.appendChild(el); 230 | 231 | el = document.createElement('pre'); 232 | if (['createOfferOnSuccess', 'createAnswerOnSuccess', 'setRemoteDescription', 'setLocalDescription'].indexOf(event.type) !== -1) { 233 | el.innerText = 'SDP ' + event.value.type + ':' + event.value.sdp; 234 | } else { 235 | el.innerText = JSON.stringify(event.value, null, ' '); 236 | } 237 | details.appendChild(el); 238 | 239 | el = document.createElement('td'); 240 | el.appendChild(details); 241 | 242 | row.appendChild(el); 243 | 244 | // guess what, if the event type contains 'Failure' one could use css to highlight it 245 | if (event.type.indexOf('Failure') !== -1) { 246 | row.style.backgroundColor = 'red'; 247 | } 248 | if (event.type === 'iceConnectionStateChange') { 249 | switch(event.value) { 250 | case 'ICEConnectionStateConnected': 251 | case 'ICEConnectionStateCompleted': 252 | row.style.backgroundColor = 'green'; 253 | break; 254 | case 'ICEConnectionStateFailed': 255 | row.style.backgroundColor = 'red'; 256 | break; 257 | } 258 | } 259 | 260 | if (event.type === 'onIceCandidate' || event.type === 'addIceCandidate') { 261 | if (event.value && event.value.candidate) { 262 | const parts = event.value.candidate.trim().split(' '); 263 | if (parts && parts.length >= 9 && parts[7] === 'typ') { 264 | details.classList.add(parts[8]); 265 | } 266 | } 267 | } 268 | table.appendChild(row); 269 | } 270 | 271 | const graphs = {}; 272 | const containers = {}; 273 | function processConnections(connectionIds, data) { 274 | const connid = connectionIds.shift(); 275 | if (!connid) return; 276 | window.setTimeout(processConnections, 0, connectionIds, data); 277 | 278 | let reportname, statname; 279 | const connection = data.peerConnections[connid]; 280 | const container = createContainers(connid, data.url); 281 | document.getElementById('tables').appendChild(container); 282 | 283 | for (let i = 0; i < connection.length; i++) { 284 | if (connection[i].type !== 'getStats' && connection[i].type !== 'getstats') { 285 | processTraceEvent(containers[connid].updateLog, connection[i]); 286 | } 287 | } 288 | 289 | // then, update the stats displays 290 | const series = {}; 291 | let connectedOrCompleted = false; 292 | let firstStats; 293 | let lastStats; 294 | for (let i = 0; i < connection.length; i++) { 295 | if (connection[i].type === 'oniceconnectionstatechange' && (connection[i].value === 'connected' || connection[i].value === 'completed')) { 296 | connectedOrCompleted = true; 297 | } 298 | if (connection[i].type === 'getStats' || connection[i].type === 'getstats') { 299 | const stats = connection[i].value; 300 | Object.keys(stats).forEach(id => { 301 | if (stats[id].type === 'localcandidate' || stats[id].type === 'remotecandidate') return; 302 | Object.keys(stats[id]).forEach(name => { 303 | if (name === 'timestamp') return; 304 | //if (name === 'googMinPlayoutDelayMs') stats[id][name] = parseInt(stats[id][name], 10); 305 | if (stats[id].type === 'ssrc' && !isNaN(parseFloat(stats[id][name]))) { 306 | stats[id][name] = parseFloat(stats[id][name]); 307 | } 308 | if (stats[id].type === 'ssrc' && name === 'ssrc') return; // ignore ssrc on ssrc reports. 309 | if (typeof stats[id][name] === 'number') { 310 | if (!series[id]) { 311 | series[id] = {}; 312 | series[id].type = stats[id].type; 313 | } 314 | if (!series[id][name]) { 315 | series[id][name] = []; 316 | } else { 317 | const lastTime = series[id][name][series[id][name].length - 1][0]; 318 | if (lastTime && stats[id].timestamp && stats[id].timestamp - lastTime > 20000) { 319 | series[id][name].push([stats[id].timestamp || new Date(connection[i].time).getTime(), null]); 320 | } 321 | } 322 | if (fileFormat >= 2) { 323 | series[id][name].push([stats[id].timestamp, stats[id][name]]); 324 | } else { 325 | series[id][name].push([new Date(connection[i].time).getTime(), stats[id][name]]); 326 | } 327 | } 328 | }); 329 | }); 330 | } 331 | if (connection[i].type === 'getStats' || connection[i].type === 'getstats') { 332 | if (!firstStats && connectedOrCompleted) firstStats = connection[i].value; 333 | lastStats = connection[i].value; 334 | } 335 | } 336 | const interestingStats = lastStats; // might be last stats which contain more counters 337 | if (interestingStats) { 338 | const stun = []; 339 | let t; 340 | for (reportname in interestingStats) { 341 | if (reportname.indexOf('Conn-') === 0) { 342 | t = reportname.split('-'); 343 | comp = t.pop(); 344 | t = t.join('-'); 345 | stats = interestingStats[reportname]; 346 | stun.push(stats); 347 | } 348 | } 349 | for (t in stun) { 350 | const row = document.createElement('tr'); 351 | let el; 352 | 353 | el = document.createElement('td'); 354 | el.innerText = stun[t].googLocalAddress; 355 | row.appendChild(el); 356 | 357 | el = document.createElement('td'); 358 | el.innerText = stun[t].googLocalCandidateType; 359 | row.appendChild(el); 360 | 361 | el = document.createElement('td'); 362 | el.innerText = stun[t].googRemoteAddress; 363 | row.appendChild(el); 364 | 365 | el = document.createElement('td'); 366 | el.innerText = stun[t].googRemoteCandidateType; 367 | row.appendChild(el); 368 | 369 | el = document.createElement('td'); 370 | el.innerText = stun[t].requestsSent; 371 | row.appendChild(el); 372 | 373 | el = document.createElement('td'); 374 | el.innerText = stun[t].responsesReceived; 375 | row.appendChild(el); 376 | 377 | el = document.createElement('td'); 378 | el.innerText = stun[t].requestsReceived; 379 | row.appendChild(el); 380 | 381 | el = document.createElement('td'); 382 | el.innerText = stun[t].responsesSent; 383 | row.appendChild(el); 384 | 385 | el = document.createElement('td'); 386 | el.innerText = stun[t].googActiveConnection; 387 | row.appendChild(el); 388 | /* 389 | el = document.createElement('td'); 390 | el.innerText = stun[t].consentRequestsSent; 391 | row.appendChild(el); 392 | */ 393 | 394 | containers[connid].candidates.appendChild(row); 395 | } 396 | } 397 | 398 | const graphTypes = {}; 399 | const graphSelectorContainer = document.createElement('div'); 400 | containers[connid].graphs.appendChild(graphSelectorContainer); 401 | 402 | graphs[connid] = {}; 403 | const reportobj = {}; 404 | for (reportname in series) { 405 | const graphType = series[reportname].type; 406 | graphTypes[graphType] = true; 407 | 408 | const container = document.createElement('details'); 409 | container.open = true; 410 | container.classList.add('webrtc-' + graphType); 411 | containers[connid].graphs.appendChild(container); 412 | 413 | const title = connid + ' type=' + graphType + ' ' + reportname; 414 | 415 | const summary = document.createElement('summary'); 416 | summary.innerText = title; 417 | container.appendChild(summary); 418 | 419 | const chartContainer = document.createElement('div'); 420 | chartContainer.id = 'chart_' + Date.now(); 421 | container.appendChild(chartContainer); 422 | 423 | const da = []; 424 | Object.keys(series[reportname]).forEach(name => { 425 | if (name === 'type') return; 426 | da.push({ 427 | name: name, 428 | data: series[reportname][name] 429 | }); 430 | }); 431 | const graph = new Highcharts.Chart({ 432 | title: { 433 | text: title 434 | }, 435 | xAxis: { 436 | type: 'datetime' 437 | }, 438 | /* 439 | yAxis: { 440 | min: 0 441 | }, 442 | */ 443 | chart: { 444 | zoomType: 'x', 445 | renderTo : chartContainer.id 446 | }, 447 | series: da 448 | }); 449 | graphs[connid][reportname] = graph; 450 | 451 | // draw checkbox to turn off everything 452 | ((reportname, container, graph) => { 453 | container.ontoggle = () => container.open && graph.reflow(); 454 | const checkbox = document.createElement('input'); 455 | checkbox.type = 'checkbox'; 456 | container.appendChild(checkbox); 457 | const label = document.createElement('label'); 458 | label.innerText = 'Turn on/off all data series in ' + connid + ' ' + reportname; 459 | container.appendChild(label); 460 | checkbox.onchange = function() { 461 | graph.series.forEach(series => { 462 | series.setVisible(!checkbox.checked, false); 463 | }); 464 | graph.redraw(); 465 | }; 466 | })(reportname, container, graph); 467 | } 468 | 469 | Object.keys(graphTypes).forEach(type => { 470 | const checkbox = document.createElement('input'); 471 | checkbox.type = 'checkbox'; 472 | checkbox.checked = true; 473 | graphSelectorContainer.appendChild(checkbox); 474 | 475 | const label = document.createElement('label'); 476 | label.innerText = 'Toggle graphs for type=' + type; 477 | graphSelectorContainer.appendChild(label); 478 | 479 | const selector = '.webrtc-' + type; 480 | checkbox.onchange = function() { 481 | containers[connid].graphs.querySelectorAll(selector).forEach(el => { 482 | el.open = checkbox.checked; 483 | }); 484 | }; 485 | }); 486 | } 487 | 488 | function importUpdatesAndStats(data) { 489 | document.getElementById('userAgent').innerText = data.userAgent; 490 | processGUM(data.getUserMedia); 491 | window.setTimeout(processConnections, 0, Object.keys(data.peerConnections), data); 492 | } 493 | -------------------------------------------------------------------------------- /import.js: -------------------------------------------------------------------------------- 1 | import {createContainers, processGetUserMedia, createCandidateTable, processDescriptionEvent, createGraphOptions} from './import-common.js'; 2 | 3 | const SDPUtils = window.adapter.sdp; 4 | 5 | export class WebRTCInternalsDumpImporter { 6 | constructor() { 7 | this.graphs = {}; 8 | this.containers = {}; 9 | } 10 | 11 | process(blob) { 12 | this.data = JSON.parse(blob); 13 | this.processGetUserMedia(); 14 | this.importUpdatesAndStats(); 15 | } 16 | 17 | processGetUserMedia() { 18 | // FIXME: also display GUM calls (can they be correlated to addStream?) 19 | processGetUserMedia(this.data.getUserMedia, document.getElementById('tables')); 20 | } 21 | 22 | importUpdatesAndStats() { 23 | if (this.data.UserAgentData && this.data.UserAgentData.length >= 2) { 24 | document.getElementById('userAgent').innerText += 25 | this.data.UserAgentData[2].brand + ' ' + 26 | this.data.UserAgentData[1].version + ' / ' ; 27 | } 28 | document.getElementById('userAgent').innerText += this.data.UserAgent; 29 | 30 | for (let connectionId in this.data.PeerConnections) { 31 | const container = createContainers(connectionId, this.data.PeerConnections[connectionId].url, this.containers); 32 | document.getElementById('tables').appendChild(container); 33 | } 34 | for (let connectionId in this.data.PeerConnections) { 35 | const connection = this.data.PeerConnections[connectionId]; 36 | let legacy = false; 37 | for (let reportname in connection.stats) { 38 | if (reportname.startsWith('Conn-')) { 39 | legacy = true; 40 | break; 41 | } 42 | } 43 | if (legacy) { 44 | document.getElementById('legacy').style.display = 'block'; 45 | } 46 | } 47 | setTimeout(this.processConnections.bind(this), 0, Object.keys(this.data.PeerConnections)); 48 | } 49 | 50 | processConnections(connectionIds) { 51 | const connectionId = connectionIds.shift(); 52 | if (!connectionId) return; 53 | setTimeout(this.processConnections.bind(this), 0, connectionIds) 54 | 55 | const connection = this.data.PeerConnections[connectionId]; 56 | const container = this.containers[connectionId]; 57 | 58 | // Display the updateLog 59 | this.containers[connectionId].url.innerText = 'Origin: ' + connection.url; 60 | this.containers[connectionId].configuration.innerText = 'Configuration: ' + JSON.stringify(connection.rtcConfiguration, null, ' ') + '\n'; 61 | this.containers[connectionId].configuration.innerText += 'Legacy (chrome) constraints: ' + JSON.stringify(connection.constraints, null, ' '); 62 | 63 | const state = {}; 64 | connection.updateLog.forEach(traceEvent => { 65 | const row = this.processTraceEvent(traceEvent, state); 66 | if (row) { 67 | this.containers[connectionId].updateLog.appendChild(row); 68 | } 69 | if (traceEvent.type === 'createOfferOnSuccess') { 70 | state.lastCreatedOffer = traceEvent.value; 71 | } else if (traceEvent.type === 'createAnswerOnSuccess') { 72 | state.lastCreatedAnswer = traceEvent.value; 73 | } else if (traceEvent.type === 'setLocalDescription') { 74 | state.lastCreatedOffer = undefined; 75 | state.lastCreatedAnswer = undefined; 76 | } else if (traceEvent.type === 'setRemoteDescription') { 77 | state.lastRemoteDescription = traceEvent.value; 78 | } else if (traceEvent.type == 'signalingstatechange' && traceEvent.value === 'stable') { 79 | state.lastRemoteDescription = undefined; 80 | } 81 | }); 82 | connection.updateLog.forEach(traceEvent => { 83 | // update state displays 84 | if (traceEvent.type === 'iceconnectionstatechange') { 85 | this.containers[connectionId].iceConnectionState.textContent += ' => ' + traceEvent.value; 86 | } 87 | if (traceEvent.type === 'connectionstatechange') { 88 | this.containers[connectionId].connectionState.textContent += ' => ' + traceEvent.value; 89 | } 90 | }); 91 | connection.updateLog.forEach(traceEvent => { 92 | // FIXME: would be cool if a click on this would jump to the table row 93 | if (traceEvent.type === 'signalingstatechange') { 94 | this.containers[connectionId].signalingState.textContent += ' => ' + traceEvent.value; 95 | } 96 | }); 97 | 98 | const referenceTime = document.getElementById('useReferenceTime').checked && connection.updateLog.length 99 | ? new Date(connection.updateLog[0].time).getTime() 100 | : undefined; 101 | this.graphs[connectionId] = {}; 102 | 103 | const reportobj = createInternalsTimeSeries(connection); 104 | if (reportobj) { 105 | const lastStats = {}; 106 | for (let id in reportobj) { 107 | const report = reportobj[id]; 108 | const lastReport = {type: report.type}; 109 | Object.keys(report).forEach(property => { 110 | if (!Array.isArray(report[property])) return; 111 | const [key, values] = report[property]; 112 | lastReport[key] = values[values.length - 1][1]; 113 | }); 114 | lastStats[id] = lastReport; 115 | } 116 | createCandidateTable(lastStats, this.containers[connectionId].candidates); 117 | } 118 | 119 | Object.keys(reportobj).forEach(reportname => { 120 | const reports = reportobj[reportname]; 121 | const statsType = reports.type; 122 | // ignore useless graphs 123 | if (['local-candidate', 'remote-candidate', 'codec', 'stream', 'track'].includes(statsType)) return; 124 | 125 | const graphOptions = createGraphOptions(reportname, statsType, reports, referenceTime); 126 | if (!graphOptions) { 127 | return; 128 | } 129 | 130 | const container = document.createElement('details'); 131 | if (graphOptions.series.statsType) { 132 | container.attributes['data-statsType'] = graphOptions.series.statsType; 133 | } 134 | this.containers[connectionId].graphs.appendChild(container); 135 | // TODO: keep in sync with 136 | // https://source.chromium.org/chromium/chromium/src/+/main:content/browser/webrtc/resources/stats_helper.js 137 | const title = [ 138 | 'type', 'kind', 139 | 'ssrc', 'rtxSsrc', 'fecSsrc', 140 | 'mid', 'rid', 141 | 'label', 142 | '[codec]', 143 | 'encoderImplementation', 'decoderImplementation', 144 | 'trackIdentifier', 145 | 'id', 146 | ].filter(key => graphOptions.labels[key] !== undefined) 147 | .map(key => { 148 | return ({statsType: 'type', trackIdentifier: 'track'}[key] || key) + '=' + JSON.stringify(graphOptions.labels[key]); 149 | }).join(', '); 150 | 151 | const titleElement = document.createElement('summary'); 152 | titleElement.innerText = title; 153 | container.appendChild(titleElement); 154 | 155 | const d = document.createElement('div'); 156 | d.id = 'chart_' + Date.now(); 157 | d.classList.add('graph'); 158 | container.appendChild(d); 159 | 160 | const graph = new Highcharts.Chart(d, graphOptions); 161 | this.graphs[connectionId][reportname] = graph; 162 | 163 | // expand the graph when opening 164 | container.ontoggle = () => container.open && graph.reflow(); 165 | 166 | // draw checkbox to turn off everything 167 | ((reportname, container, graph) => { 168 | const checkbox = document.createElement('input'); 169 | checkbox.type = 'checkbox'; 170 | container.appendChild(checkbox); 171 | const label = document.createElement('label'); 172 | label.innerText = 'Turn on/off all data series' 173 | container.appendChild(label); 174 | checkbox.onchange = function() { 175 | graph.series.forEach(series => { 176 | series.setVisible(!checkbox.checked, false); 177 | }); 178 | graph.redraw(); 179 | }; 180 | })(reportname, container, graph); 181 | }); 182 | } 183 | 184 | processTraceEvent(traceEvent, state) { 185 | const row = document.createElement('tr'); 186 | let el = document.createElement('td'); 187 | el.setAttribute('nowrap', ''); 188 | el.innerText = traceEvent.time; 189 | row.appendChild(el); 190 | 191 | // recreate the HTML of webrtc-internals 192 | const details = document.createElement('details'); 193 | el = document.createElement('summary'); 194 | el.innerText = traceEvent.type; 195 | details.appendChild(el); 196 | 197 | if (['іcecandidate', 'addIceCandidate'].includes(traceEvent.type)) { 198 | if (traceEvent.value) { 199 | const parts = traceEvent.value.split(', ') 200 | .map(part => part.split(': ')); 201 | const toShow = []; 202 | parts.forEach(part => { 203 | if (['sdpMid', 'sdpMLineIndex'].includes(part[0])) { 204 | toShow.push(part.join(': ')); 205 | } else if (part[0] === 'candidate') { 206 | const candidate = SDPUtils.parseCandidate(part[1].trim()); 207 | if (candidate) { 208 | toShow.push('port:' + candidate.port); 209 | toShow.push('type: ' + candidate.type); 210 | } 211 | } else if (part[0] === 'relayProtocol') { 212 | toShow.push('relayProtocol: ' + part[1]); 213 | } 214 | }); 215 | el.innerText += ' (' + toShow.join(', ') + ')'; 216 | } 217 | } 218 | if (traceEvent.value.indexOf(', sdp: ') != -1) { 219 | const [type, sdp] = traceEvent.value.substr(6).split(', sdp: '); 220 | let last_sections; 221 | let remote_sections; 222 | if (traceEvent.type === 'setLocalDescription') { 223 | const [last_type, last_sdp] = (type === 'offer' ? state.lastCreatedOffer : state.lastCreatedAnswer) 224 | .substr(6).split(', sdp: '); 225 | if (sdp != last_sdp) { 226 | last_sections = SDPUtils.splitSections(last_sdp); 227 | } 228 | if (state.remoteDescription) { 229 | const [remote_type, remote_sdp] = state.remoteDescription.substr(6).split(', sdp: '); 230 | remote_sections = SDPUtils.splitSections(remote_sdp); 231 | } 232 | } 233 | processDescriptionEvent(el, traceEvent.type, {type, sdp}, last_sections, remote_sections); 234 | } else { 235 | el = document.createElement('pre'); 236 | el.innerText = traceEvent.value; 237 | } 238 | details.appendChild(el); 239 | el = document.createElement('td'); 240 | if (traceEvent.value !== '') { 241 | el.appendChild(details); 242 | } else { 243 | el.innerText = traceEvent.type; 244 | } 245 | row.appendChild(el); 246 | 247 | // If the traceEvent type ends with 'Failure' hightlight it 248 | if (traceEvent.type.endsWith('Failure')) { 249 | row.style.backgroundColor = 'red'; 250 | } 251 | // Likewise, highlight (ice)connectionstates. 252 | if (['iceconnectionstatechange', 'connectionstatechange'].includes(traceEvent.type)) { 253 | switch(traceEvent.value) { 254 | case 'connected': 255 | case 'completed': 256 | row.style.backgroundColor = 'green'; 257 | break; 258 | case 'failed': 259 | row.style.backgroundColor = 'red'; 260 | break; 261 | } 262 | } 263 | return row; 264 | } 265 | } 266 | 267 | function createInternalsTimeSeries(connection) { 268 | const reportobj = {}; 269 | for (let reportname in connection.stats) { 270 | if (reportname.startsWith('Conn-')) { 271 | return {}; // legacy stats, no longer supported. Warning is shown above. 272 | } 273 | } 274 | for (let reportname in connection.stats) { 275 | // special casing of computed stats, in particular [a-b] 276 | let stat; 277 | let comp; 278 | if (reportname.indexOf('[') !== -1) { 279 | const t = reportname.split('['); 280 | comp = '[' + t.pop(); 281 | stat = t.join(''); 282 | stat = stat.substr(0, stat.length - 1); 283 | } else { 284 | const t = reportname.split('-'); 285 | comp = t.pop(); 286 | stat = t.join('-'); 287 | } 288 | 289 | if (!reportobj.hasOwnProperty(stat)) { 290 | reportobj[stat] = []; 291 | reportobj[stat].type = connection.stats[reportname].statsType; 292 | reportobj[stat].startTime = new Date(connection.stats[reportname].startTime).getTime(); 293 | reportobj[stat].endTime = new Date(connection.stats[reportname].endTime).getTime(); 294 | } 295 | let values = JSON.parse(connection.stats[reportname].values); 296 | // Individual timestamps were added in crbug.com/1462567 in M117. 297 | if (connection.stats[stat + '-timestamp']) { 298 | const timestamps = JSON.parse(connection.stats[stat + '-timestamp'].values); 299 | values = values.map((currentValue, index) => [timestamps[index], currentValue]); 300 | } else { 301 | // Fallback to the assumption that stats were gathered every second. 302 | values = values.map((currentValue, index) => [reportobj[stat].startTime + 1000 * index, currentValue]); 303 | } 304 | reportobj[stat].push([comp, values]); 305 | } 306 | return reportobj; 307 | } 308 | 309 | 310 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Import webrtc-internals dumps 5 | 8 | 9 | 10 | 11 | 45 | 46 | 47 |

This is an import tool for dumps from chrome://webrtc-internals. See this blog post for a lengthy description of what it does and how to interpret some of the data. 48 |

49 | 50 | 51 |
52 | 53 |
54 | 59 |
60 | User Agent: 61 |
62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import {WebRTCInternalsDumpImporter} from './import.js'; 2 | 3 | document.getElementById('import').onchange = function(evt) { 4 | evt.target.disabled = 'disabled'; 5 | const files = evt.target.files; 6 | const file = files[0]; 7 | const reader = new FileReader(); 8 | reader.onload = (function(file) { 9 | return function(e) { 10 | let result = e.target.result; 11 | if (typeof result === 'object') { 12 | result = pako.inflate(result, {to: 'string'}); 13 | } 14 | window.importer = new WebRTCInternalsDumpImporter(); 15 | importer.process(result); 16 | }; 17 | })(file); 18 | if (file.type === 'application/gzip') { 19 | reader.readAsArrayBuffer(files[0]); 20 | } else { 21 | reader.readAsText(files[0]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "devDependencies": { 4 | "@stylistic/eslint-plugin": "^4.2.0", 5 | "@stylistic/eslint-plugin-js": "^4.2.0" 6 | }, 7 | "scripts": { 8 | "test": "eslint *.js" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /rtcstats-utils.js: -------------------------------------------------------------------------------- 1 | function decompress(baseStats, newStats) { 2 | const timestamp = newStats.timestamp 3 | delete newStats.timestamp; 4 | Object.keys(newStats).forEach(id => { 5 | if (!baseStats[id]) { 6 | if (newStats[id].timestamp === 0) { 7 | newStats[id].timestamp = timestamp; 8 | } 9 | baseStats[id] = newStats[id]; 10 | } else { 11 | const report = newStats[id]; 12 | if (report.timestamp === 0) { 13 | report.timestamp = timestamp; 14 | } else if (!report.timestamp) { 15 | report.timestamp = new Date(baseStats[id].timestamp).getTime(); 16 | } 17 | Object.keys(report).forEach(name => { 18 | baseStats[id][name] = report[name]; 19 | }); 20 | } 21 | }); 22 | return baseStats; 23 | } 24 | -------------------------------------------------------------------------------- /rtcstats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Import rtcstats dumps 5 | 8 | 9 | 10 | 11 | 12 | 52 | 53 | 54 |
55 |
User Agent:
56 |
57 |
58 |
59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /videoreplay-config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generate a video replay config from SDP 5 | 12 | 13 | 75 | 76 | 77 | 78 |
79 | 80 |
81 | 82 | 83 | 84 | --------------------------------------------------------------------------------