aaWa)a&a&aX'ag(am*aK;aP~aӧaaYaa a1aaapaawaaVa~mar}aKa Accessibility

Accessibility

Global accessibility mode:
/* * 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. */ body { font-family: Arial, sans-serif; font-size: 12px; margin: 10px; min-width: 47em; padding-bottom: 65px; } img { float: left; height: 16px; padding-right: 5px; width: 16px; } .row { border-bottom: 1px solid #A0A0A0; padding: 5px; } .url { color: #A0A0A0; } // 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. cr.define('accessibility', function() { 'use strict'; // Keep in sync with view_message_enums.h var AccessibilityModeFlag = { Platform: 1 << 0, FullTree: 1 << 1 } var AccessibilityMode = { Off: 0, Complete: AccessibilityModeFlag.Platform | AccessibilityModeFlag.FullTree, EditableTextOnly: AccessibilityModeFlag.Platform, TreeOnly: AccessibilityModeFlag.FullTree } function isAccessibilityComplete(mode) { return ((mode & AccessibilityMode.Complete) == AccessibilityMode.Complete); } function requestData() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'targets-data.json', false); xhr.send(null); if (xhr.status === 200) { console.log(xhr.responseText); return JSON.parse(xhr.responseText); } return []; } // TODO(aboxhall): add a mechanism to request individual and global a11y // mode, xhr them on toggle... or just re-requestData and be smarter about // ID-ing rows? function toggleAccessibility(data, element) { chrome.send('toggleAccessibility', [String(data.processId), String(data.routeId)]); var a11y_was_on = (element.textContent.match(/on/) != null); element.textContent = ' accessibility ' + (a11y_was_on ? ' off' : ' on'); var row = element.parentElement; if (a11y_was_on) { while (row.lastChild != element) row.removeChild(row.lastChild); } else { row.appendChild(document.createTextNode(' | ')); row.appendChild(createShowAccessibilityTreeElement(data, row, false)); } } function requestAccessibilityTree(data, element) { chrome.send('requestAccessibilityTree', [String(data.processId), String(data.routeId)]); } function toggleGlobalAccessibility() { chrome.send('toggleGlobalAccessibility'); document.location.reload(); // FIXME see TODO above } function initialize() { console.log('initialize'); var data = requestData(); addGlobalAccessibilityModeToggle(data['global_a11y_mode']); $('pages').textContent = ''; var list = data['list']; for (var i = 0; i < list.length; i++) { addToPagesList(list[i]); } } function addGlobalAccessibilityModeToggle(global_a11y_mode) { var full_a11y_on = isAccessibilityComplete(global_a11y_mode); $('toggle_global').textContent = (full_a11y_on ? 'on' : 'off'); $('toggle_global').setAttribute('aria-pressed', (full_a11y_on ? 'true' : 'false')); $('toggle_global').addEventListener('click', toggleGlobalAccessibility); } function addToPagesList(data) { // TODO: iterate through data and pages rows instead var id = data['processId'] + '.' + data['routeId']; var row = document.createElement('div'); row.className = 'row'; row.id = id; formatRow(row, data); row.processId = data.processId; row.routeId = data.routeId; var list = $('pages'); list.appendChild(row); } function formatRow(row, data) { if (!('url' in data)) { if ('error' in data) { row.appendChild(createErrorMessageElement(data, row)); return; } } var properties = ['favicon_url', 'name', 'url']; for (var j = 0; j < properties.length; j++) row.appendChild(formatValue(data, properties[j])); row.appendChild(createToggleAccessibilityElement(data)); if (isAccessibilityComplete(data['a11y_mode'])) { row.appendChild(document.createTextNode(' | ')); if ('tree' in data) { row.appendChild(createShowAccessibilityTreeElement(data, row, true)); row.appendChild(document.createTextNode(' | ')); row.appendChild(createHideAccessibilityTreeElement(row.id)); row.appendChild(createAccessibilityTreeElement(data)); } else { row.appendChild(createShowAccessibilityTreeElement(data, row, false)); if ('error' in data) row.appendChild(createErrorMessageElement(data, row)); } } } function formatValue(data, property) { var value = data[property]; if (property == 'favicon_url') { var faviconElement = document.createElement('img'); if (value) faviconElement.src = value; faviconElement.alt = ""; return faviconElement; } var text = value ? String(value) : ''; if (text.length > 100) text = text.substring(0, 100) + '\u2026'; // ellipsis var span = document.createElement('span'); span.textContent = ' ' + text + ' '; span.className = property; return span; } function createToggleAccessibilityElement(data) { var link = document.createElement('a', 'action-link'); link.setAttribute('role', 'button'); var full_a11y_on = isAccessibilityComplete(data['a11y_mode']); link.textContent = 'accessibility ' + (full_a11y_on ? 'on' : 'off'); link.setAttribute('aria-pressed', (full_a11y_on ? 'true' : 'false')); link.addEventListener('click', toggleAccessibility.bind(this, data, link)); return link; } function createShowAccessibilityTreeElement(data, row, opt_refresh) { var link = document.createElement('a', 'action-link'); link.setAttribute('role', 'button'); if (opt_refresh) link.textContent = 'refresh accessibility tree'; else link.textContent = 'show accessibility tree'; link.id = row.id + ':showTree'; link.addEventListener('click', requestAccessibilityTree.bind(this, data, link)); return link; } function createHideAccessibilityTreeElement(id) { var link = document.createElement('a', 'action-link'); link.setAttribute('role', 'button'); link.textContent = 'hide accessibility tree'; link.addEventListener('click', function() { $(id + ':showTree').textContent = 'show accessibility tree'; var existingTreeElements = $(id).getElementsByTagName('pre'); for (var i = 0; i < existingTreeElements.length; i++) $(id).removeChild(existingTreeElements[i]); var row = $(id); while (row.lastChild != $(id + ':showTree')) row.removeChild(row.lastChild); }); return link; } function createErrorMessageElement(data) { var errorMessageElement = document.createElement('div'); var errorMessage = data.error; errorMessageElement.innerHTML = errorMessage + ' '; var closeLink = document.createElement('a'); closeLink.href='#'; closeLink.textContent = '[close]'; closeLink.addEventListener('click', function() { var parentElement = errorMessageElement.parentElement; parentElement.removeChild(errorMessageElement); if (parentElement.childElementCount == 0) parentElement.parentElement.removeChild(parentElement); }); errorMessageElement.appendChild(closeLink); return errorMessageElement; } function showTree(data) { var id = data.processId + '.' + data.routeId; var row = $(id); if (!row) return; row.textContent = ''; formatRow(row, data); } function createAccessibilityTreeElement(data) { var treeElement = document.createElement('pre'); var tree = data.tree; treeElement.textContent = tree; return treeElement; } return { initialize: initialize, showTree: showTree }; }); document.addEventListener('DOMContentLoaded', accessibility.initialize); PNG  IHDRaRIDATx^SA 07;:usKHjtAa%81d/T8|!姜?S:[üIENDB`PNG  IHDR g PLTElZ tRNS@fkIDATxM #dFrB8&I 7ЏcU[?Me=P11v)+Q:\z`֜9 䚚c>B|LOU`pW^}\㾺VnФaQDXΑ%4aOضP6\"}KߞbJgIENDB`

Graphics Feature Status

Driver Bug Workarounds

Problems Detected

Version Information

Driver Information

Diagnostics

... loading ...
None

Log Messages

  • :
title value
// Copyright (c) 2011 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. // Copyright (c) 2011 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. cr.define('gpu', function() { /** * This class provides a 'bridge' for communicating between javascript and the * browser. When run outside of WebUI, e.g. as a regular webpage, it provides * synthetic data to assist in testing. * @constructor */ function BrowserBridge() { // If we are not running inside WebUI, output chrome.send messages // to the console to help with quick-iteration debugging. this.debugMode_ = (chrome.send === undefined && console.log); if (this.debugMode_) { var browserBridgeTests = document.createElement('script'); browserBridgeTests.src = './gpu_internals/browser_bridge_tests.js'; document.body.appendChild(browserBridgeTests); } this.nextRequestId_ = 0; this.pendingCallbacks_ = []; this.logMessages_ = []; // Tell c++ code that we are ready to receive GPU Info. if (!this.debugMode_) { chrome.send('browserBridgeInitialized'); this.beginRequestClientInfo_(); this.beginRequestLogMessages_(); } } BrowserBridge.prototype = { __proto__: cr.EventTarget.prototype, applySimulatedData_: function applySimulatedData(data) { // set up things according to the simulated data this.gpuInfo_ = data.gpuInfo; this.clientInfo_ = data.clientInfo; this.logMessages_ = data.logMessages; cr.dispatchSimpleEvent(this, 'gpuInfoUpdate'); cr.dispatchSimpleEvent(this, 'clientInfoChange'); cr.dispatchSimpleEvent(this, 'logMessagesChange'); }, /** * Returns true if the page is hosted inside Chrome WebUI * Helps have behavior conditional to emulate_webui.py */ get debugMode() { return this.debugMode_; }, /** * Sends a message to the browser with specified args. The * browser will reply asynchronously via the provided callback. */ callAsync: function(submessage, args, callback) { var requestId = this.nextRequestId_; this.nextRequestId_ += 1; this.pendingCallbacks_[requestId] = callback; if (!args) { chrome.send('callAsync', [requestId.toString(), submessage]); } else { var allArgs = [requestId.toString(), submessage].concat(args); chrome.send('callAsync', allArgs); } }, /** * Called by gpu c++ code when client info is ready. */ onCallAsyncReply: function(requestId, args) { if (this.pendingCallbacks_[requestId] === undefined) { throw new Error('requestId ' + requestId + ' is not pending'); } var callback = this.pendingCallbacks_[requestId]; callback(args); delete this.pendingCallbacks_[requestId]; }, /** * Get gpuInfo data. */ get gpuInfo() { return this.gpuInfo_; }, /** * Called from gpu c++ code when GPU Info is updated. */ onGpuInfoUpdate: function(gpuInfo) { this.gpuInfo_ = gpuInfo; cr.dispatchSimpleEvent(this, 'gpuInfoUpdate'); }, /** * This function begins a request for the ClientInfo. If it comes back * as undefined, then we will issue the request again in 250ms. */ beginRequestClientInfo_: function() { this.callAsync('requestClientInfo', undefined, (function(data) { if (data === undefined) { // try again in 250 ms window.setTimeout(this.beginRequestClientInfo_.bind(this), 250); } else { this.clientInfo_ = data; cr.dispatchSimpleEvent(this, 'clientInfoChange'); } }).bind(this)); }, /** * Returns information about the currently running Chrome build. */ get clientInfo() { return this.clientInfo_; }, /** * This function checks for new GPU_LOG messages. * If any are found, a refresh is triggered. */ beginRequestLogMessages_: function() { this.callAsync('requestLogMessages', undefined, (function(messages) { if (messages.length != this.logMessages_.length) { this.logMessages_ = messages; cr.dispatchSimpleEvent(this, 'logMessagesChange'); } // check again in 250 ms window.setTimeout(this.beginRequestLogMessages_.bind(this), 250); }).bind(this)); }, /** * Returns an array of log messages issued by the GPU process, if any. */ get logMessages() { return this.logMessages_; }, }; return { BrowserBridge: BrowserBridge }; }); // Copyright (c) 2012 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. /** * @fileoverview This view displays information on the current GPU * hardware. Its primary usefulness is to allow users to copy-paste * their data in an easy to read format for bug reports. */ cr.define('gpu', function() { /** * Provides information on the GPU process and underlying graphics hardware. * @constructor * @extends {cr.ui.TabPanel} */ var InfoView = cr.ui.define(cr.ui.TabPanel); InfoView.prototype = { __proto__: cr.ui.TabPanel.prototype, decorate: function() { cr.ui.TabPanel.prototype.decorate.apply(this); browserBridge.addEventListener('gpuInfoUpdate', this.refresh.bind(this)); browserBridge.addEventListener('logMessagesChange', this.refresh.bind(this)); browserBridge.addEventListener('clientInfoChange', this.refresh.bind(this)); this.refresh(); }, /** * Updates the view based on its currently known data */ refresh: function(data) { // Client info if (browserBridge.clientInfo) { var clientInfo = browserBridge.clientInfo; var commandLineParts = clientInfo.command_line.split(' '); commandLineParts.shift(); // Pop off the exe path var commandLineString = commandLineParts.join(' ') this.setTable_('client-info', [ { description: 'Data exported', value: (new Date()).toLocaleString() }, { description: 'Chrome version', value: clientInfo.version }, { description: 'Operating system', value: clientInfo.operating_system }, { description: 'Software rendering list version', value: clientInfo.blacklist_version }, { description: 'Driver bug list version', value: clientInfo.driver_bug_list_version }, { description: 'ANGLE commit id', value: clientInfo.angle_commit_id }, { description: '2D graphics backend', value: clientInfo.graphics_backend }, { description: 'Command Line Args', value: commandLineString }]); } else { this.setText_('client-info', '... loading...'); } // Feature map var featureLabelMap = { '2d_canvas': 'Canvas', 'gpu_compositing': 'Compositing', 'webgl': 'WebGL', 'multisampling': 'WebGL multisampling', 'flash_3d': 'Flash', 'flash_stage3d': 'Flash Stage3D', 'flash_stage3d_baseline': 'Flash Stage3D Baseline profile', 'texture_sharing': 'Texture Sharing', 'video_decode': 'Video Decode', 'video_encode': 'Video Encode', 'panel_fitting': 'Panel Fitting', 'rasterization': 'Rasterization', 'threaded_rasterization': 'Threaded Rasterization', 'multiple_raster_threads': 'Multiple Raster Threads', }; var statusMap = { 'disabled_software': { 'label': 'Software only. Hardware acceleration disabled', 'class': 'feature-yellow' }, 'disabled_off': { 'label': 'Disabled', 'class': 'feature-red' }, 'disabled_off_ok': { 'label': 'Disabled', 'class': 'feature-yellow' }, 'unavailable_software': { 'label': 'Software only, hardware acceleration unavailable', 'class': 'feature-yellow' }, 'unavailable_off': { 'label': 'Unavailable', 'class': 'feature-red' }, 'unavailable_off_ok': { 'label': 'Unavailable', 'class': 'feature-yellow' }, 'enabled_readback': { 'label': 'Hardware accelerated but at reduced performance', 'class': 'feature-yellow' }, 'enabled_force': { 'label': 'Hardware accelerated on all pages', 'class': 'feature-green' }, 'enabled': { 'label': 'Hardware accelerated', 'class': 'feature-green' }, 'enabled_on': { 'label': 'Enabled', 'class': 'feature-green' }, 'enabled_force_on': { 'label': 'Force enabled', 'class': 'feature-green' }, }; // GPU info, basic var diagnosticsDiv = this.querySelector('.diagnostics'); var diagnosticsLoadingDiv = this.querySelector('.diagnostics-loading'); var featureStatusList = this.querySelector('.feature-status-list'); var problemsDiv = this.querySelector('.problems-div'); var problemsList = this.querySelector('.problems-list'); var workaroundsDiv = this.querySelector('.workarounds-div'); var workaroundsList = this.querySelector('.workarounds-list'); var gpuInfo = browserBridge.gpuInfo; var i; if (gpuInfo) { // Not using jstemplate here for blacklist status because we construct // href from data, which jstemplate can't seem to do. if (gpuInfo.featureStatus) { // feature status list featureStatusList.textContent = ''; for (var featureName in gpuInfo.featureStatus.featureStatus) { var featureStatus = gpuInfo.featureStatus.featureStatus[featureName]; var featureEl = document.createElement('li'); var nameEl = document.createElement('span'); if (!featureLabelMap[featureName]) console.log('Missing featureLabel for', featureName); nameEl.textContent = featureLabelMap[featureName] + ': '; featureEl.appendChild(nameEl); var statusEl = document.createElement('span'); var statusInfo = statusMap[featureStatus]; if (!statusInfo) { console.log('Missing status for ', featureStatus); statusEl.textContent = 'Unknown'; statusEl.className = 'feature-red'; } else { statusEl.textContent = statusInfo['label']; statusEl.className = statusInfo['class']; } featureEl.appendChild(statusEl); featureStatusList.appendChild(featureEl); } // problems list if (gpuInfo.featureStatus.problems.length) { problemsDiv.hidden = false; problemsList.textContent = ''; for (i = 0; i < gpuInfo.featureStatus.problems.length; i++) { var problem = gpuInfo.featureStatus.problems[i]; var problemEl = this.createProblemEl_(problem); problemsList.appendChild(problemEl); } } else { problemsDiv.hidden = true; } // driver bug workarounds list if (gpuInfo.featureStatus.workarounds.length) { workaroundsDiv.hidden = false; workaroundsList.textContent = ''; for (i = 0; i < gpuInfo.featureStatus.workarounds.length; i++) { var workaroundEl = document.createElement('li'); workaroundEl.textContent = gpuInfo.featureStatus.workarounds[i]; workaroundsList.appendChild(workaroundEl); } } else { workaroundsDiv.hidden = true; } } else { featureStatusList.textContent = ''; problemsList.hidden = true; workaroundsList.hidden = true; } if (gpuInfo.basic_info) this.setTable_('basic-info', gpuInfo.basic_info); else this.setTable_('basic-info', []); if (gpuInfo.diagnostics) { diagnosticsDiv.hidden = false; diagnosticsLoadingDiv.hidden = true; $('diagnostics-table').hidden = false; this.setTable_('diagnostics-table', gpuInfo.diagnostics); } else if (gpuInfo.diagnostics === null) { // gpu_internals.cc sets diagnostics to null when it is being loaded diagnosticsDiv.hidden = false; diagnosticsLoadingDiv.hidden = false; $('diagnostics-table').hidden = true; } else { diagnosticsDiv.hidden = true; } } else { this.setText_('basic-info', '... loading ...'); diagnosticsDiv.hidden = true; featureStatusList.textContent = ''; problemsDiv.hidden = true; } // Log messages jstProcess(new JsEvalContext({values: browserBridge.logMessages}), $('log-messages')); }, createProblemEl_: function(problem) { var problemEl; problemEl = document.createElement('li'); // Description of issue var desc = document.createElement('a'); desc.textContent = problem.description; problemEl.appendChild(desc); // Spacing ':' element if (problem.crBugs.length + problem.webkitBugs.length > 0) { var tmp = document.createElement('span'); tmp.textContent = ': '; problemEl.appendChild(tmp); } var nbugs = 0; var j; // crBugs for (j = 0; j < problem.crBugs.length; ++j) { if (nbugs > 0) { var tmp = document.createElement('span'); tmp.textContent = ', '; problemEl.appendChild(tmp); } var link = document.createElement('a'); var bugid = parseInt(problem.crBugs[j]); link.textContent = bugid; link.href = 'http://crbug.com/' + bugid; problemEl.appendChild(link); nbugs++; } for (j = 0; j < problem.webkitBugs.length; ++j) { if (nbugs > 0) { var tmp = document.createElement('span'); tmp.textContent = ', '; problemEl.appendChild(tmp); } var link = document.createElement('a'); var bugid = parseInt(problem.webkitBugs[j]); link.textContent = bugid; link.href = 'https://bugs.webkit.org/show_bug.cgi?id=' + bugid; problemEl.appendChild(link); nbugs++; } if (problem.affectedGpuSettings.length > 0) { var brNode = document.createElement('br'); problemEl.appendChild(brNode); var iNode = document.createElement('i'); problemEl.appendChild(iNode); var headNode = document.createElement('span'); if (problem.tag == 'disabledFeatures') headNode.textContent = 'Disabled Features: '; else // problem.tag == 'workarounds' headNode.textContent = 'Applied Workarounds: '; iNode.appendChild(headNode); for (j = 0; j < problem.affectedGpuSettings.length; ++j) { if (j > 0) { var separateNode = document.createElement('span'); separateNode.textContent = ', '; iNode.appendChild(separateNode); } var nameNode = document.createElement('span'); if (problem.tag == 'disabledFeatures') nameNode.classList.add('feature-red'); else // problem.tag == 'workarounds' nameNode.classList.add('feature-yellow'); nameNode.textContent = problem.affectedGpuSettings[j]; iNode.appendChild(nameNode); } } return problemEl; }, setText_: function(outputElementId, text) { var peg = document.getElementById(outputElementId); peg.textContent = text; }, setTable_: function(outputElementId, inputData) { var template = jstGetTemplate('info-view-table-template'); jstProcess(new JsEvalContext({value: inputData}), template); var peg = document.getElementById(outputElementId); if (!peg) throw new Error('Node ' + outputElementId + ' not found'); peg.innerHTML = ''; peg.appendChild(template); } }; return { InfoView: InfoView }; }); var browserBridge; /** * Main entry point. called once the page has loaded. */ function onLoad() { browserBridge = new gpu.BrowserBridge(); // Create the views. cr.ui.decorate('#info-view', gpu.InfoView); } document.addEventListener('DOMContentLoaded', onLoad); IndexedDB
Instances in: Instances: Incognito
Size:
Last modified:
Open connections:
Paths:
Force close Download
Open database:
Connections: open: pending opens: pending upgrades: running upgrades: pending deletes:
Transactions:
Process ID ID Mode Scope Completed Requests Pending Requests Age (ms) Runtime (ms) Status

IndexedDB

// 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. cr.define('indexeddb', function() { 'use strict'; function initialize() { chrome.send('getAllOrigins'); } function progressNodeFor(link) { return link.parentNode.querySelector('.download-status'); } function downloadOriginData(event) { var link = event.target; progressNodeFor(link).style.display = 'inline'; chrome.send('downloadOriginData', [link.idb_partition_path, link.idb_origin_url]); return false; } function forceClose(event) { var link = event.target; progressNodeFor(link).style.display = 'inline'; chrome.send('forceClose', [link.idb_partition_path, link.idb_origin_url]); return false; } function withNode(selector, partition_path, origin_url, callback) { var links = document.querySelectorAll(selector); for (var i = 0; i < links.length; ++i) { var link = links[i]; if (partition_path == link.idb_partition_path && origin_url == link.idb_origin_url) { callback(link); } } } // Fired from the backend after the data has been zipped up, and the // download manager has begun downloading the file. function onOriginDownloadReady(partition_path, origin_url, connection_count) { withNode('a.download', partition_path, origin_url, function(link) { progressNodeFor(link).style.display = 'none'; }); withNode('.connection-count', partition_path, origin_url, function(span) { span.innerText = connection_count; }); } function onForcedClose(partition_path, origin_url, connection_count) { withNode('a.force-close', partition_path, origin_url, function(link) { progressNodeFor(link).style.display = 'none'; }); withNode('.connection-count', partition_path, origin_url, function(span) { span.innerText = connection_count; }); } // Fired from the backend with a single partition's worth of // IndexedDB metadata. function onOriginsReady(origins, partition_path) { var template = jstGetTemplate('indexeddb-list-template'); var container = $('indexeddb-list'); container.appendChild(template); jstProcess(new JsEvalContext({ idbs: origins, partition_path: partition_path}), template); var downloadLinks = container.querySelectorAll('a.download'); for (var i = 0; i < downloadLinks.length; ++i) { downloadLinks[i].addEventListener('click', downloadOriginData, false); } var forceCloseLinks = container.querySelectorAll('a.force-close'); for (i = 0; i < forceCloseLinks.length; ++i) { forceCloseLinks[i].addEventListener('click', forceClose, false); } } return { initialize: initialize, onForcedClose: onForcedClose, onOriginDownloadReady: onOriginDownloadReady, onOriginsReady: onOriginsReady, }; }); document.addEventListener('DOMContentLoaded', indexeddb.initialize); /* 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. */ .indexeddb-summary { background-color: rgb(235, 239, 249); border-top: 1px solid rgb(156, 194, 239); margin-bottom: 6px; margin-top: 12px; padding: 3px; font-weight: bold; } .indexeddb-item { margin-bottom: 15px; margin-top: 6px; position: relative; } .indexeddb-url { color: rgb(85, 102, 221); display: inline-block; max-width: 500px; overflow: hidden; padding-bottom: 1px; padding-top: 4px; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; } .indexeddb-database { margin-bottom: 6px; margin-top: 6px; margin-left: 12px; position: relative; } .indexeddb-database > div { margin-left: 12px; } .indexeddb-connection-count { margin: 0 8px; } .indexeddb-connection-count.pending { font-weight: bold; } .indexeddb-path { display: block; margin-left: 1em; } .indexeddb-transaction-list { margin-left: 10px; border-collapse: collapse; } .indexeddb-transaction-list th, .indexeddb-transaction-list td { padding: 2px 10px; min-width: 50px; max-width: 75px; } td.indexeddb-transaction-scope { min-width: 200px; max-width: 500px; } .indexeddb-transaction-list th { background-color: rgb(249, 249, 249); border: 1px solid rgb(156, 194, 239); font-weight: normal; text-align: left; } .indexeddb-transaction { background-color: rgb(235, 239, 249); border-bottom: 2px solid white; } .indexeddb-transaction.created { font-weight: italic; } .indexeddb-transaction.started { font-weight: bold; } .indexeddb-transaction.running { font-weight: bold; } .indexeddb-transaction.committing { font-weight: bold; } .indexeddb-transaction.blocked { } .indexeddb-transaction.started .indexeddb-transaction-state { background-color: rgb(249, 249, 235); } .indexeddb-transaction.running .indexeddb-transaction-state { background-color: rgb(235, 249, 235); } .indexeddb-transaction.committing .indexeddb-transaction-state { background-color: rgb(235, 235, 249); } .indexeddb-transaction.blocked .indexeddb-transaction-state { background-color: rgb(249, 235, 235); } .controls a { -webkit-margin-end: 16px; color: #777; } Players Audio Video Capture

Players

    Player Properties

    Property Value

    Log

    Timestamp Property Value

      Input Controllers

        Output Controllers

          Output Streams

            Properties

            Property Value

            Video Capture Device Capabilities

            Device Name Formats Capture API Device ID
            // Copyright 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 media = {}; // Copyright 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. /** * A global object that gets used by the C++ interface. */ var media = (function() { 'use strict'; var manager = null; // A number->string mapping that is populated through the backend that // describes the phase that the network entity is in. var eventPhases = {}; // A number->string mapping that is populated through the backend that // describes the type of event sent from the network. var eventTypes = {}; // A mapping of number->CacheEntry where the number is a unique id for that // network request. var cacheEntries = {}; // A mapping of url->CacheEntity where the url is the url of the resource. var cacheEntriesByKey = {}; var requrestURLs = {}; var media = { BAR_WIDTH: 200, BAR_HEIGHT: 25 }; /** * Users of |media| must call initialize prior to calling other methods. */ media.initialize = function(theManager) { manager = theManager; }; media.onReceiveAudioStreamData = function(audioStreamData) { for (var component in audioStreamData) { media.updateAudioComponent(audioStreamData[component]); } }; media.onReceiveVideoCaptureCapabilities = function(videoCaptureCapabilities) { manager.updateVideoCaptureCapabilities(videoCaptureCapabilities) } media.onReceiveConstants = function(constants) { for (var key in constants.eventTypes) { var value = constants.eventTypes[key]; eventTypes[value] = key; } for (var key in constants.eventPhases) { var value = constants.eventPhases[key]; eventPhases[value] = key; } }; media.cacheForUrl = function(url) { return cacheEntriesByKey[url]; }; media.onNetUpdate = function(updates) { updates.forEach(function(update) { var id = update.source.id; if (!cacheEntries[id]) cacheEntries[id] = new media.CacheEntry; switch (eventPhases[update.phase] + '.' + eventTypes[update.type]) { case 'PHASE_BEGIN.DISK_CACHE_ENTRY_IMPL': var key = update.params.key; // Merge this source with anything we already know about this key. if (cacheEntriesByKey[key]) { cacheEntriesByKey[key].merge(cacheEntries[id]); cacheEntries[id] = cacheEntriesByKey[key]; } else { cacheEntriesByKey[key] = cacheEntries[id]; } cacheEntriesByKey[key].key = key; break; case 'PHASE_BEGIN.SPARSE_READ': cacheEntries[id].readBytes(update.params.offset, update.params.buff_len); cacheEntries[id].sparse = true; break; case 'PHASE_BEGIN.SPARSE_WRITE': cacheEntries[id].writeBytes(update.params.offset, update.params.buff_len); cacheEntries[id].sparse = true; break; case 'PHASE_BEGIN.URL_REQUEST_START_JOB': requrestURLs[update.source.id] = update.params.url; break; case 'PHASE_NONE.HTTP_TRANSACTION_READ_RESPONSE_HEADERS': // Record the total size of the file if this was a range request. var range = /content-range:\s*bytes\s*\d+-\d+\/(\d+)/i.exec( update.params.headers); var key = requrestURLs[update.source.id]; delete requrestURLs[update.source.id]; if (range && key) { if (!cacheEntriesByKey[key]) { cacheEntriesByKey[key] = new media.CacheEntry; cacheEntriesByKey[key].key = key; } cacheEntriesByKey[key].size = range[1]; } break; } }); }; media.onRendererTerminated = function(renderId) { util.object.forEach(manager.players_, function(playerInfo, id) { if (playerInfo.properties['render_id'] == renderId) { manager.removePlayer(id); } }); }; media.updateAudioComponent = function(component) { var uniqueComponentId = component.owner_id + ':' + component.component_id; switch (component.status) { case 'closed': manager.removeAudioComponent( component.component_type, uniqueComponentId); break; default: manager.updateAudioComponent( component.component_type, uniqueComponentId, component); break; } }; media.onPlayerOpen = function(id, timestamp) { manager.addPlayer(id, timestamp); }; media.onMediaEvent = function(event) { var source = event.renderer + ':' + event.player; // Although this gets called on every event, there is nothing we can do // because there is no onOpen event. media.onPlayerOpen(source); manager.updatePlayerInfoNoRecord( source, event.ticksMillis, 'render_id', event.renderer); manager.updatePlayerInfoNoRecord( source, event.ticksMillis, 'player_id', event.player); var propertyCount = 0; util.object.forEach(event.params, function(value, key) { key = key.trim(); // These keys get spammed *a lot*, so put them on the display // but don't log list. if (key === 'buffer_start' || key === 'buffer_end' || key === 'buffer_current' || key === 'is_downloading_data') { manager.updatePlayerInfoNoRecord( source, event.ticksMillis, key, value); } else { manager.updatePlayerInfo(source, event.ticksMillis, key, value); } propertyCount += 1; }); if (propertyCount === 0) { manager.updatePlayerInfo( source, event.ticksMillis, 'EVENT', event.type); } }; // |chrome| is not defined during tests. if (window.chrome && window.chrome.send) { chrome.send('getEverything'); } return media; }()); // Copyright 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. /** * @fileoverview Some utility functions that don't belong anywhere else in the * code. */ var util = (function() { var util = {}; util.object = {}; /** * Calls a function for each element in an object/map/hash. * * @param obj The object to iterate over. * @param f The function to call on every value in the object. F should have * the following arguments: f(value, key, object) where value is the value * of the property, key is the corresponding key, and obj is the object that * was passed in originally. * @param optObj The object use as 'this' within f. */ util.object.forEach = function(obj, f, optObj) { 'use strict'; var key; for (key in obj) { if (obj.hasOwnProperty(key)) { f.call(optObj, obj[key], key, obj); } } }; util.millisecondsToString = function(timeMillis) { function pad(num) { num = num.toString(); if (num.length < 2) { return '0' + num; } return num; } var date = new Date(timeMillis); return pad(date.getUTCHours()) + ':' + pad(date.getUTCMinutes()) + ':' + pad(date.getUTCSeconds()) + ' ' + pad((date.getMilliseconds()) % 1000); }; return util; }()); // Copyright (c) 2011 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. cr.define('media', function() { 'use strict'; /** * This class represents a file cached by net. */ function CacheEntry() { this.read_ = new media.DisjointRangeSet; this.written_ = new media.DisjointRangeSet; this.available_ = new media.DisjointRangeSet; // Set to true when we know the entry is sparse. this.sparse = false; this.key = null; this.size = null; // The
            element representing this CacheEntry. this.details_ = document.createElement('details'); this.details_.className = 'cache-entry'; this.details_.open = false; // The
            summary line. It contains a chart of requested file ranges // and the url if we know it. var summary = document.createElement('summary'); this.summaryText_ = document.createTextNode(''); summary.appendChild(this.summaryText_); summary.appendChild(document.createTextNode(' ')); // Controls to modify this CacheEntry. var controls = document.createElement('span'); controls.className = 'cache-entry-controls'; summary.appendChild(controls); summary.appendChild(document.createElement('br')); // A link to clear recorded data from this CacheEntry. var clearControl = document.createElement('a'); clearControl.href = 'javascript:void(0)'; clearControl.onclick = this.clear.bind(this); clearControl.textContent = '(clear entry)'; controls.appendChild(clearControl); this.details_.appendChild(summary); // The canvas for drawing cache writes. this.writeCanvas = document.createElement('canvas'); this.writeCanvas.width = media.BAR_WIDTH; this.writeCanvas.height = media.BAR_HEIGHT; this.details_.appendChild(this.writeCanvas); // The canvas for drawing cache reads. this.readCanvas = document.createElement('canvas'); this.readCanvas.width = media.BAR_WIDTH; this.readCanvas.height = media.BAR_HEIGHT; this.details_.appendChild(this.readCanvas); // A tabular representation of the data in the above canvas. this.detailTable_ = document.createElement('table'); this.detailTable_.className = 'cache-table'; this.details_.appendChild(this.detailTable_); } CacheEntry.prototype = { /** * Mark a range of bytes as read from the cache. * @param {int} start The first byte read. * @param {int} length The number of bytes read. */ readBytes: function(start, length) { start = parseInt(start); length = parseInt(length); this.read_.add(start, start + length); this.available_.add(start, start + length); this.sparse = true; }, /** * Mark a range of bytes as written to the cache. * @param {int} start The first byte written. * @param {int} length The number of bytes written. */ writeBytes: function(start, length) { start = parseInt(start); length = parseInt(length); this.written_.add(start, start + length); this.available_.add(start, start + length); this.sparse = true; }, /** * Merge this CacheEntry with another, merging recorded ranges and flags. * @param {CacheEntry} other The CacheEntry to merge into this one. */ merge: function(other) { this.read_.merge(other.read_); this.written_.merge(other.written_); this.available_.merge(other.available_); this.sparse = this.sparse || other.sparse; this.key = this.key || other.key; this.size = this.size || other.size; }, /** * Clear all recorded ranges from this CacheEntry and redraw this.details_. */ clear: function() { this.read_ = new media.DisjointRangeSet; this.written_ = new media.DisjointRangeSet; this.available_ = new media.DisjointRangeSet; this.generateDetails(); }, /** * Helper for drawCacheReadsToCanvas() and drawCacheWritesToCanvas(). * * Accepts the entries to draw, a canvas fill style, and the canvas to * draw on. */ drawCacheEntriesToCanvas: function(entries, fillStyle, canvas) { // Don't bother drawing anything if we don't know the total size. if (!this.size) { return; } var width = canvas.width; var height = canvas.height; var context = canvas.getContext('2d'); var fileSize = this.size; context.fillStyle = '#aaa'; context.fillRect(0, 0, width, height); function drawRange(start, end) { var left = start / fileSize * width; var right = end / fileSize * width; context.fillRect(left, 0, right - left, height); } context.fillStyle = fillStyle; entries.map(function(start, end) { drawRange(start, end); }); }, /** * Draw cache writes to the given canvas. * * It should consist of a horizontal bar with highlighted sections to * represent which parts of a file have been written to the cache. * * e.g. |xxxxxx----------x| */ drawCacheWritesToCanvas: function(canvas) { this.drawCacheEntriesToCanvas(this.written_, '#00a', canvas); }, /** * Draw cache reads to the given canvas. * * It should consist of a horizontal bar with highlighted sections to * represent which parts of a file have been read from the cache. * * e.g. |xxxxxx----------x| */ drawCacheReadsToCanvas: function(canvas) { this.drawCacheEntriesToCanvas(this.read_, '#0a0', canvas); }, /** * Update this.details_ to contain everything we currently know about * this file. */ generateDetails: function() { function makeElement(tag, content) { var toReturn = document.createElement(tag); toReturn.textContent = content; return toReturn; } this.details_.id = this.key; this.summaryText_.textContent = this.key || 'Unknown File'; this.detailTable_.textContent = ''; var header = document.createElement('thead'); var footer = document.createElement('tfoot'); var body = document.createElement('tbody'); this.detailTable_.appendChild(header); this.detailTable_.appendChild(footer); this.detailTable_.appendChild(body); var headerRow = document.createElement('tr'); headerRow.appendChild(makeElement('th', 'Read From Cache')); headerRow.appendChild(makeElement('th', 'Written To Cache')); header.appendChild(headerRow); var footerRow = document.createElement('tr'); var footerCell = document.createElement('td'); footerCell.textContent = 'Out of ' + (this.size || 'unkown size'); footerCell.setAttribute('colspan', 2); footerRow.appendChild(footerCell); footer.appendChild(footerRow); var read = this.read_.map(function(start, end) { return start + ' - ' + end; }); var written = this.written_.map(function(start, end) { return start + ' - ' + end; }); var length = Math.max(read.length, written.length); for (var i = 0; i < length; i++) { var row = document.createElement('tr'); row.appendChild(makeElement('td', read[i] || '')); row.appendChild(makeElement('td', written[i] || '')); body.appendChild(row); } this.drawCacheWritesToCanvas(this.writeCanvas); this.drawCacheReadsToCanvas(this.readCanvas); }, /** * Render this CacheEntry as a
          • . * @return {HTMLElement} A
          • representing this CacheEntry. */ toListItem: function() { this.generateDetails(); var result = document.createElement('li'); result.appendChild(this.details_); return result; } }; return { CacheEntry: CacheEntry }; }); // Copyright (c) 2011 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. cr.define('media', function() { /** * This class represents a collection of non-intersecting ranges. Ranges * specified by (start, end) can be added and removed at will. It is used to * record which sections of a media file have been cached, e.g. the first and * last few kB plus several MB in the middle. * * Example usage: * someRange.add(0, 100); // Contains 0-100. * someRange.add(150, 200); // Contains 0-100, 150-200. * someRange.remove(25, 75); // Contains 0-24, 76-100, 150-200. * someRange.add(25, 149); // Contains 0-200. */ function DisjointRangeSet() { this.ranges_ = {}; } DisjointRangeSet.prototype = { /** * Deletes all ranges intersecting with (start ... end) and returns the * extents of the cleared area. * @param {int} start The start of the range to remove. * @param {int} end The end of the range to remove. * @param {int} sloppiness 0 removes only strictly overlapping ranges, and * 1 removes adjacent ones. * @return {Object} The start and end of the newly cleared range. */ clearRange: function(start, end, sloppiness) { var ranges = this.ranges_; var result = {start: start, end: end}; for (var rangeStart in this.ranges_) { rangeEnd = this.ranges_[rangeStart]; // A range intersects another if its start lies within the other range // or vice versa. if ((rangeStart >= start && rangeStart <= (end + sloppiness)) || (start >= rangeStart && start <= (rangeEnd + sloppiness))) { delete ranges[rangeStart]; result.start = Math.min(result.start, rangeStart); result.end = Math.max(result.end, rangeEnd); } } return result; }, /** * Adds a range to this DisjointRangeSet. * Joins adjacent and overlapping ranges together. * @param {int} start The beginning of the range to add, inclusive. * @param {int} end The end of the range to add, inclusive. */ add: function(start, end) { if (end < start) return; // Remove all touching ranges. result = this.clearRange(start, end, 1); // Add back a single contiguous range. this.ranges_[Math.min(start, result.start)] = Math.max(end, result.end); }, /** * Combines a DisjointRangeSet with this one. * @param {DisjointRangeSet} ranges A DisjointRangeSet to be squished into * this one. */ merge: function(other) { var ranges = this; other.forEach(function(start, end) { ranges.add(start, end); }); }, /** * Removes a range from this DisjointRangeSet. * Will split existing ranges if necessary. * @param {int} start The beginning of the range to remove, inclusive. * @param {int} end The end of the range to remove, inclusive. */ remove: function(start, end) { if (end < start) return; // Remove instersecting ranges. result = this.clearRange(start, end, 0); // Add back non-overlapping ranges. if (result.start < start) this.ranges_[result.start] = start - 1; if (result.end > end) this.ranges_[end + 1] = result.end; }, /** * Iterates over every contiguous range in this DisjointRangeSet, calling a * function for each (start, end). * @param {function(int, int)} iterator The function to call on each range. */ forEach: function(iterator) { for (var start in this.ranges_) iterator(start, this.ranges_[start]); }, /** * Maps this DisjointRangeSet to an array by calling a given function on the * start and end of each contiguous range, sorted by start. * @param {function(int, int)} mapper Maps a range to an array element. * @return {Array} An array of each mapper(range). */ map: function(mapper) { var starts = []; for (var start in this.ranges_) starts.push(parseInt(start)); starts.sort(function(a, b) { return a - b; }); var ranges = this.ranges_; var results = starts.map(function(s) { return mapper(s, ranges[s]); }); return results; }, /** * Finds the maximum value present in any of the contained ranges. * @return {int} The maximum value contained by this DisjointRangeSet. */ max: function() { var max = -Infinity; for (var start in this.ranges_) max = Math.max(max, this.ranges_[start]); return max; }, }; return { DisjointRangeSet: DisjointRangeSet }; }); // Copyright 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. /** * @fileoverview A class for keeping track of the details of a player. */ var PlayerInfo = (function() { 'use strict'; /** * A class that keeps track of properties on a media player. * @param id A unique id that can be used to identify this player. */ function PlayerInfo(id) { this.id = id; // The current value of the properties for this player. this.properties = {}; // All of the past (and present) values of the properties. this.pastValues = {}; // Every single event in the order in which they were received. this.allEvents = []; this.lastRendered = 0; this.firstTimestamp_ = -1; } PlayerInfo.prototype = { /** * Adds or set a property on this player. * This is the default logging method as it keeps track of old values. * @param timestamp The time in milliseconds since the Epoch. * @param key A String key that describes the property. * @param value The value of the property. */ addProperty: function(timestamp, key, value) { // The first timestamp that we get will be recorded. // Then, all future timestamps are deltas of that. if (this.firstTimestamp_ === -1) { this.firstTimestamp_ = timestamp; } if (typeof key !== 'string') { throw new Error(typeof key + ' is not a valid key type'); } this.properties[key] = value; if (!this.pastValues[key]) { this.pastValues[key] = []; } var recordValue = { time: timestamp - this.firstTimestamp_, key: key, value: value }; this.pastValues[key].push(recordValue); this.allEvents.push(recordValue); }, /** * Adds or set a property on this player. * Does not keep track of old values. This is better for * values that get spammed repeatedly. * @param timestamp The time in milliseconds since the Epoch. * @param key A String key that describes the property. * @param value The value of the property. */ addPropertyNoRecord: function(timestamp, key, value) { this.addProperty(timestamp, key, value); this.allEvents.pop(); } }; return PlayerInfo; }()); // Copyright 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. /** * @fileoverview Keeps track of all the existing PlayerInfo and * audio stream objects and is the entry-point for messages from the backend. * * The events captured by Manager (add, remove, update) are relayed * to the clientRenderer which it can choose to use to modify the UI. */ var Manager = (function() { 'use strict'; function Manager(clientRenderer) { this.players_ = {}; this.audioComponents_ = []; this.clientRenderer_ = clientRenderer; } Manager.prototype = { /** * Updates an audio-component. * @param componentType Integer AudioComponent enum value; must match values * from the AudioLogFactory::AudioComponent enum. * @param componentId The unique-id of the audio-component. * @param componentData The actual component data dictionary. */ updateAudioComponent: function(componentType, componentId, componentData) { if (!(componentType in this.audioComponents_)) this.audioComponents_[componentType] = {}; if (!(componentId in this.audioComponents_[componentType])) { this.audioComponents_[componentType][componentId] = componentData; } else { for (var key in componentData) { this.audioComponents_[componentType][componentId][key] = componentData[key]; } } this.clientRenderer_.audioComponentAdded( componentType, this.audioComponents_[componentType]); }, /** * Removes an audio-stream from the manager. * @param id The unique-id of the audio-stream. */ removeAudioComponent: function(componentType, componentId) { if (!(componentType in this.audioComponents_) || !(componentId in this.audioComponents_[componentType])) { return; } delete this.audioComponents_[componentType][componentId]; this.clientRenderer_.audioComponentRemoved( componentType, this.audioComponents_[componentType]); }, /** * Adds a player to the list of players to manage. */ addPlayer: function(id) { if (this.players_[id]) { return; } // Make the PlayerProperty and add it to the mapping this.players_[id] = new PlayerInfo(id); this.clientRenderer_.playerAdded(this.players_, this.players_[id]); }, /** * Attempts to remove a player from the UI. * @param id The ID of the player to remove. */ removePlayer: function(id) { delete this.players_[id]; this.clientRenderer_.playerRemoved(this.players_, this.players_[id]); }, updatePlayerInfoNoRecord: function(id, timestamp, key, value) { if (!this.players_[id]) { console.error('[updatePlayerInfo] Id ' + id + ' does not exist'); return; } this.players_[id].addPropertyNoRecord(timestamp, key, value); this.clientRenderer_.playerUpdated(this.players_, this.players_[id], key, value); }, /** * * @param id The unique ID that identifies the player to be updated. * @param timestamp The timestamp of when the change occured. This * timestamp is *not* normalized. * @param key The name of the property to be added/changed. * @param value The value of the property. */ updatePlayerInfo: function(id, timestamp, key, value) { if (!this.players_[id]) { console.error('[updatePlayerInfo] Id ' + id + ' does not exist'); return; } this.players_[id].addProperty(timestamp, key, value); this.clientRenderer_.playerUpdated(this.players_, this.players_[id], key, value); }, parseVideoCaptureFormat_: function(format) { /** * Example: * * format: * "resolution: 1280x720, fps: 30.000000, pixel format: I420" * * formatDict: * {'resolution':'1280x720', 'fps': '30.00'} */ var parts = format.split(', '); var formatDict = {}; for (var i in parts) { var kv = parts[i].split(': '); formatDict[kv[0]] = kv[1]; } // Round down the FPS to 2 decimals. formatDict['fps'] = parseFloat(formatDict['fps']).toFixed(2); // The camera does not actually output I420 so this info is misleading. delete formatDict['pixel format']; return formatDict; }, updateVideoCaptureCapabilities: function(videoCaptureCapabilities) { // Parse the video formats to be structured for the table. for (var i in videoCaptureCapabilities) { for (var j in videoCaptureCapabilities[i]['formats']) { videoCaptureCapabilities[i]['formats'][j] = this.parseVideoCaptureFormat_( videoCaptureCapabilities[i]['formats'][j]); } } // The keys of each device to be shown in order of appearance. var videoCaptureDeviceKeys = ['name','formats','captureApi','id']; this.clientRenderer_.redrawVideoCaptureCapabilities( videoCaptureCapabilities, videoCaptureDeviceKeys); } }; return Manager; }()); // Copyright 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 ClientRenderer = (function() { var ClientRenderer = function() { this.playerListElement = document.getElementById('player-list'); this.audioPropertiesTable = document.getElementById('audio-property-table').querySelector('tbody'); this.playerPropertiesTable = document.getElementById('player-property-table').querySelector('tbody'); this.logTable = document.getElementById('log').querySelector('tbody'); this.graphElement = document.getElementById('graphs'); this.audioPropertyName = document.getElementById('audio-property-name'); this.selectedPlayer = null; this.selectedAudioComponentType = null; this.selectedAudioComponentId = null; this.selectedAudioCompontentData = null; this.selectedPlayerLogIndex = 0; this.filterFunction = function() { return true; }; this.filterText = document.getElementById('filter-text'); this.filterText.onkeyup = this.onTextChange_.bind(this); this.bufferCanvas = document.createElement('canvas'); this.bufferCanvas.width = media.BAR_WIDTH; this.bufferCanvas.height = media.BAR_HEIGHT; this.clipboardTextarea = document.getElementById('clipboard-textarea'); var clipboardButtons = document.getElementsByClassName('copy-button'); for (var i = 0; i < clipboardButtons.length; i++) { clipboardButtons[i].onclick = this.copyToClipboard_.bind(this); } this.hiddenKeys = ['component_id', 'component_type', 'owner_id']; // Tell CSS to hide certain content prior to making selections. document.body.classList.add(ClientRenderer.Css_.NO_PLAYERS_SELECTED); document.body.classList.add(ClientRenderer.Css_.NO_COMPONENTS_SELECTED); }; /** * CSS classes added / removed in JS to trigger styling changes. * @private @enum {string} */ ClientRenderer.Css_ = { NO_PLAYERS_SELECTED: 'no-players-selected', NO_COMPONENTS_SELECTED: 'no-components-selected', SELECTABLE_BUTTON: 'selectable-button' }; function removeChildren(element) { while (element.hasChildNodes()) { element.removeChild(element.lastChild); } }; function createSelectableButton(id, groupName, text, select_cb) { // For CSS styling. var radioButton = document.createElement('input'); radioButton.classList.add(ClientRenderer.Css_.SELECTABLE_BUTTON); radioButton.type = 'radio'; radioButton.id = id; radioButton.name = groupName; var buttonLabel = document.createElement('label'); buttonLabel.classList.add(ClientRenderer.Css_.SELECTABLE_BUTTON); buttonLabel.setAttribute('for', radioButton.id); buttonLabel.appendChild(document.createTextNode(text)); var fragment = document.createDocumentFragment(); fragment.appendChild(radioButton); fragment.appendChild(buttonLabel); // Listen to 'change' rather than 'click' to keep styling in sync with // button behavior. radioButton.addEventListener('change', function() { select_cb(); }); return fragment; }; function selectSelectableButton(id) { var element = document.getElementById(id); if (!element) { console.error('failed to select button with id: ' + id); return; } element.checked = true; } ClientRenderer.prototype = { /** * Called when an audio component is added to the collection. * @param componentType Integer AudioComponent enum value; must match values * from the AudioLogFactory::AudioComponent enum. * @param components The entire map of components (name -> dict). */ audioComponentAdded: function(componentType, components) { this.redrawAudioComponentList_(componentType, components); // Redraw the component if it's currently selected. if (this.selectedAudioComponentType == componentType && this.selectedAudioComponentId && this.selectedAudioComponentId in components) { // TODO(chcunningham): This path is used both for adding and updating // the components. Split this up to have a separate update method. // At present, this selectAudioComponent call is key to *updating* the // the property table for existing audio components. this.selectAudioComponent_( componentType, this.selectedAudioComponentId, components[this.selectedAudioComponentId]); } }, /** * Called when an audio component is removed from the collection. * @param componentType Integer AudioComponent enum value; must match values * from the AudioLogFactory::AudioComponent enum. * @param components The entire map of components (name -> dict). */ audioComponentRemoved: function(componentType, components) { this.redrawAudioComponentList_(componentType, components); }, /** * Called when a player is added to the collection. * @param players The entire map of id -> player. * @param player_added The player that is added. */ playerAdded: function(players, playerAdded) { this.redrawPlayerList_(players); }, /** * Called when a playre is removed from the collection. * @param players The entire map of id -> player. * @param player_added The player that was removed. */ playerRemoved: function(players, playerRemoved) { this.redrawPlayerList_(players); }, /** * Called when a property on a player is changed. * @param players The entire map of id -> player. * @param player The player that had its property changed. * @param key The name of the property that was changed. * @param value The new value of the property. */ playerUpdated: function(players, player, key, value) { if (player === this.selectedPlayer) { this.drawProperties_(player.properties, this.playerPropertiesTable); this.drawLog_(); this.drawGraphs_(); } if (key === 'name' || key === 'url') { this.redrawPlayerList_(players); } }, createVideoCaptureFormatTable: function(formats) { if (!formats || formats.length == 0) return document.createTextNode('No formats'); var table = document.createElement('table'); var thead = document.createElement('thead'); var theadRow = document.createElement('tr'); for (var key in formats[0]) { var th = document.createElement('th'); th.appendChild(document.createTextNode(key)); theadRow.appendChild(th); } thead.appendChild(theadRow); table.appendChild(thead); var tbody = document.createElement('tbody'); for (var i=0; i < formats.length; ++i) { var tr = document.createElement('tr') for (var key in formats[i]) { var td = document.createElement('td'); td.appendChild(document.createTextNode(formats[i][key])); tr.appendChild(td); } tbody.appendChild(tr); } table.appendChild(tbody); table.classList.add('video-capture-formats-table'); return table; }, redrawVideoCaptureCapabilities: function(videoCaptureCapabilities, keys) { var copyButtonElement = document.getElementById('video-capture-capabilities-copy-button'); copyButtonElement.onclick = function() { window.prompt('Copy to clipboard: Ctrl+C, Enter', JSON.stringify(videoCaptureCapabilities)) } var videoTableBodyElement = document.getElementById('video-capture-capabilities-tbody'); removeChildren(videoTableBodyElement); for (var component in videoCaptureCapabilities) { var tableRow = document.createElement('tr'); var device = videoCaptureCapabilities[ component ]; for (var i in keys) { var value = device[keys[i]]; var tableCell = document.createElement('td'); var cellElement; if ((typeof value) == (typeof [])) { cellElement = this.createVideoCaptureFormatTable(value); } else { cellElement = document.createTextNode( ((typeof value) == 'undefined') ? 'n/a' : value); } tableCell.appendChild(cellElement) tableRow.appendChild(tableCell); } videoTableBodyElement.appendChild(tableRow); } }, getAudioComponentName_ : function(componentType, id) { var baseName; switch (componentType) { case 0: case 1: baseName = 'Controller'; break; case 2: baseName = 'Stream'; break; default: baseName = 'UnknownType' console.error('Unrecognized component type: ' + componentType); break; } return baseName + ' ' + id; }, getListElementForAudioComponent_ : function(componentType) { var listElement; switch (componentType) { case 0: listElement = document.getElementById( 'audio-input-controller-list'); break; case 1: listElement = document.getElementById( 'audio-output-controller-list'); break; case 2: listElement = document.getElementById( 'audio-output-stream-list'); break; default: console.error('Unrecognized component type: ' + componentType); listElement = null; break; } return listElement; }, redrawAudioComponentList_: function(componentType, components) { // Group name imposes rule that only one component can be selected // (and have its properties displayed) at a time. var buttonGroupName = 'audio-components'; var listElement = this.getListElementForAudioComponent_(componentType); if (!listElement) { console.error('Failed to find list element for component type: ' + componentType); return; } var fragment = document.createDocumentFragment(); for (id in components) { var li = document.createElement('li'); var button_cb = this.selectAudioComponent_.bind( this, componentType, id, components[id]); var friendlyName = this.getAudioComponentName_(componentType, id); li.appendChild(createSelectableButton( id, buttonGroupName, friendlyName, button_cb)); fragment.appendChild(li); } removeChildren(listElement); listElement.appendChild(fragment); if (this.selectedAudioComponentType && this.selectedAudioComponentType == componentType && this.selectedAudioComponentId in components) { // Re-select the selected component since the button was just recreated. selectSelectableButton(this.selectedAudioComponentId); } }, selectAudioComponent_: function( componentType, componentId, componentData) { document.body.classList.remove( ClientRenderer.Css_.NO_COMPONENTS_SELECTED); this.selectedAudioComponentType = componentType; this.selectedAudioComponentId = componentId; this.selectedAudioCompontentData = componentData; this.drawProperties_(componentData, this.audioPropertiesTable); removeChildren(this.audioPropertyName); this.audioPropertyName.appendChild(document.createTextNode( this.getAudioComponentName_(componentType, componentId))); }, redrawPlayerList_: function(players) { // Group name imposes rule that only one component can be selected // (and have its properties displayed) at a time. var buttonGroupName = 'player-buttons'; var fragment = document.createDocumentFragment(); for (id in players) { var player = players[id]; var usableName = player.properties.name || player.properties.url || 'Player ' + player.id; var li = document.createElement('li'); var button_cb = this.selectPlayer_.bind(this, player); li.appendChild(createSelectableButton( id, buttonGroupName, usableName, button_cb)); fragment.appendChild(li); } removeChildren(this.playerListElement); this.playerListElement.appendChild(fragment); if (this.selectedPlayer && this.selectedPlayer.id in players) { // Re-select the selected player since the button was just recreated. selectSelectableButton(this.selectedPlayer.id); } }, selectPlayer_: function(player) { document.body.classList.remove(ClientRenderer.Css_.NO_PLAYERS_SELECTED); this.selectedPlayer = player; this.selectedPlayerLogIndex = 0; this.selectedAudioComponentType = null; this.selectedAudioComponentId = null; this.selectedAudioCompontentData = null; this.drawProperties_(player.properties, this.playerPropertiesTable); removeChildren(this.logTable); removeChildren(this.graphElement); this.drawLog_(); this.drawGraphs_(); }, drawProperties_: function(propertyMap, propertiesTable) { removeChildren(propertiesTable); var sortedKeys = Object.keys(propertyMap).sort(); for (var i = 0; i < sortedKeys.length; ++i) { var key = sortedKeys[i]; if (this.hiddenKeys.indexOf(key) >= 0) continue; var value = propertyMap[key]; var row = propertiesTable.insertRow(-1); var keyCell = row.insertCell(-1); var valueCell = row.insertCell(-1); keyCell.appendChild(document.createTextNode(key)); valueCell.appendChild(document.createTextNode(value)); } }, appendEventToLog_: function(event) { if (this.filterFunction(event.key)) { var row = this.logTable.insertRow(-1); var timestampCell = row.insertCell(-1); timestampCell.classList.add('timestamp'); timestampCell.appendChild(document.createTextNode( util.millisecondsToString(event.time))); row.insertCell(-1).appendChild(document.createTextNode(event.key)); row.insertCell(-1).appendChild(document.createTextNode(event.value)); } }, drawLog_: function() { var toDraw = this.selectedPlayer.allEvents.slice( this.selectedPlayerLogIndex); toDraw.forEach(this.appendEventToLog_.bind(this)); this.selectedPlayerLogIndex = this.selectedPlayer.allEvents.length; }, drawGraphs_: function() { function addToGraphs(name, graph, graphElement) { var li = document.createElement('li'); li.appendChild(graph); li.appendChild(document.createTextNode(name)); graphElement.appendChild(li); } var url = this.selectedPlayer.properties.url; if (!url) { return; } var cache = media.cacheForUrl(url); var player = this.selectedPlayer; var props = player.properties; var cacheExists = false; var bufferExists = false; if (props['buffer_start'] !== undefined && props['buffer_current'] !== undefined && props['buffer_end'] !== undefined && props['total_bytes'] !== undefined) { this.drawBufferGraph_(props['buffer_start'], props['buffer_current'], props['buffer_end'], props['total_bytes']); bufferExists = true; } if (cache) { if (player.properties['total_bytes']) { cache.size = Number(player.properties['total_bytes']); } cache.generateDetails(); cacheExists = true; } if (!this.graphElement.hasChildNodes()) { if (bufferExists) { addToGraphs('buffer', this.bufferCanvas, this.graphElement); } if (cacheExists) { addToGraphs('cache read', cache.readCanvas, this.graphElement); addToGraphs('cache write', cache.writeCanvas, this.graphElement); } } }, drawBufferGraph_: function(start, current, end, size) { var ctx = this.bufferCanvas.getContext('2d'); var width = this.bufferCanvas.width; var height = this.bufferCanvas.height; ctx.fillStyle = '#aaa'; ctx.fillRect(0, 0, width, height); var scale_factor = width / size; var left = start * scale_factor; var middle = current * scale_factor; var right = end * scale_factor; ctx.fillStyle = '#a0a'; ctx.fillRect(left, 0, middle - left, height); ctx.fillStyle = '#aa0'; ctx.fillRect(middle, 0, right - middle, height); }, copyToClipboard_: function() { if (!this.selectedPlayer && !this.selectedAudioCompontentData) { return; } var properties = this.selectedAudioCompontentData || this.selectedPlayer.properties; var stringBuffer = []; for (var key in properties) { var value = properties[key]; stringBuffer.push(key.toString()); stringBuffer.push(': '); stringBuffer.push(value.toString()); stringBuffer.push('\n'); } this.clipboardTextarea.value = stringBuffer.join(''); this.clipboardTextarea.classList.remove('hiddenClipboard'); this.clipboardTextarea.focus(); this.clipboardTextarea.select(); // Hide the clipboard element when it loses focus. this.clipboardTextarea.onblur = function(event) { setTimeout(function(element) { event.target.classList.add('hiddenClipboard'); }, 0); }; }, onTextChange_: function(event) { var text = this.filterText.value.toLowerCase(); var parts = text.split(',').map(function(part) { return part.trim(); }).filter(function(part) { return part.trim().length > 0; }); this.filterFunction = function(text) { text = text.toLowerCase(); return parts.length === 0 || parts.some(function(part) { return text.indexOf(part) != -1; }); }; if (this.selectedPlayer) { removeChildren(this.logTable); this.selectedPlayerLogIndex = 0; this.drawLog_(); } }, }; return ClientRenderer; })(); media.initialize(new Manager(new ClientRenderer())); cr.ui.decorate('tabbox', cr.ui.TabBox); ServiceWorker
            Installation Status:
            Running Status:
            Script:
            Version ID:
            Renderer process ID:
            Renderer thread ID:
            DevTools agent route ID:
            Log:
            Scope:
            Registration ID: (unregistered)
            Active worker:
            Waiting worker:
            Registrations in: Registrations: Incognito
            Unregistered worker:

            ServiceWorker

            // Copyright 2014 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. cr.define('serviceworker', function() { 'use strict'; function initialize() { update(); } function update() { chrome.send('GetOptions'); chrome.send('getAllRegistrations'); } function onOptions(options) { var template; var container = $('serviceworker-options'); if (container.childNodes) { template = container.childNodes[0]; } if (!template) { template = jstGetTemplate('serviceworker-options-template'); container.appendChild(template); } jstProcess(new JsEvalContext(options), template); var inputs = container.querySelectorAll('input[type=\'checkbox\']'); for (var i = 0; i < inputs.length; ++i) { if (!inputs[i].hasClickEvent) { inputs[i].addEventListener('click', (function(event) { chrome.send('SetOption', [event.target.className, event.target.checked]); }).bind(this), false); inputs[i].hasClickEvent = true; } } } function progressNodeFor(link) { return link.parentNode.querySelector('.operation-status'); } // All commands are completed with 'onOperationComplete'. var COMMANDS = ['stop', 'sync', 'push', 'inspect', 'unregister', 'start']; function commandHandler(command) { return function(event) { var link = event.target; progressNodeFor(link).style.display = 'inline'; sendCommand(command, link.cmdArgs, (function(status) { progressNodeFor(link).style.display = 'none'; }).bind(null, link)); return false; }; }; var commandCallbacks = []; function sendCommand(command, args, callback) { var callbackId = 0; while (callbackId in commandCallbacks) { callbackId++; } commandCallbacks[callbackId] = callback; chrome.send(command, [callbackId, args]); } // Fired from the backend after the command call has completed. function onOperationComplete(status, callbackId) { var callback = commandCallbacks[callbackId]; delete commandCallbacks[callbackId]; if (callback) { callback(status); } update(); } var allLogMessages = {}; // Set log for a worker version. function fillLogForVersion(container, partition_id, version) { if (!version) { return; } if (!(partition_id in allLogMessages)) { allLogMessages[partition_id] = {}; } var logMessages = allLogMessages[partition_id]; if (version.version_id in logMessages) { version.log = logMessages[version.version_id]; } else { version.log = ''; } var logAreas = container.querySelectorAll('textarea.serviceworker-log'); for (var i = 0; i < logAreas.length; ++i) { var logArea = logAreas[i]; if (logArea.partition_id == partition_id && logArea.version_id == version.version_id) { logArea.value = version.log; } } } // Get the unregistered workers. // |unregistered_registrations| will be filled with the registrations which // are in |live_registrations| but not in |stored_registrations|. // |unregistered_versions| will be filled with the versions which // are in |live_versions| but not in |stored_registrations| nor in // |live_registrations|. function getUnregisteredWorkers(stored_registrations, live_registrations, live_versions, unregistered_registrations, unregistered_versions) { var registration_id_set = {}; var version_id_set = {}; stored_registrations.forEach(function(registration) { registration_id_set[registration.registration_id] = true; }); [stored_registrations, live_registrations].forEach(function(registrations) { registrations.forEach(function(registration) { [registration.active, registration.waiting].forEach(function(version) { if (version) { version_id_set[version.version_id] = true; } }); }); }); live_registrations.forEach(function(registration) { if (!registration_id_set[registration.registration_id]) { registration.unregistered = true; unregistered_registrations.push(registration); } }); live_versions.forEach(function(version) { if (!version_id_set[version.version_id]) { unregistered_versions.push(version); } }); } // Fired once per partition from the backend. function onPartitionData(live_registrations, live_versions, stored_registrations, partition_id, partition_path) { var unregistered_registrations = []; var unregistered_versions = []; getUnregisteredWorkers(stored_registrations, live_registrations, live_versions, unregistered_registrations, unregistered_versions); var template; var container = $('serviceworker-list'); // Existing templates are keyed by partition_id. This allows // the UI to be updated in-place rather than refreshing the // whole page. for (var i = 0; i < container.childNodes.length; ++i) { if (container.childNodes[i].partition_id == partition_id) { template = container.childNodes[i]; } } // This is probably the first time we're loading. if (!template) { template = jstGetTemplate('serviceworker-list-template'); container.appendChild(template); } var fillLogFunc = fillLogForVersion.bind(this, container, partition_id); stored_registrations.forEach(function(registration) { [registration.active, registration.waiting].forEach(fillLogFunc); }); unregistered_registrations.forEach(function(registration) { [registration.active, registration.waiting].forEach(fillLogFunc); }); unregistered_versions.forEach(fillLogFunc); jstProcess(new JsEvalContext({ stored_registrations: stored_registrations, unregistered_registrations: unregistered_registrations, unregistered_versions: unregistered_versions, partition_id: partition_id, partition_path: partition_path}), template); for (var i = 0; i < COMMANDS.length; ++i) { var handler = commandHandler(COMMANDS[i]); var links = container.querySelectorAll('button.' + COMMANDS[i]); for (var j = 0; j < links.length; ++j) { if (!links[j].hasClickEvent) { links[j].addEventListener('click', handler, false); links[j].hasClickEvent = true; } } } } function onRunningStateChanged(partition_id, version_id) { update(); } function onErrorReported(partition_id, version_id, process_id, thread_id, error_info) { outputLogMessage(partition_id, version_id, 'Error: ' + JSON.stringify(error_info) + '\n'); } function onConsoleMessageReported(partition_id, version_id, process_id, thread_id, message) { outputLogMessage(partition_id, version_id, 'Console: ' + JSON.stringify(message) + '\n'); } function onVersionStateChanged(partition_id, version_id) { update(); } function onRegistrationStored(scope) { update(); } function onRegistrationDeleted(scope) { update(); } function outputLogMessage(partition_id, version_id, message) { if (!(partition_id in allLogMessages)) { allLogMessages[partition_id] = {}; } var logMessages = allLogMessages[partition_id]; if (version_id in logMessages) { logMessages[version_id] += message; } else { logMessages[version_id] = message; } var logAreas = document.querySelectorAll('textarea.serviceworker-log'); for (var i = 0; i < logAreas.length; ++i) { var logArea = logAreas[i]; if (logArea.partition_id == partition_id && logArea.version_id == version_id) { logArea.value += message; } } } return { initialize: initialize, onOptions: onOptions, onOperationComplete: onOperationComplete, onPartitionData: onPartitionData, onRunningStateChanged: onRunningStateChanged, onErrorReported: onErrorReported, onConsoleMessageReported: onConsoleMessageReported, onVersionStateChanged: onVersionStateChanged, onRegistrationStored: onRegistrationStored, onRegistrationDeleted: onRegistrationDeleted, }; }); document.addEventListener('DOMContentLoaded', serviceworker.initialize); /* Copyright 2014 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. */ .serviceworker-summary { background-color: rgb(235, 239, 249); border-top: 1px solid rgb(156, 194, 239); margin-bottom: 6px; margin-top: 12px; padding: 3px; font-weight: bold; } .serviceworker-item { margin-bottom: 15px; margin-top: 6px; position: relative; } .serviceworker-registration { padding: 5px; } .serviceworker-scope { color: rgb(85, 102, 221); display: inline-block; padding-bottom: 1px; padding-top: 4px; text-decoration: none; white-space: nowrap; } .serviceworker-version { padding-bottom: 3px; padding-left: 10px; } .controls a { -webkit-margin-end: 16px; color: #777; } WebRTC Internals

            // 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'; 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 = []; /** 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; }, /** * @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; // Copyright 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. /** * A TabView provides the ability to create tabs and switch between tabs. It's * responsible for creating the DOM and managing the visibility of each tab. * The first added tab is active by default and the others hidden. */ var TabView = (function() { 'use strict'; /** * @constructor * @param {Element} root The root DOM element containing the tabs. */ function TabView(root) { this.root_ = root; this.ACTIVE_TAB_HEAD_CLASS_ = 'active-tab-head'; this.ACTIVE_TAB_BODY_CLASS_ = 'active-tab-body'; this.TAB_HEAD_CLASS_ = 'tab-head'; this.TAB_BODY_CLASS_ = 'tab-body'; /** * A mapping for an id to the tab elements. * @type {!Object} * @private */ this.tabElements_ = {}; this.headBar_ = null; this.activeTabId_ = null; this.initializeHeadBar_(); } // Creates a simple object containing the tab head and body elements. function TabDom(h, b) { this.head = h; this.body = b; } TabView.prototype = { /** * Adds a tab with the specified id and title. * @param {string} id * @param {string} title * @return {!Element} The tab body element. */ addTab: function(id, title) { if (this.tabElements_[id]) throw 'Tab already exists: ' + id; var head = document.createElement('span'); head.className = this.TAB_HEAD_CLASS_; head.textContent = title; head.title = title; this.headBar_.appendChild(head); head.addEventListener('click', this.switchTab_.bind(this, id)); var body = document.createElement('div'); body.className = this.TAB_BODY_CLASS_; body.id = id; this.root_.appendChild(body); this.tabElements_[id] = new TabDom(head, body); if (!this.activeTabId_) { this.switchTab_(id); } return this.tabElements_[id].body; }, /** Removes the tab. @param {string} id */ removeTab: function(id) { if (!this.tabElements_[id]) return; this.tabElements_[id].head.parentNode.removeChild( this.tabElements_[id].head); this.tabElements_[id].body.parentNode.removeChild( this.tabElements_[id].body); delete this.tabElements_[id]; if (this.activeTabId_ == id) { this.switchTab_(Object.keys(this.tabElements_)[0]); } }, /** * Switches the specified tab into view. * * @param {string} activeId The id the of the tab that should be switched to * active state. * @private */ switchTab_: function(activeId) { if (this.activeTabId_ && this.tabElements_[this.activeTabId_]) { this.tabElements_[this.activeTabId_].body.classList.remove( this.ACTIVE_TAB_BODY_CLASS_); this.tabElements_[this.activeTabId_].head.classList.remove( this.ACTIVE_TAB_HEAD_CLASS_); } this.activeTabId_ = activeId; if (this.tabElements_[activeId]) { this.tabElements_[activeId].body.classList.add( this.ACTIVE_TAB_BODY_CLASS_); this.tabElements_[activeId].head.classList.add( this.ACTIVE_TAB_HEAD_CLASS_); } }, /** Initializes the bar containing the tab heads. */ initializeHeadBar_: function() { this.headBar_ = document.createElement('div'); this.root_.appendChild(this.headBar_); this.headBar_.style.textAlign = 'center'; }, }; return TabView; })(); // 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. /** * A TimelineDataSeries collects an ordered series of (time, value) pairs, * and converts them to graph points. It also keeps track of its color and * current visibility state. * It keeps MAX_STATS_DATA_POINT_BUFFER_SIZE data points at most. Old data * points will be dropped when it reaches this size. */ var TimelineDataSeries = (function() { 'use strict'; /** * @constructor */ function TimelineDataSeries() { // List of DataPoints in chronological order. this.dataPoints_ = []; // Default color. Should always be overridden prior to display. this.color_ = 'red'; // Whether or not the data series should be drawn. this.isVisible_ = true; this.cacheStartTime_ = null; this.cacheStepSize_ = 0; this.cacheValues_ = []; } TimelineDataSeries.prototype = { /** * @override */ toJSON: function() { if (this.dataPoints_.length < 1) return {}; var values = []; for (var i = 0; i < this.dataPoints_.length; ++i) { values.push(this.dataPoints_[i].value); } return { startTime: this.dataPoints_[0].time, endTime: this.dataPoints_[this.dataPoints_.length - 1].time, values: JSON.stringify(values), }; }, /** * Adds a DataPoint to |this| with the specified time and value. * DataPoints are assumed to be received in chronological order. */ addPoint: function(timeTicks, value) { var time = new Date(timeTicks); this.dataPoints_.push(new DataPoint(time, value)); if (this.dataPoints_.length > MAX_STATS_DATA_POINT_BUFFER_SIZE) this.dataPoints_.shift(); }, isVisible: function() { return this.isVisible_; }, show: function(isVisible) { this.isVisible_ = isVisible; }, getColor: function() { return this.color_; }, setColor: function(color) { this.color_ = color; }, getCount: function() { return this.dataPoints_.length; }, /** * Returns a list containing the values of the data series at |count| * points, starting at |startTime|, and |stepSize| milliseconds apart. * Caches values, so showing/hiding individual data series is fast. */ getValues: function(startTime, stepSize, count) { // Use cached values, if we can. if (this.cacheStartTime_ == startTime && this.cacheStepSize_ == stepSize && this.cacheValues_.length == count) { return this.cacheValues_; } // Do all the work. this.cacheValues_ = this.getValuesInternal_(startTime, stepSize, count); this.cacheStartTime_ = startTime; this.cacheStepSize_ = stepSize; return this.cacheValues_; }, /** * Returns the cached |values| in the specified time period. */ getValuesInternal_: function(startTime, stepSize, count) { var values = []; var nextPoint = 0; var currentValue = 0; var time = startTime; for (var i = 0; i < count; ++i) { while (nextPoint < this.dataPoints_.length && this.dataPoints_[nextPoint].time < time) { currentValue = this.dataPoints_[nextPoint].value; ++nextPoint; } values[i] = currentValue; time += stepSize; } return values; } }; /** * A single point in a data series. Each point has a time, in the form of * milliseconds since the Unix epoch, and a numeric value. * @constructor */ function DataPoint(time, value) { this.time = time; this.value = value; } return TimelineDataSeries; })(); // 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. /** * Get the ssrc if |report| is an ssrc report. * * @param {!Object} report The object 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. * @return {?string} The ssrc. */ function GetSsrcFromReport(report) { if (report.type != 'ssrc') { console.warn("Trying to get ssrc from non-ssrc report."); return null; } // If the 'ssrc' name-value pair exists, return the value; otherwise, return // the report id. // The 'ssrc' name-value pair only exists in an upcoming Libjingle change. Old // versions use id to refer to the ssrc. // // TODO(jiayl): remove the fallback to id once the Libjingle change is rolled // to Chrome. if (report.stats && report.stats.values) { for (var i = 0; i < report.stats.values.length - 1; i += 2) { if (report.stats.values[i] == 'ssrc') { return report.stats.values[i + 1]; } } } return report.id; }; /** * SsrcInfoManager stores the ssrc stream info extracted from SDP. */ var SsrcInfoManager = (function() { 'use strict'; /** * @constructor */ function SsrcInfoManager() { /** * Map from ssrc id to an object containing all the stream properties. * @type {!Object>} * @private */ this.streamInfoContainer_ = {}; /** * The string separating attibutes in an SDP. * @type {string} * @const * @private */ this.ATTRIBUTE_SEPARATOR_ = /[\r,\n]/; /** * The regex separating fields within an ssrc description. * @type {RegExp} * @const * @private */ this.FIELD_SEPARATOR_REGEX_ = / .*:/; /** * The prefix string of an ssrc description. * @type {string} * @const * @private */ this.SSRC_ATTRIBUTE_PREFIX_ = 'a=ssrc:'; /** * The className of the ssrc info parent element. * @type {string} * @const */ this.SSRC_INFO_BLOCK_CLASS = 'ssrc-info-block'; } SsrcInfoManager.prototype = { /** * Extracts the stream information from |sdp| and saves it. * For example: * a=ssrc:1234 msid:abcd * a=ssrc:1234 label:hello * * @param {string} sdp The SDP string. */ addSsrcStreamInfo: function(sdp) { var attributes = sdp.split(this.ATTRIBUTE_SEPARATOR_); for (var i = 0; i < attributes.length; ++i) { // Check if this is a ssrc attribute. if (attributes[i].indexOf(this.SSRC_ATTRIBUTE_PREFIX_) != 0) continue; var nextFieldIndex = attributes[i].search(this.FIELD_SEPARATOR_REGEX_); if (nextFieldIndex == -1) continue; var ssrc = attributes[i].substring(this.SSRC_ATTRIBUTE_PREFIX_.length, nextFieldIndex); if (!this.streamInfoContainer_[ssrc]) this.streamInfoContainer_[ssrc] = {}; // Make |rest| starting at the next field. var rest = attributes[i].substring(nextFieldIndex + 1); var name, value; while (rest.length > 0) { nextFieldIndex = rest.search(this.FIELD_SEPARATOR_REGEX_); if (nextFieldIndex == -1) nextFieldIndex = rest.length; // The field name is the string before the colon. name = rest.substring(0, rest.indexOf(':')); // The field value is from after the colon to the next field. value = rest.substring(rest.indexOf(':') + 1, nextFieldIndex); this.streamInfoContainer_[ssrc][name] = value; // Move |rest| to the start of the next field. rest = rest.substring(nextFieldIndex + 1); } } }, /** * @param {string} sdp The ssrc id. * @return {!Object} The object containing the ssrc infomation. */ getStreamInfo: function(ssrc) { return this.streamInfoContainer_[ssrc]; }, /** * Populate the ssrc information into |parentElement|, each field as a * DIV element. * * @param {!Element} parentElement The parent element for the ssrc info. * @param {string} ssrc The ssrc id. */ populateSsrcInfo: function(parentElement, ssrc) { if (!this.streamInfoContainer_[ssrc]) return; parentElement.className = this.SSRC_INFO_BLOCK_CLASS; var fieldElement; for (var property in this.streamInfoContainer_[ssrc]) { fieldElement = document.createElement('div'); parentElement.appendChild(fieldElement); fieldElement.textContent = property + ':' + this.streamInfoContainer_[ssrc][property]; } } }; return SsrcInfoManager; })(); // 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. // // This file contains helper methods to draw the stats timeline graphs. // Each graph represents a series of stats report for a PeerConnection, // e.g. 1234-0-ssrc-abcd123-bytesSent is the graph for the series of bytesSent // for ssrc-abcd123 of PeerConnection 0 in process 1234. // The graphs are drawn as CANVAS, grouped per report type per PeerConnection. // Each group has an expand/collapse button and is collapsed initially. // // 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. /** * A TimelineGraphView displays a timeline graph on a canvas element. */ var TimelineGraphView = (function() { 'use strict'; // Maximum number of labels placed vertically along the sides of the graph. var MAX_VERTICAL_LABELS = 6; // Vertical spacing between labels and between the graph and labels. var LABEL_VERTICAL_SPACING = 4; // Horizontal spacing between vertically placed labels and the edges of the // graph. var LABEL_HORIZONTAL_SPACING = 3; // Horizintal spacing between two horitonally placed labels along the bottom // of the graph. var LABEL_LABEL_HORIZONTAL_SPACING = 25; // Length of ticks, in pixels, next to y-axis labels. The x-axis only has // one set of labels, so it can use lines instead. var Y_AXIS_TICK_LENGTH = 10; var GRID_COLOR = '#CCC'; var TEXT_COLOR = '#000'; var BACKGROUND_COLOR = '#FFF'; var MAX_DECIMAL_PRECISION = 2; /** * @constructor */ function TimelineGraphView(divId, canvasId) { this.scrollbar_ = {position_: 0, range_: 0}; this.graphDiv_ = $(divId); this.canvas_ = $(canvasId); // Set the range and scale of the graph. Times are in milliseconds since // the Unix epoch. // All measurements we have must be after this time. this.startTime_ = 0; // The current rightmost position of the graph is always at most this. this.endTime_ = 1; this.graph_ = null; // Horizontal scale factor, in terms of milliseconds per pixel. this.scale_ = 1000; // Initialize the scrollbar. this.updateScrollbarRange_(true); } TimelineGraphView.prototype = { setScale: function(scale) { this.scale_ = scale; }, // Returns the total length of the graph, in pixels. getLength_: function() { var timeRange = this.endTime_ - this.startTime_; // Math.floor is used to ignore the last partial area, of length less // than this.scale_. return Math.floor(timeRange / this.scale_); }, /** * Returns true if the graph is scrolled all the way to the right. */ graphScrolledToRightEdge_: function() { return this.scrollbar_.position_ == this.scrollbar_.range_; }, /** * Update the range of the scrollbar. If |resetPosition| is true, also * sets the slider to point at the rightmost position and triggers a * repaint. */ updateScrollbarRange_: function(resetPosition) { var scrollbarRange = this.getLength_() - this.canvas_.width; if (scrollbarRange < 0) scrollbarRange = 0; // If we've decreased the range to less than the current scroll position, // we need to move the scroll position. if (this.scrollbar_.position_ > scrollbarRange) resetPosition = true; this.scrollbar_.range_ = scrollbarRange; if (resetPosition) { this.scrollbar_.position_ = scrollbarRange; this.repaint(); } }, /** * Sets the date range displayed on the graph, switches to the default * scale factor, and moves the scrollbar all the way to the right. */ setDateRange: function(startDate, endDate) { this.startTime_ = startDate.getTime(); this.endTime_ = endDate.getTime(); // Safety check. if (this.endTime_ <= this.startTime_) this.startTime_ = this.endTime_ - 1; this.updateScrollbarRange_(true); }, /** * Updates the end time at the right of the graph to be the current time. * Specifically, updates the scrollbar's range, and if the scrollbar is * all the way to the right, keeps it all the way to the right. Otherwise, * leaves the view as-is and doesn't redraw anything. */ updateEndDate: function(opt_date) { this.endTime_ = opt_date || (new Date()).getTime(); this.updateScrollbarRange_(this.graphScrolledToRightEdge_()); }, getStartDate: function() { return new Date(this.startTime_); }, /** * Replaces the current TimelineDataSeries with |dataSeries|. */ setDataSeries: function(dataSeries) { // Simply recreates the Graph. this.graph_ = new Graph(); for (var i = 0; i < dataSeries.length; ++i) this.graph_.addDataSeries(dataSeries[i]); this.repaint(); }, /** * Adds |dataSeries| to the current graph. */ addDataSeries: function(dataSeries) { if (!this.graph_) this.graph_ = new Graph(); this.graph_.addDataSeries(dataSeries); this.repaint(); }, /** * Draws the graph on |canvas_|. */ repaint: function() { this.repaintTimerRunning_ = false; var width = this.canvas_.width; var height = this.canvas_.height; var context = this.canvas_.getContext('2d'); // Clear the canvas. context.fillStyle = BACKGROUND_COLOR; context.fillRect(0, 0, width, height); // Try to get font height in pixels. Needed for layout. var fontHeightString = context.font.match(/([0-9]+)px/)[1]; var fontHeight = parseInt(fontHeightString); // Safety check, to avoid drawing anything too ugly. if (fontHeightString.length == 0 || fontHeight <= 0 || fontHeight * 4 > height || width < 50) { return; } // Save current transformation matrix so we can restore it later. context.save(); // The center of an HTML canvas pixel is technically at (0.5, 0.5). This // makes near straight lines look bad, due to anti-aliasing. This // translation reduces the problem a little. context.translate(0.5, 0.5); // Figure out what time values to display. var position = this.scrollbar_.position_; // If the entire time range is being displayed, align the right edge of // the graph to the end of the time range. if (this.scrollbar_.range_ == 0) position = this.getLength_() - this.canvas_.width; var visibleStartTime = this.startTime_ + position * this.scale_; // Make space at the bottom of the graph for the time labels, and then // draw the labels. var textHeight = height; height -= fontHeight + LABEL_VERTICAL_SPACING; this.drawTimeLabels(context, width, height, textHeight, visibleStartTime); // Draw outline of the main graph area. context.strokeStyle = GRID_COLOR; context.strokeRect(0, 0, width - 1, height - 1); if (this.graph_) { // Layout graph and have them draw their tick marks. this.graph_.layout( width, height, fontHeight, visibleStartTime, this.scale_); this.graph_.drawTicks(context); // Draw the lines of all graphs, and then draw their labels. this.graph_.drawLines(context); this.graph_.drawLabels(context); } // Restore original transformation matrix. context.restore(); }, /** * Draw time labels below the graph. Takes in start time as an argument * since it may not be |startTime_|, when we're displaying the entire * time range. */ drawTimeLabels: function(context, width, height, textHeight, startTime) { // Draw the labels 1 minute apart. var timeStep = 1000 * 60; // Find the time for the first label. This time is a perfect multiple of // timeStep because of how UTC times work. var time = Math.ceil(startTime / timeStep) * timeStep; context.textBaseline = 'bottom'; context.textAlign = 'center'; context.fillStyle = TEXT_COLOR; context.strokeStyle = GRID_COLOR; // Draw labels and vertical grid lines. while (true) { var x = Math.round((time - startTime) / this.scale_); if (x >= width) break; var text = (new Date(time)).toLocaleTimeString(); context.fillText(text, x, textHeight); context.beginPath(); context.lineTo(x, 0); context.lineTo(x, height); context.stroke(); time += timeStep; } }, getDataSeriesCount: function() { if (this.graph_) return this.graph_.dataSeries_.length; return 0; }, hasDataSeries: function(dataSeries) { if (this.graph_) return this.graph_.hasDataSeries(dataSeries); return false; }, }; /** * A Graph is responsible for drawing all the TimelineDataSeries that have * the same data type. Graphs are responsible for scaling the values, laying * out labels, and drawing both labels and lines for its data series. */ var Graph = (function() { /** * @constructor */ function Graph() { this.dataSeries_ = []; // Cached properties of the graph, set in layout. this.width_ = 0; this.height_ = 0; this.fontHeight_ = 0; this.startTime_ = 0; this.scale_ = 0; // The lowest/highest values adjusted by the vertical label step size // in the displayed range of the graph. Used for scaling and setting // labels. Set in layoutLabels. this.min_ = 0; this.max_ = 0; // Cached text of equally spaced labels. Set in layoutLabels. this.labels_ = []; } /** * A Label is the label at a particular position along the y-axis. * @constructor */ function Label(height, text) { this.height = height; this.text = text; } Graph.prototype = { addDataSeries: function(dataSeries) { this.dataSeries_.push(dataSeries); }, hasDataSeries: function(dataSeries) { for (var i = 0; i < this.dataSeries_.length; ++i) { if (this.dataSeries_[i] == dataSeries) return true; } return false; }, /** * Returns a list of all the values that should be displayed for a given * data series, using the current graph layout. */ getValues: function(dataSeries) { if (!dataSeries.isVisible()) return null; return dataSeries.getValues(this.startTime_, this.scale_, this.width_); }, /** * Updates the graph's layout. In particular, both the max value and * label positions are updated. Must be called before calling any of the * drawing functions. */ layout: function(width, height, fontHeight, startTime, scale) { this.width_ = width; this.height_ = height; this.fontHeight_ = fontHeight; this.startTime_ = startTime; this.scale_ = scale; // Find largest value. var max = 0, min = 0; for (var i = 0; i < this.dataSeries_.length; ++i) { var values = this.getValues(this.dataSeries_[i]); if (!values) continue; for (var j = 0; j < values.length; ++j) { if (values[j] > max) max = values[j]; else if (values[j] < min) min = values[j]; } } this.layoutLabels_(min, max); }, /** * Lays out labels and sets |max_|/|min_|, taking the time units into * consideration. |maxValue| is the actual maximum value, and * |max_| will be set to the value of the largest label, which * will be at least |maxValue|. Similar for |min_|. */ layoutLabels_: function(minValue, maxValue) { if (maxValue - minValue < 1024) { this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION); return; } // Find appropriate units to use. var units = ['', 'k', 'M', 'G', 'T', 'P']; // Units to use for labels. 0 is '1', 1 is K, etc. // We start with 1, and work our way up. var unit = 1; minValue /= 1024; maxValue /= 1024; while (units[unit + 1] && maxValue - minValue >= 1024) { minValue /= 1024; maxValue /= 1024; ++unit; } // Calculate labels. this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION); // Append units to labels. for (var i = 0; i < this.labels_.length; ++i) this.labels_[i] += ' ' + units[unit]; // Convert |min_|/|max_| back to unit '1'. this.min_ *= Math.pow(1024, unit); this.max_ *= Math.pow(1024, unit); }, /** * Same as layoutLabels_, but ignores units. |maxDecimalDigits| is the * maximum number of decimal digits allowed. The minimum allowed * difference between two adjacent labels is 10^-|maxDecimalDigits|. */ layoutLabelsBasic_: function(minValue, maxValue, maxDecimalDigits) { this.labels_ = []; var range = maxValue - minValue; // No labels if the range is 0. if (range == 0) { this.min_ = this.max_ = maxValue; return; } // The maximum number of equally spaced labels allowed. |fontHeight_| // is doubled because the top two labels are both drawn in the same // gap. var minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING; // The + 1 is for the top label. var maxLabels = 1 + this.height_ / minLabelSpacing; if (maxLabels < 2) { maxLabels = 2; } else if (maxLabels > MAX_VERTICAL_LABELS) { maxLabels = MAX_VERTICAL_LABELS; } // Initial try for step size between conecutive labels. var stepSize = Math.pow(10, -maxDecimalDigits); // Number of digits to the right of the decimal of |stepSize|. // Used for formating label strings. var stepSizeDecimalDigits = maxDecimalDigits; // Pick a reasonable step size. while (true) { // If we use a step size of |stepSize| between labels, we'll need: // // Math.ceil(range / stepSize) + 1 // // labels. The + 1 is because we need labels at both at 0 and at // the top of the graph. // Check if we can use steps of size |stepSize|. if (Math.ceil(range / stepSize) + 1 <= maxLabels) break; // Check |stepSize| * 2. if (Math.ceil(range / (stepSize * 2)) + 1 <= maxLabels) { stepSize *= 2; break; } // Check |stepSize| * 5. if (Math.ceil(range / (stepSize * 5)) + 1 <= maxLabels) { stepSize *= 5; break; } stepSize *= 10; if (stepSizeDecimalDigits > 0) --stepSizeDecimalDigits; } // Set the min/max so it's an exact multiple of the chosen step size. this.max_ = Math.ceil(maxValue / stepSize) * stepSize; this.min_ = Math.floor(minValue / stepSize) * stepSize; // Create labels. for (var label = this.max_; label >= this.min_; label -= stepSize) this.labels_.push(label.toFixed(stepSizeDecimalDigits)); }, /** * Draws tick marks for each of the labels in |labels_|. */ drawTicks: function(context) { var x1; var x2; x1 = this.width_ - 1; x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH; context.fillStyle = GRID_COLOR; context.beginPath(); for (var i = 1; i < this.labels_.length - 1; ++i) { // The rounding is needed to avoid ugly 2-pixel wide anti-aliased // lines. var y = Math.round(this.height_ * i / (this.labels_.length - 1)); context.moveTo(x1, y); context.lineTo(x2, y); } context.stroke(); }, /** * Draws a graph line for each of the data series. */ drawLines: function(context) { // Factor by which to scale all values to convert them to a number from // 0 to height - 1. var scale = 0; var bottom = this.height_ - 1; if (this.max_) scale = bottom / (this.max_ - this.min_); // Draw in reverse order, so earlier data series are drawn on top of // subsequent ones. for (var i = this.dataSeries_.length - 1; i >= 0; --i) { var values = this.getValues(this.dataSeries_[i]); if (!values) continue; context.strokeStyle = this.dataSeries_[i].getColor(); context.beginPath(); for (var x = 0; x < values.length; ++x) { // The rounding is needed to avoid ugly 2-pixel wide anti-aliased // horizontal lines. context.lineTo( x, bottom - Math.round((values[x] - this.min_) * scale)); } context.stroke(); } }, /** * Draw labels in |labels_|. */ drawLabels: function(context) { if (this.labels_.length == 0) return; var x = this.width_ - LABEL_HORIZONTAL_SPACING; // Set up the context. context.fillStyle = TEXT_COLOR; context.textAlign = 'right'; // Draw top label, which is the only one that appears below its tick // mark. context.textBaseline = 'top'; context.fillText(this.labels_[0], x, 0); // Draw all the other labels. context.textBaseline = 'bottom'; var step = (this.height_ - 1) / (this.labels_.length - 1); for (var i = 1; i < this.labels_.length; ++i) context.fillText(this.labels_[i], x, step * i); } }; return Graph; })(); return TimelineGraphView; })(); var STATS_GRAPH_CONTAINER_HEADING_CLASS = 'stats-graph-container-heading'; var RECEIVED_PROPAGATION_DELTA_LABEL = 'googReceivedPacketGroupPropagationDeltaDebug'; var RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL = 'googReceivedPacketGroupArrivalTimeDebug'; // Specifies which stats should be drawn on the 'bweCompound' graph and how. var bweCompoundGraphConfig = { googAvailableSendBandwidth: {color: 'red'}, googTargetEncBitrateCorrected: {color: 'purple'}, googActualEncBitrate: {color: 'orange'}, googRetransmitBitrate: {color: 'blue'}, googTransmitBitrate: {color: 'green'}, }; // Converts the last entry of |srcDataSeries| from the total amount to the // amount per second. var totalToPerSecond = function(srcDataSeries) { var length = srcDataSeries.dataPoints_.length; if (length >= 2) { var lastDataPoint = srcDataSeries.dataPoints_[length - 1]; var secondLastDataPoint = srcDataSeries.dataPoints_[length - 2]; return (lastDataPoint.value - secondLastDataPoint.value) * 1000 / (lastDataPoint.time - secondLastDataPoint.time); } return 0; }; // Converts the value of total bytes to bits per second. var totalBytesToBitsPerSecond = function(srcDataSeries) { return totalToPerSecond(srcDataSeries) * 8; }; // Specifies which stats should be converted before drawn and how. // |convertedName| is the name of the converted value, |convertFunction| // is the function used to calculate the new converted value based on the // original dataSeries. var dataConversionConfig = { packetsSent: { convertedName: 'packetsSentPerSecond', convertFunction: totalToPerSecond, }, bytesSent: { convertedName: 'bitsSentPerSecond', convertFunction: totalBytesToBitsPerSecond, }, packetsReceived: { convertedName: 'packetsReceivedPerSecond', convertFunction: totalToPerSecond, }, bytesReceived: { convertedName: 'bitsReceivedPerSecond', convertFunction: totalBytesToBitsPerSecond, }, // This is due to a bug of wrong units reported for googTargetEncBitrate. // TODO (jiayl): remove this when the unit bug is fixed. googTargetEncBitrate: { convertedName: 'googTargetEncBitrateCorrected', convertFunction: function (srcDataSeries) { var length = srcDataSeries.dataPoints_.length; var lastDataPoint = srcDataSeries.dataPoints_[length - 1]; if (lastDataPoint.value < 5000) return lastDataPoint.value * 1000; return lastDataPoint.value; } } }; // The object contains the stats names that should not be added to the graph, // even if they are numbers. var statsNameBlackList = { 'ssrc': true, 'googTrackId': true, 'googComponent': true, 'googLocalAddress': true, 'googRemoteAddress': true, 'googFingerprint': true, }; var graphViews = {}; // Returns number parsed from |value|, or NaN if the stats name is black-listed. function getNumberFromValue(name, value) { if (statsNameBlackList[name]) return NaN; return parseFloat(value); } // Adds the stats report |report| to the timeline graph for the given // |peerConnectionElement|. function drawSingleReport(peerConnectionElement, report) { var reportType = report.type; var reportId = report.id; var stats = report.stats; if (!stats || !stats.values) return; for (var i = 0; i < stats.values.length - 1; i = i + 2) { var rawLabel = stats.values[i]; // Propagation deltas are handled separately. if (rawLabel == RECEIVED_PROPAGATION_DELTA_LABEL) { drawReceivedPropagationDelta( peerConnectionElement, report, stats.values[i + 1]); continue; } var rawDataSeriesId = reportId + '-' + rawLabel; var rawValue = getNumberFromValue(rawLabel, stats.values[i + 1]); if (isNaN(rawValue)) { // We do not draw non-numerical values, but still want to record it in the // data series. addDataSeriesPoints(peerConnectionElement, rawDataSeriesId, rawLabel, [stats.timestamp], [stats.values[i + 1]]); continue; } var finalDataSeriesId = rawDataSeriesId; var finalLabel = rawLabel; var finalValue = rawValue; // We need to convert the value if dataConversionConfig[rawLabel] exists. if (dataConversionConfig[rawLabel]) { // Updates the original dataSeries before the conversion. addDataSeriesPoints(peerConnectionElement, rawDataSeriesId, rawLabel, [stats.timestamp], [rawValue]); // Convert to another value to draw on graph, using the original // dataSeries as input. finalValue = dataConversionConfig[rawLabel].convertFunction( peerConnectionDataStore[peerConnectionElement.id].getDataSeries( rawDataSeriesId)); finalLabel = dataConversionConfig[rawLabel].convertedName; finalDataSeriesId = reportId + '-' + finalLabel; } // Updates the final dataSeries to draw. addDataSeriesPoints(peerConnectionElement, finalDataSeriesId, finalLabel, [stats.timestamp], [finalValue]); // Updates the graph. var graphType = bweCompoundGraphConfig[finalLabel] ? 'bweCompound' : finalLabel; var graphViewId = peerConnectionElement.id + '-' + reportId + '-' + graphType; if (!graphViews[graphViewId]) { graphViews[graphViewId] = createStatsGraphView(peerConnectionElement, report, graphType); var date = new Date(stats.timestamp); graphViews[graphViewId].setDateRange(date, date); } // Adds the new dataSeries to the graphView. We have to do it here to cover // both the simple and compound graph cases. var dataSeries = peerConnectionDataStore[peerConnectionElement.id].getDataSeries( finalDataSeriesId); if (!graphViews[graphViewId].hasDataSeries(dataSeries)) graphViews[graphViewId].addDataSeries(dataSeries); graphViews[graphViewId].updateEndDate(); } } // Makes sure the TimelineDataSeries with id |dataSeriesId| is created, // and adds the new data points to it. |times| is the list of timestamps for // each data point, and |values| is the list of the data point values. function addDataSeriesPoints( peerConnectionElement, dataSeriesId, label, times, values) { var dataSeries = peerConnectionDataStore[peerConnectionElement.id].getDataSeries( dataSeriesId); if (!dataSeries) { dataSeries = new TimelineDataSeries(); peerConnectionDataStore[peerConnectionElement.id].setDataSeries( dataSeriesId, dataSeries); if (bweCompoundGraphConfig[label]) { dataSeries.setColor(bweCompoundGraphConfig[label].color); } } for (var i = 0; i < times.length; ++i) dataSeries.addPoint(times[i], values[i]); } // Draws the received propagation deltas using the packet group arrival time as // the x-axis. For example, |report.stats.values| should be like // ['googReceivedPacketGroupArrivalTimeDebug', '[123456, 234455, 344566]', // 'googReceivedPacketGroupPropagationDeltaDebug', '[23, 45, 56]', ...]. function drawReceivedPropagationDelta(peerConnectionElement, report, deltas) { var reportId = report.id; var stats = report.stats; var times = null; // Find the packet group arrival times. for (var i = 0; i < stats.values.length - 1; i = i + 2) { if (stats.values[i] == RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL) { times = stats.values[i + 1]; break; } } // Unexpected. if (times == null) return; // Convert |deltas| and |times| from strings to arrays of numbers. try { deltas = JSON.parse(deltas); times = JSON.parse(times); } catch (e) { console.log(e); return; } // Update the data series. var dataSeriesId = reportId + '-' + RECEIVED_PROPAGATION_DELTA_LABEL; addDataSeriesPoints( peerConnectionElement, dataSeriesId, RECEIVED_PROPAGATION_DELTA_LABEL, times, deltas); // Update the graph. var graphViewId = peerConnectionElement.id + '-' + reportId + '-' + RECEIVED_PROPAGATION_DELTA_LABEL; var date = new Date(times[times.length - 1]); if (!graphViews[graphViewId]) { graphViews[graphViewId] = createStatsGraphView( peerConnectionElement, report, RECEIVED_PROPAGATION_DELTA_LABEL); graphViews[graphViewId].setScale(10); graphViews[graphViewId].setDateRange(date, date); var dataSeries = peerConnectionDataStore[peerConnectionElement.id] .getDataSeries(dataSeriesId); graphViews[graphViewId].addDataSeries(dataSeries); } graphViews[graphViewId].updateEndDate(date); } // Ensures a div container to hold all stats graphs for one track is created as // a child of |peerConnectionElement|. function ensureStatsGraphTopContainer(peerConnectionElement, report) { var containerId = peerConnectionElement.id + '-' + report.type + '-' + report.id + '-graph-container'; var container = $(containerId); if (!container) { container = document.createElement('details'); container.id = containerId; container.className = 'stats-graph-container'; peerConnectionElement.appendChild(container); container.innerHTML =''; container.firstChild.firstChild.className = STATS_GRAPH_CONTAINER_HEADING_CLASS; container.firstChild.firstChild.textContent = 'Stats graphs for ' + report.id; if (report.type == 'ssrc') { var ssrcInfoElement = document.createElement('div'); container.firstChild.appendChild(ssrcInfoElement); ssrcInfoManager.populateSsrcInfo(ssrcInfoElement, GetSsrcFromReport(report)); } } return container; } // Creates the container elements holding a timeline graph // and the TimelineGraphView object. function createStatsGraphView( peerConnectionElement, report, statsName) { var topContainer = ensureStatsGraphTopContainer(peerConnectionElement, report); var graphViewId = peerConnectionElement.id + '-' + report.id + '-' + statsName; var divId = graphViewId + '-div'; var canvasId = graphViewId + '-canvas'; var container = document.createElement("div"); container.className = 'stats-graph-sub-container'; topContainer.appendChild(container); container.innerHTML = '
            ' + statsName + '
            ' + '
            '; if (statsName == 'bweCompound') { container.insertBefore( createBweCompoundLegend(peerConnectionElement, report.id), $(divId)); } return new TimelineGraphView(divId, canvasId); } // Creates the legend section for the bweCompound graph. // Returns the legend element. function createBweCompoundLegend(peerConnectionElement, reportId) { var legend = document.createElement('div'); for (var prop in bweCompoundGraphConfig) { var div = document.createElement('div'); legend.appendChild(div); div.innerHTML = '' + prop; div.style.color = bweCompoundGraphConfig[prop].color; div.dataSeriesId = reportId + '-' + prop; div.graphViewId = peerConnectionElement.id + '-' + reportId + '-bweCompound'; div.firstChild.addEventListener('click', function(event) { var target = peerConnectionDataStore[peerConnectionElement.id].getDataSeries( event.target.parentNode.dataSeriesId); target.show(event.target.checked); graphViews[event.target.parentNode.graphViewId].repaint(); }); } return legend; } // 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. /** * Maintains the stats table. * @param {SsrcInfoManager} ssrcInfoManager The source of the ssrc info. */ var StatsTable = (function(ssrcInfoManager) { 'use strict'; /** * @param {SsrcInfoManager} ssrcInfoManager The source of the ssrc info. * @constructor */ function StatsTable(ssrcInfoManager) { /** * @type {SsrcInfoManager} * @private */ this.ssrcInfoManager_ = ssrcInfoManager; } StatsTable.prototype = { /** * Adds |report| to the stats table of |peerConnectionElement|. * * @param {!Element} peerConnectionElement The root element. * @param {!Object} report The object containing stats, which 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. */ addStatsReport: function(peerConnectionElement, report) { var statsTable = this.ensureStatsTable_(peerConnectionElement, report); if (report.stats) { this.addStatsToTable_(statsTable, report.stats.timestamp, report.stats.values); } }, /** * Ensure the DIV container for the stats tables is created as a child of * |peerConnectionElement|. * * @param {!Element} peerConnectionElement The root element. * @return {!Element} The stats table container. * @private */ ensureStatsTableContainer_: function(peerConnectionElement) { var containerId = peerConnectionElement.id + '-table-container'; var container = $(containerId); if (!container) { container = document.createElement('div'); container.id = containerId; container.className = 'stats-table-container'; var head = document.createElement('div'); head.textContent = 'Stats Tables'; container.appendChild(head); peerConnectionElement.appendChild(container); } return container; }, /** * Ensure the stats table for track specified by |report| of PeerConnection * |peerConnectionElement| is created. * * @param {!Element} peerConnectionElement The root element. * @param {!Object} report The object containing stats, which 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. * @return {!Element} The stats table element. * @private */ ensureStatsTable_: function(peerConnectionElement, report) { var tableId = peerConnectionElement.id + '-table-' + report.id; var table = $(tableId); if (!table) { var container = this.ensureStatsTableContainer_(peerConnectionElement); var details = document.createElement('details'); container.appendChild(details); var summary = document.createElement('summary'); summary.textContent = report.id; details.appendChild(summary); table = document.createElement('table'); details.appendChild(table); table.id = tableId; table.border = 1; table.innerHTML = ''; table.rows[0].cells[0].textContent = 'Statistics ' + report.id; if (report.type == 'ssrc') { table.insertRow(1); table.rows[1].innerHTML = ''; this.ssrcInfoManager_.populateSsrcInfo( table.rows[1].cells[0], GetSsrcFromReport(report)); } } return table; }, /** * Update |statsTable| with |time| and |statsData|. * * @param {!Element} statsTable Which table to update. * @param {number} time The number of miliseconds since epoch. * @param {Array} statsData An array of stats name and value pairs. * @private */ addStatsToTable_: function(statsTable, time, statsData) { var date = new Date(time); this.updateStatsTableRow_(statsTable, 'timestamp', date.toLocaleString()); for (var i = 0; i < statsData.length - 1; i = i + 2) { this.updateStatsTableRow_(statsTable, statsData[i], statsData[i + 1]); } }, /** * Update the value column of the stats row of |rowName| to |value|. * A new row is created is this is the first report of this stats. * * @param {!Element} statsTable Which table to update. * @param {string} rowName The name of the row to update. * @param {string} value The new value to set. * @private */ updateStatsTableRow_: function(statsTable, rowName, value) { var trId = statsTable.id + '-' + rowName; var trElement = $(trId); if (!trElement) { trElement = document.createElement('tr'); trElement.id = trId; statsTable.firstChild.appendChild(trElement); trElement.innerHTML = '' + rowName + ''; } trElement.cells[1].textContent = value; // Highlights the table for the active connection. if (rowName == 'googActiveConnection' && value == true) statsTable.parentElement.classList.add('stats-table-active-connection'); } }; return StatsTable; })(); // 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. /** * 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. * @constructor */ var PeerConnectionUpdateEntry = function(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. */ var PeerConnectionUpdateTable = (function() { 'use strict'; /** * @constructor */ function PeerConnectionUpdateTable() { /** * @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'; } PeerConnectionUpdateTable.prototype = { /** * 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: function(peerConnectionElement, update) { var tableElement = this.ensureUpdateContainer_(peerConnectionElement); var row = document.createElement('tr'); tableElement.firstChild.appendChild(row); var time = new Date(parseFloat(update.time)); row.innerHTML = '' + time.toLocaleString() + ''; if (update.value.length == 0) { row.innerHTML += '' + update.type + ''; return; } row.innerHTML += '
            ' + update.type + '
            '; var valueContainer = document.createElement('pre'); var details = row.cells[1].childNodes[0]; details.appendChild(valueContainer); 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_: function(peerConnectionElement) { var tableId = peerConnectionElement.id + this.UPDATE_LOG_ID_SUFFIX_; var tableElement = $(tableId); if (!tableElement) { var 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.innerHTML = 'Time' + 'Event'; } return tableElement; } }; return PeerConnectionUpdateTable; })(); // 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. /** * Provides the UI for dump creation. */ var DumpCreator = (function() { /** * @param {Element} containerElement The parent element of the dump creation * UI. * @constructor */ function DumpCreator(containerElement) { /** * The root element of the dump creation UI. * @type {Element} * @private */ this.root_ = document.createElement('details'); this.root_.className = 'peer-connection-dump-root'; containerElement.appendChild(this.root_); var summary = document.createElement('summary'); this.root_.appendChild(summary); summary.textContent = 'Create Dump'; var content = document.createElement('div'); this.root_.appendChild(content); content.innerHTML = '' + '

            ' + '

            A diagnostic audio recording is used for analyzing audio' + ' problems. It contains the audio played out from the speaker and' + ' recorded from the microphone and is saved to the local disk.' + ' Checking this box will enable the recording for ongoing WebRTC' + ' calls and for future WebRTC calls. When the box is unchecked or' + ' this page is closed, all ongoing recordings will be stopped and' + ' this recording functionality will be disabled for future WebRTC' + ' calls. Recordings in multiple tabs are supported as well as' + ' multiple recordings in the same tab. When enabling, you select a' + ' base filename to save the dump(s) to. The base filename will have a' + ' suffix appended to it as <base filename>.<render process' + ' ID>.<recording ID>. If recordings are' + ' disabled and then enabled using the same base filename, the' + ' file(s) will be appended to and may become invalid. It is' + ' recommended to choose a new base filename each time or move' + ' the resulting files before enabling again. If track processing is' + ' disabled (--disable-audio-track-processing): (1) Only one recording' + ' per render process is supported. (2) When the box is unchecked or' + ' this page is closed, ongoing recordings will continue until the' + ' call ends or the page with the recording is closed

            '; content.getElementsByTagName('a')[0].addEventListener( 'click', this.onDownloadData_.bind(this)); content.getElementsByTagName('input')[0].addEventListener( 'click', this.onAecRecordingChanged_.bind(this)); } DumpCreator.prototype = { // Mark the AEC recording checkbox checked. enableAecRecording: function() { this.root_.getElementsByTagName('input')[0].checked = true; }, // Mark the AEC recording checkbox unchecked. disableAecRecording: function() { this.root_.getElementsByTagName('input')[0].checked = false; }, /** * Downloads the PeerConnection updates and stats data as a file. * * @private */ onDownloadData_: function() { var dump_object = { 'getUserMedia': userMediaRequests, 'PeerConnections': peerConnectionDataStore, }; var textBlob = new Blob([JSON.stringify(dump_object, null, ' ')], {type: 'octet/stream'}); var URL = window.URL.createObjectURL(textBlob); var anchor = this.root_.getElementsByTagName('a')[0]; anchor.href = URL; anchor.download = 'webrtc_internals_dump.txt'; // The default action of the anchor will download the URL. }, /** * Handles the event of toggling the AEC recording state. * * @private */ onAecRecordingChanged_: function() { var enabled = this.root_.getElementsByTagName('input')[0].checked; if (enabled) { chrome.send('enableAecRecording'); } else { chrome.send('disableAecRecording'); } }, }; return DumpCreator; })(); function initialize() { dumpCreator = new DumpCreator($('content-root')); 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); /** Sends a request to the browser to get peer connection statistics. */ function requestStats() { if (Object.keys(peerConnectionDataStore).length > 0) chrome.send('getAllStats'); } /** * A helper function for getting a peer connection element id. * * @param {!Object} 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} 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} 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. * * @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 addStats(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); } } /** * 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, '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 AEC recording file selection dialog was cancelled, * i.e. AEC has not been enabled. */ function aecRecordingFileSelectionCancelled() { dumpCreator.disableAecRecording(); } /** * Set */ function enableAecRecording() { dumpCreator.enableAecRecording(); } // Copyright 2014 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. define("mojo/public/js/bindings", [ "mojo/public/js/router", "mojo/public/js/core", ], function(router, core) { var Router = router.Router; var kProxyProperties = Symbol("proxyProperties"); var kStubProperties = Symbol("stubProperties"); // Public proxy class properties that are managed at runtime by the JS // bindings. See ProxyBindings below. function ProxyProperties(receiver) { this.receiver = receiver; } // TODO(hansmuller): remove then after 'Client=' has been removed from Mojom. ProxyProperties.prototype.getLocalDelegate = function() { return this.local && StubBindings(this.local).delegate; } // TODO(hansmuller): remove then after 'Client=' has been removed from Mojom. ProxyProperties.prototype.setLocalDelegate = function(impl) { if (this.local) StubBindings(this.local).delegate = impl; else throw new Error("no stub object"); } function connectionHandle(connection) { return connection && connection.router && connection.router.connector_ && connection.router.connector_.handle_; } ProxyProperties.prototype.close = function() { var handle = connectionHandle(this.connection); if (handle) core.close(handle); } // Public stub class properties that are managed at runtime by the JS // bindings. See StubBindings below. function StubProperties(delegate) { this.delegate = delegate; } StubProperties.prototype.close = function() { var handle = connectionHandle(this.connection); if (handle) core.close(handle); } // The base class for generated proxy classes. function ProxyBase(receiver) { this[kProxyProperties] = new ProxyProperties(receiver); // TODO(hansmuller): Temporary, for Chrome backwards compatibility. if (receiver instanceof Router) this.receiver_ = receiver; } // The base class for generated stub classes. function StubBase(delegate) { this[kStubProperties] = new StubProperties(delegate); } // TODO(hansmuller): remove everything except the connection property doc // after 'Client=' has been removed from Mojom. // Provides access to properties added to a proxy object without risking // Mojo interface name collisions. Unless otherwise specified, the initial // value of all properties is undefined. // // ProxyBindings(proxy).connection - The Connection object that links the // proxy for a remote Mojo service to an optional local stub for a local // service. The value of ProxyBindings(proxy).connection.remote == proxy. // // ProxyBindings(proxy).local - The "local" stub object whose delegate // implements the proxy's Mojo client interface. // // ProxyBindings(proxy).setLocalDelegate(impl) - Sets the implementation // delegate of the proxy's client stub object. This is just shorthand // for |StubBindings(ProxyBindings(proxy).local).delegate = impl|. // // ProxyBindings(proxy).getLocalDelegate() - Returns the implementation // delegate of the proxy's client stub object. This is just shorthand // for |StubBindings(ProxyBindings(proxy).local).delegate|. function ProxyBindings(proxy) { return (proxy instanceof ProxyBase) ? proxy[kProxyProperties] : proxy; } // TODO(hansmuller): remove the remote doc after 'Client=' has been // removed from Mojom. // Provides access to properties added to a stub object without risking // Mojo interface name collisions. Unless otherwise specified, the initial // value of all properties is undefined. // // StubBindings(stub).delegate - The optional implementation delegate for // the Mojo interface stub. // // StubBindings(stub).connection - The Connection object that links an // optional proxy for a remote service to this stub. The value of // StubBindings(stub).connection.local == stub. // // StubBindings(stub).remote - A proxy for the the stub's Mojo client // service. function StubBindings(stub) { return stub instanceof StubBase ? stub[kStubProperties] : stub; } var exports = {}; exports.EmptyProxy = ProxyBase; exports.EmptyStub = StubBase; exports.ProxyBase = ProxyBase; exports.ProxyBindings = ProxyBindings; exports.StubBase = StubBase; exports.StubBindings = StubBindings; return exports; });// Copyright 2014 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. define("mojo/public/js/buffer", function() { var kHostIsLittleEndian = (function () { var endianArrayBuffer = new ArrayBuffer(2); var endianUint8Array = new Uint8Array(endianArrayBuffer); var endianUint16Array = new Uint16Array(endianArrayBuffer); endianUint16Array[0] = 1; return endianUint8Array[0] == 1; })(); var kHighWordMultiplier = 0x100000000; function Buffer(sizeOrArrayBuffer) { if (sizeOrArrayBuffer instanceof ArrayBuffer) this.arrayBuffer = sizeOrArrayBuffer; else this.arrayBuffer = new ArrayBuffer(sizeOrArrayBuffer); this.dataView = new DataView(this.arrayBuffer); this.next = 0; } Object.defineProperty(Buffer.prototype, "byteLength", { get: function() { return this.arrayBuffer.byteLength; } }); Buffer.prototype.alloc = function(size) { var pointer = this.next; this.next += size; if (this.next > this.byteLength) { var newSize = (1.5 * (this.byteLength + size)) | 0; this.grow(newSize); } return pointer; }; function copyArrayBuffer(dstArrayBuffer, srcArrayBuffer) { (new Uint8Array(dstArrayBuffer)).set(new Uint8Array(srcArrayBuffer)); } Buffer.prototype.grow = function(size) { var newArrayBuffer = new ArrayBuffer(size); copyArrayBuffer(newArrayBuffer, this.arrayBuffer); this.arrayBuffer = newArrayBuffer; this.dataView = new DataView(this.arrayBuffer); }; Buffer.prototype.trim = function() { this.arrayBuffer = this.arrayBuffer.slice(0, this.next); this.dataView = new DataView(this.arrayBuffer); }; Buffer.prototype.getUint8 = function(offset) { return this.dataView.getUint8(offset); } Buffer.prototype.getUint16 = function(offset) { return this.dataView.getUint16(offset, kHostIsLittleEndian); } Buffer.prototype.getUint32 = function(offset) { return this.dataView.getUint32(offset, kHostIsLittleEndian); } Buffer.prototype.getUint64 = function(offset) { var lo, hi; if (kHostIsLittleEndian) { lo = this.dataView.getUint32(offset, kHostIsLittleEndian); hi = this.dataView.getUint32(offset + 4, kHostIsLittleEndian); } else { hi = this.dataView.getUint32(offset, kHostIsLittleEndian); lo = this.dataView.getUint32(offset + 4, kHostIsLittleEndian); } return lo + hi * kHighWordMultiplier; } Buffer.prototype.getInt8 = function(offset) { return this.dataView.getInt8(offset); } Buffer.prototype.getInt16 = function(offset) { return this.dataView.getInt16(offset, kHostIsLittleEndian); } Buffer.prototype.getInt32 = function(offset) { return this.dataView.getInt32(offset, kHostIsLittleEndian); } Buffer.prototype.getInt64 = function(offset) { var lo, hi; if (kHostIsLittleEndian) { lo = this.dataView.getUint32(offset, kHostIsLittleEndian); hi = this.dataView.getInt32(offset + 4, kHostIsLittleEndian); } else { hi = this.dataView.getInt32(offset, kHostIsLittleEndian); lo = this.dataView.getUint32(offset + 4, kHostIsLittleEndian); } return lo + hi * kHighWordMultiplier; } Buffer.prototype.getFloat32 = function(offset) { return this.dataView.getFloat32(offset, kHostIsLittleEndian); } Buffer.prototype.getFloat64 = function(offset) { return this.dataView.getFloat64(offset, kHostIsLittleEndian); } Buffer.prototype.setUint8 = function(offset, value) { this.dataView.setUint8(offset, value); } Buffer.prototype.setUint16 = function(offset, value) { this.dataView.setUint16(offset, value, kHostIsLittleEndian); } Buffer.prototype.setUint32 = function(offset, value) { this.dataView.setUint32(offset, value, kHostIsLittleEndian); } Buffer.prototype.setUint64 = function(offset, value) { var hi = (value / kHighWordMultiplier) | 0; if (kHostIsLittleEndian) { this.dataView.setInt32(offset, value, kHostIsLittleEndian); this.dataView.setInt32(offset + 4, hi, kHostIsLittleEndian); } else { this.dataView.setInt32(offset, hi, kHostIsLittleEndian); this.dataView.setInt32(offset + 4, value, kHostIsLittleEndian); } } Buffer.prototype.setInt8 = function(offset, value) { this.dataView.setInt8(offset, value); } Buffer.prototype.setInt16 = function(offset, value) { this.dataView.setInt16(offset, value, kHostIsLittleEndian); } Buffer.prototype.setInt32 = function(offset, value) { this.dataView.setInt32(offset, value, kHostIsLittleEndian); } Buffer.prototype.setInt64 = function(offset, value) { var hi = Math.floor(value / kHighWordMultiplier); if (kHostIsLittleEndian) { this.dataView.setInt32(offset, value, kHostIsLittleEndian); this.dataView.setInt32(offset + 4, hi, kHostIsLittleEndian); } else { this.dataView.setInt32(offset, hi, kHostIsLittleEndian); this.dataView.setInt32(offset + 4, value, kHostIsLittleEndian); } } Buffer.prototype.setFloat32 = function(offset, value) { this.dataView.setFloat32(offset, value, kHostIsLittleEndian); } Buffer.prototype.setFloat64 = function(offset, value) { this.dataView.setFloat64(offset, value, kHostIsLittleEndian); } var exports = {}; exports.Buffer = Buffer; return exports; }); // Copyright 2014 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. define("mojo/public/js/codec", [ "mojo/public/js/unicode", "mojo/public/js/buffer", ], function(unicode, buffer) { var kErrorUnsigned = "Passing negative value to unsigned"; var kErrorArray = "Passing non Array for array type"; var kErrorString = "Passing non String for string type"; var kErrorMap = "Passing non Map for map type"; // Memory ------------------------------------------------------------------- var kAlignment = 8; function align(size) { return size + (kAlignment - (size % kAlignment)) % kAlignment; } function isAligned(offset) { return offset >= 0 && (offset % kAlignment) === 0; } // Constants ---------------------------------------------------------------- var kArrayHeaderSize = 8; var kStructHeaderSize = 8; var kMessageHeaderSize = 16; var kMessageWithRequestIDHeaderSize = 24; var kMapStructPayloadSize = 16; var kStructHeaderNumBytesOffset = 0; var kStructHeaderVersionOffset = 4; var kEncodedInvalidHandleValue = 0xFFFFFFFF; // Decoder ------------------------------------------------------------------ function Decoder(buffer, handles, base) { this.buffer = buffer; this.handles = handles; this.base = base; this.next = base; } Decoder.prototype.skip = function(offset) { this.next += offset; }; Decoder.prototype.readInt8 = function() { var result = this.buffer.getInt8(this.next); this.next += 1; return result; }; Decoder.prototype.readUint8 = function() { var result = this.buffer.getUint8(this.next); this.next += 1; return result; }; Decoder.prototype.readInt16 = function() { var result = this.buffer.getInt16(this.next); this.next += 2; return result; }; Decoder.prototype.readUint16 = function() { var result = this.buffer.getUint16(this.next); this.next += 2; return result; }; Decoder.prototype.readInt32 = function() { var result = this.buffer.getInt32(this.next); this.next += 4; return result; }; Decoder.prototype.readUint32 = function() { var result = this.buffer.getUint32(this.next); this.next += 4; return result; }; Decoder.prototype.readInt64 = function() { var result = this.buffer.getInt64(this.next); this.next += 8; return result; }; Decoder.prototype.readUint64 = function() { var result = this.buffer.getUint64(this.next); this.next += 8; return result; }; Decoder.prototype.readFloat = function() { var result = this.buffer.getFloat32(this.next); this.next += 4; return result; }; Decoder.prototype.readDouble = function() { var result = this.buffer.getFloat64(this.next); this.next += 8; return result; }; Decoder.prototype.decodePointer = function() { // TODO(abarth): To correctly decode a pointer, we need to know the real // base address of the array buffer. var offsetPointer = this.next; var offset = this.readUint64(); if (!offset) return 0; return offsetPointer + offset; }; Decoder.prototype.decodeAndCreateDecoder = function(pointer) { return new Decoder(this.buffer, this.handles, pointer); }; Decoder.prototype.decodeHandle = function() { return this.handles[this.readUint32()] || null; }; Decoder.prototype.decodeString = function() { var numberOfBytes = this.readUint32(); var numberOfElements = this.readUint32(); var base = this.next; this.next += numberOfElements; return unicode.decodeUtf8String( new Uint8Array(this.buffer.arrayBuffer, base, numberOfElements)); }; Decoder.prototype.decodeArray = function(cls) { var numberOfBytes = this.readUint32(); var numberOfElements = this.readUint32(); var val = new Array(numberOfElements); if (cls === PackedBool) { var byte; for (var i = 0; i < numberOfElements; ++i) { if (i % 8 === 0) byte = this.readUint8(); val[i] = (byte & (1 << i % 8)) ? true : false; } } else { for (var i = 0; i < numberOfElements; ++i) { val[i] = cls.decode(this); } } return val; }; Decoder.prototype.decodeStruct = function(cls) { return cls.decode(this); }; Decoder.prototype.decodeStructPointer = function(cls) { var pointer = this.decodePointer(); if (!pointer) { return null; } return cls.decode(this.decodeAndCreateDecoder(pointer)); }; Decoder.prototype.decodeArrayPointer = function(cls) { var pointer = this.decodePointer(); if (!pointer) { return null; } return this.decodeAndCreateDecoder(pointer).decodeArray(cls); }; Decoder.prototype.decodeStringPointer = function() { var pointer = this.decodePointer(); if (!pointer) { return null; } return this.decodeAndCreateDecoder(pointer).decodeString(); }; Decoder.prototype.decodeMap = function(keyClass, valueClass) { this.skip(4); // numberOfBytes this.skip(4); // version var keys = this.decodeArrayPointer(keyClass); var values = this.decodeArrayPointer(valueClass); var val = new Map(); for (var i = 0; i < keys.length; i++) val.set(keys[i], values[i]); return val; }; Decoder.prototype.decodeMapPointer = function(keyClass, valueClass) { var pointer = this.decodePointer(); if (!pointer) { return null; } var decoder = this.decodeAndCreateDecoder(pointer); return decoder.decodeMap(keyClass, valueClass); }; // Encoder ------------------------------------------------------------------ function Encoder(buffer, handles, base) { this.buffer = buffer; this.handles = handles; this.base = base; this.next = base; } Encoder.prototype.skip = function(offset) { this.next += offset; }; Encoder.prototype.writeInt8 = function(val) { this.buffer.setInt8(this.next, val); this.next += 1; }; Encoder.prototype.writeUint8 = function(val) { if (val < 0) { throw new Error(kErrorUnsigned); } this.buffer.setUint8(this.next, val); this.next += 1; }; Encoder.prototype.writeInt16 = function(val) { this.buffer.setInt16(this.next, val); this.next += 2; }; Encoder.prototype.writeUint16 = function(val) { if (val < 0) { throw new Error(kErrorUnsigned); } this.buffer.setUint16(this.next, val); this.next += 2; }; Encoder.prototype.writeInt32 = function(val) { this.buffer.setInt32(this.next, val); this.next += 4; }; Encoder.prototype.writeUint32 = function(val) { if (val < 0) { throw new Error(kErrorUnsigned); } this.buffer.setUint32(this.next, val); this.next += 4; }; Encoder.prototype.writeInt64 = function(val) { this.buffer.setInt64(this.next, val); this.next += 8; }; Encoder.prototype.writeUint64 = function(val) { if (val < 0) { throw new Error(kErrorUnsigned); } this.buffer.setUint64(this.next, val); this.next += 8; }; Encoder.prototype.writeFloat = function(val) { this.buffer.setFloat32(this.next, val); this.next += 4; }; Encoder.prototype.writeDouble = function(val) { this.buffer.setFloat64(this.next, val); this.next += 8; }; Encoder.prototype.encodePointer = function(pointer) { if (!pointer) return this.writeUint64(0); // TODO(abarth): To correctly encode a pointer, we need to know the real // base address of the array buffer. var offset = pointer - this.next; this.writeUint64(offset); }; Encoder.prototype.createAndEncodeEncoder = function(size) { var pointer = this.buffer.alloc(align(size)); this.encodePointer(pointer); return new Encoder(this.buffer, this.handles, pointer); }; Encoder.prototype.encodeHandle = function(handle) { this.handles.push(handle); this.writeUint32(this.handles.length - 1); }; Encoder.prototype.encodeString = function(val) { var base = this.next + kArrayHeaderSize; var numberOfElements = unicode.encodeUtf8String( val, new Uint8Array(this.buffer.arrayBuffer, base)); var numberOfBytes = kArrayHeaderSize + numberOfElements; this.writeUint32(numberOfBytes); this.writeUint32(numberOfElements); this.next += numberOfElements; }; Encoder.prototype.encodeArray = function(cls, val, numberOfElements, encodedSize) { if (numberOfElements === undefined) numberOfElements = val.length; if (encodedSize === undefined) encodedSize = kArrayHeaderSize + cls.encodedSize * numberOfElements; this.writeUint32(encodedSize); this.writeUint32(numberOfElements); if (cls === PackedBool) { var byte = 0; for (i = 0; i < numberOfElements; ++i) { if (val[i]) byte |= (1 << i % 8); if (i % 8 === 7 || i == numberOfElements - 1) { Uint8.encode(this, byte); byte = 0; } } } else { for (var i = 0; i < numberOfElements; ++i) cls.encode(this, val[i]); } }; Encoder.prototype.encodeStruct = function(cls, val) { return cls.encode(this, val); }; Encoder.prototype.encodeStructPointer = function(cls, val) { if (val == null) { // Also handles undefined, since undefined == null. this.encodePointer(val); return; } var encoder = this.createAndEncodeEncoder(cls.encodedSize); cls.encode(encoder, val); }; Encoder.prototype.encodeArrayPointer = function(cls, val) { if (val == null) { // Also handles undefined, since undefined == null. this.encodePointer(val); return; } var numberOfElements = val.length; if (!Number.isSafeInteger(numberOfElements) || numberOfElements < 0) throw new Error(kErrorArray); var encodedSize = kArrayHeaderSize + ((cls === PackedBool) ? Math.ceil(numberOfElements / 8) : cls.encodedSize * numberOfElements); var encoder = this.createAndEncodeEncoder(encodedSize); encoder.encodeArray(cls, val, numberOfElements, encodedSize); }; Encoder.prototype.encodeStringPointer = function(val) { if (val == null) { // Also handles undefined, since undefined == null. this.encodePointer(val); return; } // Only accepts string primivites, not String Objects like new String("foo") if (typeof(val) !== "string") { throw new Error(kErrorString); } var encodedSize = kArrayHeaderSize + unicode.utf8Length(val); var encoder = this.createAndEncodeEncoder(encodedSize); encoder.encodeString(val); }; Encoder.prototype.encodeMap = function(keyClass, valueClass, val) { var keys = new Array(val.size); var values = new Array(val.size); var i = 0; val.forEach(function(value, key) { values[i] = value; keys[i++] = key; }); this.writeUint32(kStructHeaderSize + kMapStructPayloadSize); // TODO(yzshen): In order to work with other bindings which still interprets // the |version| field as |num_fields|, set it to version 2 for now. this.writeUint32(2); // version this.encodeArrayPointer(keyClass, keys); this.encodeArrayPointer(valueClass, values); } Encoder.prototype.encodeMapPointer = function(keyClass, valueClass, val) { if (val == null) { // Also handles undefined, since undefined == null. this.encodePointer(val); return; } if (!(val instanceof Map)) { throw new Error(kErrorMap); } var encodedSize = kStructHeaderSize + kMapStructPayloadSize; var encoder = this.createAndEncodeEncoder(encodedSize); encoder.encodeMap(keyClass, valueClass, val); }; // Message ------------------------------------------------------------------ var kMessageNameOffset = kStructHeaderSize; var kMessageFlagsOffset = kMessageNameOffset + 4; var kMessageRequestIDOffset = kMessageFlagsOffset + 4; var kMessageExpectsResponse = 1 << 0; var kMessageIsResponse = 1 << 1; function Message(buffer, handles) { this.buffer = buffer; this.handles = handles; } Message.prototype.getHeaderNumBytes = function() { return this.buffer.getUint32(kStructHeaderNumBytesOffset); }; Message.prototype.getHeaderVersion = function() { return this.buffer.getUint32(kStructHeaderVersionOffset); }; Message.prototype.getName = function() { return this.buffer.getUint32(kMessageNameOffset); }; Message.prototype.getFlags = function() { return this.buffer.getUint32(kMessageFlagsOffset); }; Message.prototype.isResponse = function() { return (this.getFlags() & kMessageIsResponse) != 0; }; Message.prototype.expectsResponse = function() { return (this.getFlags() & kMessageExpectsResponse) != 0; }; Message.prototype.setRequestID = function(requestID) { // TODO(darin): Verify that space was reserved for this field! this.buffer.setUint64(kMessageRequestIDOffset, requestID); }; // MessageBuilder ----------------------------------------------------------- function MessageBuilder(messageName, payloadSize) { // Currently, we don't compute the payload size correctly ahead of time. // Instead, we resize the buffer at the end. var numberOfBytes = kMessageHeaderSize + payloadSize; this.buffer = new buffer.Buffer(numberOfBytes); this.handles = []; var encoder = this.createEncoder(kMessageHeaderSize); encoder.writeUint32(kMessageHeaderSize); // TODO(yzshen): In order to work with other bindings which still interprets // the |version| field as |num_fields|, set it to version 2 for now. encoder.writeUint32(2); // version. encoder.writeUint32(messageName); encoder.writeUint32(0); // flags. } MessageBuilder.prototype.createEncoder = function(size) { var pointer = this.buffer.alloc(size); return new Encoder(this.buffer, this.handles, pointer); }; MessageBuilder.prototype.encodeStruct = function(cls, val) { cls.encode(this.createEncoder(cls.encodedSize), val); }; MessageBuilder.prototype.finish = function() { // TODO(abarth): Rather than resizing the buffer at the end, we could // compute the size we need ahead of time, like we do in C++. this.buffer.trim(); var message = new Message(this.buffer, this.handles); this.buffer = null; this.handles = null; this.encoder = null; return message; }; // MessageWithRequestIDBuilder ----------------------------------------------- function MessageWithRequestIDBuilder(messageName, payloadSize, flags, requestID) { // Currently, we don't compute the payload size correctly ahead of time. // Instead, we resize the buffer at the end. var numberOfBytes = kMessageWithRequestIDHeaderSize + payloadSize; this.buffer = new buffer.Buffer(numberOfBytes); this.handles = []; var encoder = this.createEncoder(kMessageWithRequestIDHeaderSize); encoder.writeUint32(kMessageWithRequestIDHeaderSize); // TODO(yzshen): In order to work with other bindings which still interprets // the |version| field as |num_fields|, set it to version 3 for now. encoder.writeUint32(3); // version. encoder.writeUint32(messageName); encoder.writeUint32(flags); encoder.writeUint64(requestID); } MessageWithRequestIDBuilder.prototype = Object.create(MessageBuilder.prototype); MessageWithRequestIDBuilder.prototype.constructor = MessageWithRequestIDBuilder; // MessageReader ------------------------------------------------------------ function MessageReader(message) { this.decoder = new Decoder(message.buffer, message.handles, 0); var messageHeaderSize = this.decoder.readUint32(); this.payloadSize = message.buffer.byteLength - messageHeaderSize; var version = this.decoder.readUint32(); this.messageName = this.decoder.readUint32(); this.flags = this.decoder.readUint32(); if (version >= 3) this.requestID = this.decoder.readUint64(); this.decoder.skip(messageHeaderSize - this.decoder.next); } MessageReader.prototype.decodeStruct = function(cls) { return cls.decode(this.decoder); }; // Built-in types ----------------------------------------------------------- // This type is only used with ArrayOf(PackedBool). function PackedBool() { } function Int8() { } Int8.encodedSize = 1; Int8.decode = function(decoder) { return decoder.readInt8(); }; Int8.encode = function(encoder, val) { encoder.writeInt8(val); }; Uint8.encode = function(encoder, val) { encoder.writeUint8(val); }; function Uint8() { } Uint8.encodedSize = 1; Uint8.decode = function(decoder) { return decoder.readUint8(); }; Uint8.encode = function(encoder, val) { encoder.writeUint8(val); }; function Int16() { } Int16.encodedSize = 2; Int16.decode = function(decoder) { return decoder.readInt16(); }; Int16.encode = function(encoder, val) { encoder.writeInt16(val); }; function Uint16() { } Uint16.encodedSize = 2; Uint16.decode = function(decoder) { return decoder.readUint16(); }; Uint16.encode = function(encoder, val) { encoder.writeUint16(val); }; function Int32() { } Int32.encodedSize = 4; Int32.decode = function(decoder) { return decoder.readInt32(); }; Int32.encode = function(encoder, val) { encoder.writeInt32(val); }; function Uint32() { } Uint32.encodedSize = 4; Uint32.decode = function(decoder) { return decoder.readUint32(); }; Uint32.encode = function(encoder, val) { encoder.writeUint32(val); }; function Int64() { } Int64.encodedSize = 8; Int64.decode = function(decoder) { return decoder.readInt64(); }; Int64.encode = function(encoder, val) { encoder.writeInt64(val); }; function Uint64() { } Uint64.encodedSize = 8; Uint64.decode = function(decoder) { return decoder.readUint64(); }; Uint64.encode = function(encoder, val) { encoder.writeUint64(val); }; function String() { }; String.encodedSize = 8; String.decode = function(decoder) { return decoder.decodeStringPointer(); }; String.encode = function(encoder, val) { encoder.encodeStringPointer(val); }; function NullableString() { } NullableString.encodedSize = String.encodedSize; NullableString.decode = String.decode; NullableString.encode = String.encode; function Float() { } Float.encodedSize = 4; Float.decode = function(decoder) { return decoder.readFloat(); }; Float.encode = function(encoder, val) { encoder.writeFloat(val); }; function Double() { } Double.encodedSize = 8; Double.decode = function(decoder) { return decoder.readDouble(); }; Double.encode = function(encoder, val) { encoder.writeDouble(val); }; function PointerTo(cls) { this.cls = cls; } PointerTo.prototype.encodedSize = 8; PointerTo.prototype.decode = function(decoder) { var pointer = decoder.decodePointer(); if (!pointer) { return null; } return this.cls.decode(decoder.decodeAndCreateDecoder(pointer)); }; PointerTo.prototype.encode = function(encoder, val) { if (!val) { encoder.encodePointer(val); return; } var objectEncoder = encoder.createAndEncodeEncoder(this.cls.encodedSize); this.cls.encode(objectEncoder, val); }; function NullablePointerTo(cls) { PointerTo.call(this, cls); } NullablePointerTo.prototype = Object.create(PointerTo.prototype); function ArrayOf(cls, length) { this.cls = cls; this.length = length || 0; } ArrayOf.prototype.encodedSize = 8; ArrayOf.prototype.dimensions = function() { return [this.length].concat( (this.cls instanceof ArrayOf) ? this.cls.dimensions() : []); } ArrayOf.prototype.decode = function(decoder) { return decoder.decodeArrayPointer(this.cls); }; ArrayOf.prototype.encode = function(encoder, val) { encoder.encodeArrayPointer(this.cls, val); }; function NullableArrayOf(cls) { ArrayOf.call(this, cls); } NullableArrayOf.prototype = Object.create(ArrayOf.prototype); function Handle() { } Handle.encodedSize = 4; Handle.decode = function(decoder) { return decoder.decodeHandle(); }; Handle.encode = function(encoder, val) { encoder.encodeHandle(val); }; function NullableHandle() { } NullableHandle.encodedSize = Handle.encodedSize; NullableHandle.decode = Handle.decode; NullableHandle.encode = Handle.encode; function MapOf(keyClass, valueClass) { this.keyClass = keyClass; this.valueClass = valueClass; } MapOf.prototype.encodedSize = 8; MapOf.prototype.decode = function(decoder) { return decoder.decodeMapPointer(this.keyClass, this.valueClass); }; MapOf.prototype.encode = function(encoder, val) { encoder.encodeMapPointer(this.keyClass, this.valueClass, val); }; function NullableMapOf(keyClass, valueClass) { MapOf.call(this, keyClass, valueClass); } NullableMapOf.prototype = Object.create(MapOf.prototype); var exports = {}; exports.align = align; exports.isAligned = isAligned; exports.Message = Message; exports.MessageBuilder = MessageBuilder; exports.MessageWithRequestIDBuilder = MessageWithRequestIDBuilder; exports.MessageReader = MessageReader; exports.kArrayHeaderSize = kArrayHeaderSize; exports.kMapStructPayloadSize = kMapStructPayloadSize; exports.kStructHeaderSize = kStructHeaderSize; exports.kEncodedInvalidHandleValue = kEncodedInvalidHandleValue; exports.kMessageHeaderSize = kMessageHeaderSize; exports.kMessageWithRequestIDHeaderSize = kMessageWithRequestIDHeaderSize; exports.kMessageExpectsResponse = kMessageExpectsResponse; exports.kMessageIsResponse = kMessageIsResponse; exports.Int8 = Int8; exports.Uint8 = Uint8; exports.Int16 = Int16; exports.Uint16 = Uint16; exports.Int32 = Int32; exports.Uint32 = Uint32; exports.Int64 = Int64; exports.Uint64 = Uint64; exports.Float = Float; exports.Double = Double; exports.String = String; exports.NullableString = NullableString; exports.PointerTo = PointerTo; exports.NullablePointerTo = NullablePointerTo; exports.ArrayOf = ArrayOf; exports.NullableArrayOf = NullableArrayOf; exports.PackedBool = PackedBool; exports.Handle = Handle; exports.NullableHandle = NullableHandle; exports.MapOf = MapOf; exports.NullableMapOf = NullableMapOf; return exports; }); // Copyright 2014 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. define("mojo/public/js/connection", [ "mojo/public/js/bindings", "mojo/public/js/connector", "mojo/public/js/core", "mojo/public/js/router", ], function(bindings, connector, core, router) { var Router = router.Router; var EmptyProxy = bindings.EmptyProxy; var EmptyStub = bindings.EmptyStub; var ProxyBindings = bindings.ProxyBindings; var StubBindings = bindings.StubBindings; var TestConnector = connector.TestConnector; var TestRouter = router.TestRouter; // TODO(hansmuller): the proxy receiver_ property should be receiver$ function BaseConnection(localStub, remoteProxy, router) { this.router_ = router; this.local = localStub; this.remote = remoteProxy; this.router_.setIncomingReceiver(localStub); if (this.remote) this.remote.receiver_ = router; // Validate incoming messages: remote responses and local requests. var validateRequest = localStub && localStub.validator; var validateResponse = remoteProxy && remoteProxy.validator; var payloadValidators = []; if (validateRequest) payloadValidators.push(validateRequest); if (validateResponse) payloadValidators.push(validateResponse); this.router_.setPayloadValidators(payloadValidators); } BaseConnection.prototype.close = function() { this.router_.close(); this.router_ = null; this.local = null; this.remote = null; }; BaseConnection.prototype.encounteredError = function() { return this.router_.encounteredError(); }; function Connection( handle, localFactory, remoteFactory, routerFactory, connectorFactory) { var routerClass = routerFactory || Router; var router = new routerClass(handle, connectorFactory); var remoteProxy = remoteFactory && new remoteFactory(router); var localStub = localFactory && new localFactory(remoteProxy); BaseConnection.call(this, localStub, remoteProxy, router); } Connection.prototype = Object.create(BaseConnection.prototype); // The TestConnection subclass is only intended to be used in unit tests. function TestConnection(handle, localFactory, remoteFactory) { Connection.call(this, handle, localFactory, remoteFactory, TestRouter, TestConnector); } TestConnection.prototype = Object.create(Connection.prototype); // Return a handle for a message pipe that's connected to a proxy // for remoteInterface. Used by generated code for outgoing interface& // (request) parameters: the caller is given the generated proxy via // |proxyCallback(proxy)| and the generated code sends the handle // returned by this function. function bindProxy(proxyCallback, remoteInterface) { var messagePipe = core.createMessagePipe(); if (messagePipe.result != core.RESULT_OK) throw new Error("createMessagePipe failed " + messagePipe.result); var proxy = new remoteInterface.proxyClass; var router = new Router(messagePipe.handle0); var connection = new BaseConnection(undefined, proxy, router); ProxyBindings(proxy).connection = connection; if (proxyCallback) proxyCallback(proxy); return messagePipe.handle1; } // Return a handle for a message pipe that's connected to a stub for // localInterface. Used by generated code for outgoing interface // parameters: the caller is given the generated stub via // |stubCallback(stub)| and the generated code sends the handle // returned by this function. The caller is responsible for managing // the lifetime of the stub and for setting it's implementation // delegate with: StubBindings(stub).delegate = myImpl; function bindImpl(stubCallback, localInterface) { var messagePipe = core.createMessagePipe(); if (messagePipe.result != core.RESULT_OK) throw new Error("createMessagePipe failed " + messagePipe.result); var stub = new localInterface.stubClass; var router = new Router(messagePipe.handle0); var connection = new BaseConnection(stub, undefined, router); StubBindings(stub).connection = connection; if (stubCallback) stubCallback(stub); return messagePipe.handle1; } // Return a remoteInterface proxy for handle. Used by generated code // for converting incoming interface parameters to proxies. function bindHandleToProxy(handle, remoteInterface) { if (!core.isHandle(handle)) throw new Error("Not a handle " + handle); var proxy = new remoteInterface.proxyClass; var router = new Router(handle); var connection = new BaseConnection(undefined, proxy, router); ProxyBindings(proxy).connection = connection; return proxy; } // Return a localInterface stub for handle. Used by generated code // for converting incoming interface& request parameters to localInterface // stubs. The caller can specify the stub's implementation of localInterface // like this: StubBindings(stub).delegate = myStubImpl. function bindHandleToStub(handle, localInterface) { if (!core.isHandle(handle)) throw new Error("Not a handle " + handle); var stub = new localInterface.stubClass; var router = new Router(handle); var connection = new BaseConnection(stub, undefined, router); StubBindings(stub).connection = connection; return stub; } var exports = {}; exports.Connection = Connection; exports.TestConnection = TestConnection; exports.bindProxy = bindProxy; exports.bindImpl = bindImpl; exports.bindHandleToProxy = bindHandleToProxy; exports.bindHandleToStub = bindHandleToStub; return exports; }); // Copyright 2014 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. define("mojo/public/js/connector", [ "mojo/public/js/buffer", "mojo/public/js/codec", "mojo/public/js/core", "mojo/public/js/support", ], function(buffer, codec, core, support) { function Connector(handle) { if (!core.isHandle(handle)) throw new Error("Connector: not a handle " + handle); this.handle_ = handle; this.dropWrites_ = false; this.error_ = false; this.incomingReceiver_ = null; this.readWaitCookie_ = null; this.errorHandler_ = null; if (handle) this.waitToReadMore_(); } Connector.prototype.close = function() { if (this.readWaitCookie_) { support.cancelWait(this.readWaitCookie_); this.readWaitCookie_ = null; } if (this.handle_ != null) { core.close(this.handle_); this.handle_ = null; } }; Connector.prototype.accept = function(message) { if (this.error_) return false; if (this.dropWrites_) return true; var result = core.writeMessage(this.handle_, new Uint8Array(message.buffer.arrayBuffer), message.handles, core.WRITE_MESSAGE_FLAG_NONE); switch (result) { case core.RESULT_OK: // The handles were successfully transferred, so we don't own them // anymore. message.handles = []; break; case core.RESULT_FAILED_PRECONDITION: // There's no point in continuing to write to this pipe since the other // end is gone. Avoid writing any future messages. Hide write failures // from the caller since we'd like them to continue consuming any // backlog of incoming messages before regarding the message pipe as // closed. this.dropWrites_ = true; break; default: // This particular write was rejected, presumably because of bad input. // The pipe is not necessarily in a bad state. return false; } return true; }; Connector.prototype.setIncomingReceiver = function(receiver) { this.incomingReceiver_ = receiver; }; Connector.prototype.setErrorHandler = function(handler) { this.errorHandler_ = handler; }; Connector.prototype.encounteredError = function() { return this.error_; }; Connector.prototype.waitToReadMore_ = function() { this.readWaitCookie_ = support.asyncWait(this.handle_, core.HANDLE_SIGNAL_READABLE, this.readMore_.bind(this)); }; Connector.prototype.readMore_ = function(result) { for (;;) { var read = core.readMessage(this.handle_, core.READ_MESSAGE_FLAG_NONE); if (this.handle_ == null) // The connector has been closed. return; if (read.result == core.RESULT_SHOULD_WAIT) { this.waitToReadMore_(); return; } if (read.result != core.RESULT_OK) { this.error_ = true; if (this.errorHandler_) this.errorHandler_.onError(read.result); return; } var messageBuffer = new buffer.Buffer(read.buffer); var message = new codec.Message(messageBuffer, read.handles); if (this.incomingReceiver_) { this.incomingReceiver_.accept(message); } } }; // The TestConnector subclass is only intended to be used in unit tests. It // enables delivering a message to the pipe's handle without an async wait. function TestConnector(handle) { Connector.call(this, handle); } TestConnector.prototype = Object.create(Connector.prototype); TestConnector.prototype.waitToReadMore_ = function() { }; TestConnector.prototype.deliverMessage = function() { this.readMore_(core.RESULT_OK); } var exports = {}; exports.Connector = Connector; exports.TestConnector = TestConnector; return exports; }); // Copyright 2014 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. define("mojo/public/js/router", [ "mojo/public/js/codec", "mojo/public/js/core", "mojo/public/js/connector", "mojo/public/js/validator", ], function(codec, core, connector, validator) { var Connector = connector.Connector; var MessageReader = codec.MessageReader; var Validator = validator.Validator; function Router(handle, connectorFactory) { if (!core.isHandle(handle)) throw new Error("Router constructor: Not a handle"); if (connectorFactory === undefined) connectorFactory = Connector; this.connector_ = new connectorFactory(handle); this.incomingReceiver_ = null; this.nextRequestID_ = 0; this.completers_ = new Map(); this.payloadValidators_ = []; this.connector_.setIncomingReceiver({ accept: this.handleIncomingMessage_.bind(this), }); this.connector_.setErrorHandler({ onError: this.handleConnectionError_.bind(this), }); } Router.prototype.close = function() { this.completers_.clear(); // Drop any responders. this.connector_.close(); }; Router.prototype.accept = function(message) { this.connector_.accept(message); }; Router.prototype.reject = function(message) { // TODO(mpcomplete): no way to trasmit errors over a Connection. }; Router.prototype.acceptAndExpectResponse = function(message) { // Reserve 0 in case we want it to convey special meaning in the future. var requestID = this.nextRequestID_++; if (requestID == 0) requestID = this.nextRequestID_++; message.setRequestID(requestID); var result = this.connector_.accept(message); if (!result) return Promise.reject(Error("Connection error")); var completer = {}; this.completers_.set(requestID, completer); return new Promise(function(resolve, reject) { completer.resolve = resolve; completer.reject = reject; }); }; Router.prototype.setIncomingReceiver = function(receiver) { this.incomingReceiver_ = receiver; }; Router.prototype.setPayloadValidators = function(payloadValidators) { this.payloadValidators_ = payloadValidators; }; Router.prototype.encounteredError = function() { return this.connector_.encounteredError(); }; Router.prototype.handleIncomingMessage_ = function(message) { var noError = validator.validationError.NONE; var messageValidator = new Validator(message); var err = messageValidator.validateMessageHeader(); for (var i = 0; err === noError && i < this.payloadValidators_.length; ++i) err = this.payloadValidators_[i](messageValidator); if (err == noError) this.handleValidIncomingMessage_(message); else this.handleInvalidIncomingMessage_(message, err); }; Router.prototype.handleValidIncomingMessage_ = function(message) { if (message.expectsResponse()) { if (this.incomingReceiver_) { this.incomingReceiver_.acceptWithResponder(message, this); } else { // If we receive a request expecting a response when the client is not // listening, then we have no choice but to tear down the pipe. this.close(); } } else if (message.isResponse()) { var reader = new MessageReader(message); var requestID = reader.requestID; var completer = this.completers_.get(requestID); this.completers_.delete(requestID); completer.resolve(message); } else { if (this.incomingReceiver_) this.incomingReceiver_.accept(message); } } Router.prototype.handleInvalidIncomingMessage_ = function(message, error) { this.close(); } Router.prototype.handleConnectionError_ = function(result) { this.completers_.forEach(function(value) { value.reject(result); }); this.close(); }; // The TestRouter subclass is only intended to be used in unit tests. // It defeats valid message handling and delgates invalid message handling. function TestRouter(handle, connectorFactory) { Router.call(this, handle, connectorFactory); } TestRouter.prototype = Object.create(Router.prototype); TestRouter.prototype.handleValidIncomingMessage_ = function() { }; TestRouter.prototype.handleInvalidIncomingMessage_ = function(message, error) { this.validationErrorHandler(error); }; var exports = {}; exports.Router = Router; exports.TestRouter = TestRouter; return exports; }); // Copyright 2014 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. /** * Defines functions for translating between JavaScript strings and UTF8 strings * stored in ArrayBuffers. There is much room for optimization in this code if * it proves necessary. */ define("mojo/public/js/unicode", function() { /** * Decodes the UTF8 string from the given buffer. * @param {ArrayBufferView} buffer The buffer containing UTF8 string data. * @return {string} The corresponding JavaScript string. */ function decodeUtf8String(buffer) { return decodeURIComponent(escape(String.fromCharCode.apply(null, buffer))); } /** * Encodes the given JavaScript string into UTF8. * @param {string} str The string to encode. * @param {ArrayBufferView} outputBuffer The buffer to contain the result. * Should be pre-allocated to hold enough space. Use |utf8Length| to determine * how much space is required. * @return {number} The number of bytes written to |outputBuffer|. */ function encodeUtf8String(str, outputBuffer) { var utf8String = unescape(encodeURIComponent(str)); if (outputBuffer.length < utf8String.length) throw new Error("Buffer too small for encodeUtf8String"); for (var i = 0; i < outputBuffer.length && i < utf8String.length; i++) outputBuffer[i] = utf8String.charCodeAt(i); return i; } /** * Returns the number of bytes that a UTF8 encoding of the JavaScript string * |str| would occupy. */ function utf8Length(str) { var utf8String = unescape(encodeURIComponent(str)); return utf8String.length; } var exports = {}; exports.decodeUtf8String = decodeUtf8String; exports.encodeUtf8String = encodeUtf8String; exports.utf8Length = utf8Length; return exports; }); // Copyright 2014 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. define("mojo/public/js/validator", [ "mojo/public/js/codec", ], function(codec) { var validationError = { NONE: 'VALIDATION_ERROR_NONE', MISALIGNED_OBJECT: 'VALIDATION_ERROR_MISALIGNED_OBJECT', ILLEGAL_MEMORY_RANGE: 'VALIDATION_ERROR_ILLEGAL_MEMORY_RANGE', UNEXPECTED_STRUCT_HEADER: 'VALIDATION_ERROR_UNEXPECTED_STRUCT_HEADER', UNEXPECTED_ARRAY_HEADER: 'VALIDATION_ERROR_UNEXPECTED_ARRAY_HEADER', ILLEGAL_HANDLE: 'VALIDATION_ERROR_ILLEGAL_HANDLE', UNEXPECTED_INVALID_HANDLE: 'VALIDATION_ERROR_UNEXPECTED_INVALID_HANDLE', ILLEGAL_POINTER: 'VALIDATION_ERROR_ILLEGAL_POINTER', UNEXPECTED_NULL_POINTER: 'VALIDATION_ERROR_UNEXPECTED_NULL_POINTER', MESSAGE_HEADER_INVALID_FLAG_COMBINATION: 'VALIDATION_ERROR_MESSAGE_HEADER_INVALID_FLAG_COMBINATION', MESSAGE_HEADER_MISSING_REQUEST_ID: 'VALIDATION_ERROR_MESSAGE_HEADER_MISSING_REQUEST_ID', DIFFERENT_SIZED_ARRAYS_IN_MAP: 'VALIDATION_ERROR_DIFFERENT_SIZED_ARRAYS_IN_MAP', }; var NULL_MOJO_POINTER = "NULL_MOJO_POINTER"; function isStringClass(cls) { return cls === codec.String || cls === codec.NullableString; } function isHandleClass(cls) { return cls === codec.Handle || cls === codec.NullableHandle; } function isNullable(type) { return type === codec.NullableString || type === codec.NullableHandle || type instanceof codec.NullableArrayOf || type instanceof codec.NullablePointerTo; } function Validator(message) { this.message = message; this.offset = 0; this.handleIndex = 0; } Object.defineProperty(Validator.prototype, "offsetLimit", { get: function() { return this.message.buffer.byteLength; } }); Object.defineProperty(Validator.prototype, "handleIndexLimit", { get: function() { return this.message.handles.length; } }); // True if we can safely allocate a block of bytes from start to // to start + numBytes. Validator.prototype.isValidRange = function(start, numBytes) { // Only positive JavaScript integers that are less than 2^53 // (Number.MAX_SAFE_INTEGER) can be represented exactly. if (start < this.offset || numBytes <= 0 || !Number.isSafeInteger(start) || !Number.isSafeInteger(numBytes)) return false; var newOffset = start + numBytes; if (!Number.isSafeInteger(newOffset) || newOffset > this.offsetLimit) return false; return true; } Validator.prototype.claimRange = function(start, numBytes) { if (this.isValidRange(start, numBytes)) { this.offset = start + numBytes; return true; } return false; } Validator.prototype.claimHandle = function(index) { if (index === codec.kEncodedInvalidHandleValue) return true; if (index < this.handleIndex || index >= this.handleIndexLimit) return false; // This is safe because handle indices are uint32. this.handleIndex = index + 1; return true; } Validator.prototype.validateHandle = function(offset, nullable) { var index = this.message.buffer.getUint32(offset); if (index === codec.kEncodedInvalidHandleValue) return nullable ? validationError.NONE : validationError.UNEXPECTED_INVALID_HANDLE; if (!this.claimHandle(index)) return validationError.ILLEGAL_HANDLE; return validationError.NONE; } Validator.prototype.validateStructHeader = function(offset, minNumBytes, minVersion) { if (!codec.isAligned(offset)) return validationError.MISALIGNED_OBJECT; if (!this.isValidRange(offset, codec.kStructHeaderSize)) return validationError.ILLEGAL_MEMORY_RANGE; var numBytes = this.message.buffer.getUint32(offset); var version = this.message.buffer.getUint32(offset + 4); // Backward compatibility is not yet supported. if (numBytes < minNumBytes || version < minVersion) return validationError.UNEXPECTED_STRUCT_HEADER; if (!this.claimRange(offset, numBytes)) return validationError.ILLEGAL_MEMORY_RANGE; return validationError.NONE; } Validator.prototype.validateMessageHeader = function() { var err = this.validateStructHeader(0, codec.kMessageHeaderSize, 2); if (err != validationError.NONE) return err; var numBytes = this.message.getHeaderNumBytes(); var version = this.message.getHeaderVersion(); var validVersionAndNumBytes = (version == 2 && numBytes == codec.kMessageHeaderSize) || (version == 3 && numBytes == codec.kMessageWithRequestIDHeaderSize) || (version > 3 && numBytes >= codec.kMessageWithRequestIDHeaderSize); if (!validVersionAndNumBytes) return validationError.UNEXPECTED_STRUCT_HEADER; var expectsResponse = this.message.expectsResponse(); var isResponse = this.message.isResponse(); if (version == 2 && (expectsResponse || isResponse)) return validationError.MESSAGE_HEADER_MISSING_REQUEST_ID; if (isResponse && expectsResponse) return validationError.MESSAGE_HEADER_INVALID_FLAG_COMBINATION; return validationError.NONE; } // Returns the message.buffer relative offset this pointer "points to", // NULL_MOJO_POINTER if the pointer represents a null, or JS null if the // pointer's value is not valid. Validator.prototype.decodePointer = function(offset) { var pointerValue = this.message.buffer.getUint64(offset); if (pointerValue === 0) return NULL_MOJO_POINTER; var bufferOffset = offset + pointerValue; return Number.isSafeInteger(bufferOffset) ? bufferOffset : null; } Validator.prototype.validateArrayPointer = function( offset, elementSize, elementType, nullable, expectedDimensionSizes, currentDimension) { var arrayOffset = this.decodePointer(offset); if (arrayOffset === null) return validationError.ILLEGAL_POINTER; if (arrayOffset === NULL_MOJO_POINTER) return nullable ? validationError.NONE : validationError.UNEXPECTED_NULL_POINTER; return this.validateArray(arrayOffset, elementSize, elementType, expectedDimensionSizes, currentDimension); } Validator.prototype.validateStructPointer = function( offset, structClass, nullable) { var structOffset = this.decodePointer(offset); if (structOffset === null) return validationError.ILLEGAL_POINTER; if (structOffset === NULL_MOJO_POINTER) return nullable ? validationError.NONE : validationError.UNEXPECTED_NULL_POINTER; return structClass.validate(this, structOffset); } // This method assumes that the array at arrayPointerOffset has // been validated. Validator.prototype.arrayLength = function(arrayPointerOffset) { var arrayOffset = this.decodePointer(arrayPointerOffset); return this.message.buffer.getUint32(arrayOffset + 4); } Validator.prototype.validateMapPointer = function( offset, mapIsNullable, keyClass, valueClass, valueIsNullable) { // Validate the implicit map struct: // struct {array keys; array values}; var structOffset = this.decodePointer(offset); if (structOffset === null) return validationError.ILLEGAL_POINTER; if (structOffset === NULL_MOJO_POINTER) return mapIsNullable ? validationError.NONE : validationError.UNEXPECTED_NULL_POINTER; var mapEncodedSize = codec.kStructHeaderSize + codec.kMapStructPayloadSize; var err = this.validateStructHeader(structOffset, mapEncodedSize, 2); if (err !== validationError.NONE) return err; // Validate the keys array. var keysArrayPointerOffset = structOffset + codec.kStructHeaderSize; err = this.validateArrayPointer( keysArrayPointerOffset, keyClass.encodedSize, keyClass, false, [0], 0); if (err !== validationError.NONE) return err; // Validate the values array. var valuesArrayPointerOffset = keysArrayPointerOffset + 8; var valuesArrayDimensions = [0]; // Validate the actual length below. if (valueClass instanceof codec.ArrayOf) valuesArrayDimensions = valuesArrayDimensions.concat(valueClass.dimensions()); var err = this.validateArrayPointer(valuesArrayPointerOffset, valueClass.encodedSize, valueClass, valueIsNullable, valuesArrayDimensions, 0); if (err !== validationError.NONE) return err; // Validate the lengths of the keys and values arrays. var keysArrayLength = this.arrayLength(keysArrayPointerOffset); var valuesArrayLength = this.arrayLength(valuesArrayPointerOffset); if (keysArrayLength != valuesArrayLength) return validationError.DIFFERENT_SIZED_ARRAYS_IN_MAP; return validationError.NONE; } Validator.prototype.validateStringPointer = function(offset, nullable) { return this.validateArrayPointer( offset, codec.Uint8.encodedSize, codec.Uint8, nullable, [0], 0); } // Similar to Array_Data::Validate() // mojo/public/cpp/bindings/lib/array_internal.h Validator.prototype.validateArray = function (offset, elementSize, elementType, expectedDimensionSizes, currentDimension) { if (!codec.isAligned(offset)) return validationError.MISALIGNED_OBJECT; if (!this.isValidRange(offset, codec.kArrayHeaderSize)) return validationError.ILLEGAL_MEMORY_RANGE; var numBytes = this.message.buffer.getUint32(offset); var numElements = this.message.buffer.getUint32(offset + 4); // Note: this computation is "safe" because elementSize <= 8 and // numElements is a uint32. var elementsTotalSize = (elementType === codec.PackedBool) ? Math.ceil(numElements / 8) : (elementSize * numElements); if (numBytes < codec.kArrayHeaderSize + elementsTotalSize) return validationError.UNEXPECTED_ARRAY_HEADER; if (expectedDimensionSizes[currentDimension] != 0 && numElements != expectedDimensionSizes[currentDimension]) { return validationError.UNEXPECTED_ARRAY_HEADER; } if (!this.claimRange(offset, numBytes)) return validationError.ILLEGAL_MEMORY_RANGE; // Validate the array's elements if they are pointers or handles. var elementsOffset = offset + codec.kArrayHeaderSize; var nullable = isNullable(elementType); if (isHandleClass(elementType)) return this.validateHandleElements(elementsOffset, numElements, nullable); if (isStringClass(elementType)) return this.validateArrayElements( elementsOffset, numElements, codec.Uint8, nullable, [0], 0); if (elementType instanceof codec.PointerTo) return this.validateStructElements( elementsOffset, numElements, elementType.cls, nullable); if (elementType instanceof codec.ArrayOf) return this.validateArrayElements( elementsOffset, numElements, elementType.cls, nullable, expectedDimensionSizes, currentDimension + 1); return validationError.NONE; } // Note: the |offset + i * elementSize| computation in the validateFooElements // methods below is "safe" because elementSize <= 8, offset and // numElements are uint32, and 0 <= i < numElements. Validator.prototype.validateHandleElements = function(offset, numElements, nullable) { var elementSize = codec.Handle.encodedSize; for (var i = 0; i < numElements; i++) { var elementOffset = offset + i * elementSize; var err = this.validateHandle(elementOffset, nullable); if (err != validationError.NONE) return err; } return validationError.NONE; } // The elementClass parameter is the element type of the element arrays. Validator.prototype.validateArrayElements = function(offset, numElements, elementClass, nullable, expectedDimensionSizes, currentDimension) { var elementSize = codec.PointerTo.prototype.encodedSize; for (var i = 0; i < numElements; i++) { var elementOffset = offset + i * elementSize; var err = this.validateArrayPointer( elementOffset, elementClass.encodedSize, elementClass, nullable, expectedDimensionSizes, currentDimension); if (err != validationError.NONE) return err; } return validationError.NONE; } Validator.prototype.validateStructElements = function(offset, numElements, structClass, nullable) { var elementSize = codec.PointerTo.prototype.encodedSize; for (var i = 0; i < numElements; i++) { var elementOffset = offset + i * elementSize; var err = this.validateStructPointer(elementOffset, structClass, nullable); if (err != validationError.NONE) return err; } return validationError.NONE; } var exports = {}; exports.validationError = validationError; exports.Validator = Validator; return exports; });