1// Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 2// 3// Use of this source code is governed by a BSD-style license 4// that can be found in the LICENSE file in the root of the source 5// tree. An additional intellectual property rights grant can be found 6// in the file PATENTS. All contributing project authors may 7// be found in the AUTHORS file in the root of the source tree. 8 9/** 10 * Opens the score stats inspector dialog. 11 * @param {String} dialogId: identifier of the dialog to show. 12 * @return {DOMElement} The dialog element that has been opened. 13 */ 14function openScoreStatsInspector(dialogId) { 15 var dialog = document.getElementById(dialogId); 16 dialog.showModal(); 17 return dialog; 18} 19 20/** 21 * Closes the score stats inspector dialog. 22 */ 23function closeScoreStatsInspector() { 24 var dialog = document.querySelector('dialog[open]'); 25 if (dialog == null) 26 return; 27 dialog.close(); 28} 29 30/** 31 * Audio inspector class. 32 * @constructor 33 */ 34function AudioInspector() { 35 console.debug('Creating an AudioInspector instance.'); 36 this.audioPlayer_ = new Audio(); 37 this.metadata_ = {}; 38 this.currentScore_ = null; 39 this.audioInspector_ = null; 40 this.snackbarContainer_ = document.querySelector('#snackbar'); 41 42 // Get base URL without anchors. 43 this.baseUrl_ = window.location.href; 44 var index = this.baseUrl_.indexOf('#'); 45 if (index > 0) 46 this.baseUrl_ = this.baseUrl_.substr(0, index) 47 console.info('Base URL set to "' + window.location.href + '".'); 48 49 window.event.stopPropagation(); 50 this.createTextAreasForCopy_(); 51 this.createAudioInspector_(); 52 this.initializeEventHandlers_(); 53 54 // When MDL is ready, parse the anchor (if any) to show the requested 55 // experiment. 56 var self = this; 57 document.querySelectorAll('header a')[0].addEventListener( 58 'mdl-componentupgraded', function() { 59 if (!self.parseWindowAnchor()) { 60 // If not experiment is requested, open the first section. 61 console.info('No anchor parsing, opening the first section.'); 62 document.querySelectorAll('header a > span')[0].click(); 63 } 64 }); 65} 66 67/** 68 * Parse the anchor in the window URL. 69 * @return {bool} True if the parsing succeeded. 70 */ 71AudioInspector.prototype.parseWindowAnchor = function() { 72 var index = location.href.indexOf('#'); 73 if (index == -1) { 74 console.debug('No # found in the URL.'); 75 return false; 76 } 77 78 var anchor = location.href.substr(index - location.href.length + 1); 79 console.info('Anchor changed: "' + anchor + '".'); 80 81 var parts = anchor.split('&'); 82 if (parts.length != 3) { 83 console.info('Ignoring anchor with invalid number of fields.'); 84 return false; 85 } 86 87 var openDialog = document.querySelector('dialog[open]'); 88 try { 89 // Open the requested dialog if not already open. 90 if (!openDialog || openDialog.id != parts[1]) { 91 !openDialog || openDialog.close(); 92 document.querySelectorAll('header a > span')[ 93 parseInt(parts[0].substr(1))].click(); 94 openDialog = openScoreStatsInspector(parts[1]); 95 } 96 97 // Trigger click on cell. 98 var cell = openDialog.querySelector('td.' + parts[2]); 99 cell.focus(); 100 cell.click(); 101 102 this.showNotification_('Experiment selected.'); 103 return true; 104 } catch (e) { 105 this.showNotification_('Cannot select experiment :('); 106 console.error('Exception caught while selecting experiment: "' + e + '".'); 107 } 108 109 return false; 110} 111 112/** 113 * Set up the inspector for a new score. 114 * @param {DOMElement} element: Element linked to the selected score. 115 */ 116AudioInspector.prototype.selectedScoreChange = function(element) { 117 if (this.currentScore_ == element) { return; } 118 if (this.currentScore_ != null) { 119 this.currentScore_.classList.remove('selected-score'); 120 } 121 this.currentScore_ = element; 122 this.currentScore_.classList.add('selected-score'); 123 this.stopAudio(); 124 125 // Read metadata. 126 var matches = element.querySelectorAll('input[type=hidden]'); 127 this.metadata_ = {}; 128 for (var index = 0; index < matches.length; ++index) { 129 this.metadata_[matches[index].name] = matches[index].value; 130 } 131 132 // Show the audio inspector interface. 133 var container = element.parentNode.parentNode.parentNode.parentNode; 134 var audioInspectorPlaceholder = container.querySelector( 135 '.audio-inspector-placeholder'); 136 this.moveInspector_(audioInspectorPlaceholder); 137}; 138 139/** 140 * Stop playing audio. 141 */ 142AudioInspector.prototype.stopAudio = function() { 143 console.info('Pausing audio play out.'); 144 this.audioPlayer_.pause(); 145}; 146 147/** 148 * Show a text message using the snackbar. 149 */ 150AudioInspector.prototype.showNotification_ = function(text) { 151 try { 152 this.snackbarContainer_.MaterialSnackbar.showSnackbar({ 153 message: text, timeout: 2000}); 154 } catch (e) { 155 // Fallback to an alert. 156 alert(text); 157 console.warn('Cannot use snackbar: "' + e + '"'); 158 } 159} 160 161/** 162 * Move the audio inspector DOM node into the given parent. 163 * @param {DOMElement} newParentNode: New parent for the inspector. 164 */ 165AudioInspector.prototype.moveInspector_ = function(newParentNode) { 166 newParentNode.appendChild(this.audioInspector_); 167}; 168 169/** 170 * Play audio file from url. 171 * @param {string} metadataFieldName: Metadata field name. 172 */ 173AudioInspector.prototype.playAudio = function(metadataFieldName) { 174 if (this.metadata_[metadataFieldName] == undefined) { return; } 175 if (this.metadata_[metadataFieldName] == 'None') { 176 alert('The selected stream was not used during the experiment.'); 177 return; 178 } 179 this.stopAudio(); 180 this.audioPlayer_.src = this.metadata_[metadataFieldName]; 181 console.debug('Audio source URL: "' + this.audioPlayer_.src + '"'); 182 this.audioPlayer_.play(); 183 console.info('Playing out audio.'); 184}; 185 186/** 187 * Create hidden text areas to copy URLs. 188 * 189 * For each dialog, one text area is created since it is not possible to select 190 * text on a text area outside of the active dialog. 191 */ 192AudioInspector.prototype.createTextAreasForCopy_ = function() { 193 var self = this; 194 document.querySelectorAll('dialog.mdl-dialog').forEach(function(element) { 195 var textArea = document.createElement("textarea"); 196 textArea.classList.add('url-copy'); 197 textArea.style.position = 'fixed'; 198 textArea.style.bottom = 0; 199 textArea.style.left = 0; 200 textArea.style.width = '2em'; 201 textArea.style.height = '2em'; 202 textArea.style.border = 'none'; 203 textArea.style.outline = 'none'; 204 textArea.style.boxShadow = 'none'; 205 textArea.style.background = 'transparent'; 206 textArea.style.fontSize = '6px'; 207 element.appendChild(textArea); 208 }); 209} 210 211/** 212 * Create audio inspector. 213 */ 214AudioInspector.prototype.createAudioInspector_ = function() { 215 var buttonIndex = 0; 216 function getButtonHtml(icon, toolTipText, caption, metadataFieldName) { 217 var buttonId = 'audioInspectorButton' + buttonIndex++; 218 html = caption == null ? '' : caption; 219 html += '<button class="mdl-button mdl-js-button mdl-button--icon ' + 220 'mdl-js-ripple-effect" id="' + buttonId + '">' + 221 '<i class="material-icons">' + icon + '</i>' + 222 '<div class="mdl-tooltip" data-mdl-for="' + buttonId + '">' + 223 toolTipText + 224 '</div>'; 225 if (metadataFieldName != null) { 226 html += '<input type="hidden" value="' + metadataFieldName + '">' 227 } 228 html += '</button>' 229 230 return html; 231 } 232 233 // TODO(alessiob): Add timeline and highlight current track by changing icon 234 // color. 235 236 this.audioInspector_ = document.createElement('div'); 237 this.audioInspector_.classList.add('audio-inspector'); 238 this.audioInspector_.innerHTML = 239 '<div class="mdl-grid">' + 240 '<div class="mdl-layout-spacer"></div>' + 241 '<div class="mdl-cell mdl-cell--2-col">' + 242 getButtonHtml('play_arrow', 'Simulated echo', 'E<sub>in</sub>', 243 'echo_filepath') + 244 '</div>' + 245 '<div class="mdl-cell mdl-cell--2-col">' + 246 getButtonHtml('stop', 'Stop playing [S]', null, '__stop__') + 247 '</div>' + 248 '<div class="mdl-cell mdl-cell--2-col">' + 249 getButtonHtml('play_arrow', 'Render stream', 'R<sub>in</sub>', 250 'render_filepath') + 251 '</div>' + 252 '<div class="mdl-layout-spacer"></div>' + 253 '</div>' + 254 '<div class="mdl-grid">' + 255 '<div class="mdl-layout-spacer"></div>' + 256 '<div class="mdl-cell mdl-cell--2-col">' + 257 getButtonHtml('play_arrow', 'Capture stream (APM input) [1]', 258 'Y\'<sub>in</sub>', 'capture_filepath') + 259 '</div>' + 260 '<div class="mdl-cell mdl-cell--2-col"><strong>APM</strong></div>' + 261 '<div class="mdl-cell mdl-cell--2-col">' + 262 getButtonHtml('play_arrow', 'APM output [2]', 'Y<sub>out</sub>', 263 'apm_output_filepath') + 264 '</div>' + 265 '<div class="mdl-layout-spacer"></div>' + 266 '</div>' + 267 '<div class="mdl-grid">' + 268 '<div class="mdl-layout-spacer"></div>' + 269 '<div class="mdl-cell mdl-cell--2-col">' + 270 getButtonHtml('play_arrow', 'Echo-free capture stream', 271 'Y<sub>in</sub>', 'echo_free_capture_filepath') + 272 '</div>' + 273 '<div class="mdl-cell mdl-cell--2-col">' + 274 getButtonHtml('play_arrow', 'Clean capture stream', 275 'Y<sub>clean</sub>', 'clean_capture_input_filepath') + 276 '</div>' + 277 '<div class="mdl-cell mdl-cell--2-col">' + 278 getButtonHtml('play_arrow', 'APM reference [3]', 'Y<sub>ref</sub>', 279 'apm_reference_filepath') + 280 '</div>' + 281 '<div class="mdl-layout-spacer"></div>' + 282 '</div>'; 283 284 // Add an invisible node as initial container for the audio inspector. 285 var parent = document.createElement('div'); 286 parent.style.display = 'none'; 287 this.moveInspector_(parent); 288 document.body.appendChild(parent); 289}; 290 291/** 292 * Initialize event handlers. 293 */ 294AudioInspector.prototype.initializeEventHandlers_ = function() { 295 var self = this; 296 297 // Score cells. 298 document.querySelectorAll('td.single-score-cell').forEach(function(element) { 299 element.onclick = function() { 300 self.selectedScoreChange(this); 301 } 302 }); 303 304 // Copy anchor URLs icons. 305 if (document.queryCommandSupported('copy')) { 306 document.querySelectorAll('td.single-score-cell button').forEach( 307 function(element) { 308 element.onclick = function() { 309 // Find the text area in the dialog. 310 var textArea = element.closest('dialog').querySelector( 311 'textarea.url-copy'); 312 313 // Copy. 314 textArea.value = self.baseUrl_ + '#' + element.getAttribute( 315 'data-anchor'); 316 textArea.select(); 317 try { 318 if (!document.execCommand('copy')) 319 throw 'Copy returned false'; 320 self.showNotification_('Experiment URL copied.'); 321 } catch (e) { 322 self.showNotification_('Cannot copy experiment URL :('); 323 console.error(e); 324 } 325 } 326 }); 327 } else { 328 self.showNotification_( 329 'The copy command is disabled. URL copy is not enabled.'); 330 } 331 332 // Audio inspector buttons. 333 this.audioInspector_.querySelectorAll('button').forEach(function(element) { 334 var target = element.querySelector('input[type=hidden]'); 335 if (target == null) { return; } 336 element.onclick = function() { 337 if (target.value == '__stop__') { 338 self.stopAudio(); 339 } else { 340 self.playAudio(target.value); 341 } 342 }; 343 }); 344 345 // Dialog close handlers. 346 var dialogs = document.querySelectorAll('dialog').forEach(function(element) { 347 element.onclose = function() { 348 self.stopAudio(); 349 } 350 }); 351 352 // Keyboard shortcuts. 353 window.onkeyup = function(e) { 354 var key = e.keyCode ? e.keyCode : e.which; 355 switch (key) { 356 case 49: // 1. 357 self.playAudio('capture_filepath'); 358 break; 359 case 50: // 2. 360 self.playAudio('apm_output_filepath'); 361 break; 362 case 51: // 3. 363 self.playAudio('apm_reference_filepath'); 364 break; 365 case 83: // S. 366 case 115: // s. 367 self.stopAudio(); 368 break; 369 } 370 }; 371 372 // Hash change. 373 window.onhashchange = function(e) { 374 self.parseWindowAnchor(); 375 } 376}; 377