1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 var ClientRenderer = (function() {
6 var ClientRenderer = function() {
7 this.playerListElement = document.getElementById('player-list');
8 this.audioPropertiesTable =
9 document.getElementById('audio-property-table').querySelector('tbody');
10 this.playerPropertiesTable =
11 document.getElementById('player-property-table').querySelector('tbody');
12 this.logTable = document.getElementById('log').querySelector('tbody');
13 this.graphElement = document.getElementById('graphs');
14 this.audioPropertyName = document.getElementById('audio-property-name');
16 this.selectedPlayer = null;
17 this.selectedAudioComponentType = null;
18 this.selectedAudioComponentId = null;
19 this.selectedAudioCompontentData = null;
21 this.selectedPlayerLogIndex = 0;
23 this.filterFunction = function() { return true; };
24 this.filterText = document.getElementById('filter-text');
25 this.filterText.onkeyup = this.onTextChange_.bind(this);
27 this.bufferCanvas = document.createElement('canvas');
28 this.bufferCanvas.width = media.BAR_WIDTH;
29 this.bufferCanvas.height = media.BAR_HEIGHT;
31 this.clipboardTextarea = document.getElementById('clipboard-textarea');
32 var clipboardButtons = document.getElementsByClassName('copy-button');
33 for (var i = 0; i < clipboardButtons.length; i++) {
34 clipboardButtons[i].onclick = this.copyToClipboard_.bind(this);
37 this.hiddenKeys = ['component_id', 'component_type', 'owner_id'];
39 // Tell CSS to hide certain content prior to making selections.
40 document.body.classList.add(ClientRenderer.Css_.NO_PLAYERS_SELECTED);
41 document.body.classList.add(ClientRenderer.Css_.NO_COMPONENTS_SELECTED);
45 * CSS classes added / removed in JS to trigger styling changes.
46 * @private @enum {string}
48 ClientRenderer.Css_ = {
49 NO_PLAYERS_SELECTED: 'no-players-selected',
50 NO_COMPONENTS_SELECTED: 'no-components-selected',
51 SELECTABLE_BUTTON: 'selectable-button'
54 function removeChildren(element) {
55 while (element.hasChildNodes()) {
56 element.removeChild(element.lastChild);
60 function createSelectableButton(id, groupName, text, select_cb) {
62 var radioButton = document.createElement('input');
63 radioButton.classList.add(ClientRenderer.Css_.SELECTABLE_BUTTON);
64 radioButton.type = 'radio';
66 radioButton.name = groupName;
68 var buttonLabel = document.createElement('label');
69 buttonLabel.classList.add(ClientRenderer.Css_.SELECTABLE_BUTTON);
70 buttonLabel.setAttribute('for', radioButton.id);
71 buttonLabel.appendChild(document.createTextNode(text));
73 var fragment = document.createDocumentFragment();
74 fragment.appendChild(radioButton);
75 fragment.appendChild(buttonLabel);
77 // Listen to 'change' rather than 'click' to keep styling in sync with
79 radioButton.addEventListener('change', function() {
86 function selectSelectableButton(id) {
87 var element = document.getElementById(id);
89 console.error('failed to select button with id: ' + id);
93 element.checked = true;
96 ClientRenderer.prototype = {
98 * Called when an audio component is added to the collection.
99 * @param componentType Integer AudioComponent enum value; must match values
100 * from the AudioLogFactory::AudioComponent enum.
101 * @param components The entire map of components (name -> dict).
103 audioComponentAdded: function(componentType, components) {
104 this.redrawAudioComponentList_(componentType, components);
106 // Redraw the component if it's currently selected.
107 if (this.selectedAudioComponentType == componentType &&
108 this.selectedAudioComponentId &&
109 this.selectedAudioComponentId in components) {
110 // TODO(chcunningham): This path is used both for adding and updating
111 // the components. Split this up to have a separate update method.
112 // At present, this selectAudioComponent call is key to *updating* the
113 // the property table for existing audio components.
114 this.selectAudioComponent_(
115 componentType, this.selectedAudioComponentId,
116 components[this.selectedAudioComponentId]);
121 * Called when an audio component is removed from the collection.
122 * @param componentType Integer AudioComponent enum value; must match values
123 * from the AudioLogFactory::AudioComponent enum.
124 * @param components The entire map of components (name -> dict).
126 audioComponentRemoved: function(componentType, components) {
127 this.redrawAudioComponentList_(componentType, components);
131 * Called when a player is added to the collection.
132 * @param players The entire map of id -> player.
133 * @param player_added The player that is added.
135 playerAdded: function(players, playerAdded) {
136 this.redrawPlayerList_(players);
140 * Called when a player is removed from the collection.
141 * @param players The entire map of id -> player.
142 * @param playerRemoved The player that was removed.
144 playerRemoved: function(players, playerRemoved) {
145 if (playerRemoved === this.selectedPlayer) {
146 removeChildren(this.playerPropertiesTable);
147 removeChildren(this.logTable);
148 removeChildren(this.graphElement);
149 document.body.classList.add(ClientRenderer.Css_.NO_PLAYERS_SELECTED);
151 this.redrawPlayerList_(players);
155 * Called when a property on a player is changed.
156 * @param players The entire map of id -> player.
157 * @param player The player that had its property changed.
158 * @param key The name of the property that was changed.
159 * @param value The new value of the property.
161 playerUpdated: function(players, player, key, value) {
162 if (player === this.selectedPlayer) {
163 this.drawProperties_(player.properties, this.playerPropertiesTable);
167 if (key === 'name' || key === 'url') {
168 this.redrawPlayerList_(players);
172 createVideoCaptureFormatTable: function(formats) {
173 if (!formats || formats.length == 0)
174 return document.createTextNode('No formats');
176 var table = document.createElement('table');
177 var thead = document.createElement('thead');
178 var theadRow = document.createElement('tr');
179 for (var key in formats[0]) {
180 var th = document.createElement('th');
181 th.appendChild(document.createTextNode(key));
182 theadRow.appendChild(th);
184 thead.appendChild(theadRow);
185 table.appendChild(thead);
186 var tbody = document.createElement('tbody');
187 for (var i=0; i < formats.length; ++i) {
188 var tr = document.createElement('tr')
189 for (var key in formats[i]) {
190 var td = document.createElement('td');
191 td.appendChild(document.createTextNode(formats[i][key]));
194 tbody.appendChild(tr);
196 table.appendChild(tbody);
197 table.classList.add('video-capture-formats-table');
201 redrawVideoCaptureCapabilities: function(videoCaptureCapabilities, keys) {
202 var copyButtonElement =
203 document.getElementById('video-capture-capabilities-copy-button');
204 copyButtonElement.onclick = function() {
205 window.prompt('Copy to clipboard: Ctrl+C, Enter',
206 JSON.stringify(videoCaptureCapabilities))
209 var videoTableBodyElement =
210 document.getElementById('video-capture-capabilities-tbody');
211 removeChildren(videoTableBodyElement);
213 for (var component in videoCaptureCapabilities) {
214 var tableRow = document.createElement('tr');
215 var device = videoCaptureCapabilities[ component ];
216 for (var i in keys) {
217 var value = device[keys[i]];
218 var tableCell = document.createElement('td');
220 if ((typeof value) == (typeof [])) {
221 cellElement = this.createVideoCaptureFormatTable(value);
223 cellElement = document.createTextNode(
224 ((typeof value) == 'undefined') ? 'n/a' : value);
226 tableCell.appendChild(cellElement)
227 tableRow.appendChild(tableCell);
229 videoTableBodyElement.appendChild(tableRow);
233 getAudioComponentName_ : function(componentType, id) {
235 switch (componentType) {
238 baseName = 'Controller';
244 baseName = 'UnknownType'
245 console.error('Unrecognized component type: ' + componentType);
248 return baseName + ' ' + id;
251 getListElementForAudioComponent_ : function(componentType) {
253 switch (componentType) {
255 listElement = document.getElementById(
256 'audio-input-controller-list');
259 listElement = document.getElementById(
260 'audio-output-controller-list');
263 listElement = document.getElementById(
264 'audio-output-stream-list');
267 console.error('Unrecognized component type: ' + componentType);
274 redrawAudioComponentList_: function(componentType, components) {
275 // Group name imposes rule that only one component can be selected
276 // (and have its properties displayed) at a time.
277 var buttonGroupName = 'audio-components';
279 var listElement = this.getListElementForAudioComponent_(componentType);
281 console.error('Failed to find list element for component type: ' +
286 var fragment = document.createDocumentFragment();
287 for (id in components) {
288 var li = document.createElement('li');
289 var button_cb = this.selectAudioComponent_.bind(
290 this, componentType, id, components[id]);
291 var friendlyName = this.getAudioComponentName_(componentType, id);
292 li.appendChild(createSelectableButton(
293 id, buttonGroupName, friendlyName, button_cb));
294 fragment.appendChild(li);
296 removeChildren(listElement);
297 listElement.appendChild(fragment);
299 if (this.selectedAudioComponentType &&
300 this.selectedAudioComponentType == componentType &&
301 this.selectedAudioComponentId in components) {
302 // Re-select the selected component since the button was just recreated.
303 selectSelectableButton(this.selectedAudioComponentId);
307 selectAudioComponent_: function(
308 componentType, componentId, componentData) {
309 document.body.classList.remove(
310 ClientRenderer.Css_.NO_COMPONENTS_SELECTED);
312 this.selectedAudioComponentType = componentType;
313 this.selectedAudioComponentId = componentId;
314 this.selectedAudioCompontentData = componentData;
315 this.drawProperties_(componentData, this.audioPropertiesTable);
317 removeChildren(this.audioPropertyName);
318 this.audioPropertyName.appendChild(document.createTextNode(
319 this.getAudioComponentName_(componentType, componentId)));
322 redrawPlayerList_: function(players) {
323 // Group name imposes rule that only one component can be selected
324 // (and have its properties displayed) at a time.
325 var buttonGroupName = 'player-buttons';
327 var fragment = document.createDocumentFragment();
328 for (id in players) {
329 var player = players[id];
330 var usableName = player.properties.name ||
331 player.properties.url ||
332 'Player ' + player.id;
334 var li = document.createElement('li');
335 var button_cb = this.selectPlayer_.bind(this, player);
336 li.appendChild(createSelectableButton(
337 id, buttonGroupName, usableName, button_cb));
338 fragment.appendChild(li);
340 removeChildren(this.playerListElement);
341 this.playerListElement.appendChild(fragment);
343 if (this.selectedPlayer && this.selectedPlayer.id in players) {
344 // Re-select the selected player since the button was just recreated.
345 selectSelectableButton(this.selectedPlayer.id);
349 selectPlayer_: function(player) {
350 document.body.classList.remove(ClientRenderer.Css_.NO_PLAYERS_SELECTED);
352 this.selectedPlayer = player;
353 this.selectedPlayerLogIndex = 0;
354 this.selectedAudioComponentType = null;
355 this.selectedAudioComponentId = null;
356 this.selectedAudioCompontentData = null;
357 this.drawProperties_(player.properties, this.playerPropertiesTable);
359 removeChildren(this.logTable);
360 removeChildren(this.graphElement);
365 drawProperties_: function(propertyMap, propertiesTable) {
366 removeChildren(propertiesTable);
367 var sortedKeys = Object.keys(propertyMap).sort();
368 for (var i = 0; i < sortedKeys.length; ++i) {
369 var key = sortedKeys[i];
370 if (this.hiddenKeys.indexOf(key) >= 0)
373 var value = propertyMap[key];
374 var row = propertiesTable.insertRow(-1);
375 var keyCell = row.insertCell(-1);
376 var valueCell = row.insertCell(-1);
378 keyCell.appendChild(document.createTextNode(key));
379 valueCell.appendChild(document.createTextNode(value));
383 appendEventToLog_: function(event) {
384 if (this.filterFunction(event.key)) {
385 var row = this.logTable.insertRow(-1);
387 var timestampCell = row.insertCell(-1);
388 timestampCell.classList.add('timestamp');
389 timestampCell.appendChild(document.createTextNode(
390 util.millisecondsToString(event.time)));
391 row.insertCell(-1).appendChild(document.createTextNode(event.key));
392 row.insertCell(-1).appendChild(document.createTextNode(event.value));
396 drawLog_: function() {
397 var toDraw = this.selectedPlayer.allEvents.slice(
398 this.selectedPlayerLogIndex);
399 toDraw.forEach(this.appendEventToLog_.bind(this));
400 this.selectedPlayerLogIndex = this.selectedPlayer.allEvents.length;
403 drawGraphs_: function() {
404 function addToGraphs(name, graph, graphElement) {
405 var li = document.createElement('li');
406 li.appendChild(graph);
407 li.appendChild(document.createTextNode(name));
408 graphElement.appendChild(li);
411 var url = this.selectedPlayer.properties.url;
416 var cache = media.cacheForUrl(url);
418 var player = this.selectedPlayer;
419 var props = player.properties;
421 var cacheExists = false;
422 var bufferExists = false;
424 if (props['buffer_start'] !== undefined &&
425 props['buffer_current'] !== undefined &&
426 props['buffer_end'] !== undefined &&
427 props['total_bytes'] !== undefined) {
428 this.drawBufferGraph_(props['buffer_start'],
429 props['buffer_current'],
431 props['total_bytes']);
436 if (player.properties['total_bytes']) {
437 cache.size = Number(player.properties['total_bytes']);
439 cache.generateDetails();
444 if (!this.graphElement.hasChildNodes()) {
446 addToGraphs('buffer', this.bufferCanvas, this.graphElement);
449 addToGraphs('cache read', cache.readCanvas, this.graphElement);
450 addToGraphs('cache write', cache.writeCanvas, this.graphElement);
455 drawBufferGraph_: function(start, current, end, size) {
456 var ctx = this.bufferCanvas.getContext('2d');
457 var width = this.bufferCanvas.width;
458 var height = this.bufferCanvas.height;
459 ctx.fillStyle = '#aaa';
460 ctx.fillRect(0, 0, width, height);
462 var scale_factor = width / size;
463 var left = start * scale_factor;
464 var middle = current * scale_factor;
465 var right = end * scale_factor;
467 ctx.fillStyle = '#a0a';
468 ctx.fillRect(left, 0, middle - left, height);
469 ctx.fillStyle = '#aa0';
470 ctx.fillRect(middle, 0, right - middle, height);
473 copyToClipboard_: function() {
474 if (!this.selectedPlayer && !this.selectedAudioCompontentData) {
477 var properties = this.selectedAudioCompontentData ||
478 this.selectedPlayer.properties;
479 var stringBuffer = [];
481 for (var key in properties) {
482 var value = properties[key];
483 stringBuffer.push(key.toString());
484 stringBuffer.push(': ');
485 stringBuffer.push(value.toString());
486 stringBuffer.push('\n');
489 this.clipboardTextarea.value = stringBuffer.join('');
490 this.clipboardTextarea.classList.remove('hiddenClipboard');
491 this.clipboardTextarea.focus();
492 this.clipboardTextarea.select();
494 // Hide the clipboard element when it loses focus.
495 this.clipboardTextarea.onblur = function(event) {
496 setTimeout(function(element) {
497 event.target.classList.add('hiddenClipboard');
502 onTextChange_: function(event) {
503 var text = this.filterText.value.toLowerCase();
504 var parts = text.split(',').map(function(part) {
506 }).filter(function(part) {
507 return part.trim().length > 0;
510 this.filterFunction = function(text) {
511 text = text.toLowerCase();
512 return parts.length === 0 || parts.some(function(part) {
513 return text.indexOf(part) != -1;
517 if (this.selectedPlayer) {
518 removeChildren(this.logTable);
519 this.selectedPlayerLogIndex = 0;
525 return ClientRenderer;