Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / content / browser / resources / media / client_renderer.js
blobccee09ee122541760e991133b9b4c0a5ed7a0df0
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);
35     }
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);
42   };
44   /**
45    * CSS classes added / removed in JS to trigger styling changes.
46    * @private @enum {string}
47    */
48   ClientRenderer.Css_ = {
49     NO_PLAYERS_SELECTED: 'no-players-selected',
50     NO_COMPONENTS_SELECTED: 'no-components-selected',
51     SELECTABLE_BUTTON: 'selectable-button'
52   };
54   function removeChildren(element) {
55     while (element.hasChildNodes()) {
56       element.removeChild(element.lastChild);
57     }
58   };
60   function createSelectableButton(id, groupName, text, select_cb) {
61     // For CSS styling.
62     var radioButton = document.createElement('input');
63     radioButton.classList.add(ClientRenderer.Css_.SELECTABLE_BUTTON);
64     radioButton.type = 'radio';
65     radioButton.id = id;
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
78     // button behavior.
79     radioButton.addEventListener('change', function() {
80       select_cb();
81     });
83     return fragment;
84   };
86   function selectSelectableButton(id) {
87     var element = document.getElementById(id);
88     if (!element) {
89       console.error('failed to select button with id: ' + id);
90       return;
91     }
93     element.checked = true;
94   }
96   ClientRenderer.prototype = {
97     /**
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).
102      */
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]);
117       }
118     },
120     /**
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).
125      */
126     audioComponentRemoved: function(componentType, components) {
127       this.redrawAudioComponentList_(componentType, components);
128     },
130     /**
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.
134      */
135     playerAdded: function(players, playerAdded) {
136       this.redrawPlayerList_(players);
137     },
139     /**
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.
143      */
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);
150       }
151       this.redrawPlayerList_(players);
152     },
154     /**
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.
160      */
161     playerUpdated: function(players, player, key, value) {
162       if (player === this.selectedPlayer) {
163         this.drawProperties_(player.properties, this.playerPropertiesTable);
164         this.drawLog_();
165         this.drawGraphs_();
166       }
167       if (key === 'name' || key === 'url') {
168         this.redrawPlayerList_(players);
169       }
170     },
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);
183       }
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]));
192           tr.appendChild(td);
193         }
194         tbody.appendChild(tr);
195       }
196       table.appendChild(tbody);
197       table.classList.add('video-capture-formats-table');
198       return table;
199     },
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))
207       }
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');
219           var cellElement;
220           if ((typeof value) == (typeof [])) {
221             cellElement = this.createVideoCaptureFormatTable(value);
222           } else {
223             cellElement = document.createTextNode(
224                 ((typeof value) == 'undefined') ? 'n/a' : value);
225           }
226           tableCell.appendChild(cellElement)
227           tableRow.appendChild(tableCell);
228         }
229         videoTableBodyElement.appendChild(tableRow);
230       }
231     },
233     getAudioComponentName_ : function(componentType, id) {
234       var baseName;
235       switch (componentType) {
236         case 0:
237         case 1:
238           baseName = 'Controller';
239           break;
240         case 2:
241           baseName = 'Stream';
242           break;
243         default:
244           baseName = 'UnknownType'
245           console.error('Unrecognized component type: ' + componentType);
246           break;
247       }
248       return baseName + ' ' + id;
249     },
251     getListElementForAudioComponent_ : function(componentType) {
252       var listElement;
253       switch (componentType) {
254         case 0:
255           listElement = document.getElementById(
256               'audio-input-controller-list');
257           break;
258         case 1:
259           listElement = document.getElementById(
260               'audio-output-controller-list');
261           break;
262         case 2:
263           listElement = document.getElementById(
264               'audio-output-stream-list');
265           break;
266         default:
267           console.error('Unrecognized component type: ' + componentType);
268           listElement = null;
269           break;
270       }
271       return listElement;
272     },
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);
280       if (!listElement) {
281         console.error('Failed to find list element for component type: ' +
282             componentType);
283         return;
284       }
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);
295       }
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);
304       }
305     },
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)));
320     },
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);
339       }
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);
346       }
347     },
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);
361       this.drawLog_();
362       this.drawGraphs_();
363     },
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)
371           continue;
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));
380       }
381     },
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));
393       }
394     },
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;
401     },
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);
409       }
411       var url = this.selectedPlayer.properties.url;
412       if (!url) {
413         return;
414       }
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'],
430                               props['buffer_end'],
431                               props['total_bytes']);
432         bufferExists = true;
433       }
435       if (cache) {
436         if (player.properties['total_bytes']) {
437           cache.size = Number(player.properties['total_bytes']);
438         }
439         cache.generateDetails();
440         cacheExists = true;
442       }
444       if (!this.graphElement.hasChildNodes()) {
445         if (bufferExists) {
446           addToGraphs('buffer', this.bufferCanvas, this.graphElement);
447         }
448         if (cacheExists) {
449           addToGraphs('cache read', cache.readCanvas, this.graphElement);
450           addToGraphs('cache write', cache.writeCanvas, this.graphElement);
451         }
452       }
453     },
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);
471     },
473     copyToClipboard_: function() {
474       if (!this.selectedPlayer && !this.selectedAudioCompontentData) {
475         return;
476       }
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');
487       }
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');
498         }, 0);
499       };
500     },
502     onTextChange_: function(event) {
503       var text = this.filterText.value.toLowerCase();
504       var parts = text.split(',').map(function(part) {
505         return part.trim();
506       }).filter(function(part) {
507         return part.trim().length > 0;
508       });
510       this.filterFunction = function(text) {
511         text = text.toLowerCase();
512         return parts.length === 0 || parts.some(function(part) {
513           return text.indexOf(part) != -1;
514         });
515       };
517       if (this.selectedPlayer) {
518         removeChildren(this.logTable);
519         this.selectedPlayerLogIndex = 0;
520         this.drawLog_();
521       }
522     },
523   };
525   return ClientRenderer;
526 })();