summaryrefslogtreecommitdiff
path: root/chromium/content/browser/resources
diff options
context:
space:
mode:
authorZeno Albisser <zeno.albisser@digia.com>2013-08-15 21:46:11 +0200
committerZeno Albisser <zeno.albisser@digia.com>2013-08-15 21:46:11 +0200
commit679147eead574d186ebf3069647b4c23e8ccace6 (patch)
treefc247a0ac8ff119f7c8550879ebb6d3dd8d1ff69 /chromium/content/browser/resources
downloadqtwebengine-chromium-679147eead574d186ebf3069647b4c23e8ccace6.tar.gz
Initial import.
Diffstat (limited to 'chromium/content/browser/resources')
-rw-r--r--chromium/content/browser/resources/accessibility/accessibility.css30
-rw-r--r--chromium/content/browser/resources/accessibility/accessibility.html25
-rw-r--r--chromium/content/browser/resources/accessibility/accessibility.js212
-rw-r--r--chromium/content/browser/resources/gpu/OWNERS1
-rw-r--r--chromium/content/browser/resources/gpu/browser_bridge.js147
-rw-r--r--chromium/content/browser/resources/gpu/browser_bridge_tests.js346
-rw-r--r--chromium/content/browser/resources/gpu/gpu_internals.html54
-rw-r--r--chromium/content/browser/resources/gpu/gpu_internals.js20
-rw-r--r--chromium/content/browser/resources/gpu/info_view.css58
-rw-r--r--chromium/content/browser/resources/gpu/info_view.html73
-rw-r--r--chromium/content/browser/resources/gpu/info_view.js321
-rw-r--r--chromium/content/browser/resources/gpu/timeline_test.html46
-rw-r--r--chromium/content/browser/resources/indexed_db/OWNERS4
-rw-r--r--chromium/content/browser/resources/indexed_db/indexeddb_internals.css72
-rw-r--r--chromium/content/browser/resources/indexed_db/indexeddb_internals.html115
-rw-r--r--chromium/content/browser/resources/indexed_db/indexeddb_internals.js89
-rw-r--r--chromium/content/browser/resources/media/OWNERS11
-rw-r--r--chromium/content/browser/resources/media/cache_entry.js237
-rw-r--r--chromium/content/browser/resources/media/data_series.js132
-rw-r--r--chromium/content/browser/resources/media/disjoint_range_set.js145
-rw-r--r--chromium/content/browser/resources/media/disjoint_range_set_test.html96
-rw-r--r--chromium/content/browser/resources/media/dump_creator.js131
-rw-r--r--chromium/content/browser/resources/media/event_list.js64
-rw-r--r--chromium/content/browser/resources/media/item_store.js70
-rw-r--r--chromium/content/browser/resources/media/media_internals.css83
-rw-r--r--chromium/content/browser/resources/media/media_internals.html28
-rw-r--r--chromium/content/browser/resources/media/media_internals.js281
-rw-r--r--chromium/content/browser/resources/media/media_player.js154
-rw-r--r--chromium/content/browser/resources/media/metrics.js116
-rw-r--r--chromium/content/browser/resources/media/new/integration_test.html86
-rw-r--r--chromium/content/browser/resources/media/new/main.js134
-rw-r--r--chromium/content/browser/resources/media/new/media_internals.html18
-rw-r--r--chromium/content/browser/resources/media/new/media_internals.js18
-rw-r--r--chromium/content/browser/resources/media/new/player_info.js80
-rw-r--r--chromium/content/browser/resources/media/new/player_info_test.html146
-rw-r--r--chromium/content/browser/resources/media/new/player_manager.js111
-rw-r--r--chromium/content/browser/resources/media/new/player_manager_test.html155
-rw-r--r--chromium/content/browser/resources/media/new/util.js34
-rw-r--r--chromium/content/browser/resources/media/new/webui_resource_test.js210
-rw-r--r--chromium/content/browser/resources/media/peer_connection_update_table.js128
-rw-r--r--chromium/content/browser/resources/media/ssrc_info_manager.js166
-rw-r--r--chromium/content/browser/resources/media/stats_graph_helper.js265
-rw-r--r--chromium/content/browser/resources/media/stats_table.js137
-rw-r--r--chromium/content/browser/resources/media/timeline_graph_view.js523
-rw-r--r--chromium/content/browser/resources/media/util.js74
-rw-r--r--chromium/content/browser/resources/media/webrtc_internals.css118
-rw-r--r--chromium/content/browser/resources/media/webrtc_internals.html17
-rw-r--r--chromium/content/browser/resources/media/webrtc_internals.js264
48 files changed, 5845 insertions, 0 deletions
diff --git a/chromium/content/browser/resources/accessibility/accessibility.css b/chromium/content/browser/resources/accessibility/accessibility.css
new file mode 100644
index 00000000000..e00a9223e89
--- /dev/null
+++ b/chromium/content/browser/resources/accessibility/accessibility.css
@@ -0,0 +1,30 @@
+/*
+ * 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;
+}
+
diff --git a/chromium/content/browser/resources/accessibility/accessibility.html b/chromium/content/browser/resources/accessibility/accessibility.html
new file mode 100644
index 00000000000..c1a2d8d0bfc
--- /dev/null
+++ b/chromium/content/browser/resources/accessibility/accessibility.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+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.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Accessibility</title>
+ <link rel="stylesheet" href="accessibility.css">
+ <script src="chrome://resources/js/cr.js"></script>
+ <script src="chrome://resources/js/load_time_data.js"></script>
+ <script src="chrome://resources/js/util.js"></script>
+ <script src="strings.js"></script>
+ <script src="accessibility.js"></script>
+</head>
+<body>
+ <h1>Accessibility</h1>
+ <div id="global" class="row">Global accessibility mode:
+ <a id="toggle_global" href="#"></a></div>
+ <div id="pages" class="list"></div>
+ <script src="chrome://resources/js/i18n_template2.js"></script>
+</body>
+</html>
diff --git a/chromium/content/browser/resources/accessibility/accessibility.js b/chromium/content/browser/resources/accessibility/accessibility.js
new file mode 100644
index 00000000000..5d98b48c62f
--- /dev/null
+++ b/chromium/content/browser/resources/accessibility/accessibility.js
@@ -0,0 +1,212 @@
+// 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';
+
+ 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) {
+ $('toggle_global').textContent = (global_a11y_mode == 0 ? 'off' : 'on');
+ $('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 (data['a11y_mode'] != 0) {
+ 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');
+ link.setAttribute('href', '#');
+ var a11y_mode = data['a11y_mode'];
+ link.textContent = 'accessibility ' + (a11y_mode == 0 ? 'off' : 'on');
+ link.addEventListener('click',
+ toggleAccessibility.bind(this, data, link));
+ return link;
+ }
+
+ function createShowAccessibilityTreeElement(data, row, opt_refresh) {
+ var link = document.createElement('a');
+ link.setAttribute('href', '#');
+ 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');
+ link.setAttribute('href', '#');
+ 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 + '&nbsp;';
+ 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);
diff --git a/chromium/content/browser/resources/gpu/OWNERS b/chromium/content/browser/resources/gpu/OWNERS
new file mode 100644
index 00000000000..93d1471e1ef
--- /dev/null
+++ b/chromium/content/browser/resources/gpu/OWNERS
@@ -0,0 +1 @@
+nduca@chromium.org
diff --git a/chromium/content/browser/resources/gpu/browser_bridge.js b/chromium/content/browser/resources/gpu/browser_bridge.js
new file mode 100644
index 00000000000..cb4133a3398
--- /dev/null
+++ b/chromium/content/browser/resources/gpu/browser_bridge.js
@@ -0,0 +1,147 @@
+// 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
+ };
+});
diff --git a/chromium/content/browser/resources/gpu/browser_bridge_tests.js b/chromium/content/browser/resources/gpu/browser_bridge_tests.js
new file mode 100644
index 00000000000..ebff6a775ce
--- /dev/null
+++ b/chromium/content/browser/resources/gpu/browser_bridge_tests.js
@@ -0,0 +1,346 @@
+// 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.
+var commandLineFlags = ['--flag-switches-begin',
+ '--show-composited-layer-borders',
+ '--show-fps-counter',
+ '--flag-switches-end'];
+var commandLineStr = './out/Debug/chrome ' + commandLineFlags.join(' ');
+
+var glValueArray = ['GL_ARB_compatibility',
+ 'GL_ARB_copy_buffer',
+ 'GL_ARB_depth_buffer_float',
+ 'GL_ARB_depth_clamp',
+ 'GL_ARB_depth_texture',
+ 'GL_ARB_draw_buffers',
+ 'GL_ARB_draw_elements_base_vertex',
+ 'GL_ARB_draw_instanced',
+ 'GL_ARB_fragment_coord_conventions',
+ 'GL_ARB_fragment_program',
+ 'GL_ARB_fragment_program_shadow',
+ 'GL_ARB_fragment_shader',
+ 'GL_ARB_framebuffer_object',
+ 'GL_ARB_framebuffer_sRGB',
+ 'GL_ARB_geometry_shader4',
+ 'GL_ARB_half_float_pixel',
+ 'GL_ARB_half_float_vertex',
+ 'GL_ARB_imaging',
+ 'GL_ARB_map_buffer_range',
+ 'GL_ARB_multisample',
+ 'GL_ARB_multitexture',
+ 'GL_ARB_occlusion_query',
+ 'GL_ARB_pixel_buffer_object',
+ 'GL_ARB_point_parameters',
+ 'GL_ARB_point_sprite',
+ 'GL_ARB_provoking_vertex',
+ 'GL_ARB_seamless_cube_map',
+ 'GL_ARB_shader_objects',
+ 'GL_ARB_shading_language_100',
+ 'GL_ARB_shadow',
+ 'GL_ARB_sync',
+ 'GL_ARB_texture_border_clamp',
+ 'GL_ARB_texture_buffer_object',
+ 'GL_ARB_texture_compression',
+ 'GL_ARB_texture_compression_rgtc',
+ 'GL_ARB_texture_cube_map',
+ 'GL_ARB_texture_env_add',
+ 'GL_ARB_texture_env_combine',
+ 'GL_ARB_texture_env_crossbar',
+ 'GL_ARB_texture_env_dot3',
+ 'GL_ARB_texture_float',
+ 'GL_ARB_texture_mirrored_repeat',
+ 'GL_ARB_texture_multisample',
+ 'GL_ARB_texture_non_power_of_two',
+ 'GL_ARB_texture_rectangle',
+ 'GL_ARB_texture_rg',
+ 'GL_ARB_transpose_matrix',
+ 'GL_ARB_uniform_buffer_object',
+ 'GL_ARB_vertex_array_bgra',
+ 'GL_ARB_vertex_array_object',
+ 'GL_ARB_vertex_buffer_object',
+ 'GL_ARB_vertex_program',
+ 'GL_ARB_vertex_shader',
+ 'GL_ARB_window_pos',
+ 'GL_ATI_draw_buffers',
+ 'GL_ATI_texture_float',
+ 'GL_ATI_texture_mirror_once',
+ 'GL_S3_s3tc',
+ 'GL_EXT_texture_env_add',
+ 'GL_EXT_abgr',
+ 'GL_EXT_bgra',
+ 'GL_EXT_bindable_uniform',
+ 'GL_EXT_blend_color',
+ 'GL_EXT_blend_equation_separate',
+ 'GL_EXT_blend_func_separate',
+ 'GL_EXT_blend_minmax',
+ 'GL_EXT_blend_subtract',
+ 'GL_EXT_compiled_vertex_array',
+ 'GL_EXT_Cg_shader',
+ 'GL_EXT_depth_bounds_test',
+ 'GL_EXT_direct_state_access',
+ 'GL_EXT_draw_buffers2',
+ 'GL_EXT_draw_instanced',
+ 'GL_EXT_draw_range_elements',
+ 'GL_EXT_fog_coord',
+ 'GL_EXT_framebuffer_blit',
+ 'GL_EXT_framebuffer_multisample',
+ 'GL_EXTX_framebuffer_mixed_formats',
+ 'GL_EXT_framebuffer_object',
+ 'GL_EXT_framebuffer_sRGB',
+ 'GL_EXT_geometry_shader4',
+ 'GL_EXT_gpu_program_parameters',
+ 'GL_EXT_gpu_shader4',
+ 'GL_EXT_multi_draw_arrays',
+ 'GL_EXT_packed_depth_stencil',
+ 'GL_EXT_packed_float',
+ 'GL_EXT_packed_pixels',
+ 'GL_EXT_pixel_buffer_object',
+ 'GL_EXT_point_parameters',
+ 'GL_EXT_provoking_vertex',
+ 'GL_EXT_rescale_normal',
+ 'GL_EXT_secondary_color',
+ 'GL_EXT_separate_shader_objects',
+ 'GL_EXT_separate_specular_color',
+ 'GL_EXT_shadow_funcs',
+ 'GL_EXT_stencil_two_side',
+ 'GL_EXT_stencil_wrap',
+ 'GL_EXT_texture3D',
+ 'GL_EXT_texture_array',
+ 'GL_EXT_texture_buffer_object',
+ 'GL_EXT_texture_compression_latc',
+ 'GL_EXT_texture_compression_rgtc',
+ 'GL_EXT_texture_compression_s3tc',
+ 'GL_EXT_texture_cube_map',
+ 'GL_EXT_texture_edge_clamp',
+ 'GL_EXT_texture_env_combine',
+ 'GL_EXT_texture_env_dot3',
+ 'GL_EXT_texture_filter_anisotropic',
+ 'GL_EXT_texture_integer',
+ 'GL_EXT_texture_lod',
+ 'GL_EXT_texture_lod_bias',
+ 'GL_EXT_texture_mirror_clamp',
+ 'GL_EXT_texture_object',
+ 'GL_EXT_texture_shared_exponent',
+ 'GL_EXT_texture_sRGB',
+ 'GL_EXT_texture_swizzle',
+ 'GL_EXT_timer_query',
+ 'GL_EXT_vertex_array',
+ 'GL_EXT_vertex_array_bgra',
+ 'GL_IBM_rasterpos_clip',
+ 'GL_IBM_texture_mirrored_repeat',
+ 'GL_KTX_buffer_region',
+ 'GL_NV_blend_square',
+ 'GL_NV_conditional_render',
+ 'GL_NV_copy_depth_to_color',
+ 'GL_NV_copy_image',
+ 'GL_NV_depth_buffer_float',
+ 'GL_NV_depth_clamp',
+ 'GL_NV_explicit_multisample',
+ 'GL_NV_fence',
+ 'GL_NV_float_buffer',
+ 'GL_NV_fog_distance',
+ 'GL_NV_fragment_program',
+ 'GL_NV_fragment_program_option',
+ 'GL_NV_fragment_program2',
+ 'GL_NV_framebuffer_multisample_coverage',
+ 'GL_NV_geometry_shader4',
+ 'GL_NV_gpu_program4',
+ 'GL_NV_half_float',
+ 'GL_NV_light_max_exponent',
+ 'GL_NV_multisample_coverage',
+ 'GL_NV_multisample_filter_hint',
+ 'GL_NV_occlusion_query',
+ 'GL_NV_packed_depth_stencil',
+ 'GL_NV_parameter_buffer_object',
+ 'GL_NV_parameter_buffer_object2',
+ 'GL_NV_pixel_data_range',
+ 'GL_NV_point_sprite',
+ 'GL_NV_primitive_restart',
+ 'GL_NV_register_combiners',
+ 'GL_NV_register_combiners2',
+ 'GL_NV_shader_buffer_load',
+ 'GL_NV_texgen_reflection',
+ 'GL_NV_texture_barrier',
+ 'GL_NV_texture_compression_vtc',
+ 'GL_NV_texture_env_combine4',
+ 'GL_NV_texture_expand_normal',
+ 'GL_NV_texture_rectangle',
+ 'GL_NV_texture_shader',
+ 'GL_NV_texture_shader2',
+ 'GL_NV_texture_shader3',
+ 'GL_NV_transform_feedback',
+ 'GL_NV_vertex_array_range',
+ 'GL_NV_vertex_array_range2',
+ 'GL_NV_vertex_buffer_unified_memory',
+ 'GL_NV_vertex_program',
+ 'GL_NV_vertex_program1_1',
+ 'GL_NV_vertex_program2',
+ 'GL_NV_vertex_program2_option',
+ 'GL_NV_vertex_program3',
+ 'GL_NVX_conditional_render',
+ 'GL_NVX_gpu_memory_info',
+ 'GL_SGIS_generate_mipmap',
+ 'GL_SGIS_texture_lod',
+ 'GL_SGIX_depth_texture',
+ 'GL_SGIX_shadow',
+ 'GL_SUN_slice_accum'];
+(function() {
+ var dataSets = [
+ {
+ name: 'full_data_linux',
+ gpuInfo: {
+ basic_info: [
+ {
+ description: 'Initialization time',
+ value: '111'
+ },
+ {
+ description: 'Vendor Id',
+ value: '0x10de'
+ },
+ {
+ description: 'Device Id',
+ value: '0x0658'
+ },
+ {
+ description: 'Driver vendor',
+ value: 'NVIDIA'
+ },
+ {
+ description: 'Driver version',
+ value: '195.36.24'
+ },
+ {
+ description: 'Driver date',
+ value: ''
+ },
+ {
+ description: 'Pixel shader version',
+ value: '1.50'
+ },
+ {
+ description: 'Vertex shader version',
+ value: '1.50'
+ },
+ {
+ description: 'GL version',
+ value: '3.2'
+ },
+ {
+ description: 'GL_VENDOR',
+ value: 'NVIDIA Corporation'
+ },
+ {
+ description: 'GL_RENDERER',
+ value: 'Quadro FX 380/PCI/SSE2'
+ },
+ {
+ description: 'GL_VERSION',
+ value: '3.2.0 NVIDIA 195.36.24'
+ },
+ {
+ description: 'GL_EXTENSIONS',
+ value: glValueArray.join(' '),
+ }
+ ],
+ featureStatus: {
+ featureStatus:
+ [
+ {'status': 'enabled', name: '2d_canvas'},
+ {'status': 'enabled', name: '3d_css'},
+ {'status': 'enabled', name: 'compositing'},
+ {'status': 'enabled', name: 'webgl'},
+ {'status': 'enabled', name: 'multisampling'}
+ ],
+ problems: []
+ }
+ },
+ clientInfo: {
+ blacklist_version: '1.10',
+ command_line: commandLineStr,
+ version: 'Chrome/12.0.729.0',
+ },
+ logMessages: []
+ },
+ {
+ name: 'no_data',
+ gpuInfo: undefined,
+ clientInfo: undefined,
+ logMessages: undefined
+ },
+ {
+ name: 'logs',
+ gpuInfo: undefined,
+ clientInfo: undefined,
+ logMessages: [
+ {header: 'foo', message: 'Bar'}
+ ]
+ },
+
+ // tests for 'status'
+ {
+ name: 'feature_states',
+ gpuInfo: {
+ basic_info: undefined,
+ featureStatus: {
+ featureStatus: [
+ {'status': 'disabled_off', name: '2d_canvas'},
+ {'status': 'unavailable_software', name: '3d_css'},
+ {'status': 'disabled_software', name: 'compositing'},
+ {'status': 'software', name: 'compositing'},
+ {'status': 'unavailable_off', name: 'webgl'},
+ {'status': 'enabled', name: 'multisampling'}
+ ],
+ problems: [
+ {
+ description: 'Something wrong',
+ crBugs: [],
+ webkitBugs: []
+ },
+ {
+ description: 'SomethingElse',
+ crBugs: [],
+ webkitBugs: []
+ },
+ {
+ description: 'WebKit and Chrome bug',
+ crBugs: [23456],
+ webkitBugs: [789, 2123]
+ }
+ ]
+ }
+ },
+ clientInfo: undefined,
+ logMessages: []
+ }
+
+ ];
+
+ var selectEl = document.createElement('select');
+ for (var i = 0; i < dataSets.length; ++i) {
+ var optionEl = document.createElement('option');
+ optionEl.textContent = dataSets[i].name;
+ optionEl.dataSet = dataSets[i];
+ selectEl.add(optionEl);
+ }
+ selectEl.addEventListener('change', function() {
+ browserBridge.applySimulatedData_(dataSets[selectEl.selectedIndex]);
+ });
+ selectEl.addEventListener('keydown', function() {
+ window.setTimeout(function() {
+ browserBridge.applySimulatedData_(dataSets[selectEl.selectedIndex]);
+ }, 0);
+ });
+
+ var controlEl = document.createElement('div');
+ var textEl = document.createElement('span');
+ textEl.textContent = 'GPU Info:';
+ controlEl.appendChild(textEl);
+ controlEl.appendChild(selectEl);
+
+ document.querySelector('#debug-div').appendChild(controlEl,
+ document.body.firstChild);
+
+ browserBridge.applySimulatedData_(dataSets[0]);
+
+ })();
diff --git a/chromium/content/browser/resources/gpu/gpu_internals.html b/chromium/content/browser/resources/gpu/gpu_internals.html
new file mode 100644
index 00000000000..ad59aa817aa
--- /dev/null
+++ b/chromium/content/browser/resources/gpu/gpu_internals.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+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.
+-->
+<head i18n-values="dir:textdirection;">
+<style>
+* {
+ box-sizing: border-box;
+ -webkit-user-select: none;
+}
+
+body {
+ cursor: default;
+ font-family: sans-serif;
+ padding: 0;
+}
+
+#debug-div {
+ display: -webkit-box;
+ position: fixed;
+ top: 0px;
+ left: 50%;
+ border: 1px solid red;
+}
+
+tabbox tabpanels {
+ padding: 10px;
+}
+
+</style>
+<link rel="stylesheet" href="info_view.css">
+<link rel="stylesheet" href="chrome://resources/css/tabs.css">
+<link rel="stylesheet" href="chrome://resources/css/widgets.css">
+<script src="chrome://resources/js/cr.js"></script>
+<script src="chrome://resources/js/cr/event_target.js"></script>
+<script src="chrome://resources/js/cr/ui.js"></script>
+<script src="chrome://resources/js/cr/ui/focus_outline_manager.js"></script>
+<script src="chrome://resources/js/cr/ui/tabs.js"></script>
+<script src="chrome://resources/js/util.js"></script>
+<script src="chrome://gpu/gpu_internals.js"></script>
+<script src="chrome://gpu/strings.js"></script>
+</head>
+<body>
+ <div id="debug-div">
+ </div>
+ <include src="info_view.html">
+ <script src="chrome://resources/js/i18n_template.js"></script>
+ <script src="chrome://resources/js/i18n_process.js"></script>
+ <script src="chrome://resources/js/jstemplate_compiled.js"></script>
+</body>
+</html>
diff --git a/chromium/content/browser/resources/gpu/gpu_internals.js b/chromium/content/browser/resources/gpu/gpu_internals.js
new file mode 100644
index 00000000000..49b43e2553e
--- /dev/null
+++ b/chromium/content/browser/resources/gpu/gpu_internals.js
@@ -0,0 +1,20 @@
+// 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.
+
+<include src="browser_bridge.js"/>
+<include src="info_view.js"/>
+
+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);
diff --git a/chromium/content/browser/resources/gpu/info_view.css b/chromium/content/browser/resources/gpu/info_view.css
new file mode 100644
index 00000000000..867690952cf
--- /dev/null
+++ b/chromium/content/browser/resources/gpu/info_view.css
@@ -0,0 +1,58 @@
+/* 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. */
+
+#info-view {
+ -webkit-box-flex: 1;
+ overflow: auto;
+ padding: 10px;
+}
+
+#info-view * {
+ -webkit-user-select: text;
+}
+
+#info-view[selected] {
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+}
+
+#info-view h3,
+#info-view ul {
+ -webkit-margin-after: 0;
+ -webkit-margin-before: 0;
+}
+
+#info-view > div {
+ -webkit-margin-after: 1em;
+}
+
+#info-view .row-title {
+ font-weight: bold;
+}
+
+#info-view table {
+ border-collapse: collapse;
+ cursor: text;
+}
+
+#info-view table,
+#info-view th,
+#info-view td {
+ border: 1px solid #777;
+ padding-left: 4px;
+ padding-right: 4px;
+ text-align: top;
+}
+
+#info-view .feature-green {
+ color: rgb(0, 128, 0);
+}
+
+#info-view .feature-yellow {
+ color: rgb(128, 128, 0);
+}
+
+#info-view .feature-red {
+ color: rgb(255, 0, 0);
+}
diff --git a/chromium/content/browser/resources/gpu/info_view.html b/chromium/content/browser/resources/gpu/info_view.html
new file mode 100644
index 00000000000..5a4d9f4acc9
--- /dev/null
+++ b/chromium/content/browser/resources/gpu/info_view.html
@@ -0,0 +1,73 @@
+<!--
+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.
+-->
+<tabpanel id="info-view">
+ <div>
+ <h3>Graphics Feature Status</h3>
+ <ul class="feature-status-list">
+ </ul>
+ </div>
+ <div class='problems-div'>
+ <h3>Problems Detected</h3>
+ <ul class="problems-list">
+ </ul>
+ </div>
+
+ <div class='workarounds-div'>
+ <h3>Driver Bug Workarounds</h3>
+ <ul class="workarounds-list">
+ </ul>
+ </div>
+
+ <div>
+ <h3>Version Information</h3>
+ <div id="client-info"></div>
+ </div>
+
+ <div class="performance-div">
+ <h3>Performance Information</h3>
+ <div id="performance-info"></div>
+ </div>
+
+ <div>
+ <h3>Driver Information</h3>
+ <div id="basic-info"></div>
+ </div>
+
+ <div class="diagnostics">
+ <h3>Diagnostics</h3>
+ <div class="diagnostics-loading">... loading ...</div>
+ <div id="diagnostics-table">None</div>
+ </div>
+
+ <div id="log-messages" jsdisplay="values.length">
+ <h3>Log Messages</h3>
+ <ul>
+ <li jsselect="values">
+ <span jscontent="header"></span>: <span jscontent="message"></span>
+ </li>
+ </ul>
+ </div>
+
+ <!-- templates -->
+ <div style="display:none">
+ <div id="info-view-table-template">
+ <table id="info-view-table">
+ <tr jsselect="value">
+ <td jsdisplay="!(value instanceof Array)">
+ <span class="row-title" jscontent="description">title</span>
+ </td>
+ <td jsdisplay="!(value instanceof Array)">
+ <span jscontent="value">value</span>
+ </td>
+ <td jsdisplay="value instanceof Array" colspan=2>
+ <span jscontent="description" class="row-title"></span>
+ <div transclude="info-view-table-template"></div>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+</tabpanel>
diff --git a/chromium/content/browser/resources/gpu/info_view.js b/chromium/content/browser/resources/gpu/info_view.js
new file mode 100644
index 00000000000..d7d21d5a749
--- /dev/null
+++ b/chromium/content/browser/resources/gpu/info_view.js
@@ -0,0 +1,321 @@
+// 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;
+ 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 revision',
+ value: clientInfo.angle_revision
+ },
+ {
+ description: '2D graphics backend',
+ value: clientInfo.graphics_backend
+ }]);
+ } else {
+ this.setText_('client-info', '... loading...');
+ }
+
+ // Feature map
+ var featureLabelMap = {
+ '2d_canvas': 'Canvas',
+ '3d_css': '3D CSS',
+ 'css_animation': 'CSS Animation',
+ 'compositing': 'Compositing',
+ 'webgl': 'WebGL',
+ 'multisampling': 'WebGL multisampling',
+ 'flash_3d': 'Flash 3D',
+ 'flash_stage3d': 'Flash Stage3D',
+ 'flash_stage3d_baseline': 'Flash Stage3D Baseline profile',
+ 'texture_sharing': 'Texture Sharing',
+ 'video_decode': 'Video Decode',
+ 'video': 'Video',
+ // GPU Switching
+ 'gpu_switching': 'GPU Switching',
+ 'panel_fitting': 'Panel Fitting',
+ 'force_compositing_mode': 'Force Compositing Mode',
+ 'raster': 'Rasterization',
+ };
+ var statusLabelMap = {
+ 'disabled_software': 'Software only. Hardware acceleration disabled.',
+ 'disabled_software_animated': 'Software animated.',
+ 'disabled_off': 'Unavailable. Hardware acceleration disabled.',
+ 'software': 'Software rendered. Hardware acceleration not enabled.',
+ 'unavailable_off': 'Unavailable. Hardware acceleration unavailable',
+ 'unavailable_software':
+ 'Software only, hardware acceleration unavailable',
+ 'enabled_readback': 'Hardware accelerated, but at reduced performance',
+ 'enabled_force': 'Hardware accelerated on all pages',
+ 'enabled_threaded': 'Hardware accelerated on demand and threaded',
+ 'enabled_force_threaded':
+ 'Hardware accelerated on all pages and threaded',
+ 'enabled': 'Hardware accelerated',
+ 'accelerated': 'Accelerated',
+ 'accelerated_threaded': 'Accelerated and threaded',
+ // GPU Switching
+ 'gpu_switching_automatic': 'Automatic switching',
+ 'gpu_switching_force_discrete': 'Always on discrete GPU',
+ 'gpu_switching_force_integrated': 'Always on integrated GPU',
+ 'disabled_software_multithreaded': 'Software only, multi-threaded',
+ };
+
+ var statusClassMap = {
+ 'disabled_software': 'feature-yellow',
+ 'disabled_software_animated': 'feature-yellow',
+ 'disabled_off': 'feature-red',
+ 'software': 'feature-yellow',
+ 'unavailable_off': 'feature-red',
+ 'unavailable_software': 'feature-yellow',
+ 'enabled_force': 'feature-green',
+ 'enabled_readback': 'feature-yellow',
+ 'enabled_threaded': 'feature-green',
+ 'enabled_force_threaded': 'feature-green',
+ 'enabled': 'feature-green',
+ 'accelerated': 'feature-green',
+ 'accelerated_threaded': 'feature-green',
+ // GPU Switching
+ 'gpu_switching_automatic': 'feature-green',
+ 'gpu_switching_force_discrete': 'feature-red',
+ 'gpu_switching_force_integrated': 'feature-red',
+ 'disabled_software_multithreaded': 'feature-yellow',
+ };
+
+ // 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 performanceDiv = this.querySelector('.performance-div');
+ 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 (i = 0; i < gpuInfo.featureStatus.featureStatus.length;
+ i++) {
+ var feature = gpuInfo.featureStatus.featureStatus[i];
+ var featureEl = document.createElement('li');
+
+ var nameEl = document.createElement('span');
+ if (!featureLabelMap[feature.name])
+ console.log('Missing featureLabel for', feature.name);
+ nameEl.textContent = featureLabelMap[feature.name] + ': ';
+ featureEl.appendChild(nameEl);
+
+ var statusEl = document.createElement('span');
+ if (!statusLabelMap[feature.status])
+ console.log('Missing statusLabel for', feature.status);
+ if (!statusClassMap[feature.status])
+ console.log('Missing statusClass for', feature.status);
+ statusEl.textContent = statusLabelMap[feature.status];
+ statusEl.className = statusClassMap[feature.status];
+ 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.performance_info) {
+ performanceDiv.hidden = false;
+ this.setTable_('performance-info', gpuInfo.performance_info);
+ } else {
+ performanceDiv.hidden = true;
+ }
+
+ 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++;
+ }
+
+ 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
+ };
+});
diff --git a/chromium/content/browser/resources/gpu/timeline_test.html b/chromium/content/browser/resources/gpu/timeline_test.html
new file mode 100644
index 00000000000..e21f90e705e
--- /dev/null
+++ b/chromium/content/browser/resources/gpu/timeline_test.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<html>
+<head>
+<title></title>
+<link rel="stylesheet" href="timeline.css">
+<!--<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script>-->
+<script src="../../../../ui/webui/resources/js/cr.js"></script>
+<script src="../../../../ui/webui/resources/js/cr/event_target.js"></script>
+<script src="../../../../ui/webui/resources/js/cr/ui.js"></script>
+<script src="fast_rect_renderer.js"></script>
+<script src="sorted_array_utils.js"></script>
+<script src="timeline.js"></script>
+<script src="timeline_track.js"></script>
+<script src="timeline_model.js"></script>
+<!--
+<script>
+
+goog.require('goog.testing.jsunit');
+
+</script>
+-->
+</head>
+<body>
+<div id="sandbox"></div>
+<script>
+
+var sandbox = document.getElementById('sandbox');
+var timeline;
+
+function testTimeline() {
+ model = new gpu.TimelineModel();
+ model.importEvents(tracingControllerDataSets);
+ timeline = new gpu.Timeline();
+ timeline.model = model;
+ sandbox.appendChild(timeline);
+}
+document.addEventListener('DOMContentLoaded', testTimeline);
+</script>
+
+</body>
+</html>
diff --git a/chromium/content/browser/resources/indexed_db/OWNERS b/chromium/content/browser/resources/indexed_db/OWNERS
new file mode 100644
index 00000000000..b106dad853f
--- /dev/null
+++ b/chromium/content/browser/resources/indexed_db/OWNERS
@@ -0,0 +1,4 @@
+dgrogan@chromium.org
+michaeln@chromium.org
+jsbell@chromium.org
+alecflett@chromium.org
diff --git a/chromium/content/browser/resources/indexed_db/indexeddb_internals.css b/chromium/content/browser/resources/indexed_db/indexeddb_internals.css
new file mode 100644
index 00000000000..76653f0921c
--- /dev/null
+++ b/chromium/content/browser/resources/indexed_db/indexeddb_internals.css
@@ -0,0 +1,72 @@
+/* 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-transaction {
+ background-color: rgb(235, 239, 249);
+ margin: 4px 0;
+ border-radius: 4px;
+ padding: 2px;
+ position: relative;
+}
+
+.indexeddb-transaction.running {
+ font-weight: bold;
+}
+
+.indexeddb-transaction-mode,
+.indexeddb-transaction-scope,
+.indexeddb-transaction-state {
+ margin: 0 8px;
+}
+
+.controls a {
+ -webkit-margin-end: 16px;
+ color: #777;
+}
diff --git a/chromium/content/browser/resources/indexed_db/indexeddb_internals.html b/chromium/content/browser/resources/indexed_db/indexeddb_internals.html
new file mode 100644
index 00000000000..153aef1aa11
--- /dev/null
+++ b/chromium/content/browser/resources/indexed_db/indexeddb_internals.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<html i18n-values="dir:textdirection;">
+<head>
+ <meta charset="utf-8">
+ <title>IndexedDB</title>
+ <link rel="stylesheet" href="chrome://resources/css/tabs.css">
+ <link rel="stylesheet" href="chrome://resources/css/widgets.css">
+ <link rel="stylesheet" href="indexeddb_internals.css">
+</head>
+<body i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize">
+ <!-- templates -->
+ <div style="display:none">
+ <div id="indexeddb-list-template"
+ jsvalues="$partition_path:$this.partition_path">
+ <div class="indexeddb-summary">
+ <span>Instances in: </span>
+ <span jscontent="$this.partition_path"></span>
+ <span jscontent="'(' + $this.idbs.length + ')'"></span>
+ </div>
+ <div class="indexeddb-item" jsselect="$this.idbs">
+ <a class="indexeddb-url" jscontent="url" jsvalues="href:url"
+ target="_blank"></a>
+ <div class="indexeddb-size">
+ <span>Size:</span>
+ <span jscontent="size"></span>
+ </div>
+ <div class="indexeddb-last-modified">
+ <span>Last modified:</span>
+ <span jscontent="new Date(last_modified)"></span>
+ </div>
+ <div>
+ <span>Open connections:</span>
+ <span class="connection-count"
+ jsvalues=".idb_origin_url:url;.idb_partition_path:$partition_path"
+ jscontent="connection_count">
+ </div>
+ <div class="indexeddb-last-modified">
+ <span>Path:</span>
+ <span jscontent="path"></span>
+ </div>
+ <div class="controls">
+ <a href="#" class="force-close"
+ jsvalues=".idb_origin_url:url;.idb_partition_path:$partition_path">Force close</a>
+ <a href="#" class="download"
+ jsvalues=".idb_origin_url:url;.idb_partition_path:$partition_path">Download</a>
+ <span class="download-status" style="display: none">Loading...</span>
+ </div>
+ <div class="indexeddb-database" jsselect="$this.databases">
+
+ <span>Open database:</span>
+ <span jscontent="name"></span>
+
+ <div>
+ <span>Connections:</span>
+
+ <span class="indexeddb-connection-count"
+ jsdisplay="connection_count">
+ <span>open:</span>
+ <span jscontent="connection_count"></span>
+ </span>
+
+ <span class="indexeddb-connection-count pending"
+ jsdisplay="pending_opens">
+ <span>pending opens:</span>
+ <span jscontent="pending_opens"></span>
+ </span>
+
+ <span class="indexeddb-connection-count pending"
+ jsdisplay="pending_upgrades">
+ <span>pending upgrades:</span>
+ <span jscontent="pending_upgrades"></span>
+ </span>
+
+ <span class="indexeddb-connection-count pending"
+ jsdisplay="running_upgrades">
+ <span>running upgrades:</span>
+ <span jscontent="running_upgrades"></span>
+ </span>
+
+ <span class="indexeddb-connection-count pending"
+ jsdisplay="pending_deletes">
+ <span>pending deletes:</span>
+ <span jscontent="pending_deletes"></span>
+ </span>
+
+ </div>
+ <div class="indexeddb-transaction"
+ jsselect="$this.transactions"
+ jseval="this.classList.add($this.running ? 'running' : 'waiting')">
+ <span>Transaction:</span>
+ <span class="indexeddb-transaction-mode" jscontent="mode"></span>
+ <span class="indexeddb-transaction-scope" jsdisplay="scope">
+ <span>scope:</span> <span jscontent="'[ ' + scope.join(', ') + ' ]'"></span>
+ </span>
+ <span class="indexeddb-transaction-state"
+ jscontent="running ? '(running)' : '(blocked)'"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <h1>IndexedDB</h1>
+ <div class="content">
+ <div id="indexeddb-list">
+ </div>
+ <script src="chrome://resources/js/util.js"></script>
+ <script src="chrome://resources/js/cr.js"></script>
+ <script src="indexeddb_internals.js"></script>
+ <script src="chrome://resources/js/load_time_data.js"></script>
+ <script src="chrome://resources/js/jstemplate_compiled.js"></script>
+ <script src="strings.js"></script>
+ <script src="chrome://resources/js/i18n_template2.js"></script>
+</body>
+</html>
+
diff --git a/chromium/content/browser/resources/indexed_db/indexeddb_internals.js b/chromium/content/browser/resources/indexed_db/indexeddb_internals.js
new file mode 100644
index 00000000000..fc80a25d024
--- /dev/null
+++ b/chromium/content/browser/resources/indexed_db/indexeddb_internals.js
@@ -0,0 +1,89 @@
+// 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);
diff --git a/chromium/content/browser/resources/media/OWNERS b/chromium/content/browser/resources/media/OWNERS
new file mode 100644
index 00000000000..d132d0e6061
--- /dev/null
+++ b/chromium/content/browser/resources/media/OWNERS
@@ -0,0 +1,11 @@
+acolwell@chromium.org
+dalecurtis@chromium.org
+ddorwin@chromium.org
+fischman@chromium.org
+scherkus@chromium.org
+shadi@chromium.org
+tommi@chromium.org
+vrk@chromium.org
+wjia@chromium.org
+xhwang@chromium.org
+xians@chromium.org
diff --git a/chromium/content/browser/resources/media/cache_entry.js b/chromium/content/browser/resources/media/cache_entry.js
new file mode 100644
index 00000000000..275a8c74a50
--- /dev/null
+++ b/chromium/content/browser/resources/media/cache_entry.js
@@ -0,0 +1,237 @@
+// 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 <details> element representing this CacheEntry.
+ this.details_ = document.createElement('details');
+ this.details_.className = 'cache-entry';
+ this.details_.open = false;
+
+ // The <details> 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() {
+ 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(media.makeElement('th', 'Read From Cache'));
+ headerRow.appendChild(media.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(media.makeElement('td', read[i] || ''));
+ row.appendChild(media.makeElement('td', written[i] || ''));
+ body.appendChild(row);
+ }
+
+ this.drawCacheWritesToCanvas(this.writeCanvas);
+ this.drawCacheReadsToCanvas(this.readCanvas);
+ },
+
+ /**
+ * Render this CacheEntry as a <li>.
+ * @return {HTMLElement} A <li> representing this CacheEntry.
+ */
+ toListItem: function() {
+ this.generateDetails();
+
+ var result = document.createElement('li');
+ result.appendChild(this.details_);
+ return result;
+ }
+ };
+
+ return {
+ CacheEntry: CacheEntry
+ };
+});
diff --git a/chromium/content/browser/resources/media/data_series.js b/chromium/content/browser/resources/media/data_series.js
new file mode 100644
index 00000000000..8947492dbf4
--- /dev/null
+++ b/chromium/content/browser/resources/media/data_series.js
@@ -0,0 +1,132 @@
+// 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;
+ },
+
+ /**
+ * 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;
+})();
diff --git a/chromium/content/browser/resources/media/disjoint_range_set.js b/chromium/content/browser/resources/media/disjoint_range_set.js
new file mode 100644
index 00000000000..bd504bb9a37
--- /dev/null
+++ b/chromium/content/browser/resources/media/disjoint_range_set.js
@@ -0,0 +1,145 @@
+// 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
+ };
+});
diff --git a/chromium/content/browser/resources/media/disjoint_range_set_test.html b/chromium/content/browser/resources/media/disjoint_range_set_test.html
new file mode 100644
index 00000000000..39db9b34b45
--- /dev/null
+++ b/chromium/content/browser/resources/media/disjoint_range_set_test.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<!--
+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.
+-->
+ <head>
+ <title></title>
+ <script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script>
+ <script src="../../../../ui/webui/resources/js/cr.js"></script>
+ <script src="disjoint_range_set.js"></script>
+ <script>
+ goog.require('goog.testing.jsunit');
+ </script>
+ </head>
+ <body>
+ <script>
+
+ var range;
+
+ function assertRangeEquals(ranges) {
+ assertArrayEquals(
+ ranges, range.map(function(start, end) { return [start, end]; }));
+ };
+
+ function setUp() {
+ range = new media.DisjointRangeSet;
+ };
+
+ function testAdd() {
+ range.add(1, 6);
+ assertRangeEquals([[1, 6]]);
+ range.add(-5, -3);
+ assertRangeEquals([[-5, -3], [1, 6]]);
+ };
+
+ function testAddAdjacent() {
+ range.add(3, 6);
+ assertRangeEquals([[3, 6]]);
+ range.add(1, 2);
+ assertRangeEquals([[1, 6]]);
+ range.add(7, 9);
+ assertRangeEquals([[1, 9]]);
+ };
+
+ function testAddNotQuiteAdjacent() {
+ range.add(3, 6);
+ assertRangeEquals([[3, 6]]);
+ range.add(0, 1);
+ assertRangeEquals([[0, 1], [3, 6]]);
+ range.add(8, 9);
+ assertRangeEquals([[0, 1], [3, 6], [8, 9]]);
+ };
+
+ function testAddOverlapping() {
+ range.add(1, 6);
+ assertRangeEquals([[1, 6]]);
+ range.add(5, 8);
+ assertRangeEquals([[1, 8]]);
+ range.add(0, 1);
+ assertRangeEquals([[0, 8]]);
+ };
+
+ function testMax() {
+ assertNull(range.max());
+ range.add(1, 6);
+ assertEquals(range.max(), 6);
+ range.add(3, 8);
+ assertEquals(range.max(), 8);
+ range.remove(2, 3);
+ assertEquals(range.max(), 8);
+ range.remove(4, 10);
+ assertEquals(range.max(), 1);
+ range.remove(1, 1);
+ assertNull(range.max());
+ };
+
+ function testRemove() {
+ range.add(1, 20);
+ assertRangeEquals([[1, 20]]);
+ range.remove(0, 3);
+ assertRangeEquals([[4, 20]]);
+ range.remove(18, 20);
+ assertRangeEquals([[4, 17]]);
+ range.remove(5, 16);
+ assertRangeEquals([[4, 4], [17, 17]]);
+ };
+
+ function testStartsEmpty() {
+ assertRangeEquals([]);
+ };
+
+ </script>
+ </body>
+</html>
diff --git a/chromium/content/browser/resources/media/dump_creator.js b/chromium/content/browser/resources/media/dump_creator.js
new file mode 100644
index 00000000000..5daddabc61c
--- /dev/null
+++ b/chromium/content/browser/resources/media/dump_creator.js
@@ -0,0 +1,131 @@
+// 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 to start and stop RTP recording, forwards the start/stop
+ * commands to Chrome, and updates the UI based on dump updates. Also provides
+ * creating a file containing all PeerConnection updates and stats.
+ */
+var DumpCreator = (function() {
+ /**
+ * @param {Element} containerElement The parent element of the dump creation
+ * UI.
+ * @constructor
+ */
+ function DumpCreator(containerElement) {
+ /**
+ * True if the RTP packets are being recorded.
+ * @type {bool}
+ * @private
+ */
+ this.recording_ = false;
+
+ /**
+ * @type {!Object.<string>}
+ * @private
+ * @const
+ */
+ this.StatusStrings_ = {
+ NOT_STARTED: 'not started.',
+ RECORDING: 'recording...',
+ },
+
+ /**
+ * The status of dump creation.
+ * @type {string}
+ * @private
+ */
+ this.status_ = this.StatusStrings_.NOT_STARTED;
+
+ /**
+ * 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('pre');
+ this.root_.appendChild(content);
+
+ content.innerHTML = '<button disabled></button> Status: <span></span>' +
+ '<div><form><button>' +
+ 'Download the PeerConnection updates and stats data' +
+ '</button></form></div>';
+ content.getElementsByTagName('button')[0].addEventListener(
+ 'click', this.onRtpToggled_.bind(this));
+ content.getElementsByTagName('button')[1].addEventListener(
+ 'click', this.onDownloadData_.bind(this));
+
+ this.updateDisplay_();
+ }
+
+ DumpCreator.prototype = {
+ /**
+ * Downloads the PeerConnection updates and stats data as a file.
+ *
+ * @private
+ */
+ onDownloadData_: function() {
+ var textBlob =
+ new Blob([JSON.stringify(peerConnectionDataStore, null, ' ')],
+ {type: 'octet/stream'});
+ var URL = window.webkitURL.createObjectURL(textBlob);
+ this.root_.getElementsByTagName('form')[0].action = URL;
+ // The default action of the button will submit the form.
+ },
+
+ /**
+ * Handles the event of toggling the rtp recording state.
+ *
+ * @private
+ */
+ onRtpToggled_: function() {
+ if (this.recording_) {
+ this.recording_ = false;
+ this.status_ = this.StatusStrings_.NOT_STARTED;
+ chrome.send('stopRtpRecording');
+ } else {
+ this.recording_ = true;
+ this.status_ = this.StatusStrings_.RECORDING;
+ chrome.send('startRtpRecording');
+ }
+ this.updateDisplay_();
+ },
+
+ /**
+ * Updates the UI based on the recording status.
+ *
+ * @private
+ */
+ updateDisplay_: function() {
+ if (this.recording_) {
+ this.root_.getElementsByTagName('button')[0].textContent =
+ 'Stop Recording RTP Packets';
+ } else {
+ this.root_.getElementsByTagName('button')[0].textContent =
+ 'Start Recording RTP Packets';
+ }
+
+ this.root_.getElementsByTagName('span')[0].textContent = this.status_;
+ },
+
+ /**
+ * Set the status to the content of the update.
+ * @param {!Object} update
+ */
+ onUpdate: function(update) {
+ if (this.recording_) {
+ this.status_ = JSON.stringify(update);
+ this.updateDisplay_();
+ }
+ },
+ };
+ return DumpCreator;
+})();
diff --git a/chromium/content/browser/resources/media/event_list.js b/chromium/content/browser/resources/media/event_list.js
new file mode 100644
index 00000000000..df4d4273063
--- /dev/null
+++ b/chromium/content/browser/resources/media/event_list.js
@@ -0,0 +1,64 @@
+// 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.
+
+cr.define('media', function() {
+ 'use strict';
+
+ /**
+ * This class holds a list of MediaLogEvents.
+ * It inherits from <li> and contains a tabular list of said events,
+ * the time at which they occurred, and their parameters.
+ */
+ var EventList = cr.ui.define('li');
+
+ EventList.prototype = {
+ __proto__: HTMLLIElement.prototype,
+ startTime_: null,
+
+ /**
+ * Decorate this list item as an EventList.
+ */
+ decorate: function() {
+ this.table_ = document.createElement('table');
+ var details = document.createElement('details');
+ var summary = media.makeElement('summary', 'Log:');
+ details.appendChild(summary);
+ details.appendChild(this.table_);
+ this.appendChild(details);
+
+ var hRow = document.createElement('tr');
+ hRow.appendChild(media.makeElement('th', 'Time:'));
+ hRow.appendChild(media.makeElement('th', 'Event:'));
+ hRow.appendChild(media.makeElement('th', 'Parameters:'));
+ var header = document.createElement('thead');
+ header.appendChild(hRow);
+ this.table_.appendChild(header);
+ },
+
+ /**
+ * Add an event to the list. It is stored as a new row in this.table_.
+ * @param {Object} event The MediaLogEvent that has occurred.
+ */
+ addEvent: function(event) {
+ this.startTime_ = this.startTime_ || event.ticksMillis;
+ var normalizedTicksMillis = event.ticksMillis - this.startTime_;
+
+ var row = document.createElement('tr');
+ row.appendChild(media.makeElement(
+ 'td', normalizedTicksMillis.toFixed(1)));
+ row.appendChild(media.makeElement('td', event.type));
+ var params = [];
+ for (var key in event.params) {
+ params.push(key + ': ' + event.params[key]);
+ }
+
+ row.appendChild(media.makeElement('td', params.join(', ')));
+ this.table_.appendChild(row);
+ }
+ };
+
+ return {
+ EventList: EventList
+ };
+});
diff --git a/chromium/content/browser/resources/media/item_store.js b/chromium/content/browser/resources/media/item_store.js
new file mode 100644
index 00000000000..a6e3a6c4577
--- /dev/null
+++ b/chromium/content/browser/resources/media/item_store.js
@@ -0,0 +1,70 @@
+// 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 stores hashes by their id field and provides basic methods for
+ * iterating over the collection.
+ * @constructor
+ */
+ function ItemStore() {
+ this.items_ = {};
+ }
+
+ ItemStore.prototype = {
+ /**
+ * Get a sorted list of item ids.
+ * @return {Array} A sorted array of ids.
+ */
+ ids: function() {
+ var ids = [];
+ for (var i in this.items_)
+ ids.push(i);
+ return ids.sort();
+ },
+
+ /**
+ * Add an item to the store.
+ * @param {Object} item The item to be added.
+ * @param {string} item.id The id of the item.
+ */
+ addItem: function(item) {
+ this.items_[item.id] = item;
+ },
+
+ /**
+ * Add a dictionary of items to the store.
+ * @param {Object} items A dictionary of individual items. The keys are
+ * irrelevant but each must have an id field.
+ */
+ addItems: function(items) {
+ for (id in items)
+ this.addItem(items[id]);
+ },
+
+ /**
+ * Remove an item from the store.
+ * @param {string} id The id of the item to be removed.
+ */
+ removeItem: function(id) {
+ delete this.items_[id];
+ },
+
+ /**
+ * Map this itemStore to an Array. Items are sorted by id.
+ * @param {function(*)} mapper The mapping function applied to each item.
+ * @return {Array} An array of mapped items.
+ */
+ map: function(mapper) {
+ var items = this.items_;
+ var ids = this.ids();
+ return ids.map(function(id) { return mapper(items[id]); });
+ }
+ };
+
+ return {
+ ItemStore: ItemStore
+ };
+});
diff --git a/chromium/content/browser/resources/media/media_internals.css b/chromium/content/browser/resources/media/media_internals.css
new file mode 100644
index 00000000000..d83b6b71eeb
--- /dev/null
+++ b/chromium/content/browser/resources/media/media_internals.css
@@ -0,0 +1,83 @@
+/* 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. */
+
+body {
+ font-family: sans-serif;
+}
+
+h2 {
+ margin: 15px 0 5px 0;
+}
+
+ul,
+p,
+canvas {
+ margin: 0;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+#media-players td,
+#media-players th {
+ padding: 0 10px;
+}
+
+.audio-stream[status='created'] {
+ color: blue;
+}
+
+.audio-stream[status='closed'] {
+ text-decoration: line-through;
+}
+
+.audio-stream[status='error'] {
+ color: red;
+}
+
+#cache-entries ul,
+#media-players ul,
+#media-players {
+ list-style-type: none;
+}
+
+.cache-entry {
+ margin: 0 0 5px 0;
+}
+
+.cache-entry-controls {
+ font-size: smaller;
+}
+
+.cache-table {
+ table-layout: fixed;
+ width: 500px;
+}
+
+thead {
+ text-align: left;
+}
+
+tfoot {
+ text-align: right;
+}
+
+.buffered {
+ display: table;
+}
+
+.buffered > div {
+ display: table-row;
+}
+
+.buffered > div > div {
+ display: table-cell;
+ vertical-align: bottom;
+}
+
+.buffered > div > div:first-child {
+ font-weight: bold;
+ padding-right: 2px;
+}
diff --git a/chromium/content/browser/resources/media/media_internals.html b/chromium/content/browser/resources/media/media_internals.html
new file mode 100644
index 00000000000..05d321f0ac7
--- /dev/null
+++ b/chromium/content/browser/resources/media/media_internals.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html i18n-values="dir:textdirection;">
+<!--
+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.
+-->
+ <head>
+ <link rel="stylesheet" href="media_internals.css">
+ <script src="chrome://resources/js/cr.js"></script>
+ <script src="chrome://resources/js/cr/ui.js"></script>
+ <script src="chrome://resources/js/util.js"></script>
+ <script src="chrome://media-internals/media_internals.js"></script>
+ <script src="chrome://media-internals/strings.js"></script>
+ <title>Media Internals</title>
+ </head>
+ <body>
+ <h2>Active media players:</h2>
+ <ul id="media-players"></ul>
+ <h2>Active audio streams:</h2>
+ <div id="audio-streams"></div>
+ <h2>Cached resources:</h2>
+ <div id="cache-entries"></div>
+ <script src="chrome://resources/js/i18n_template.js"></script>
+ <script src="chrome://resources/js/i18n_process.js"></script>
+ <script src="chrome://resources/js/jstemplate_compiled.js"></script>
+ </body>
+</html>
diff --git a/chromium/content/browser/resources/media/media_internals.js b/chromium/content/browser/resources/media/media_internals.js
new file mode 100644
index 00000000000..c3e3a1ed1d1
--- /dev/null
+++ b/chromium/content/browser/resources/media/media_internals.js
@@ -0,0 +1,281 @@
+// 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.
+
+<include src="cache_entry.js"/>
+<include src="disjoint_range_set.js"/>
+<include src="event_list.js"/>
+<include src="item_store.js"/>
+<include src="media_player.js"/>
+<include src="metrics.js"/>
+<include src="util.js"/>
+
+cr.define('media', function() {
+ 'use strict';
+
+ // Stores information on open audio streams, referenced by id.
+ var audioStreams = new media.ItemStore;
+
+ // Active media players, indexed by 'render_id:player_id'.
+ var mediaPlayers = {};
+
+ // Cached files indexed by key and source id.
+ var cacheEntriesByKey = {};
+ var cacheEntries = {};
+
+ // Map of event source -> url.
+ var requestURLs = {};
+
+ // Constants passed to us from Chrome.
+ var eventTypes = {};
+ var eventPhases = {};
+
+ // The <div>s on the page in which to display information.
+ var audioStreamDiv;
+ var cacheDiv;
+
+ // A timer used to limit the rate of redrawing the Media Players section.
+ var redrawTimer = null;
+
+ /**
+ * Initialize variables and ask MediaInternals for all its data.
+ */
+ function initialize() {
+ audioStreamDiv = $('audio-streams');
+ cacheDiv = $('cache-entries');
+
+ // Get information about all currently active media.
+ chrome.send('getEverything');
+ }
+
+ /**
+ * Write the set of audio streams to the DOM.
+ */
+ function printAudioStreams() {
+
+ /**
+ * Render a single stream as a <li>.
+ * @param {Object} stream The stream to render.
+ * @return {HTMLElement} A <li> containing the stream information.
+ */
+ function printStream(stream) {
+ var out = document.createElement('li');
+ out.id = stream.id;
+ out.className = 'audio-stream';
+ out.setAttribute('status', stream.status);
+
+ out.textContent += 'Audio stream ' + stream.id.split('.')[1];
+ out.textContent += ' is ' + (stream.playing ? 'playing' : 'paused');
+ if (typeof stream.volume != 'undefined') {
+ out.textContent += ' at ' + (stream.volume * 100).toFixed(0);
+ out.textContent += '% volume.';
+ }
+ return out;
+ }
+
+ var out = document.createElement('ul');
+ audioStreams.map(printStream).forEach(function(s) {
+ out.appendChild(s);
+ });
+
+ audioStreamDiv.textContent = '';
+ audioStreamDiv.appendChild(out);
+ }
+
+ /**
+ * Redraw each MediaPlayer.
+ */
+ function printMediaPlayers() {
+ for (var key in mediaPlayers) {
+ mediaPlayers[key].redraw();
+ }
+ redrawTimer = null;
+ }
+
+ /**
+ * Write the set of sparse CacheEntries to the DOM.
+ */
+ function printSparseCacheEntries() {
+ var out = document.createElement('ul');
+ for (var key in cacheEntriesByKey) {
+ if (cacheEntriesByKey[key].sparse)
+ out.appendChild(cacheEntriesByKey[key].toListItem());
+ }
+
+ cacheDiv.textContent = '';
+ cacheDiv.appendChild(out);
+ }
+
+ /**
+ * Receiving data for an audio stream.
+ * Add it to audioStreams and update the page.
+ * @param {Object} stream JSON representation of an audio stream.
+ */
+ function addAudioStream(stream) {
+ audioStreams.addItem(stream);
+ printAudioStreams();
+ }
+
+ /**
+ * Receiving all data.
+ * Add it all to the appropriate stores and update the page.
+ * @param {Object} stuff JSON containing lists of data.
+ * @param {Object} stuff.audio_streams A dictionary of audio streams.
+ */
+ function onReceiveEverything(stuff) {
+ audioStreams.addItems(stuff.audio_streams);
+ printAudioStreams();
+ }
+
+ /**
+ * Removing an item from the appropriate store.
+ * @param {string} id The id of the item to be removed, in the format
+ * "item_type.identifying_info".
+ */
+ function onItemDeleted(id) {
+ var type = id.split('.')[0];
+ switch (type) {
+ case 'audio_streams':
+ audioStreams.removeItem(id);
+ printAudioStreams();
+ break;
+ }
+ }
+
+ /**
+ * A render process has ended, delete any media players associated with it.
+ * @param {number} renderer The id of the render process.
+ */
+ function onRendererTerminated(renderer) {
+ for (var key in mediaPlayers) {
+ if (mediaPlayers[key].renderer == renderer) {
+ $('media-players').removeChild(mediaPlayers[key]);
+ delete mediaPlayers[key];
+ break;
+ }
+ }
+ printMediaPlayers();
+ }
+
+ /**
+ * Receiving net events.
+ * Update cache information and update that section of the page.
+ * @param {Array} updates A list of net events that have occurred.
+ */
+ function onNetUpdate(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':
+ requestURLs[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 = requestURLs[update.source.id];
+ delete requestURLs[update.source.id];
+ if (range && key) {
+ if (!cacheEntriesByKey[key]) {
+ cacheEntriesByKey[key] = new media.CacheEntry;
+ cacheEntriesByKey[key].key = key;
+ }
+ cacheEntriesByKey[key].size = range[1];
+ }
+ break;
+ }
+ });
+
+ printSparseCacheEntries();
+ }
+
+ /**
+ * Receiving values for constants. Store them for later use.
+ * @param {Object} constants A dictionary of constants.
+ * @param {Object} constants.eventTypes A dictionary of event name -> int.
+ * @param {Object} constants.eventPhases A dictionary of event phase -> int.
+ */
+ function onReceiveConstants(constants) {
+ var events = constants.eventTypes;
+ for (var e in events) {
+ eventTypes[events[e]] = e;
+ }
+
+ var phases = constants.eventPhases;
+ for (var p in phases) {
+ eventPhases[phases[p]] = p;
+ }
+ }
+
+ /**
+ * Receiving notification of a media event.
+ * @param {Object} event The json representation of a MediaLogEvent.
+ */
+ function onMediaEvent(event) {
+ var source = event.renderer + ':' + event.player;
+ var item = mediaPlayers[source] ||
+ new media.MediaPlayer({id: source, renderer: event.renderer});
+ mediaPlayers[source] = item;
+ item.addEvent(event);
+
+ // Both media and net events could provide the size of the file.
+ // Media takes priority, but keep the size in both places synchronized.
+ if (cacheEntriesByKey[item.properties.url]) {
+ item.properties.total_bytes = item.properties.total_bytes ||
+ cacheEntriesByKey[item.properties.url].size;
+ cacheEntriesByKey[item.properties.url].size = item.properties.total_bytes;
+ }
+
+ // Events tend to arrive in groups; don't redraw the page too often.
+ if (!redrawTimer)
+ redrawTimer = setTimeout(printMediaPlayers, 50);
+ }
+
+ return {
+ initialize: initialize,
+ addAudioStream: addAudioStream,
+ cacheEntriesByKey: cacheEntriesByKey,
+ onReceiveEverything: onReceiveEverything,
+ onItemDeleted: onItemDeleted,
+ onRendererTerminated: onRendererTerminated,
+ onNetUpdate: onNetUpdate,
+ onReceiveConstants: onReceiveConstants,
+ onMediaEvent: onMediaEvent
+ };
+});
+
+/**
+ * Initialize everything once we have access to the DOM.
+ */
+document.addEventListener('DOMContentLoaded', function() {
+ media.initialize();
+});
diff --git a/chromium/content/browser/resources/media/media_player.js b/chromium/content/browser/resources/media/media_player.js
new file mode 100644
index 00000000000..a9d5d6b3333
--- /dev/null
+++ b/chromium/content/browser/resources/media/media_player.js
@@ -0,0 +1,154 @@
+// 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 inherits from <li> and is designed to store and display
+ * information about an open media player.
+ */
+ var MediaPlayer = cr.ui.define('li');
+
+ MediaPlayer.prototype = {
+ __proto__: HTMLLIElement.prototype,
+ renderer: null,
+ id: null,
+
+ /**
+ * Decorate this <li> as a MediaPlayer.
+ */
+ decorate: function() {
+ this.properties = {};
+
+ this.url_ = document.createElement('span');
+ this.url_.textContent = 'URL Unknown';
+
+ this.summary_ = document.createElement('summary');
+ this.summary_.appendChild(this.url_);
+
+ var bufferedDiv = document.createElement('div');
+ bufferedDiv.className = 'buffered';
+ this.summary_.appendChild(bufferedDiv);
+
+ // Create our canvii.
+ function createCanvas(label) {
+ var canvas = document.createElement('canvas');
+ canvas.width = media.BAR_WIDTH;
+ canvas.height = media.BAR_HEIGHT;
+ return canvas;
+ }
+ this.bufferedCanvas_ = createCanvas();
+ this.cacheReadsCanvas_ = createCanvas();
+ this.cacheWritesCanvas_ = createCanvas();
+
+ // Create our per-canvas entry divs that are initially hidden.
+ function addEntry(label, canvas) {
+ var labelDiv = document.createElement('div');
+ labelDiv.textContent = label;
+ var canvasDiv = document.createElement('div');
+ canvasDiv.appendChild(canvas);
+ var entryDiv = document.createElement('div');
+ entryDiv.appendChild(labelDiv);
+ entryDiv.appendChild(canvasDiv);
+ entryDiv.hidden = true;
+ bufferedDiv.appendChild(entryDiv);
+ return entryDiv;
+ }
+ this.bufferedEntry_ = addEntry('Buffered', this.bufferedCanvas_);
+ this.cacheReadsEntry_ = addEntry('Cache Reads', this.cacheReadsCanvas_);
+ this.cacheWritesEntry_ = addEntry(
+ 'Cache Writes', this.cacheWritesCanvas_);
+
+ this.details_ = document.createElement('details');
+ this.details_.appendChild(this.summary_);
+
+ this.propertyTable_ = document.createElement('table');
+ this.events_ = new media.EventList;
+ this.metrics_ = new media.Metrics;
+
+ var properties = media.createDetailsLi();
+ properties.summary.textContent = 'Properties:';
+ properties.details.appendChild(this.propertyTable_);
+
+ var ul = document.createElement('ul');
+ ul.appendChild(properties);
+ ul.appendChild(this.metrics_);
+ ul.appendChild(this.events_);
+ this.details_.appendChild(ul);
+
+ this.appendChild(this.details_);
+ $('media-players').appendChild(this);
+ },
+
+ /**
+ * Record an event and update statistics etc.
+ * @param {Object} event The event that occurred.
+ */
+ addEvent: function(event) {
+ for (var key in event.params) {
+ this.properties[key] = event.params[key];
+ }
+
+ if (event.type == 'LOAD' && event.params['url']) {
+ this.url_.textContent = event.params['url'];
+ }
+
+ if (event.type == 'BUFFERED_EXTENTS_CHANGED') {
+ return;
+ }
+ this.events_.addEvent(event);
+ this.metrics_.addEvent(event);
+ },
+
+ /**
+ * Update the summary line and properties table and redraw the canvas.
+ * @return {HTMLElement} A <li> representing this MediaPlayer.
+ */
+ redraw: function() {
+ media.appendDictionaryToTable(this.properties, this.propertyTable_);
+
+ this.setAttribute('status', this.properties.state);
+
+ // Don't bother drawing anything if we don't know the total size.
+ var size = this.properties.total_bytes;
+ if (!size) {
+ return;
+ }
+
+ // Draw the state of BufferedResourceLoader.
+ this.bufferedEntry_.hidden = false;
+ var canvas = this.bufferedCanvas_;
+ var context = canvas.getContext('2d');
+ context.fillStyle = '#aaa';
+ context.fillRect(0, 0, canvas.width, canvas.height);
+
+ var left = this.properties.buffer_start / size * canvas.width;
+ var middle = this.properties.buffer_current / size * canvas.width;
+ var right = this.properties.buffer_end / size * canvas.width;
+ context.fillStyle = '#a0a';
+ context.fillRect(left, 0, middle - left, canvas.height);
+ context.fillStyle = '#aa0';
+ context.fillRect(middle, 0, right - middle, canvas.height);
+
+ // Only show cached file information if we have something.
+ var cacheEntry = media.cacheEntriesByKey[this.properties.url];
+ if (!cacheEntry) {
+ return;
+ }
+
+ // Draw cache reads.
+ this.cacheReadsEntry_.hidden = false;
+ cacheEntry.drawCacheReadsToCanvas(this.cacheReadsCanvas_);
+
+ // Draw cache writes.
+ this.cacheWritesEntry_.hidden = false;
+ cacheEntry.drawCacheWritesToCanvas(this.cacheWritesCanvas_);
+ },
+ };
+
+ return {
+ MediaPlayer: MediaPlayer
+ };
+});
diff --git a/chromium/content/browser/resources/media/metrics.js b/chromium/content/browser/resources/media/metrics.js
new file mode 100644
index 00000000000..c812d44f56c
--- /dev/null
+++ b/chromium/content/browser/resources/media/metrics.js
@@ -0,0 +1,116 @@
+// 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';
+
+ // A set of parameter names. An entry of 'abc' allows metrics to specify
+ // events with specific values of 'abc'.
+ var metricProperties = {
+ 'pipeline_state': true,
+ };
+
+ // A set of metrics to measure. The user will see the most recent and average
+ // measurement of the time between each metric's start and end events.
+ var metrics = {
+ 'seek': {
+ 'start': 'SEEK',
+ 'end': 'pipeline_state=started'
+ },
+ 'first frame': {
+ 'start': 'WEBMEDIAPLAYER_CREATED',
+ 'end': 'pipeline_state=started'
+ },
+ };
+
+ /**
+ * This class measures times between the events specified above. It inherits
+ * <li> and contains a table that displays the measurements.
+ */
+ var Metrics = cr.ui.define('li');
+
+ Metrics.prototype = {
+ __proto__: HTMLLIElement.prototype,
+
+ /**
+ * Decorate this <li> as a Metrics.
+ */
+ decorate: function() {
+ this.table_ = document.createElement('table');
+ var details = document.createElement('details');
+ var summary = media.makeElement('summary', 'Metrics:');
+ details.appendChild(summary);
+ details.appendChild(this.table_);
+ this.appendChild(details);
+
+ var hRow = document.createElement('tr');
+ hRow.appendChild(media.makeElement('th', 'Metric:'));
+ hRow.appendChild(media.makeElement('th', 'Last Measure:'));
+ hRow.appendChild(media.makeElement('th', 'Average:'));
+ var header = document.createElement('thead');
+ header.appendChild(hRow);
+ this.table_.appendChild(header);
+
+ for (var metric in metrics) {
+ var last = document.createElement('td');
+ var avg = document.createElement('td');
+ this[metric] = {
+ count: 0,
+ total: 0,
+ start: null,
+ last: last,
+ avg: avg
+ };
+ var row = document.createElement('tr');
+ row.appendChild(media.makeElement('td', metric + ':'));
+ row.appendChild(last);
+ row.appendChild(avg);
+ this.table_.appendChild(row);
+ }
+ },
+
+ /**
+ * An event has occurred. Update any metrics that refer to this type
+ * of event. Can be called multiple times by addEvent below if the metrics
+ * refer to specific parameters.
+ * @param {Object} event The MediaLogEvent that has occurred.
+ * @param {string} type The type of event.
+ */
+ addEventInternal: function(event, type) {
+ for (var metric in metrics) {
+ var m = this[metric];
+ if (type == metrics[metric].start && !m.start) {
+ m.start = event.ticksMillis;
+ } else if (type == metrics[metric].end && m.start != null) {
+ var last = event.ticksMillis - m.start;
+ m.last.textContent = last.toFixed(1);
+ m.total += last;
+ m.count++;
+ if (m.count > 1)
+ m.avg.textContent = (m.total / m.count).toFixed(1);
+ m.start = null;
+ }
+ }
+ },
+
+ /**
+ * An event has occurred. Update any metrics that refer to events of this
+ * type or with this event's parameters.
+ * @param {Object} event The MediaLogEvent that has occurred.
+ */
+ addEvent: function(event) {
+ this.addEventInternal(event, event.type);
+ for (var p in event.params) {
+ if (p in metricProperties) {
+ var type = p + '=' + event.params[p];
+ this.addEventInternal(event, type);
+ }
+ }
+ },
+ };
+
+ return {
+ Metrics: Metrics,
+ };
+});
diff --git a/chromium/content/browser/resources/media/new/integration_test.html b/chromium/content/browser/resources/media/new/integration_test.html
new file mode 100644
index 00000000000..3a5225cb647
--- /dev/null
+++ b/chromium/content/browser/resources/media/new/integration_test.html
@@ -0,0 +1,86 @@
+<!--
+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.
+-->
+<!DOCTYPE html>
+<html>
+ <head>
+ <script src="webui_resource_test.js"></script>
+ <script src="util.js"></script>
+ <script src="player_manager.js"></script>
+ <script src="player_info.js"></script>
+ <script src="main.js"></script>
+ </head>
+ <body>
+ <script>
+ window.setUp = function() {
+ var doNothing = function() {};
+ var mockRenderer = {
+ redrawList: doNothing,
+ update: doNothing,
+ select: doNothing
+ };
+
+ var manager = new PlayerManager(mockRenderer);
+ media.initialize(manager);
+
+ window.playerManager = manager;
+ };
+
+ // The renderer and player ids are completely arbitrarily.
+ var TEST_RENDERER = 12;
+ var TEST_PLAYER = 4;
+ var TEST_NAME = TEST_RENDERER + ':' + TEST_PLAYER;
+
+ // Correctly use the information from a media event.
+ window.testOnMediaEvent = function() {
+ var event = {
+ ticksMillis: 132,
+ renderer: TEST_RENDERER,
+ player: TEST_PLAYER,
+ params: {
+ fps: 60,
+ other: 'hi'
+ }
+ };
+
+ window.media.onMediaEvent(event);
+ var info = window.playerManager.players_[TEST_NAME];
+
+ assertEquals(event.ticksMillis, info.firstTimestamp_);
+ assertEquals(TEST_NAME, info.id);
+ assertEquals(event.params.fps, info.properties.fps);
+ };
+
+ // Remove a player.
+ window.testOnRenderTerminated = function() {
+ window.testOnMediaEvent();
+
+ window.playerManager.shouldRemovePlayer_ = function() {
+ return true;
+ };
+
+ window.media.onRendererTerminated(TEST_RENDERER);
+ assertEquals(undefined, window.playerManager.players_[TEST_NAME]);
+ };
+
+ // Audio Streams are weird, they are handled separately
+ window.testAddAudioStream = function() {
+ var event = {
+ id: 'ID',
+ status: 'created',
+ playing: true
+ };
+
+ window.media.addAudioStream(event);
+
+ var player = window.playerManager.players_[event.id];
+ assertTrue(undefined !== player);
+ assertEquals(event.playing, player.properties['playing']);
+ };
+
+ runTests();
+ </script>
+ </body>
+</html>
diff --git a/chromium/content/browser/resources/media/new/main.js b/chromium/content/browser/resources/media/new/main.js
new file mode 100644
index 00000000000..61f6407bcf5
--- /dev/null
+++ b/chromium/content/browser/resources/media/new/main.js
@@ -0,0 +1,134 @@
+// 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;
+
+ /**
+ * Users of |media| must call initialize prior to calling other methods.
+ */
+ function initialize(playerManager) {
+ manager = playerManager;
+ }
+
+ /**
+ * Call to modify or add a system property.
+ */
+ function onSystemProperty(timestamp, key, value) {
+ console.log('System properties not yet implemented');
+ }
+
+ /**
+ * Call to modify or add a property on a player.
+ */
+ function onPlayerProperty(id, timestamp, key, value) {
+ manager.updatePlayerInfo(id, timestamp, key, value);
+ }
+
+ function onPlayerPropertyNoRecord(id, timestamp, key, value) {
+ manager.updatePlayerInfoNoRecord(id, timestamp, key, value);
+ }
+
+ /**
+ * Call to add a player.
+ */
+ function onPlayerOpen(id, timestamp) {
+ manager.addPlayer(id, timestamp);
+ }
+
+ /**
+ * Call to remove a player.
+ */
+ function onPlayerClose(id) {
+ manager.removePlayer(id);
+ }
+
+ var media = {
+ onSystemProperty: onSystemProperty,
+ onPlayerProperty: onPlayerProperty,
+ onPlayerPropertyNoRecord: onPlayerPropertyNoRecord,
+ onPlayerOpen: onPlayerOpen,
+ onPlayerClose: onPlayerClose,
+
+ initialize: initialize
+ };
+
+ // Everything beyond this point is for backwards compatibility reasons.
+ // It will go away when the backend is updated.
+
+ media.onNetUpdate = function(update) {
+ // TODO(tyoverby): Implement
+ };
+
+ media.onRendererTerminated = function(renderId) {
+ util.object.forEach(manager.players_, function(playerInfo, id) {
+ if (playerInfo.properties['render_id'] == renderId) {
+ media.onPlayerClose(id);
+ }
+ });
+ };
+
+ // For whatever reason, addAudioStream is also called on
+ // the removal of audio streams.
+ media.addAudioStream = function(event) {
+ switch (event.status) {
+ case 'created':
+ media.onPlayerOpen(event.id);
+ // We have to simulate the timestamp since it isn't provided to us.
+ media.onPlayerProperty(
+ event.id, (new Date()).getTime(), 'playing', event.playing);
+ break;
+ case 'closed':
+ media.onPlayerClose(event.id);
+ break;
+ }
+ };
+ media.onItemDeleted = function() {
+ // This only gets called when an audio stream is removed, which
+ // for whatever reason is also handled by addAudioStream...
+ // Because it is already handled, we can safely ignore it.
+ };
+
+ media.onMediaEvent = function(event) {
+ var source = event.renderer + ':' + event.player;
+
+ // Although this gets called on every event, there is nothing we can do
+ // about this because there is no onOpen event.
+ media.onPlayerOpen(source);
+ media.onPlayerPropertyNoRecord(
+ source, event.ticksMillis, 'render_id', event.renderer);
+ media.onPlayerPropertyNoRecord(
+ 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') {
+ media.onPlayerPropertyNoRecord(
+ source, event.ticksMillis, key, value);
+ } else {
+ media.onPlayerProperty(source, event.ticksMillis, key, value);
+ }
+ propertyCount += 1;
+ });
+
+ if (propertyCount === 0) {
+ media.onPlayerProperty(
+ source, event.ticksMillis, 'EVENT', event.type);
+ }
+ };
+
+ return media;
+}());
diff --git a/chromium/content/browser/resources/media/new/media_internals.html b/chromium/content/browser/resources/media/new/media_internals.html
new file mode 100644
index 00000000000..0e95353aeef
--- /dev/null
+++ b/chromium/content/browser/resources/media/new/media_internals.html
@@ -0,0 +1,18 @@
+<!--
+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.
+-->
+<!DOCTYPE html>
+<html i18n-values="dir:textdirection">
+<head>
+ <meta charset="utf-8">
+ <title i18n-content="Media Internals"></title>
+
+ <script src="chrome://media-internals/media_internals.js"></script>
+</head>
+
+<body>
+ Hello World
+</body>
+</html>
diff --git a/chromium/content/browser/resources/media/new/media_internals.js b/chromium/content/browser/resources/media/new/media_internals.js
new file mode 100644
index 00000000000..103ef74fbc0
--- /dev/null
+++ b/chromium/content/browser/resources/media/new/media_internals.js
@@ -0,0 +1,18 @@
+// 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 = {};
+
+var doNothing = function() {};
+
+// Silence the backend calls.
+media.initialize = doNothing;
+media.addAudioStream = doNothing;
+media.cacheEntriesByKey = doNothing;
+media.onReceiveEverything = doNothing;
+media.onItemDeleted = doNothing;
+media.onRendererTerminated = doNothing;
+media.onNetUpdate = doNothing;
+media.onReceiveConstants = doNothing;
+media.onMediaEvent = doNothing;
diff --git a/chromium/content/browser/resources/media/new/player_info.js b/chromium/content/browser/resources/media/new/player_info.js
new file mode 100644
index 00000000000..af1f1944518
--- /dev/null
+++ b/chromium/content/browser/resources/media/new/player_info.js
@@ -0,0 +1,80 @@
+// 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;
+}());
diff --git a/chromium/content/browser/resources/media/new/player_info_test.html b/chromium/content/browser/resources/media/new/player_info_test.html
new file mode 100644
index 00000000000..46cc05ee3d9
--- /dev/null
+++ b/chromium/content/browser/resources/media/new/player_info_test.html
@@ -0,0 +1,146 @@
+<!--
+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.
+-->
+<!DOCTYPE html>
+<html>
+ <head>
+ <script src="webui_resource_test.js"></script>
+ <script src="player_manager.js"></script>
+ <script src="player_info.js"></script>
+ </head>
+ <body>
+ <script>
+ window.setUp = function() {
+ window.pi = new PlayerInfo('example_id');
+ };
+
+ window.tearDown = function() {
+ window.pi = null;
+ };
+
+ // Test that an ID is set correctly.
+ window.testConstructorStringID = function() {
+ assertEquals('example_id', window.pi.id);
+ };
+
+ // Test that numerical IDs are valid.
+ window.testConstructorNumberId = function() {
+ var pi = new PlayerInfo(5);
+ assertEquals(5, pi.id);
+ };
+
+ // Make sure that a new PlayerInfo has no events.
+ window.testEmptyEvents = function() {
+ assertEquals(0, window.pi.allEvents.length);
+ };
+
+ // Check that the most recent property gets updated.
+ window.testAddProperty = function() {
+ var key = 'key',
+ value = 'value',
+ value2 = 'value2';
+
+ window.pi.addProperty(0, key, value);
+ assertEquals(value, window.pi.properties[key]);
+
+ window.pi.addProperty(0, key, value2);
+ assertEquals(value2, window.pi.properties[key]);
+
+ };
+
+ // Make sure that the first timestamp that gets sent
+ // is recorded as the base timestamp.
+ window.testFirstTimestamp = function() {
+ var pi = new PlayerInfo('example_ID');
+ var timestamp = 5000;
+ pi.addProperty(timestamp, 'key', 'value');
+
+ assertEquals(timestamp, pi.firstTimestamp_);
+ };
+
+ // Adding a property with a non-string key should
+ // throw an exception.
+ window.testWrongKeyType = function() {
+ var pi = new PlayerInfo('example_ID');
+ assertThrows(function() {
+ pi.addProperty(0, 5, 'some value');
+ });
+ };
+
+ // Subsequent events should have their log offset based
+ // on the first timestamp added.
+ window.testAddPropertyTimestampOffset = function() {
+ var firstTimestamp = 500,
+ secondTimestamp = 550,
+ deltaT = secondTimestamp - firstTimestamp,
+ key = 'key',
+ value = 'value';
+
+ var pi = new PlayerInfo('example_ID');
+ pi.addProperty(firstTimestamp, key, value);
+ pi.addProperty(secondTimestamp, key, value);
+
+ assertEquals(firstTimestamp, pi.firstTimestamp_);
+ assertEquals(0, pi.allEvents[0].time);
+ assertEquals(deltaT, pi.allEvents[1].time);
+
+ assertTrue(undefined !== pi.pastValues[key]);
+
+ console.log(pi.pastValues);
+
+ assertEquals(0, pi.pastValues[key][0].time);
+ assertEquals(deltaT, pi.pastValues[key][1].time);
+ };
+
+ // Check to make sure that properties are correctly
+ // added to the relevant pastValues array.
+ window.testAddPropertyPastValues = function() {
+ var pi = new PlayerInfo('example_ID'),
+ timestamp = 50,
+ key = 'key',
+ value = 'value';
+
+ pi.addProperty(timestamp, key, value);
+
+ assertEquals(value, pi.pastValues[key][0].value);
+ assertEquals(key, pi.pastValues[key][0].key);
+ assertEquals(0, pi.pastValues[key][0].time);
+ };
+
+ // The list of all events should be recorded in correctly.
+ window.testAllEvents = function() {
+ var pi = new PlayerInfo('example_ID'),
+ timestamp = 50,
+ key = 'key',
+ value = 'value',
+ key2 = 'key2',
+ value2 = 'value2';
+
+ pi.addProperty(timestamp, key, value);
+ assertEquals(value, pi.allEvents[0].value);
+ assertEquals(key, pi.allEvents[0].key);
+
+ pi.addProperty(timestamp, key2, value2);
+ assertEquals(value2, pi.allEvents[1].value);
+ assertEquals(key2, pi.allEvents[1].key);
+ };
+
+ // Using noRecord should make it not show up in allEvents,
+ // but it should still show up in pastValues[key].
+ window.testNoRecord = function() {
+ var pi = new PlayerInfo('example_ID'),
+ timestamp = 50,
+ key = 'key',
+ value = 'value';
+ pi.addPropertyNoRecord(timestamp, key, value);
+
+ assertEquals(value, pi.properties[key]);
+ assertEquals(0, pi.allEvents.length);
+ assertEquals(1, pi.pastValues[key].length);
+ };
+ runTests();
+ </script>
+ </body>
+</html>
diff --git a/chromium/content/browser/resources/media/new/player_manager.js b/chromium/content/browser/resources/media/new/player_manager.js
new file mode 100644
index 00000000000..3de93357f98
--- /dev/null
+++ b/chromium/content/browser/resources/media/new/player_manager.js
@@ -0,0 +1,111 @@
+// 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
+ * PlayerProperty objects and is the entry-point for messages from the backend.
+ */
+var PlayerManager = (function() {
+ 'use strict';
+
+ function PlayerManager(renderManager) {
+ this.players_ = {};
+ this.renderman_ = renderManager;
+ renderManager.playerManager = this;
+
+ this.shouldRemovePlayer_ = function() {
+ // This is only temporary until we get the UI hooked up.
+ return true;
+ };
+ }
+
+ PlayerManager.prototype = {
+
+ /**
+ * 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.renderman_.redrawList();
+ },
+
+ /**
+ * Attempts to remove a player from the UI.
+ * @param id The ID of the player to remove.
+ */
+ removePlayer: function(id) {
+ // Look at the check box to see if we should actually
+ // remove it from the UI
+ if (this.shouldRemovePlayer_()) {
+ delete this.players_[id];
+ this.renderman_.redrawList();
+ } else if (this.players_[id]) {
+ // Set a property on it to be removed at a later time
+ this.players_[id].toRemove = true;
+ }
+ },
+
+ /**
+ * Selects a player and displays it on the UI.
+ * This method is called from the UI.
+ * @param id The ID of the player to display.
+ */
+ selectPlayer: function(id) {
+ if (!this.players_[id]) {
+ throw new Error('[selectPlayer] Id ' + id + ' does not exist.');
+ }
+
+ this.renderman_.select(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);
+
+ // If we can potentially rename the player, do so.
+ if (key === 'name' || key === 'url') {
+ this.renderman_.redrawList();
+ }
+
+ this.renderman_.update();
+ },
+
+ /**
+ *
+ * @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);
+
+ // If we can potentially rename the player, do so.
+ if (key === 'name' || key === 'url') {
+ this.renderman_.redrawList();
+ }
+
+ this.renderman_.update();
+ }
+ };
+
+ return PlayerManager;
+}());
diff --git a/chromium/content/browser/resources/media/new/player_manager_test.html b/chromium/content/browser/resources/media/new/player_manager_test.html
new file mode 100644
index 00000000000..eff78b53c59
--- /dev/null
+++ b/chromium/content/browser/resources/media/new/player_manager_test.html
@@ -0,0 +1,155 @@
+<!--
+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.
+-->
+<!DOCTYPE html>
+<html>
+ <head>
+ <script src="webui_resource_test.js"></script>
+ <script src="player_manager.js"></script>
+ <script src="player_info.js"></script>
+ </head>
+ <body>
+ <script>
+ var doNothing = function() {
+ };
+
+ var emptyRenderMan = {
+ redrawList: doNothing,
+ update: doNothing,
+ select: doNothing
+ };
+
+ window.setUp = function() {
+ window.pm = new PlayerManager(emptyRenderMan);
+ };
+
+ window.tearDown = function() {
+ window.pm = null;
+ };
+
+ // Test a normal case of .addPlayer
+ window.testAddPlayer = function() {
+ window.pm.addPlayer('someid');
+ assertTrue(undefined !== window.pm.players_['someid']);
+ };
+
+ // Make sure that adding a player forces a redraw
+ // on the renderManager.
+ window.testAddPlayerForceRedraw = function() {
+ var redrew = false;
+ var mockRenderManager = {
+ redrawList: function() {
+ redrew = true;
+ }
+ };
+ var pm = new PlayerManager(mockRenderManager);
+
+ pm.addPlayer('someid');
+ assertTrue(redrew);
+ };
+
+ // On occasion, the backend will add an existing ID multiple times.
+ // make sure this doesn't break anything.
+ window.testAddPlayerAlreadyExisting = function() {
+ window.pm.addPlayer('someid');
+ window.pm.addPlayer('someid');
+ assertTrue(undefined !== window.pm.players_['someid']);
+ };
+
+ // If the removal is set, make sure that a player
+ // gets removed from the PlayerManager.
+ window.testRemovePlayerShouldRemove = function() {
+ // Because we don't have the checkbox.
+ window.pm.shouldRemovePlayer_ = function() {
+ return true;
+ };
+ window.pm.addPlayer('someid');
+ assertTrue(undefined !== window.pm.players_['someid']);
+ window.pm.removePlayer('someid');
+ assertTrue(undefined === window.pm.players_['someid']);
+ };
+
+ // On the removal of a player, the renderer should be forced
+ // to redraw the list.
+ window.testRemovePlayerRedraw = function() {
+ var redrew = false;
+
+ var fakeObj = {
+ redrawList: function() {
+ redrew = true;
+ }
+ };
+
+ var pm = new PlayerManager(fakeObj);
+ // Because we don't have the checkbox;
+ pm.shouldRemovePlayer_ = function() {
+ return true;
+ };
+
+
+ pm.addPlayer('someid');
+ assertTrue(undefined !== pm.players_['someid']);
+ pm.removePlayer('someid');
+ assertTrue(undefined === pm.players_['someid']);
+
+ assertTrue(redrew);
+ };
+
+ // If you shouldn't remove the player, the player shouldn't be
+ // removed.
+ window.testRemovePlayerNoRemove = function() {
+ window.pm = new PlayerManager(emptyRenderMan);
+ // Because we don't have the checkbox;
+ window.pm.shouldRemovePlayer_ = function() {
+ return false;
+ };
+ window.pm.addPlayer('someid');
+ assertTrue(undefined !== window.pm.players_['someid']);
+ window.pm.removePlayer('someid');
+ assertTrue(undefined !== window.pm.players_['someid']);
+ };
+
+
+ // Removing a nonexistant player shouldn't break anything
+ // The backend also occasionally does this.
+ window.testRemovePlayerNonExistant = function() {
+ // Because we don't have the checkbox;
+ window.pm.shouldRemovePlayer_ = function() {
+ return false;
+ };
+ window.pm.removePlayer('someid');
+ assertTrue(undefined === window.pm.players_['someid']);
+ };
+
+ // Trying to select a non-existant player should throw
+ // an exception
+ window.testSelectNonExistant = function() {
+ assertThrows(function() {
+ window.pm.selectPlayer('someId');
+ });
+ };
+
+ // Selecting an existing player should trigger a redraw
+ window.testSelectExistingPlayer = function() {
+ var selected = false;
+ var redrew = false;
+ var pm = new PlayerManager({
+ select: function() {
+ selected = true;
+ },
+ redrawList: function() {
+ redrew = true;
+ }
+ });
+ pm.addPlayer('someId');
+ pm.selectPlayer('someId');
+
+ assertTrue(selected);
+ assertTrue(redrew);
+ };
+ runTests();
+ </script>
+ </body>
+</html>
diff --git a/chromium/content/browser/resources/media/new/util.js b/chromium/content/browser/resources/media/new/util.js
new file mode 100644
index 00000000000..5909e9ee1eb
--- /dev/null
+++ b/chromium/content/browser/resources/media/new/util.js
@@ -0,0 +1,34 @@
+// 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);
+ }
+ }
+ };
+
+ return util;
+}());
diff --git a/chromium/content/browser/resources/media/new/webui_resource_test.js b/chromium/content/browser/resources/media/new/webui_resource_test.js
new file mode 100644
index 00000000000..6b05a305a70
--- /dev/null
+++ b/chromium/content/browser/resources/media/new/webui_resource_test.js
@@ -0,0 +1,210 @@
+// 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.
+
+/**
+ * Tests that an observation matches the expected value.
+ * @param {Object} expected The expected value.
+ * @param {Object} observed The actual value.
+ * @param {string=} opt_message Optional message to include with a test
+ * failure.
+ */
+function assertEquals(expected, observed, opt_message) {
+ if (observed !== expected) {
+ var message = 'Assertion Failed\n Observed: ' + observed +
+ '\n Expected: ' + expected;
+ if (opt_message)
+ message = message + '\n ' + opt_message;
+ throw new Error(message);
+ }
+}
+
+/**
+ * Verifies that a test result is true.
+ * @param {boolean} observed The observed value.
+ * @param {string=} opt_message Optional message to include with a test
+ * failure.
+ */
+function assertTrue(observed, opt_message) {
+ assertEquals(true, observed, opt_message);
+}
+
+/**
+ * Verifies that a test result is false.
+ * @param {boolean} observed The observed value.
+ * @param {string=} opt_message Optional message to include with a test
+ * failure.
+ */
+function assertFalse(observed, opt_message) {
+ assertEquals(false, observed, opt_message);
+}
+
+/**
+ * Verifies that the observed and reference values differ.
+ * @param {Object} reference The target value for comparison.
+ * @param {Object} observed The test result.
+ * @param {string=} opt_message Optional message to include with a test
+ * failure.
+ */
+function assertNotEqual(reference, observed, opt_message) {
+ if (observed === reference) {
+ var message = 'Assertion Failed\n Observed: ' + observed +
+ '\n Reference: ' + reference;
+ if (opt_message)
+ message = message + '\n ' + opt_message;
+ throw new Error(message);
+ }
+}
+
+/**
+ * Verifies that a test evaluation results in an exception.
+ * @param {!Function} f The test function.
+ */
+function assertThrows(f) {
+ var triggeredError = false;
+ try {
+ f();
+ } catch (err) {
+ triggeredError = true;
+ }
+ if (!triggeredError)
+ throw new Error('Assertion Failed: throw expected.');
+}
+
+/**
+ * Verifies that the contents of the expected and observed arrays match.
+ * @param {!Array} expected The expected result.
+ * @param {!Array} observed The actual result.
+ */
+function assertArrayEquals(expected, observed) {
+ var v1 = Array.prototype.slice.call(expected);
+ var v2 = Array.prototype.slice.call(observed);
+ var equal = v1.length == v2.length;
+ if (equal) {
+ for (var i = 0; i < v1.length; i++) {
+ if (v1[i] !== v2[i]) {
+ equal = false;
+ break;
+ }
+ }
+ }
+ if (!equal) {
+ var message =
+ ['Assertion Failed', 'Observed: ' + v2, 'Expected: ' + v1].join('\n ');
+ throw new Error(message);
+ }
+}
+
+/**
+ * Verifies that the expected and observed result have the same content.
+ * @param {*} expected The expected result.
+ * @param {*} observed The actual result.
+ */
+function assertDeepEquals(expected, observed, opt_message) {
+ if (typeof expected == 'object' && expected != null) {
+ assertNotEqual(null, observed);
+ for (var key in expected) {
+ assertTrue(key in observed, opt_message);
+ assertDeepEquals(expected[key], observed[key], opt_message);
+ }
+ for (var key in observed) {
+ assertTrue(key in expected, opt_message);
+ }
+ } else {
+ assertEquals(expected, observed, opt_message);
+ }
+}
+
+/**
+ * Defines runTests.
+ */
+(function(exports) {
+ /**
+ * List of test cases.
+ * @type {Array.<string>} List of function names for tests to run.
+ */
+ var testCases = [];
+
+ /**
+ * Indicates if all tests have run successfully.
+ * @type {boolean}
+ */
+ var cleanTestRun = true;
+
+ /**
+ * Armed during setup of a test to call the matching tear down code.
+ * @type {Function}
+ */
+ var pendingTearDown = null;
+
+ /**
+ * Runs all functions starting with test and reports success or
+ * failure of the test suite.
+ */
+ function runTests() {
+ for (var name in window) {
+ if (typeof window[name] == 'function' && /^test/.test(name))
+ testCases.push(name);
+ }
+ if (!testCases.length) {
+ console.error('Failed to find test cases.');
+ cleanTestRun = false;
+ }
+ continueTesting();
+ }
+
+ function reportPass(name) {
+ 'use strict';
+ var text = document.createTextNode(name + ': PASSED');
+ var span = document.createElement('span');
+ span.appendChild(text);
+ document.body.appendChild(span);
+ document.body.appendChild(document.createElement('br'));
+ }
+
+ function reportFail(name) {
+ 'use strict';
+ var text = document.createTextNode(name + ': =========FAILED=======');
+ var span = document.createElement('span');
+ span.appendChild(text);
+ document.body.appendChild(span);
+ document.body.appendChild(document.createElement('br'));
+ }
+
+ /**
+ * Runs the next test in the queue. Reports the test results if the queue is
+ * empty.
+ */
+ function continueTesting() {
+ if (pendingTearDown) {
+ pendingTearDown();
+ pendingTearDown = null;
+ }
+ if (testCases.length > 0) {
+ var fn = testCases.pop();
+ var isAsyncTest = window[fn].length;
+ try {
+ if (window.setUp)
+ window.setUp();
+ pendingTearDown = window.tearDown;
+ window[fn](continueTesting);
+ reportPass(fn);
+ } catch (err) {
+ reportFail(fn);
+ console.error('Failure in test ' + fn + '\n' + err);
+ console.log(err.stack);
+ cleanTestRun = false;
+ }
+ // Asynchronous tests must manually call continueTesting when complete.
+ if (!isAsyncTest)
+ continueTesting();
+ }
+ if (testCases.length) {
+ domAutomationController.setAutomationId(1);
+ domAutomationController.send('PENDING');
+ }
+ };
+
+ exports.runTests = runTests;
+})(this);
+
diff --git a/chromium/content/browser/resources/media/peer_connection_update_table.js b/chromium/content/browser/resources/media/peer_connection_update_table.js
new file mode 100644
index 00000000000..0f4cc0cde90
--- /dev/null
+++ b/chromium/content/browser/resources/media/peer_connection_update_table.js
@@ -0,0 +1,128 @@
+// 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);
+
+ row.innerHTML = '<td>' + (new Date()).toLocaleString() + '</td>';
+
+ if (update.value.length == 0) {
+ row.innerHTML += '<td>' + update.type + '</td>';
+ return;
+ }
+
+ row.innerHTML += '<td><details><summary>' + update.type +
+ '</summary></details></td>';
+
+ 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 = '<tr><th>Time</th>' +
+ '<th class="update-log-header-event">Event</th></tr>';
+ }
+ return tableElement;
+ }
+ };
+
+ return PeerConnectionUpdateTable;
+})();
diff --git a/chromium/content/browser/resources/media/ssrc_info_manager.js b/chromium/content/browser/resources/media/ssrc_info_manager.js
new file mode 100644
index 00000000000..bb99f81d7e9
--- /dev/null
+++ b/chromium/content/browser/resources/media/ssrc_info_manager.js
@@ -0,0 +1,166 @@
+// 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.<string, !Object.<string>>}
+ * @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.<string>} 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;
+})();
diff --git a/chromium/content/browser/resources/media/stats_graph_helper.js b/chromium/content/browser/resources/media/stats_graph_helper.js
new file mode 100644
index 00000000000..ce9e7185776
--- /dev/null
+++ b/chromium/content/browser/resources/media/stats_graph_helper.js
@@ -0,0 +1,265 @@
+// 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.
+//
+
+<include src="timeline_graph_view.js"/>
+
+var STATS_GRAPH_CONTAINER_HEADING_CLASS = 'stats-graph-container-heading';
+
+// 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,
+};
+
+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];
+ 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.
+ addDataSeriesPoint(peerConnectionElement,
+ rawDataSeriesId, stats.timestamp,
+ rawLabel, 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.
+ addDataSeriesPoint(peerConnectionElement,
+ rawDataSeriesId, stats.timestamp,
+ rawLabel, 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.
+ addDataSeriesPoint(peerConnectionElement,
+ finalDataSeriesId,
+ stats.timestamp,
+ finalLabel,
+ 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 point to it.
+function addDataSeriesPoint(
+ peerConnectionElement, dataSeriesId, time, label, value) {
+ 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);
+ }
+ }
+ dataSeries.addPoint(time, value);
+}
+
+// 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 ='<summary><span></span></summary>';
+ 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 = '<div>' + statsName + '</div>' +
+ '<div id=' + divId + '><canvas id=' + canvasId + '></canvas></div>';
+ 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 = '<input type=checkbox checked></input>' + 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;
+}
diff --git a/chromium/content/browser/resources/media/stats_table.js b/chromium/content/browser/resources/media/stats_table.js
new file mode 100644
index 00000000000..6b3ae5230de
--- /dev/null
+++ b/chromium/content/browser/resources/media/stats_table.js
@@ -0,0 +1,137 @@
+// 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';
+ 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);
+ table = document.createElement('table');
+ container.appendChild(table);
+ table.id = tableId;
+ table.border = 1;
+
+ table.innerHTML = '<tr><th colspan=2></th></tr>';
+ table.rows[0].cells[0].textContent = 'Statistics ' + report.id;
+ if (report.type == 'ssrc') {
+ table.insertRow(1);
+ table.rows[1].innerHTML = '<td colspan=2></td>';
+ 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.<string>} statsData An array of stats name and value pairs.
+ * @private
+ */
+ addStatsToTable_: function(statsTable, time, statsData) {
+ var date = 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 = '<td>' + rowName + '</td><td></td>';
+ }
+ trElement.cells[1].textContent = value;
+ }
+ };
+
+ return StatsTable;
+})();
diff --git a/chromium/content/browser/resources/media/timeline_graph_view.js b/chromium/content/browser/resources/media/timeline_graph_view.js
new file mode 100644
index 00000000000..89b557e1710
--- /dev/null
+++ b/chromium/content/browser/resources/media/timeline_graph_view.js
@@ -0,0 +1,523 @@
+// 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';
+
+ // Default starting scale factor, in terms of milliseconds per pixel.
+ var DEFAULT_SCALE = 1000;
+
+ // 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';
+
+ /**
+ * @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;
+
+ // Initialize the scrollbar.
+ this.updateScrollbarRange_(true);
+ }
+
+ TimelineGraphView.prototype = {
+ // 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 DEFAULT_SCALE.
+ return Math.floor(timeRange / DEFAULT_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() {
+ this.endTime_ = (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 * DEFAULT_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, DEFAULT_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) / DEFAULT_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;
+
+ // At least the highest value in the displayed range of the graph.
+ // Used for scaling and setting labels. Set in layoutLabels.
+ 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;
+ 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];
+ }
+ }
+
+ this.layoutLabels_(max);
+ },
+
+ /**
+ * Lays out labels and sets |max_|, 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|.
+ */
+ layoutLabels_: function(maxValue) {
+ if (maxValue < 1024) {
+ this.layoutLabelsBasic_(maxValue, 0);
+ 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;
+ maxValue /= 1024;
+ while (units[unit + 1] && maxValue >= 1024) {
+ maxValue /= 1024;
+ ++unit;
+ }
+
+ // Calculate labels.
+ this.layoutLabelsBasic_(maxValue, 1);
+
+ // Append units to labels.
+ for (var i = 0; i < this.labels_.length; ++i)
+ this.labels_[i] += ' ' + units[unit];
+
+ // Convert |max_| back to unit '1'.
+ 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(maxValue, maxDecimalDigits) {
+ this.labels_ = [];
+ // No labels if |maxValue| is 0.
+ if (maxValue == 0) {
+ 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(maxValue / 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(maxValue / stepSize) + 1 <= maxLabels)
+ break;
+ // Check |stepSize| * 2.
+ if (Math.ceil(maxValue / (stepSize * 2)) + 1 <= maxLabels) {
+ stepSize *= 2;
+ break;
+ }
+ // Check |stepSize| * 5.
+ if (Math.ceil(maxValue / (stepSize * 5)) + 1 <= maxLabels) {
+ stepSize *= 5;
+ break;
+ }
+ stepSize *= 10;
+ if (stepSizeDecimalDigits > 0)
+ --stepSizeDecimalDigits;
+ }
+
+ // Set the max so it's an exact multiple of the chosen step size.
+ this.max_ = Math.ceil(maxValue / stepSize) * stepSize;
+
+ // Create labels.
+ for (var label = this.max_; label >= 0; 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_;
+
+ // 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] * 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;
+})();
diff --git a/chromium/content/browser/resources/media/util.js b/chromium/content/browser/resources/media/util.js
new file mode 100644
index 00000000000..c61ae0e9793
--- /dev/null
+++ b/chromium/content/browser/resources/media/util.js
@@ -0,0 +1,74 @@
+// 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';
+
+ /**
+ * The width and height of a bar drawn on a file canvas in pixels.
+ */
+ var BAR_WIDTH = 500;
+ var BAR_HEIGHT = 16;
+
+ /**
+ * Draws a 1px white horizontal line across |context|.
+ */
+ function drawLine(context, top) {
+ context.moveTo(0, top);
+ context.lineTo(BAR_WIDTH, top);
+ context.strokeStyle = '#fff';
+ context.stroke();
+ }
+
+ /**
+ * Creates an HTMLElement of type |type| with textContent |content|.
+ * @param {string} type The type of element to create.
+ * @param {string} content The content to place in the element.
+ * @return {HTMLElement} A newly initialized element.
+ */
+ function makeElement(type, content) {
+ var element = document.createElement(type);
+ element.textContent = content;
+ return element;
+ }
+
+ /**
+ * Creates a new <li> containing a <details> with a <summary> and sets
+ * properties to reference them.
+ * @return {Object} The new <li>.
+ */
+ function createDetailsLi() {
+ var li = document.createElement('li');
+ li.details = document.createElement('details');
+ li.summary = document.createElement('summary');
+ li.appendChild(li.details);
+ li.details.appendChild(li.summary);
+ return li
+ }
+
+ /**
+ * Appends each key-value pair in a dictionary to a row in a table.
+ * @param {Object} dict The dictionary to append.
+ * @param {HTMLElement} table The <table> element to append to.
+ */
+ function appendDictionaryToTable(dict, table) {
+ table.textContent = '';
+ for (var key in dict) {
+ var tr = document.createElement('tr');
+ tr.appendChild(makeElement('td', key + ':'));
+ tr.appendChild(makeElement('td', dict[key]));
+ table.appendChild(tr);
+ }
+ return table;
+ }
+
+ return {
+ BAR_WIDTH: BAR_WIDTH,
+ BAR_HEIGHT: BAR_HEIGHT,
+ drawLine: drawLine,
+ makeElement: makeElement,
+ createDetailsLi: createDetailsLi,
+ appendDictionaryToTable: appendDictionaryToTable
+ };
+});
diff --git a/chromium/content/browser/resources/media/webrtc_internals.css b/chromium/content/browser/resources/media/webrtc_internals.css
new file mode 100644
index 00000000000..c0ac402728f
--- /dev/null
+++ b/chromium/content/browser/resources/media/webrtc_internals.css
@@ -0,0 +1,118 @@
+/* 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. */
+
+
+.peer-connection-dump-root {
+ font-size: 0.8em;
+ padding-bottom: 3px;
+}
+
+.peer-connection-hidden > *:not(h3) {
+ display:none;
+}
+
+.update-log-container {
+ float: left;
+}
+
+.ssrc-info-block {
+ color: #999;
+ font-size: 0.8em;
+}
+
+.stats-graph-container {
+ clear: both;
+ margin: 0.5em 0 0.5em 0;
+}
+
+.stats-graph-container-heading {
+ font-size: 0.8em;
+ font-weight: bold;
+}
+
+.stats-graph-sub-container {
+ float: left;
+ margin: 0.5em;
+}
+
+.stats-graph-sub-container > div {
+ float: left;
+}
+
+.stats-graph-sub-container > div:first-child {
+ float: none;
+}
+
+.stats-table-container {
+ float: left;
+ padding: 0 0 0 0;
+}
+
+body {
+ font-family: 'Lucida Grande', sans-serif;
+ padding: 20px;
+}
+
+details {
+ min-width: 30em;
+}
+
+h3 + div {
+ font-size: 0.8em;
+ margin: 0 0 1.2em 0;
+}
+
+h2 {
+ border-bottom: 1px solid #eee;
+ color: #666;
+ font-size: 1.1em;
+ margin: 0 0 1.6em 0;
+ padding: 0 0 0.7em 0;
+}
+
+h3 {
+ color: #555;
+ cursor: pointer;
+ font-size: 0.9em;
+ margin: 2em 0 0.5em 0;
+ text-decoration: underline;
+}
+
+li {
+ clear: both;
+ list-style-type: none;
+ margin: none;
+ padding: none;
+}
+
+table {
+ border: none;
+ margin: 0 1em 1em 0;
+}
+
+td {
+ border: none;
+ font-size: 0.8em;
+ padding: 0 1em 0.5em 0;
+}
+
+td:first-child {
+ padding-top: 0.3em;
+}
+
+table > tr {
+ vertical-align: top;
+}
+
+th {
+ border: none;
+ font-size: 0.8em;
+ padding: 0 0 0.5em 0;
+}
+
+ul {
+ margin: none;
+ padding: none;
+ -webkit-padding-start: 0px;
+}
diff --git a/chromium/content/browser/resources/media/webrtc_internals.html b/chromium/content/browser/resources/media/webrtc_internals.html
new file mode 100644
index 00000000000..72576207f88
--- /dev/null
+++ b/chromium/content/browser/resources/media/webrtc_internals.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html i18n-values="dir:textdirection;">
+ <head>
+ <meta charset="utf-8">
+ <title>WebRTC Internals</title>
+ <link rel="stylesheet" href="webrtc_internals.css">
+ <script src="chrome://resources/js/util.js"></script>
+ <script src="chrome://webrtc-internals/webrtc_internals.js"></script>
+ </head>
+ <body>
+ <h2>WebRTC Internals</h2>
+ <p>
+ <ul id="peer-connections-list">
+ </ul>
+ </p>
+ </body>
+</html>
diff --git a/chromium/content/browser/resources/media/webrtc_internals.js b/chromium/content/browser/resources/media/webrtc_internals.js
new file mode 100644
index 00000000000..fd2d34a3faf
--- /dev/null
+++ b/chromium/content/browser/resources/media/webrtc_internals.js
@@ -0,0 +1,264 @@
+// 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 peerConnectionsListElem = 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 simple class to store the updates and stats data for a peer connection. */
+var PeerConnectionRecord = (function() {
+ /** @constructor */
+ function PeerConnectionRecord() {
+ /** @private */
+ this.record_ = {
+ constraints: {},
+ servers: [],
+ 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} servers STUN servers used by the peer connection.
+ * @param {!Object} constraints Media constraints.
+ */
+ initialize: function(url, servers, constraints) {
+ this.record_.url = url;
+ this.record_.servers = servers;
+ 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 {string} type The type of the update.
+ * @param {string} value The value of the update.
+ */
+ addUpdate: function(type, value) {
+ this.record_.updateLog.push({
+ time: (new Date()).toLocaleString(),
+ type: type,
+ value: value,
+ });
+ },
+ };
+
+ return PeerConnectionRecord;
+})();
+
+// The maximum number of data points bufferred for each stats. Old data points
+// will be shifted out when the buffer is full.
+var MAX_STATS_DATA_POINT_BUFFER_SIZE = 1000;
+
+<include src="data_series.js"/>
+<include src="ssrc_info_manager.js"/>
+<include src="stats_graph_helper.js"/>
+<include src="stats_table.js"/>
+<include src="peer_connection_update_table.js"/>
+<include src="dump_creator.js"/>
+
+
+function initialize() {
+ peerConnectionsListElem = $('peer-connections-list');
+ dumpCreator = new DumpCreator(peerConnectionsListElem);
+ ssrcInfoManager = new SsrcInfoManager();
+ peerConnectionUpdateTable = new PeerConnectionUpdateTable();
+ statsTable = new StatsTable(ssrcInfoManager);
+
+ chrome.send('getAllUpdates');
+
+ // Requests stats from all peer connections every second.
+ window.setInterval(function() {
+ if (peerConnectionsListElem.getElementsByTagName('li').length > 0)
+ chrome.send('getAllStats');
+ }, 1000);
+}
+document.addEventListener('DOMContentLoaded', initialize);
+
+
+/**
+ * A helper function for getting a peer connection element id.
+ *
+ * @param {!Object.<string, number>} data The object containing the pid and lid
+ * of the peer connection.
+ * @return {string} The peer connection element id.
+ */
+function getPeerConnectionId(data) {
+ return data.pid + '-' + data.lid;
+}
+
+
+/**
+ * Extracts ssrc info from a setLocal/setRemoteDescription update.
+ *
+ * @param {!PeerConnectionUpdateEntry} data The peer connection update data.
+ */
+function extractSsrcInfo(data) {
+ if (data.type == 'setLocalDescription' ||
+ data.type == 'setRemoteDescription') {
+ ssrcInfoManager.addSsrcStreamInfo(data.value);
+ }
+}
+
+
+/**
+ * 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.type, update.value);
+}
+
+
+/** Browser message handlers. */
+
+
+/**
+ * Removes all information about a peer connection.
+ *
+ * @param {!Object.<string, number>} data The object containing the pid and lid
+ * of a peer connection.
+ */
+function removePeerConnection(data) {
+ var element = $(getPeerConnectionId(data));
+ if (element) {
+ delete peerConnectionDataStore[element.id];
+ peerConnectionsListElem.removeChild(element);
+ }
+}
+
+
+/**
+ * Adds a peer connection.
+ *
+ * @param {!Object} data The object containing the pid, lid, url, servers, 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.servers, data.constraints);
+
+ var peerConnectionElement = $(id);
+ if (!peerConnectionElement) {
+ peerConnectionElement = document.createElement('li');
+ peerConnectionsListElem.appendChild(peerConnectionElement);
+ peerConnectionElement.id = id;
+ }
+ peerConnectionElement.innerHTML =
+ '<h3>PeerConnection ' + peerConnectionElement.id + '</h3>' +
+ '<div>' + data.url + ' ' + data.servers + ' ' + data.constraints +
+ '</div>';
+
+ // Clicking the heading can expand or collapse the peer connection item.
+ peerConnectionElement.firstChild.title = 'Click to collapse or expand';
+ peerConnectionElement.firstChild.addEventListener('click', function(e) {
+ if (e.target.parentElement.className == '')
+ e.target.parentElement.className = 'peer-connection-hidden';
+ else
+ e.target.parentElement.className = '';
+ });
+
+ return peerConnectionElement;
+}
+
+
+/**
+ * Adds a peer connection update.
+ *
+ * @param {!PeerConnectionUpdateEntry} data The peer connection update data.
+ */
+function updatePeerConnection(data) {
+ var peerConnectionElement = $(getPeerConnectionId(data));
+ addPeerConnectionUpdate(peerConnectionElement, data);
+}
+
+
+/**
+ * Adds the information of all peer connections created so far.
+ *
+ * @param {Array.<!Object>} data An array of the information of all peer
+ * connections. Each array item contains pid, lid, url, servers,
+ * 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;
+ for (var j = 0; j < log.length; ++j) {
+ addPeerConnectionUpdate(peerConnection, log[j]);
+ }
+ }
+}
+
+
+/**
+ * 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);
+ }
+}
+
+
+/**
+ * Delegates to dumpCreator to update the recording status.
+ * @param {!Object.<string>} update Key-value pairs describing the status of the
+ * RTP recording.
+ */
+function updateDumpStatus(update) {
+ dumpCreator.onUpdate(update);
+}