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
;