diff options
author | Zeno Albisser <zeno.albisser@digia.com> | 2013-08-15 21:46:11 +0200 |
---|---|---|
committer | Zeno Albisser <zeno.albisser@digia.com> | 2013-08-15 21:46:11 +0200 |
commit | 679147eead574d186ebf3069647b4c23e8ccace6 (patch) | |
tree | fc247a0ac8ff119f7c8550879ebb6d3dd8d1ff69 /chromium/content/browser/resources | |
download | qtwebengine-chromium-679147eead574d186ebf3069647b4c23e8ccace6.tar.gz |
Initial import.
Diffstat (limited to 'chromium/content/browser/resources')
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 + ' '; + 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); +} |