| // Copyright 2013 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. |
| |
| var ClientRenderer = (function() { |
| var ClientRenderer = function() { |
| this.playerListElement = document.getElementById('player-list'); |
| this.audioPropertiesTable = |
| document.getElementById('audio-property-table').querySelector('tbody'); |
| this.playerPropertiesTable = |
| document.getElementById('player-property-table').querySelector('tbody'); |
| this.logTable = document.getElementById('log').querySelector('tbody'); |
| this.graphElement = document.getElementById('graphs'); |
| this.audioPropertyName = document.getElementById('audio-property-name'); |
| |
| this.players = null; |
| this.selectedPlayer = null; |
| this.selectedAudioComponentType = null; |
| this.selectedAudioComponentId = null; |
| this.selectedAudioCompontentData = null; |
| |
| this.selectedPlayerLogIndex = 0; |
| |
| this.filterFunction = function() { return true; }; |
| this.filterText = document.getElementById('filter-text'); |
| this.filterText.onkeyup = this.onTextChange_.bind(this); |
| |
| this.bufferCanvas = document.createElement('canvas'); |
| this.bufferCanvas.width = media.BAR_WIDTH; |
| this.bufferCanvas.height = media.BAR_HEIGHT; |
| |
| this.clipboardDialog = document.getElementById('clipboard-dialog'); |
| |
| this.clipboardTextarea = document.getElementById('clipboard-textarea'); |
| this.clipboardTextarea.onblur = this.hideClipboard_.bind(this); |
| var clipboardButtons = document.getElementsByClassName('copy-button'); |
| for (var i = 0; i < clipboardButtons.length; i++) { |
| clipboardButtons[i].onclick = this.copyToClipboard_.bind(this); |
| } |
| |
| this.saveLogButton = document.getElementById('save-log-button'); |
| this.saveLogButton.onclick = this.saveLog_.bind(this); |
| |
| this.hiddenKeys = ['component_id', 'component_type', 'owner_id']; |
| |
| // Tell CSS to hide certain content prior to making selections. |
| document.body.classList.add(ClientRenderer.Css_.NO_PLAYERS_SELECTED); |
| document.body.classList.add(ClientRenderer.Css_.NO_COMPONENTS_SELECTED); |
| }; |
| |
| /** |
| * CSS classes added / removed in JS to trigger styling changes. |
| * @private @enum {string} |
| */ |
| ClientRenderer.Css_ = { |
| NO_PLAYERS_SELECTED: 'no-players-selected', |
| NO_COMPONENTS_SELECTED: 'no-components-selected', |
| SELECTABLE_BUTTON: 'selectable-button' |
| }; |
| |
| function removeChildren(element) { |
| while (element.hasChildNodes()) { |
| element.removeChild(element.lastChild); |
| } |
| }; |
| |
| function createSelectableButton(id, groupName, text, select_cb) { |
| // For CSS styling. |
| var radioButton = document.createElement('input'); |
| radioButton.classList.add(ClientRenderer.Css_.SELECTABLE_BUTTON); |
| radioButton.type = 'radio'; |
| radioButton.id = id; |
| radioButton.name = groupName; |
| |
| var buttonLabel = document.createElement('label'); |
| buttonLabel.classList.add(ClientRenderer.Css_.SELECTABLE_BUTTON); |
| buttonLabel.setAttribute('for', radioButton.id); |
| buttonLabel.appendChild(document.createTextNode(text)); |
| |
| var fragment = document.createDocumentFragment(); |
| fragment.appendChild(radioButton); |
| fragment.appendChild(buttonLabel); |
| |
| // Listen to 'change' rather than 'click' to keep styling in sync with |
| // button behavior. |
| radioButton.addEventListener('change', function() { |
| select_cb(); |
| }); |
| |
| return fragment; |
| }; |
| |
| function selectSelectableButton(id) { |
| var element = document.getElementById(id); |
| if (!element) { |
| console.error('failed to select button with id: ' + id); |
| return; |
| } |
| |
| element.checked = true; |
| } |
| |
| function downloadLog(text) { |
| var file = new Blob([text], {type: 'text/plain'}); |
| var a = document.createElement('a'); |
| a.href = URL.createObjectURL(file); |
| a.download = "media-internals.txt"; |
| a.click(); |
| } |
| |
| ClientRenderer.prototype = { |
| /** |
| * Called when an audio component is added to the collection. |
| * @param componentType Integer AudioComponent enum value; must match values |
| * from the AudioLogFactory::AudioComponent enum. |
| * @param components The entire map of components (name -> dict). |
| */ |
| audioComponentAdded: function(componentType, components) { |
| this.redrawAudioComponentList_(componentType, components); |
| |
| // Redraw the component if it's currently selected. |
| if (this.selectedAudioComponentType == componentType && |
| this.selectedAudioComponentId && |
| this.selectedAudioComponentId in components) { |
| // TODO(chcunningham): This path is used both for adding and updating |
| // the components. Split this up to have a separate update method. |
| // At present, this selectAudioComponent call is key to *updating* the |
| // the property table for existing audio components. |
| this.selectAudioComponent_( |
| componentType, this.selectedAudioComponentId, |
| components[this.selectedAudioComponentId]); |
| } |
| }, |
| |
| /** |
| * Called when an audio component is removed from the collection. |
| * @param componentType Integer AudioComponent enum value; must match values |
| * from the AudioLogFactory::AudioComponent enum. |
| * @param components The entire map of components (name -> dict). |
| */ |
| audioComponentRemoved: function(componentType, components) { |
| this.redrawAudioComponentList_(componentType, components); |
| }, |
| |
| /** |
| * Called when a player is added to the collection. |
| * @param players The entire map of id -> player. |
| * @param player_added The player that is added. |
| */ |
| playerAdded: function(players, playerAdded) { |
| this.redrawPlayerList_(players); |
| }, |
| |
| /** |
| * Called when a player is removed from the collection. |
| * @param players The entire map of id -> player. |
| * @param playerRemoved The player that was removed. |
| */ |
| playerRemoved: function(players, playerRemoved) { |
| if (playerRemoved === this.selectedPlayer) { |
| removeChildren(this.playerPropertiesTable); |
| removeChildren(this.logTable); |
| removeChildren(this.graphElement); |
| document.body.classList.add(ClientRenderer.Css_.NO_PLAYERS_SELECTED); |
| } |
| this.redrawPlayerList_(players); |
| }, |
| |
| /** |
| * Called when a property on a player is changed. |
| * @param players The entire map of id -> player. |
| * @param player The player that had its property changed. |
| * @param key The name of the property that was changed. |
| * @param value The new value of the property. |
| */ |
| playerUpdated: function(players, player, key, value) { |
| if (player === this.selectedPlayer) { |
| this.drawProperties_(player.properties, this.playerPropertiesTable); |
| this.drawLog_(); |
| this.drawGraphs_(); |
| } |
| if (key === 'name' || key === 'url') { |
| this.redrawPlayerList_(players); |
| } |
| }, |
| |
| createVideoCaptureFormatTable: function(formats) { |
| if (!formats || formats.length == 0) |
| return document.createTextNode('No formats'); |
| |
| var table = document.createElement('table'); |
| var thead = document.createElement('thead'); |
| var theadRow = document.createElement('tr'); |
| for (var key in formats[0]) { |
| var th = document.createElement('th'); |
| th.appendChild(document.createTextNode(key)); |
| theadRow.appendChild(th); |
| } |
| thead.appendChild(theadRow); |
| table.appendChild(thead); |
| var tbody = document.createElement('tbody'); |
| for (var i=0; i < formats.length; ++i) { |
| var tr = document.createElement('tr') |
| for (var key in formats[i]) { |
| var td = document.createElement('td'); |
| td.appendChild(document.createTextNode(formats[i][key])); |
| tr.appendChild(td); |
| } |
| tbody.appendChild(tr); |
| } |
| table.appendChild(tbody); |
| table.classList.add('video-capture-formats-table'); |
| return table; |
| }, |
| |
| redrawVideoCaptureCapabilities: function(videoCaptureCapabilities, keys) { |
| var copyButtonElement = |
| document.getElementById('video-capture-capabilities-copy-button'); |
| copyButtonElement.onclick = function() { |
| this.showClipboard(JSON.stringify(videoCaptureCapabilities, null, 2)); |
| }.bind(this); |
| |
| var videoTableBodyElement = |
| document.getElementById('video-capture-capabilities-tbody'); |
| removeChildren(videoTableBodyElement); |
| |
| for (var component in videoCaptureCapabilities) { |
| var tableRow = document.createElement('tr'); |
| var device = videoCaptureCapabilities[ component ]; |
| for (var i in keys) { |
| var value = device[keys[i]]; |
| var tableCell = document.createElement('td'); |
| var cellElement; |
| if ((typeof value) == (typeof [])) { |
| cellElement = this.createVideoCaptureFormatTable(value); |
| } else { |
| cellElement = document.createTextNode( |
| ((typeof value) == 'undefined') ? 'n/a' : value); |
| } |
| tableCell.appendChild(cellElement) |
| tableRow.appendChild(tableCell); |
| } |
| videoTableBodyElement.appendChild(tableRow); |
| } |
| }, |
| |
| getAudioComponentName_: function(componentType, id) { |
| var baseName; |
| switch (componentType) { |
| case 0: |
| case 1: |
| baseName = 'Controller'; |
| break; |
| case 2: |
| baseName = 'Stream'; |
| break; |
| default: |
| baseName = 'UnknownType' |
| console.error('Unrecognized component type: ' + componentType); |
| break; |
| } |
| return baseName + ' ' + id; |
| }, |
| |
| getListElementForAudioComponent_: function(componentType) { |
| var listElement; |
| switch (componentType) { |
| case 0: |
| listElement = document.getElementById( |
| 'audio-input-controller-list'); |
| break; |
| case 1: |
| listElement = document.getElementById( |
| 'audio-output-controller-list'); |
| break; |
| case 2: |
| listElement = document.getElementById( |
| 'audio-output-stream-list'); |
| break; |
| default: |
| console.error('Unrecognized component type: ' + componentType); |
| listElement = null; |
| break; |
| } |
| return listElement; |
| }, |
| |
| redrawAudioComponentList_: function(componentType, components) { |
| // Group name imposes rule that only one component can be selected |
| // (and have its properties displayed) at a time. |
| var buttonGroupName = 'audio-components'; |
| |
| var listElement = this.getListElementForAudioComponent_(componentType); |
| if (!listElement) { |
| console.error('Failed to find list element for component type: ' + |
| componentType); |
| return; |
| } |
| |
| var fragment = document.createDocumentFragment(); |
| for (var id in components) { |
| var li = document.createElement('li'); |
| var button_cb = this.selectAudioComponent_.bind( |
| this, componentType, id, components[id]); |
| var friendlyName = this.getAudioComponentName_(componentType, id); |
| li.appendChild(createSelectableButton( |
| id, buttonGroupName, friendlyName, button_cb)); |
| fragment.appendChild(li); |
| } |
| removeChildren(listElement); |
| listElement.appendChild(fragment); |
| |
| if (this.selectedAudioComponentType && |
| this.selectedAudioComponentType == componentType && |
| this.selectedAudioComponentId in components) { |
| // Re-select the selected component since the button was just recreated. |
| selectSelectableButton(this.selectedAudioComponentId); |
| } |
| }, |
| |
| selectAudioComponent_: function(componentType, componentId, componentData) { |
| document.body.classList.remove( |
| ClientRenderer.Css_.NO_COMPONENTS_SELECTED); |
| |
| this.selectedAudioComponentType = componentType; |
| this.selectedAudioComponentId = componentId; |
| this.selectedAudioCompontentData = componentData; |
| this.drawProperties_(componentData, this.audioPropertiesTable); |
| |
| removeChildren(this.audioPropertyName); |
| this.audioPropertyName.appendChild(document.createTextNode( |
| this.getAudioComponentName_(componentType, componentId))); |
| }, |
| |
| redrawPlayerList_: function(players) { |
| this.players = players; |
| |
| // Group name imposes rule that only one component can be selected |
| // (and have its properties displayed) at a time. |
| var buttonGroupName = 'player-buttons'; |
| |
| var hasPlayers = false; |
| var fragment = document.createDocumentFragment(); |
| for (var id in players) { |
| hasPlayers = true; |
| var player = players[id]; |
| var usableName = player.properties.name || |
| player.properties.url || |
| 'Player ' + player.id; |
| |
| var li = document.createElement('li'); |
| var button_cb = this.selectPlayer_.bind(this, player); |
| li.appendChild(createSelectableButton( |
| id, buttonGroupName, usableName, button_cb)); |
| fragment.appendChild(li); |
| } |
| removeChildren(this.playerListElement); |
| this.playerListElement.appendChild(fragment); |
| |
| if (this.selectedPlayer && this.selectedPlayer.id in players) { |
| // Re-select the selected player since the button was just recreated. |
| selectSelectableButton(this.selectedPlayer.id); |
| } |
| |
| this.saveLogButton.style.display = hasPlayers ? "inline-block" : "none"; |
| }, |
| |
| selectPlayer_: function(player) { |
| document.body.classList.remove(ClientRenderer.Css_.NO_PLAYERS_SELECTED); |
| |
| this.selectedPlayer = player; |
| this.selectedPlayerLogIndex = 0; |
| this.selectedAudioComponentType = null; |
| this.selectedAudioComponentId = null; |
| this.selectedAudioCompontentData = null; |
| this.drawProperties_(player.properties, this.playerPropertiesTable); |
| |
| removeChildren(this.logTable); |
| removeChildren(this.graphElement); |
| this.drawLog_(); |
| this.drawGraphs_(); |
| }, |
| |
| drawProperties_: function(propertyMap, propertiesTable) { |
| removeChildren(propertiesTable); |
| var sortedKeys = Object.keys(propertyMap).sort(); |
| for (var i = 0; i < sortedKeys.length; ++i) { |
| var key = sortedKeys[i]; |
| if (this.hiddenKeys.indexOf(key) >= 0) |
| continue; |
| |
| var value = propertyMap[key]; |
| var row = propertiesTable.insertRow(-1); |
| var keyCell = row.insertCell(-1); |
| var valueCell = row.insertCell(-1); |
| |
| keyCell.appendChild(document.createTextNode(key)); |
| valueCell.appendChild(document.createTextNode(value)); |
| } |
| }, |
| |
| appendEventToLog_: function(event) { |
| if (this.filterFunction(event.key)) { |
| var row = this.logTable.insertRow(-1); |
| |
| var timestampCell = row.insertCell(-1); |
| timestampCell.classList.add('timestamp'); |
| timestampCell.appendChild(document.createTextNode( |
| util.millisecondsToString(event.time))); |
| row.insertCell(-1).appendChild(document.createTextNode(event.key)); |
| row.insertCell(-1).appendChild(document.createTextNode(event.value)); |
| } |
| }, |
| |
| drawLog_: function() { |
| var toDraw = this.selectedPlayer.allEvents.slice( |
| this.selectedPlayerLogIndex); |
| toDraw.forEach(this.appendEventToLog_.bind(this)); |
| this.selectedPlayerLogIndex = this.selectedPlayer.allEvents.length; |
| }, |
| |
| saveLog_: function() { |
| var strippedPlayers = [] |
| for (var id in this.players) { |
| var p = this.players[id]; |
| strippedPlayers.push({properties: p.properties, events: p.allEvents}); |
| } |
| downloadLog(JSON.stringify(strippedPlayers, null, 2)); |
| }, |
| |
| drawGraphs_: function() { |
| function addToGraphs(name, graph, graphElement) { |
| var li = document.createElement('li'); |
| li.appendChild(graph); |
| li.appendChild(document.createTextNode(name)); |
| graphElement.appendChild(li); |
| } |
| |
| var url = this.selectedPlayer.properties.url; |
| if (!url) { |
| return; |
| } |
| |
| var cache = media.cacheForUrl(url); |
| |
| var player = this.selectedPlayer; |
| var props = player.properties; |
| |
| var cacheExists = false; |
| var bufferExists = false; |
| |
| if (props['buffer_start'] !== undefined && |
| props['buffer_current'] !== undefined && |
| props['buffer_end'] !== undefined && |
| props['total_bytes'] !== undefined) { |
| this.drawBufferGraph_(props['buffer_start'], |
| props['buffer_current'], |
| props['buffer_end'], |
| props['total_bytes']); |
| bufferExists = true; |
| } |
| |
| if (cache) { |
| if (player.properties['total_bytes']) { |
| cache.size = Number(player.properties['total_bytes']); |
| } |
| cache.generateDetails(); |
| cacheExists = true; |
| |
| } |
| |
| if (!this.graphElement.hasChildNodes()) { |
| if (bufferExists) { |
| addToGraphs('buffer', this.bufferCanvas, this.graphElement); |
| } |
| if (cacheExists) { |
| addToGraphs('cache read', cache.readCanvas, this.graphElement); |
| addToGraphs('cache write', cache.writeCanvas, this.graphElement); |
| } |
| } |
| }, |
| |
| drawBufferGraph_: function(start, current, end, size) { |
| var ctx = this.bufferCanvas.getContext('2d'); |
| var width = this.bufferCanvas.width; |
| var height = this.bufferCanvas.height; |
| ctx.fillStyle = '#aaa'; |
| ctx.fillRect(0, 0, width, height); |
| |
| var scale_factor = width / size; |
| var left = start * scale_factor; |
| var middle = current * scale_factor; |
| var right = end * scale_factor; |
| |
| ctx.fillStyle = '#a0a'; |
| ctx.fillRect(left, 0, middle - left, height); |
| ctx.fillStyle = '#aa0'; |
| ctx.fillRect(middle, 0, right - middle, height); |
| }, |
| |
| showClipboard: function(string) { |
| this.clipboardTextarea.value = string; |
| this.clipboardDialog.showModal(); |
| this.clipboardTextarea.focus(); |
| this.clipboardTextarea.select(); |
| }, |
| |
| hideClipboard_: function() { |
| this.clipboardDialog.close(); |
| }, |
| |
| copyToClipboard_: function() { |
| if (!this.selectedPlayer && !this.selectedAudioCompontentData) { |
| return; |
| } |
| var properties = this.selectedAudioCompontentData || |
| this.selectedPlayer.properties; |
| var stringBuffer = []; |
| |
| for (var key in properties) { |
| var value = properties[key]; |
| stringBuffer.push(key.toString()); |
| stringBuffer.push(': '); |
| stringBuffer.push(value.toString()); |
| stringBuffer.push('\n'); |
| } |
| |
| this.showClipboard(stringBuffer.join('')); |
| }, |
| |
| onTextChange_: function(event) { |
| var text = this.filterText.value.toLowerCase(); |
| var parts = text.split(',').map(function(part) { |
| return part.trim(); |
| }).filter(function(part) { |
| return part.trim().length > 0; |
| }); |
| |
| this.filterFunction = function(text) { |
| text = text.toLowerCase(); |
| return parts.length === 0 || parts.some(function(part) { |
| return text.indexOf(part) != -1; |
| }); |
| }; |
| |
| if (this.selectedPlayer) { |
| removeChildren(this.logTable); |
| this.selectedPlayerLogIndex = 0; |
| this.drawLog_(); |
| } |
| }, |
| }; |
| |
| return ClientRenderer; |
| })(); |