264 lines
9.1 KiB
JavaScript
264 lines
9.1 KiB
JavaScript
|
// Derived from Chromium WebRTC Internals Dashboard - see Acknowledgements for full license details
|
||
|
|
||
|
import {$} from './util.js';
|
||
|
|
||
|
const MAX_NUMBER_OF_STATE_CHANGES_DISPLAYED = 10;
|
||
|
const MAX_NUMBER_OF_EXPANDED_MEDIASECTIONS = 10;
|
||
|
/**
|
||
|
* The data of a peer connection update.
|
||
|
* @param {number} pid The id of the renderer.
|
||
|
* @param {number} lid The id of the peer conneciton inside a renderer.
|
||
|
* @param {string} type The type of the update.
|
||
|
* @param {string} value The details of the update.
|
||
|
*/
|
||
|
class PeerConnectionUpdateEntry {
|
||
|
constructor(pid, lid, type, value) {
|
||
|
/**
|
||
|
* @type {number}
|
||
|
*/
|
||
|
this.pid = pid;
|
||
|
|
||
|
/**
|
||
|
* @type {number}
|
||
|
*/
|
||
|
this.lid = lid;
|
||
|
|
||
|
/**
|
||
|
* @type {string}
|
||
|
*/
|
||
|
this.type = type;
|
||
|
|
||
|
/**
|
||
|
* @type {string}
|
||
|
*/
|
||
|
this.value = value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Maintains the peer connection update log table.
|
||
|
*/
|
||
|
export class PeerConnectionUpdateTable {
|
||
|
constructor() {
|
||
|
/**
|
||
|
* @type {string}
|
||
|
* @const
|
||
|
* @private
|
||
|
*/
|
||
|
this.UPDATE_LOG_ID_SUFFIX_ = '-update-log';
|
||
|
|
||
|
/**
|
||
|
* @type {string}
|
||
|
* @const
|
||
|
* @private
|
||
|
*/
|
||
|
this.UPDATE_LOG_CONTAINER_CLASS_ = 'update-log-container';
|
||
|
|
||
|
/**
|
||
|
* @type {string}
|
||
|
* @const
|
||
|
* @private
|
||
|
*/
|
||
|
this.UPDATE_LOG_TABLE_CLASS = 'update-log-table';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds the update to the update table as a new row. The type of the update
|
||
|
* is set to the summary of the cell; clicking the cell will reveal or hide
|
||
|
* the details as the content of a TextArea element.
|
||
|
*
|
||
|
* @param {!Element} peerConnectionElement The root element.
|
||
|
* @param {!PeerConnectionUpdateEntry} update The update to add.
|
||
|
*/
|
||
|
addPeerConnectionUpdate(peerConnectionElement, update) {
|
||
|
const tableElement = this.ensureUpdateContainer_(peerConnectionElement);
|
||
|
|
||
|
const row = document.createElement('tr');
|
||
|
tableElement.firstChild.appendChild(row);
|
||
|
|
||
|
const time = new Date(parseFloat(update.time));
|
||
|
const timeItem = document.createElement('td');
|
||
|
timeItem.textContent = time.toLocaleString();
|
||
|
row.appendChild(timeItem);
|
||
|
|
||
|
// `type` is a display variant of update.type which does not get serialized
|
||
|
// into the JSON dump.
|
||
|
let type = update.type;
|
||
|
|
||
|
if (update.value.length === 0) {
|
||
|
const typeItem = document.createElement('td');
|
||
|
typeItem.textContent = type;
|
||
|
row.appendChild(typeItem);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (update.type === 'icecandidate' || update.type === 'addIceCandidate') {
|
||
|
const parts = update.value.split(', ');
|
||
|
type += '(' + parts[0] + ', ' + parts[1]; // show sdpMid/sdpMLineIndex.
|
||
|
const candidateParts = parts[2].substr(11).split(' ');
|
||
|
if (candidateParts && candidateParts[7]) { // show candidate type.
|
||
|
type += ', type: ' + candidateParts[7];
|
||
|
}
|
||
|
type += ')';
|
||
|
} else if (
|
||
|
update.type === 'createOfferOnSuccess' ||
|
||
|
update.type === 'createAnswerOnSuccess') {
|
||
|
this.setLastOfferAnswer_(tableElement, update);
|
||
|
} else if (update.type === 'setLocalDescription') {
|
||
|
const lastOfferAnswer = this.getLastOfferAnswer_(tableElement);
|
||
|
if (update.value.startsWith('type: rollback')) {
|
||
|
this.setLastOfferAnswer_(tableElement, {value: undefined})
|
||
|
} else if (lastOfferAnswer && update.value !== lastOfferAnswer) {
|
||
|
type += ' (munged)';
|
||
|
}
|
||
|
} else if (update.type === 'setConfiguration') {
|
||
|
// Update the configuration that is displayed at the top.
|
||
|
peerConnectionElement.firstChild.children[2].textContent = update.value;
|
||
|
} else if (['transceiverAdded',
|
||
|
'transceiverModified'].includes(update.type)) {
|
||
|
// Show the transceiver index.
|
||
|
const indexLine = update.value.split('\n', 3)[2];
|
||
|
if (indexLine.startsWith('getTransceivers()[')) {
|
||
|
type += ' ' + indexLine.substring(17, indexLine.length - 2);
|
||
|
}
|
||
|
const kindLine = update.value.split('\n', 5)[4].trim();
|
||
|
if (kindLine.startsWith('kind:')) {
|
||
|
type += ', ' + kindLine.substring(6, kindLine.length - 2);
|
||
|
}
|
||
|
} else if (['iceconnectionstatechange', 'connectionstatechange',
|
||
|
'signalingstatechange'].includes(update.type)) {
|
||
|
const fieldName = {
|
||
|
'iceconnectionstatechange' : 'iceconnectionstate',
|
||
|
'connectionstatechange' : 'connectionstate',
|
||
|
'signalingstatechange' : 'signalingstate',
|
||
|
}[update.type];
|
||
|
const el = peerConnectionElement.getElementsByClassName(fieldName)[0];
|
||
|
const numberOfEvents = el.textContent.split(' => ').length;
|
||
|
if (numberOfEvents < MAX_NUMBER_OF_STATE_CHANGES_DISPLAYED) {
|
||
|
el.textContent += ' => ' + update.value;
|
||
|
} else if (numberOfEvents >= MAX_NUMBER_OF_STATE_CHANGES_DISPLAYED) {
|
||
|
el.textContent += ' => ...';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const summaryItem = $('summary-template').content.cloneNode(true);
|
||
|
const summary = summaryItem.querySelector('summary');
|
||
|
summary.textContent = type;
|
||
|
row.appendChild(summaryItem);
|
||
|
|
||
|
const valueContainer = document.createElement('pre');
|
||
|
const details = row.cells[1].childNodes[0];
|
||
|
details.appendChild(valueContainer);
|
||
|
|
||
|
// Highlight ICE/DTLS failures and failure callbacks.
|
||
|
if ((update.type === 'iceconnectionstatechange' &&
|
||
|
update.value === 'failed') ||
|
||
|
(update.type === 'connectionstatechange' &&
|
||
|
update.value === 'failed') ||
|
||
|
update.type.indexOf('OnFailure') !== -1 ||
|
||
|
update.type === 'addIceCandidateFailed') {
|
||
|
valueContainer.parentElement.classList.add('update-log-failure');
|
||
|
}
|
||
|
|
||
|
// RTCSessionDescription is serialized as 'type: <type>, sdp:'
|
||
|
if (update.value.indexOf(', sdp:') !== -1) {
|
||
|
const [type, sdp] = update.value.substr(6).split(', sdp: ');
|
||
|
if (type === 'rollback') {
|
||
|
// Rollback has no SDP.
|
||
|
summary.textContent += ' (type: "rollback")';
|
||
|
} else {
|
||
|
// Create a copy-to-clipboard button.
|
||
|
const copyBtn = document.createElement('button');
|
||
|
copyBtn.textContent = 'Copy description to clipboard';
|
||
|
copyBtn.onclick = () => {
|
||
|
navigator.clipboard.writeText(JSON.stringify({type, sdp}));
|
||
|
};
|
||
|
valueContainer.appendChild(copyBtn);
|
||
|
|
||
|
// Fold the SDP sections.
|
||
|
const sections = sdp.split('\nm=')
|
||
|
.map((part, index) => (index > 0 ?
|
||
|
'm=' + part : part).trim() + '\r\n');
|
||
|
summary.textContent +=
|
||
|
' (type: "' + type + '", ' + sections.length + ' sections)';
|
||
|
sections.forEach(section => {
|
||
|
const lines = section.trim().split('\n');
|
||
|
// Extract the mid attribute.
|
||
|
const mid = lines
|
||
|
.filter(line => line.startsWith('a=mid:'))
|
||
|
.map(line => line.substr(6))[0];
|
||
|
const sectionDetails = document.createElement('details');
|
||
|
// Fold by default for large SDP.
|
||
|
sectionDetails.open =
|
||
|
sections.length <= MAX_NUMBER_OF_EXPANDED_MEDIASECTIONS;
|
||
|
sectionDetails.textContent = lines.slice(1).join('\n');
|
||
|
|
||
|
const sectionSummary = document.createElement('summary');
|
||
|
sectionSummary.textContent =
|
||
|
lines[0].trim() +
|
||
|
' (' + (lines.length - 1) + ' more lines)' +
|
||
|
(mid ? ' mid=' + mid : '');
|
||
|
sectionDetails.appendChild(sectionSummary);
|
||
|
|
||
|
valueContainer.appendChild(sectionDetails);
|
||
|
});
|
||
|
}
|
||
|
} else {
|
||
|
valueContainer.textContent = update.value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Makes sure the update log table of the peer connection is created.
|
||
|
*
|
||
|
* @param {!Element} peerConnectionElement The root element.
|
||
|
* @return {!Element} The log table element.
|
||
|
* @private
|
||
|
*/
|
||
|
ensureUpdateContainer_(peerConnectionElement) {
|
||
|
const tableId = peerConnectionElement.id + this.UPDATE_LOG_ID_SUFFIX_;
|
||
|
|
||
|
// Disable getElementById restriction here, since |tableId| is not always
|
||
|
// a valid selector.
|
||
|
// eslint-disable-next-line no-restricted-properties
|
||
|
let tableElement = document.getElementById(tableId);
|
||
|
if (!tableElement) {
|
||
|
const tableContainer = document.createElement('div');
|
||
|
tableContainer.className = this.UPDATE_LOG_CONTAINER_CLASS_;
|
||
|
peerConnectionElement.appendChild(tableContainer);
|
||
|
|
||
|
tableElement = document.createElement('table');
|
||
|
tableElement.className = this.UPDATE_LOG_TABLE_CLASS;
|
||
|
tableElement.id = tableId;
|
||
|
tableElement.border = 1;
|
||
|
tableContainer.appendChild(tableElement);
|
||
|
tableElement.appendChild(
|
||
|
$('time-event-template').content.cloneNode(true));
|
||
|
}
|
||
|
return tableElement;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Store the last createOfferOnSuccess/createAnswerOnSuccess to compare to
|
||
|
* setLocalDescription and visualize SDP munging.
|
||
|
*
|
||
|
* @param {!Element} tableElement The peerconnection update element.
|
||
|
* @param {!PeerConnectionUpdateEntry} update The update to add.
|
||
|
* @private
|
||
|
*/
|
||
|
setLastOfferAnswer_(tableElement, update) {
|
||
|
tableElement['data-lastofferanswer'] = update.value;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the last createOfferOnSuccess/createAnswerOnSuccess to compare
|
||
|
* to setLocalDescription and visualize SDP munging.
|
||
|
*
|
||
|
* @param {!Element} tableElement The peerconnection update element.
|
||
|
* @private
|
||
|
*/
|
||
|
getLastOfferAnswer_(tableElement) {
|
||
|
return tableElement['data-lastofferanswer'];
|
||
|
}
|
||
|
}
|