blob: 4a16fdf2f0f673a3852725f71d6566b08d9c40d9 [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var USER_MEDIA_TAB_ID = 'user-media-tab-id';
const OPTION_GETSTATS_STANDARD = 'Standardized (promise-based) getStats() API';
const OPTION_GETSTATS_LEGACY =
'Legacy Non-Standard (callback-based) getStats() API';
let currentGetStatsMethod = OPTION_GETSTATS_STANDARD;
var tabView = null;
var ssrcInfoManager = null;
var peerConnectionUpdateTable = null;
var statsTable = null;
var dumpCreator = null;
/** A map from peer connection id to the PeerConnectionRecord. */
var peerConnectionDataStore = {};
/** A list of getUserMedia requests. */
var userMediaRequests = [];
/** Maps from id (see getPeerConnectionId) to StatsRatesCalculator. */
statsRatesCalculatorById = new Map();
/** A simple class to store the updates and stats data for a peer connection. */
var PeerConnectionRecord = (function() {
/** @constructor */
function PeerConnectionRecord() {
/** @private */
this.record_ = {
constraints: {},
rtcConfiguration: [],
stats: {},
updateLog: [],
url: '',
};
}
PeerConnectionRecord.prototype = {
/** @override */
toJSON: function() {
return this.record_;
},
/**
* Adds the initilization info of the peer connection.
* @param {string} url The URL of the web page owning the peer connection.
* @param {Array} rtcConfiguration
* @param {!Object} constraints Media constraints.
*/
initialize: function(url, rtcConfiguration, constraints) {
this.record_.url = url;
this.record_.rtcConfiguration = rtcConfiguration;
this.record_.constraints = constraints;
},
resetStats: function() {
this.record_.stats = {};
},
/**
* @param {string} dataSeriesId The TimelineDataSeries identifier.
* @return {!TimelineDataSeries}
*/
getDataSeries: function(dataSeriesId) {
return this.record_.stats[dataSeriesId];
},
/**
* @param {string} dataSeriesId The TimelineDataSeries identifier.
* @param {!TimelineDataSeries} dataSeries The TimelineDataSeries to set to.
*/
setDataSeries: function(dataSeriesId, dataSeries) {
this.record_.stats[dataSeriesId] = dataSeries;
},
/**
* @param {!Object} update The object contains keys "time", "type", and
* "value".
*/
addUpdate: function(update) {
var time = new Date(parseFloat(update.time));
this.record_.updateLog.push({
time: time.toLocaleString(),
type: update.type,
value: update.value,
});
},
};
return PeerConnectionRecord;
})();
// The maximum number of data points bufferred for each stats. Old data points
// will be shifted out when the buffer is full.
var MAX_STATS_DATA_POINT_BUFFER_SIZE = 1000;
// <include src="tab_view.js">
// <include src="data_series.js">
// <include src="ssrc_info_manager.js">
// <include src="stats_graph_helper.js">
// <include src="stats_rates_calculator.js">
// <include src="stats_table.js">
// <include src="peer_connection_update_table.js">
// <include src="dump_creator.js">
function initialize() {
dumpCreator = new DumpCreator($('content-root'));
$('content-root').appendChild(createStatsSelectionOptionElements());
tabView = new TabView($('content-root'));
ssrcInfoManager = new SsrcInfoManager();
peerConnectionUpdateTable = new PeerConnectionUpdateTable();
statsTable = new StatsTable(ssrcInfoManager);
chrome.send('finishedDOMLoad');
// Requests stats from all peer connections every second.
window.setInterval(requestStats, 1000);
}
document.addEventListener('DOMContentLoaded', initialize);
function createStatsSelectionOptionElements() {
const p = document.createElement('p');
const selectElement = document.createElement('select');
selectElement.onchange = () => {
currentGetStatsMethod = selectElement.value;
Object.keys(peerConnectionDataStore).forEach(id => {
const peerConnectionElement = $(id);
statsTable.nukeStatsLists(peerConnectionElement);
removeStatsReportGraphs(peerConnectionElement);
peerConnectionDataStore[id].resetStats();
});
};
[OPTION_GETSTATS_STANDARD, OPTION_GETSTATS_LEGACY].forEach(option => {
const optionElement = document.createElement('option');
optionElement.setAttribute('value', option);
optionElement.appendChild(document.createTextNode(option));
selectElement.appendChild(optionElement);
});
selectElement.value = currentGetStatsMethod;
p.appendChild(document.createTextNode('Read Stats From: '));
p.appendChild(selectElement);
return p;
}
function requestStats() {
if (currentGetStatsMethod == OPTION_GETSTATS_STANDARD) {
requestStandardStats();
} else if (currentGetStatsMethod == OPTION_GETSTATS_LEGACY) {
requestLegacyStats();
}
}
/**
* Sends a request to the browser to get peer connection statistics from the
* standard getStats() API (promise-based).
*/
function requestStandardStats() {
if (Object.keys(peerConnectionDataStore).length > 0) {
chrome.send('getStandardStats');
}
}
/**
* Sends a request to the browser to get peer connection statistics from the
* legacy getStats() API (callback-based non-standard API with goog-stats).
*/
function requestLegacyStats() {
if (Object.keys(peerConnectionDataStore).length > 0) {
chrome.send('getLegacyStats');
}
}
/**
* A helper function for getting a peer connection element id.
*
* @param {!Object<number>} data The object containing the pid and lid of the
* peer connection.
* @return {string} The peer connection element id.
*/
function getPeerConnectionId(data) {
return data.pid + '-' + data.lid;
}
/**
* Extracts ssrc info from a setLocal/setRemoteDescription update.
*
* @param {!PeerConnectionUpdateEntry} data The peer connection update data.
*/
function extractSsrcInfo(data) {
if (data.type == 'setLocalDescription' ||
data.type == 'setRemoteDescription') {
ssrcInfoManager.addSsrcStreamInfo(data.value);
}
}
/**
* A helper function for appending a child element to |parent|.
*
* @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) {
var child = document.createElement(tag);
child.textContent = text;
parent.appendChild(child);
return child;
}
/**
* Helper for adding a peer connection update.
*
* @param {Element} peerConnectionElement
* @param {!PeerConnectionUpdateEntry} update The peer connection update data.
*/
function addPeerConnectionUpdate(peerConnectionElement, update) {
peerConnectionUpdateTable.addPeerConnectionUpdate(
peerConnectionElement, update);
extractSsrcInfo(update);
peerConnectionDataStore[peerConnectionElement.id].addUpdate(update);
}
/** Browser message handlers. */
/**
* Removes all information about a peer connection.
*
* @param {!Object<number>} data The object containing the pid and lid of a peer
* connection.
*/
function removePeerConnection(data) {
var element = $(getPeerConnectionId(data));
if (element) {
delete peerConnectionDataStore[element.id];
tabView.removeTab(element.id);
}
}
/**
* Adds a peer connection.
*
* @param {!Object} data The object containing the pid, lid, url,
* rtcConfiguration, and constraints of a peer connection.
*/
function addPeerConnection(data) {
var id = getPeerConnectionId(data);
if (!peerConnectionDataStore[id]) {
peerConnectionDataStore[id] = new PeerConnectionRecord();
}
peerConnectionDataStore[id].initialize(
data.url, data.rtcConfiguration, data.constraints);
var peerConnectionElement = $(id);
if (!peerConnectionElement) {
peerConnectionElement = tabView.addTab(id, data.url + ' [' + id + ']');
}
var p = document.createElement('p');
p.textContent =
data.url + ', ' + data.rtcConfiguration + ', ' + data.constraints;
peerConnectionElement.appendChild(p);
return peerConnectionElement;
}
/**
* Adds a peer connection update.
*
* @param {!PeerConnectionUpdateEntry} data The peer connection update data.
*/
function updatePeerConnection(data) {
var peerConnectionElement = $(getPeerConnectionId(data));
addPeerConnectionUpdate(peerConnectionElement, data);
}
/**
* Adds the information of all peer connections created so far.
*
* @param {Array<!Object>} data An array of the information of all peer
* connections. Each array item contains pid, lid, url, rtcConfiguration,
* constraints, and an array of updates as the log.
*/
function updateAllPeerConnections(data) {
for (var i = 0; i < data.length; ++i) {
var peerConnection = addPeerConnection(data[i]);
var log = data[i].log;
if (!log) {
continue;
}
for (var j = 0; j < log.length; ++j) {
addPeerConnectionUpdate(peerConnection, log[j]);
}
}
requestStats();
}
/**
* Handles the report of stats originating from the standard getStats() API.
*
* @param {!Object} data The object containing pid, lid, and reports, where
* reports is an array of stats reports. Each report contains id, type,
* and stats, where stats is the object containing timestamp and values,
* which is an array of strings, whose even index entry is the name of the
* stat, and the odd index entry is the value.
*/
function addStandardStats(data) {
if (currentGetStatsMethod != OPTION_GETSTATS_STANDARD) {
return; // Obsolete!
}
var peerConnectionElement = $(getPeerConnectionId(data));
if (!peerConnectionElement) {
return;
}
const pcId = getPeerConnectionId(data);
let statsRatesCalculator = statsRatesCalculatorById.get(pcId);
if (!statsRatesCalculator) {
statsRatesCalculator = new StatsRatesCalculator();
statsRatesCalculatorById.set(pcId, statsRatesCalculator);
}
const r = StatsReport.fromInternalsReportList(data.reports);
statsRatesCalculator.addStatsReport(r);
data.reports = statsRatesCalculator.currentReport.toInternalsReportList();
for (var i = 0; i < data.reports.length; ++i) {
var report = data.reports[i];
statsTable.addStatsReport(peerConnectionElement, report);
drawSingleReport(peerConnectionElement, report, false);
}
}
/**
* Handles the report of stats originating from the legacy getStats() API.
*
* @param {!Object} data The object containing pid, lid, and reports, where
* reports is an array of stats reports. Each report contains id, type,
* and stats, where stats is the object containing timestamp and values,
* which is an array of strings, whose even index entry is the name of the
* stat, and the odd index entry is the value.
*/
function addLegacyStats(data) {
var peerConnectionElement = $(getPeerConnectionId(data));
if (!peerConnectionElement) {
return;
}
for (var i = 0; i < data.reports.length; ++i) {
var report = data.reports[i];
statsTable.addStatsReport(peerConnectionElement, report);
drawSingleReport(peerConnectionElement, report, true);
}
}
/**
* Adds a getUserMedia request.
*
* @param {!Object} data The object containing rid {number}, pid {number},
* origin {string}, audio {string}, video {string}.
*/
function addGetUserMedia(data) {
userMediaRequests.push(data);
if (!$(USER_MEDIA_TAB_ID)) {
tabView.addTab(USER_MEDIA_TAB_ID, 'GetUserMedia Requests');
}
var requestDiv = document.createElement('div');
requestDiv.className = 'user-media-request-div-class';
requestDiv.rid = data.rid;
$(USER_MEDIA_TAB_ID).appendChild(requestDiv);
appendChildWithText(requestDiv, 'div', 'Caller origin: ' + data.origin);
appendChildWithText(requestDiv, 'div', 'Caller process id: ' + data.pid);
appendChildWithText(requestDiv, 'div', 'Time: ' + (new Date(data.timestamp)));
appendChildWithText(requestDiv, 'span', 'Audio Constraints')
.style.fontWeight = 'bold';
appendChildWithText(requestDiv, 'div', data.audio);
appendChildWithText(requestDiv, 'span', 'Video Constraints')
.style.fontWeight = 'bold';
appendChildWithText(requestDiv, 'div', data.video);
}
/**
* Removes the getUserMedia requests from the specified |rid|.
*
* @param {!Object} data The object containing rid {number}, the render id.
*/
function removeGetUserMediaForRenderer(data) {
for (var i = userMediaRequests.length - 1; i >= 0; --i) {
if (userMediaRequests[i].rid == data.rid) {
userMediaRequests.splice(i, 1);
}
}
var requests = $(USER_MEDIA_TAB_ID).childNodes;
for (var i = 0; i < requests.length; ++i) {
if (requests[i].rid == data.rid) {
$(USER_MEDIA_TAB_ID).removeChild(requests[i]);
}
}
if ($(USER_MEDIA_TAB_ID).childNodes.length == 0) {
tabView.removeTab(USER_MEDIA_TAB_ID);
}
}
/**
* Notification that the audio debug recordings file selection dialog was
* cancelled, i.e. recordings have not been enabled.
*/
function audioDebugRecordingsFileSelectionCancelled() {
dumpCreator.clearAudioDebugRecordingsCheckbox();
}
/**
* Notification that the event log recordings file selection dialog was
* cancelled, i.e. recordings have not been enabled.
*/
function eventLogRecordingsFileSelectionCancelled() {
dumpCreator.clearEventLogRecordingsCheckbox();
}
/**
* Notification that audio debug recordings are enabled. Used e.g. on page load
* to update the UI to reflect the recording state.
*/
function setAudioDebugRecordingsEnabled() {
dumpCreator.setAudioDebugRecordingsCheckbox();
}
/**
* Notification that event log recordings are enabled. Used e.g. on page load
* to update the UI to reflect the recording state.
*/
function setEventLogRecordingsEnabled() {
dumpCreator.setEventLogRecordingsCheckbox();
}
/**
* Notification that event log recordings may be turned off/on by the user.
* Used e.g. on page load to update the UI to reflect the recording state's
* mutability.
*/
function setEventLogRecordingsToggleability(isToggleable) {
dumpCreator.setEventLogRecordingsCheckboxMutability(isToggleable);
}