diff options
author | jam@chromium.org <jam@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-02-01 02:31:56 +0000 |
---|---|---|
committer | jam@chromium.org <jam@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-02-01 02:31:56 +0000 |
commit | 11158e2d4794c6f91f4276a5160b685d2229bbc4 (patch) | |
tree | 8f2c391c422a7d7d65dad38090e47350e9f2d671 /content/browser/resources | |
parent | f5a263d76dd8280a860fc084df63be5f669223ce (diff) | |
download | chromium_src-11158e2d4794c6f91f4276a5160b685d2229bbc4.zip chromium_src-11158e2d4794c6f91f4276a5160b685d2229bbc4.tar.gz chromium_src-11158e2d4794c6f91f4276a5160b685d2229bbc4.tar.bz2 |
Move chrome://media-internals to content. This allows us to hide implementation details from the public API.
BUG=169170
Review URL: https://codereview.chromium.org/12153002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@180040 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'content/browser/resources')
-rw-r--r-- | content/browser/resources/media/cache_entry.js | 237 | ||||
-rw-r--r-- | content/browser/resources/media/disjoint_range_set.js | 145 | ||||
-rw-r--r-- | content/browser/resources/media/disjoint_range_set_test.html | 96 | ||||
-rw-r--r-- | content/browser/resources/media/event_list.js | 64 | ||||
-rw-r--r-- | content/browser/resources/media/item_store.js | 70 | ||||
-rw-r--r-- | content/browser/resources/media/media_internals.css | 83 | ||||
-rw-r--r-- | content/browser/resources/media/media_internals.html | 28 | ||||
-rw-r--r-- | content/browser/resources/media/media_internals.js | 281 | ||||
-rw-r--r-- | content/browser/resources/media/media_player.js | 154 | ||||
-rw-r--r-- | content/browser/resources/media/metrics.js | 118 | ||||
-rw-r--r-- | content/browser/resources/media/util.js | 74 |
11 files changed, 1350 insertions, 0 deletions
diff --git a/content/browser/resources/media/cache_entry.js b/content/browser/resources/media/cache_entry.js new file mode 100644 index 0000000..275a8c7 --- /dev/null +++ b/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/content/browser/resources/media/disjoint_range_set.js b/content/browser/resources/media/disjoint_range_set.js new file mode 100644 index 0000000..bd504bb --- /dev/null +++ b/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/content/browser/resources/media/disjoint_range_set_test.html b/content/browser/resources/media/disjoint_range_set_test.html new file mode 100644 index 0000000..39db9b3 --- /dev/null +++ b/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/content/browser/resources/media/event_list.js b/content/browser/resources/media/event_list.js new file mode 100644 index 0000000..fc13c05 --- /dev/null +++ b/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) { + var timeInMs = event.time * 1000; // Work with milliseconds. + this.startTime_ = this.startTime_ || timeInMs; + timeInMs -= this.startTime_; + + var row = document.createElement('tr'); + row.appendChild(media.makeElement('td', timeInMs.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/content/browser/resources/media/item_store.js b/content/browser/resources/media/item_store.js new file mode 100644 index 0000000..a6e3a6c --- /dev/null +++ b/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/content/browser/resources/media/media_internals.css b/content/browser/resources/media/media_internals.css new file mode 100644 index 0000000..d83b6b7 --- /dev/null +++ b/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/content/browser/resources/media/media_internals.html b/content/browser/resources/media/media_internals.html new file mode 100644 index 0000000..05d321f --- /dev/null +++ b/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/content/browser/resources/media/media_internals.js b/content/browser/resources/media/media_internals.js new file mode 100644 index 0000000..c3e3a1e --- /dev/null +++ b/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/content/browser/resources/media/media_player.js b/content/browser/resources/media/media_player.js new file mode 100644 index 0000000..a9d5d6b --- /dev/null +++ b/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/content/browser/resources/media/metrics.js b/content/browser/resources/media/metrics.js new file mode 100644 index 0000000..2c40f4a --- /dev/null +++ b/content/browser/resources/media/metrics.js @@ -0,0 +1,118 @@ +// 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) { + var timeInMs = event.time * 1000; // Work with milliseconds. + + for (var metric in metrics) { + var m = this[metric]; + if (type == metrics[metric].start && !m.start) { + m.start = timeInMs; + } else if (type == metrics[metric].end && m.start != null) { + var last = timeInMs - 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/content/browser/resources/media/util.js b/content/browser/resources/media/util.js new file mode 100644 index 0000000..c61ae0e --- /dev/null +++ b/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 + }; +}); |