summaryrefslogtreecommitdiffstats
path: root/content/browser/resources
diff options
context:
space:
mode:
authorjam@chromium.org <jam@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-02-01 02:31:56 +0000
committerjam@chromium.org <jam@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-02-01 02:31:56 +0000
commit11158e2d4794c6f91f4276a5160b685d2229bbc4 (patch)
tree8f2c391c422a7d7d65dad38090e47350e9f2d671 /content/browser/resources
parentf5a263d76dd8280a860fc084df63be5f669223ce (diff)
downloadchromium_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.js237
-rw-r--r--content/browser/resources/media/disjoint_range_set.js145
-rw-r--r--content/browser/resources/media/disjoint_range_set_test.html96
-rw-r--r--content/browser/resources/media/event_list.js64
-rw-r--r--content/browser/resources/media/item_store.js70
-rw-r--r--content/browser/resources/media/media_internals.css83
-rw-r--r--content/browser/resources/media/media_internals.html28
-rw-r--r--content/browser/resources/media/media_internals.js281
-rw-r--r--content/browser/resources/media/media_player.js154
-rw-r--r--content/browser/resources/media/metrics.js118
-rw-r--r--content/browser/resources/media/util.js74
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
+ };
+});