• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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