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. 4 5var ClientRenderer = (function() { 6 var ClientRenderer = function() { 7 this.playerListElement = document.getElementById('player-list'); 8 this.propertiesTable = 9 document.getElementById('property-table').querySelector('tbody'); 10 this.logTable = document.getElementById('log').querySelector('tbody'); 11 this.graphElement = document.getElementById('graphs'); 12 this.propertyName = document.getElementById('property-name'); 13 14 this.selectedPlayer = null; 15 this.selectedAudioComponentType = null; 16 this.selectedAudioComponentId = null; 17 this.selectedAudioCompontentData = null; 18 19 this.selectedPlayerLogIndex = 0; 20 21 this.filterFunction = function() { return true; }; 22 this.filterText = document.getElementById('filter-text'); 23 this.filterText.onkeyup = this.onTextChange_.bind(this); 24 25 this.bufferCanvas = document.createElement('canvas'); 26 this.bufferCanvas.width = media.BAR_WIDTH; 27 this.bufferCanvas.height = media.BAR_HEIGHT; 28 29 this.clipboardTextarea = document.getElementById('clipboard-textarea'); 30 this.clipboardButton = document.getElementById('copy-button'); 31 this.clipboardButton.onclick = this.copyToClipboard_.bind(this); 32 33 this.hiddenKeys = ['component_id', 'component_type', 'owner_id']; 34 }; 35 36 function removeChildren(element) { 37 while (element.hasChildNodes()) { 38 element.removeChild(element.lastChild); 39 } 40 }; 41 42 function createButton(text, select_cb) { 43 var button = document.createElement('button'); 44 45 button.appendChild(document.createTextNode(text)); 46 button.onclick = function() { 47 select_cb(); 48 }; 49 50 return button; 51 }; 52 53 ClientRenderer.prototype = { 54 /** 55 * Called when an audio component is added to the collection. 56 * @param componentType Integer AudioComponent enum value; must match values 57 * from the AudioLogFactory::AudioComponent enum. 58 * @param components The entire map of components (name -> dict). 59 */ 60 audioComponentAdded: function(componentType, components) { 61 this.redrawAudioComponentList_(componentType, components); 62 63 // Redraw the component if it's currently selected. 64 if (this.selectedAudioComponentType == componentType && 65 this.selectedAudioComponentId && 66 this.selectedAudioComponentId in components) { 67 this.selectAudioComponent_( 68 componentType, this.selectedAudioComponentId, 69 components[this.selectedAudioComponentId]); 70 } 71 }, 72 73 /** 74 * Called when an audio component is removed from the collection. 75 * @param componentType Integer AudioComponent enum value; must match values 76 * from the AudioLogFactory::AudioComponent enum. 77 * @param components The entire map of components (name -> dict). 78 */ 79 audioComponentRemoved: function(componentType, components) { 80 this.redrawAudioComponentList_(componentType, components); 81 82 // Clear the component if it was previously currently selected. 83 if (this.selectedAudioComponentType == componentType && 84 !(this.selectedAudioComponentId in components)) { 85 this.selectAudioComponent_(null, null, {}); 86 } 87 }, 88 89 /** 90 * Called when a player is added to the collection. 91 * @param players The entire map of id -> player. 92 * @param player_added The player that is added. 93 */ 94 playerAdded: function(players, playerAdded) { 95 this.redrawPlayerList_(players); 96 }, 97 98 /** 99 * Called when a playre is removed from the collection. 100 * @param players The entire map of id -> player. 101 * @param player_added The player that was removed. 102 */ 103 playerRemoved: function(players, playerRemoved) { 104 this.redrawPlayerList_(players); 105 }, 106 107 /** 108 * Called when a property on a player is changed. 109 * @param players The entire map of id -> player. 110 * @param player The player that had its property changed. 111 * @param key The name of the property that was changed. 112 * @param value The new value of the property. 113 */ 114 playerUpdated: function(players, player, key, value) { 115 if (player === this.selectedPlayer) { 116 this.drawProperties_(player.properties); 117 this.drawLog_(); 118 this.drawGraphs_(); 119 } 120 if (key === 'name' || key === 'url') { 121 this.redrawPlayerList_(players); 122 } 123 }, 124 125 redrawAudioComponentList_: function(componentType, components) { 126 function redrawList(renderer, baseName, element) { 127 var fragment = document.createDocumentFragment(); 128 for (id in components) { 129 var li = document.createElement('li'); 130 var friendlyName = baseName + ' ' + id; 131 li.appendChild(createButton( 132 friendlyName, renderer.selectAudioComponent_.bind( 133 renderer, componentType, id, components[id], friendlyName))); 134 fragment.appendChild(li); 135 } 136 removeChildren(element); 137 element.appendChild(fragment); 138 } 139 140 switch (componentType) { 141 case 0: 142 redrawList(this, 'Controller', document.getElementById( 143 'audio-input-controller-list')); 144 break; 145 case 1: 146 redrawList(this, 'Controller', document.getElementById( 147 'audio-output-controller-list')); 148 break; 149 case 2: 150 redrawList(this, 'Stream', document.getElementById( 151 'audio-output-stream-list')); 152 break; 153 default: 154 break; 155 } 156 }, 157 158 selectAudioComponent_: function( 159 componentType, componentId, componentData, friendlyName) { 160 this.selectedPlayer = null; 161 this.selectedAudioComponentType = componentType; 162 this.selectedAudioComponentId = componentId; 163 this.selectedAudioCompontentData = componentData; 164 this.drawProperties_(componentData); 165 removeChildren(this.logTable); 166 removeChildren(this.graphElement); 167 168 removeChildren(this.propertyName); 169 this.propertyName.appendChild(document.createTextNode(friendlyName)); 170 }, 171 172 redrawPlayerList_: function(players) { 173 var fragment = document.createDocumentFragment(); 174 for (id in players) { 175 var player = players[id]; 176 var usableName = player.properties.name || 177 player.properties.url || 178 'Player ' + player.id; 179 180 var li = document.createElement('li'); 181 li.appendChild(createButton( 182 usableName, this.selectPlayer_.bind(this, player))); 183 fragment.appendChild(li); 184 } 185 removeChildren(this.playerListElement); 186 this.playerListElement.appendChild(fragment); 187 }, 188 189 selectPlayer_: function(player) { 190 this.selectedPlayer = player; 191 this.selectedPlayerLogIndex = 0; 192 this.selectedAudioComponentType = null; 193 this.selectedAudioComponentId = null; 194 this.selectedAudioCompontentData = null; 195 this.drawProperties_(player.properties); 196 197 removeChildren(this.logTable); 198 removeChildren(this.graphElement); 199 this.drawLog_(); 200 this.drawGraphs_(); 201 removeChildren(this.propertyName); 202 this.propertyName.appendChild(document.createTextNode('Player')); 203 }, 204 205 drawProperties_: function(propertyMap) { 206 removeChildren(this.propertiesTable); 207 var sortedKeys = Object.keys(propertyMap).sort(); 208 for (var i = 0; i < sortedKeys.length; ++i) { 209 var key = sortedKeys[i]; 210 if (this.hiddenKeys.indexOf(key) >= 0) 211 continue; 212 213 var value = propertyMap[key]; 214 var row = this.propertiesTable.insertRow(-1); 215 var keyCell = row.insertCell(-1); 216 var valueCell = row.insertCell(-1); 217 218 keyCell.appendChild(document.createTextNode(key)); 219 valueCell.appendChild(document.createTextNode(value)); 220 } 221 }, 222 223 appendEventToLog_: function(event) { 224 if (this.filterFunction(event.key)) { 225 var row = this.logTable.insertRow(-1); 226 227 var timestampCell = row.insertCell(-1); 228 timestampCell.classList.add('timestamp'); 229 timestampCell.appendChild(document.createTextNode( 230 util.millisecondsToString(event.time))); 231 row.insertCell(-1).appendChild(document.createTextNode(event.key)); 232 row.insertCell(-1).appendChild(document.createTextNode(event.value)); 233 } 234 }, 235 236 drawLog_: function() { 237 var toDraw = this.selectedPlayer.allEvents.slice( 238 this.selectedPlayerLogIndex); 239 toDraw.forEach(this.appendEventToLog_.bind(this)); 240 this.selectedPlayerLogIndex = this.selectedPlayer.allEvents.length; 241 }, 242 243 drawGraphs_: function() { 244 function addToGraphs(name, graph, graphElement) { 245 var li = document.createElement('li'); 246 li.appendChild(graph); 247 li.appendChild(document.createTextNode(name)); 248 graphElement.appendChild(li); 249 } 250 251 var url = this.selectedPlayer.properties.url; 252 if (!url) { 253 return; 254 } 255 256 var cache = media.cacheForUrl(url); 257 258 var player = this.selectedPlayer; 259 var props = player.properties; 260 261 var cacheExists = false; 262 var bufferExists = false; 263 264 if (props['buffer_start'] !== undefined && 265 props['buffer_current'] !== undefined && 266 props['buffer_end'] !== undefined && 267 props['total_bytes'] !== undefined) { 268 this.drawBufferGraph_(props['buffer_start'], 269 props['buffer_current'], 270 props['buffer_end'], 271 props['total_bytes']); 272 bufferExists = true; 273 } 274 275 if (cache) { 276 if (player.properties['total_bytes']) { 277 cache.size = Number(player.properties['total_bytes']); 278 } 279 cache.generateDetails(); 280 cacheExists = true; 281 282 } 283 284 if (!this.graphElement.hasChildNodes()) { 285 if (bufferExists) { 286 addToGraphs('buffer', this.bufferCanvas, this.graphElement); 287 } 288 if (cacheExists) { 289 addToGraphs('cache read', cache.readCanvas, this.graphElement); 290 addToGraphs('cache write', cache.writeCanvas, this.graphElement); 291 } 292 } 293 }, 294 295 drawBufferGraph_: function(start, current, end, size) { 296 var ctx = this.bufferCanvas.getContext('2d'); 297 var width = this.bufferCanvas.width; 298 var height = this.bufferCanvas.height; 299 ctx.fillStyle = '#aaa'; 300 ctx.fillRect(0, 0, width, height); 301 302 var scale_factor = width / size; 303 var left = start * scale_factor; 304 var middle = current * scale_factor; 305 var right = end * scale_factor; 306 307 ctx.fillStyle = '#a0a'; 308 ctx.fillRect(left, 0, middle - left, height); 309 ctx.fillStyle = '#aa0'; 310 ctx.fillRect(middle, 0, right - middle, height); 311 }, 312 313 copyToClipboard_: function() { 314 var properties = this.selectedAudioCompontentData || 315 this.selectedPlayer.properties || false; 316 if (!properties) { 317 return; 318 } 319 var stringBuffer = []; 320 321 for (var key in properties) { 322 var value = properties[key]; 323 stringBuffer.push(key.toString()); 324 stringBuffer.push(': '); 325 stringBuffer.push(value.toString()); 326 stringBuffer.push('\n'); 327 } 328 329 this.clipboardTextarea.value = stringBuffer.join(''); 330 this.clipboardTextarea.classList.remove('hiddenClipboard'); 331 this.clipboardTextarea.focus(); 332 this.clipboardTextarea.select(); 333 334 // Hide the clipboard element when it loses focus. 335 this.clipboardTextarea.onblur = function(event) { 336 setTimeout(function(element) { 337 event.target.classList.add('hiddenClipboard'); 338 }, 0); 339 }; 340 }, 341 342 onTextChange_: function(event) { 343 var text = this.filterText.value.toLowerCase(); 344 var parts = text.split(',').map(function(part) { 345 return part.trim(); 346 }).filter(function(part) { 347 return part.trim().length > 0; 348 }); 349 350 this.filterFunction = function(text) { 351 text = text.toLowerCase(); 352 return parts.length === 0 || parts.some(function(part) { 353 return text.indexOf(part) != -1; 354 }); 355 }; 356 357 if (this.selectedPlayer) { 358 removeChildren(this.logTable); 359 this.selectedPlayerLogIndex = 0; 360 this.drawLog_(); 361 } 362 }, 363 }; 364 365 return ClientRenderer; 366})(); 367