text: /**
   * @file videojs.wavesurfer.js
   *
   * The main file for the videojs-wavesurfer project.
   * MIT license: https://github.com/collab-project/videojs-wavesurfer/blob/master/LICENSE
   */
  
  import Event from './event';
  import log from './utils/log';
  import formatTime from './utils/format-time';
  import pluginDefaultOptions from './defaults';
  import WavesurferMiddleware from './middleware';
  import window from 'global/window';
  
  import videojs from 'video.js';
  import WaveSurfer from 'wavesurfer.js';
  
  const Plugin = videojs.getPlugin('plugin');
  
  const wavesurferPluginName = 'wavesurfer';
  const wavesurferClassName = 'vjs-wavedisplay';
  const wavesurferStyleName = 'vjs-wavesurfer';
  
  // wavesurfer.js backends
  const WEBAUDIO = 'WebAudio';
  const MEDIAELEMENT = 'MediaElement';
  const MEDIAELEMENT_WEBAUDIO = 'MediaElementWebAudio';
  
  /**
   * Draw a waveform for audio and video files in a video.js player.
   *
   * @class
   * @augments videojs.Plugin
   */
  class Wavesurfer extends Plugin {
      /**
       * The constructor function for the class.
       *
       * @param {(videojs.Player|Object)} player - video.js Player object.
       * @param {Object} options - Player options.
       */
      constructor(player, options) {
          super(player, options);
  
          // add plugin style
          player.addClass(wavesurferStyleName);
  
          // parse options
          options = videojs.mergeOptions(pluginDefaultOptions, options);
          this.waveReady = false;
          this.waveFinished = false;
          this.liveMode = false;
          this.backend = null;
          this.debug = (options.debug.toString() === 'true');
          this.textTracksEnabled = (this.player.options_.tracks.length > 0);
          this.displayMilliseconds = options.displayMilliseconds;
  
          // use custom time format for video.js player
          if (options.formatTime && typeof options.formatTime === 'function') {
              // user-supplied formatTime
              this.setFormatTime(options.formatTime);
          } else {
              // plugin's default formatTime
              this.setFormatTime((seconds, guide) => {
                  return formatTime(seconds, guide, this.displayMilliseconds);
              });
          }
  
          // wait until player ui is ready
          this.player.one(Event.READY, this.initialize.bind(this));
      }
  
      /**
       * Player UI is ready: customize controls.
       *
       * @private
       */
      initialize() {
          // hide big play button
          if (this.player.bigPlayButton !== undefined) {
              this.player.bigPlayButton.hide();
          }
  
          // parse options
          let mergedOptions = this.parseOptions(this.player.options_.plugins.wavesurfer);
  
          // controls
          if (this.player.options_.controls === true) {
              // make sure controlBar is showing.
              // video.js hides the controlbar by default because it expects
              // the user to click on the 'big play button' first.
              this.player.controlBar.show();
              this.player.controlBar.el_.style.display = 'flex';
  
              // progress control is only supported with the MediaElement backend
              if (this.backend === WEBAUDIO &&
                  this.player.controlBar.progressControl !== undefined) {
                  this.player.controlBar.progressControl.hide();
              }
  
              // disable Picture-In-Picture toggle introduced in video.js 7.6.0
              // until there is support for canvas in the Picture-In-Picture
              // browser API (see https://www.chromestatus.com/features/4844605453369344)
              if (this.player.controlBar.pictureInPictureToggle !== undefined) {
                  this.player.controlBar.pictureInPictureToggle.hide();
              }
  
              // make sure time displays are visible
              let uiElements = ['currentTimeDisplay', 'timeDivider', 'durationDisplay'];
              uiElements.forEach((element) => {
                  // ignore and show when essential elements have been disabled
                  // by user
                  element = this.player.controlBar[element];
                  if (element !== undefined) {
                      element.el_.style.display = 'block';
                      element.show();
                  }
              });
              if (this.player.controlBar.remainingTimeDisplay !== undefined) {
                  this.player.controlBar.remainingTimeDisplay.hide();
              }
  
              if (this.backend === WEBAUDIO &&
                  this.player.controlBar.playToggle !== undefined) {
                  // handle play toggle interaction
                  this.player.controlBar.playToggle.on(['tap', 'click'],
                      this.onPlayToggle.bind(this));
  
                  // disable play button until waveform is ready
                  this.player.controlBar.playToggle.hide();
              }
          }
  
          // wavesurfer.js setup
          this.surfer = WaveSurfer.create(mergedOptions);
          this.surfer.on(Event.ERROR, this.onWaveError.bind(this));
          this.surfer.on(Event.FINISH, this.onWaveFinish.bind(this));
          this.backend = this.surfer.params.backend;
          this.log('Using wavesurfer.js ' + this.backend + ' backend.');
  
          // check if the wavesurfer.js microphone plugin is enabled
          if ('microphone' in this.player.wavesurfer().surfer.getActivePlugins()) {
              // enable audio input from a microphone
              this.liveMode = true;
              this.waveReady = true;
              this.log('wavesurfer.js microphone plugin enabled.');
  
              // in live mode, show play button at startup
              this.player.controlBar.playToggle.show();
  
              // listen for wavesurfer.js microphone plugin events
              this.surfer.microphone.on(Event.DEVICE_ERROR,
                  this.onWaveError.bind(this));
          }
  
          // listen for wavesurfer.js events
          this.surferReady = this.onWaveReady.bind(this);
          if (this.backend === WEBAUDIO) {
              this.surferProgress = this.onWaveProgress.bind(this);
              this.surferSeek = this.onWaveSeek.bind(this);
  
              // make sure volume is muted when requested
              if (this.player.muted()) {
                  this.setVolume(0);
              }
          }
  
          // only listen to the wavesurfer.js playback events when not
          // in live mode
          if (!this.liveMode) {
              this.setupPlaybackEvents(true);
          }
  
          // video.js player events
          this.player.on(Event.VOLUMECHANGE, this.onVolumeChange.bind(this));
          this.player.on(Event.FULLSCREENCHANGE, this.onScreenChange.bind(this));
  
          // video.js fluid option
          if (this.player.options_.fluid === true) {
              // give wave element a classname so it can be styled
              this.surfer.drawer.wrapper.className = wavesurferClassName;
          }
      }
  
      /**
       * Initializes the waveform options.
       *
       * @private
       * @param {Object} surferOpts - Plugin options.
       * @returns {Object} - Updated `surferOpts` object.
       */
      parseOptions(surferOpts = {}) {
          let rect = this.player.el_.getBoundingClientRect();
          this.originalWidth = this.player.options_.width || rect.width;
          this.originalHeight = this.player.options_.height || rect.height;
  
          // controlbar
          let controlBarHeight = this.player.controlBar.height();
          if (this.player.options_.controls === true && controlBarHeight === 0) {
              // the dimensions of the controlbar are not known yet, but we
              // need it now, so we can calculate the height of the waveform.
              // The default height is 30px, so use that instead.
              controlBarHeight = 30;
          }
  
          // set waveform element and dimensions
          // Set the container to player's container if "container" option is
          // not provided. If a waveform needs to be appended to your custom
          // element, then use below option. For example:
          // container: document.querySelector("#vjs-waveform")
          if (surferOpts.container === undefined) {
              surferOpts.container = this.player.el_;
          }
  
          // set the height of generated waveform if user has provided height
          // from options. If height of waveform need to be customized then use
          // option below. For example: waveformHeight: 30
          if (surferOpts.waveformHeight === undefined) {
              let playerHeight = rect.height;
              surferOpts.height = playerHeight - controlBarHeight;
          } else {
              surferOpts.height = surferOpts.waveformHeight;
          }
  
          // split channels
          if (surferOpts.splitChannels && surferOpts.splitChannels === true) {
              surferOpts.height /= 2;
          }
  
          // use MediaElement as default wavesurfer.js backend if one is not
          // specified
          if ('backend' in surferOpts) {
              this.backend = surferOpts.backend;
          } else {
              surferOpts.backend = this.backend = MEDIAELEMENT;
          }
  
          return surferOpts;
      }
  
      /**
       * Starts or stops listening to events related to audio-playback.
       *
       * @param {boolean} enable - Start or stop listening to playback
       *     related events.
       * @private
       */
      setupPlaybackEvents(enable) {
          if (enable === false) {
              this.surfer.un(Event.READY, this.surferReady);
              if (this.backend === WEBAUDIO) {
                  this.surfer.un(Event.AUDIOPROCESS, this.surferProgress);
                  this.surfer.un(Event.SEEK, this.surferSeek);
              }
          } else if (enable === true) {
              this.surfer.on(Event.READY, this.surferReady);
              if (this.backend === WEBAUDIO) {
                  this.surfer.on(Event.AUDIOPROCESS, this.surferProgress);
                  this.surfer.on(Event.SEEK, this.surferSeek);
              }
          }
      }
  
      /**
       * Start loading waveform data.
       *
       * @param {string|blob|file} url - Either the URL of the audio file,
       *     a Blob or a File object.
       * @param {string|number[]} peaks - Either the URL of peaks
       *     data for the audio file, or an array with peaks data.
       */
      load(url, peaks) {
          if (url instanceof Blob || url instanceof File) {
              this.log('Loading object: ' + JSON.stringify(url));
              this.surfer.loadBlob(url);
          } else {
              // load peak data from array or file
              if (peaks !== undefined) {
                  this.loadPeaks(url, peaks);
              } else {
                  // no peaks
                  if (typeof url === 'string') {
                      this.log('Loading URL: ' + url);
                  } else {
                      this.log('Loading element: ' + url);
                  }
                  this.surfer.load(url);
              }
          }
      }
  
      /**
       * Start loading waveform data.
       *
       * @param {string|blob|file} url - Either the URL of the audio file,
       *     a Blob or a File object.
       * @param {string|number[]} peaks - Either the URL of peaks
       *     data for the audio file, or an array with peaks data.
       */
      loadPeaks(url, peaks) {
          if (Array.isArray(peaks)) {
              // use supplied peaks data
              this.log('Loading URL with array of peaks: ' + url);
              this.surfer.load(url, peaks);
          } else {
              // load peak data from file
              let requestOptions = {
                  url: peaks,
                  responseType: 'json'
              };
  
              // supply xhr options, if any
              if (this.player.options_.plugins.wavesurfer.xhr !== undefined) {
                  requestOptions.xhr = this.player.options_.plugins.wavesurfer.xhr;
              }
              let request = WaveSurfer.util.fetchFile(requestOptions);
  
              request.once('success', data => {
                  this.log('Loaded Peak Data URL: ' + peaks);
                  // check for data property containing peaks
                  if (data && data.data) {
                      this.surfer.load(url, data.data);
                  } else {
                      this.player.trigger(Event.ERROR,
                          'Could not load peaks data from ' + peaks);
                      this.log(err, 'error');
                  }
              });
              request.once('error', e => {
                  this.player.trigger(Event.ERROR,
                      'Unable to retrieve peak data from ' + peaks +
                      '. Status code: ' + request.response.status);
              });
          }
      }
  
      /**
       * Start/resume playback or microphone.
       */
      play() {
          // show pause button
          if (this.player.controlBar.playToggle !== undefined &&
              this.player.controlBar.playToggle.contentEl()) {
              this.player.controlBar.playToggle.handlePlay();
          }
  
          if (this.liveMode) {
              // start/resume microphone visualization
              if (!this.surfer.microphone.active)
              {
                  this.log('Start microphone');
                  this.surfer.microphone.start();
              } else {
                  // toggle paused
                  let paused = !this.surfer.microphone.paused;
  
                  if (paused) {
                      this.pause();
                  } else {
                      this.log('Resume microphone');
                      this.surfer.microphone.play();
                  }
              }
          } else {
              this.log('Start playback');
  
              // put video.js player UI in playback mode
              this.player.play();
  
              // start surfer playback
              this.surfer.play();
          }
      }
  
      /**
       * Pauses playback or microphone visualization.
       */
      pause() {
          // show play button
          if (this.player.controlBar.playToggle !== undefined &&
              this.player.controlBar.playToggle.contentEl()) {
              this.player.controlBar.playToggle.handlePause();
          }
  
          if (this.liveMode) {
              // pause microphone visualization
              this.log('Pause microphone');
              this.surfer.microphone.pause();
          } else {
              // pause playback
              this.log('Pause playback');
  
              if (!this.waveFinished) {
                  // pause wavesurfer playback
                  this.surfer.pause();
              } else {
                  this.waveFinished = false;
              }
  
              this.setCurrentTime();
          }
      }
  
      /**
       * @private
       */
      dispose() {
          if (this.surfer) {
              if (this.liveMode && this.surfer.microphone) {
                  // destroy microphone plugin
                  this.surfer.microphone.destroy();
                  this.log('Destroyed microphone plugin');
              }
              // destroy wavesurfer instance
              this.surfer.destroy();
          }
          this.log('Destroyed plugin');
      }
  
      /**
       * Indicates whether the plugin is destroyed or not.
       *
       * @return {boolean} Plugin destroyed or not.
       */
      isDestroyed() {
          return this.player && (this.player.children() === null);
      }
  
      /**
       * Remove the player and waveform.
       */
      destroy() {
          this.player.dispose();
      }
  
      /**
       * Set the volume level.
       *
       * @param {number} volume - The new volume level.
       */
      setVolume(volume) {
          if (volume !== undefined) {
              this.log('Changing volume to: ' + volume);
  
              // update player volume
              this.player.volume(volume);
          }
      }
  
      /**
       * Save waveform image as data URI.
       *
       * The default format is `'image/png'`. Other supported types are
       * `'image/jpeg'` and `'image/webp'`.
       *
       * @param {string} format='image/png' A string indicating the image format.
       * The default format type is `'image/png'`.
       * @param {number} quality=1 A number between 0 and 1 indicating the image
       * quality to use for image formats that use lossy compression such as
       * `'image/jpeg'`` and `'image/webp'`.
       * @param {string} type Image data type to return. Either 'blob' (default)
       * or 'dataURL'.
       * @return {string|string[]|Promise} When using `'dataURL'` `type` this returns
       * a single data URL or an array of data URLs, one for each canvas. The `'blob'`
       * `type` returns a `Promise` resolving with an array of `Blob` instances, one
       * for each canvas.
       */
      exportImage(format, quality, type = 'blob') {
          return this.surfer.exportImage(format, quality, type);
      }
  
      /**
       * Change the audio output device.
       *
       * @param {string} deviceId - Id of audio output device.
       */
      setAudioOutput(deviceId) {
          if (deviceId) {
              this.surfer.setSinkId(deviceId).then((result) => {
                  // notify listeners
                  this.player.trigger(Event.AUDIO_OUTPUT_READY);
              }).catch((err) => {
                  // notify listeners
                  this.player.trigger(Event.ERROR, err);
  
                  this.log(err, 'error');
              });
          }
      }
  
      /**
       * Get the current time (in seconds) of the stream during playback.
       *
       * Returns 0 if no stream is available (yet).
       *
       * @returns {float} Current time of the stream.
       */
      getCurrentTime() {
          let currentTime = this.surfer.getCurrentTime();
          currentTime = isNaN(currentTime) ? 0 : currentTime;
  
          return currentTime;
      }
  
      /**
       * Updates the player's element displaying the current time.
       *
       * @param {number} [currentTime] - Current position of the playhead
       *     (in seconds).
       * @param {number} [duration] - Duration of the waveform (in seconds).
       * @private
       */
      setCurrentTime(currentTime, duration) {
          if (currentTime === undefined) {
              currentTime = this.surfer.getCurrentTime();
          }
  
          if (duration === undefined) {
              duration = this.surfer.getDuration();
          }
  
          currentTime = isNaN(currentTime) ? 0 : currentTime;
          duration = isNaN(duration) ? 0 : duration;
  
          // update current time display component
          if (this.player.controlBar.currentTimeDisplay &&
              this.player.controlBar.currentTimeDisplay.contentEl() &&
              this.player.controlBar.currentTimeDisplay.contentEl().lastChild) {
              let time = Math.min(currentTime, duration);
  
              this.player.controlBar.currentTimeDisplay.formattedTime_ =
                  this.player.controlBar.currentTimeDisplay.contentEl().lastChild.textContent =
                      this._formatTime(time, duration, this.displayMilliseconds);
          }
  
          if (this.textTracksEnabled && this.player.tech_ && this.player.tech_.el_) {
              // only needed for text tracks
              this.player.tech_.setCurrentTime(currentTime);
          }
      }
  
      /**
       * Get the duration of the stream in seconds.
       *
       * Returns 0 if no stream is available (yet).
       *
       * @returns {float} Duration of the stream.
       */
      getDuration() {
          let duration = this.surfer.getDuration();
          duration = isNaN(duration) ? 0 : duration;
  
          return duration;
      }
  
      /**
       * Updates the player's element displaying the duration time.
       *
       * @param {number} [duration] - Duration of the waveform (in seconds).
       * @private
       */
      setDuration(duration) {
          if (duration === undefined) {
              duration = this.surfer.getDuration();
          }
          duration = isNaN(duration) ? 0 : duration;
  
          // update duration display component
          if (this.player.controlBar.durationDisplay &&
              this.player.controlBar.durationDisplay.contentEl() &&
              this.player.controlBar.durationDisplay.contentEl().lastChild) {
              this.player.controlBar.durationDisplay.formattedTime_ =
                  this.player.controlBar.durationDisplay.contentEl().lastChild.textContent =
                      this._formatTime(duration, duration, this.displayMilliseconds);
          }
      }
  
      /**
       * Audio is loaded, decoded and the waveform is drawn.
       *
       * @fires waveReady
       * @private
       */
      onWaveReady() {
          this.waveReady = true;
          this.waveFinished = false;
          this.liveMode = false;
  
          this.log('Waveform is ready');
          this.player.trigger(Event.WAVE_READY);
  
          if (this.backend === WEBAUDIO) {
              // update time display
              this.setCurrentTime();
              this.setDuration();
  
              // enable and show play button
              if (this.player.controlBar.playToggle !== undefined &&
                  this.player.controlBar.playToggle.contentEl()) {
                  this.player.controlBar.playToggle.show();
              }
          }
  
          // hide loading spinner
          if (this.player.loadingSpinner.contentEl()) {
              this.player.loadingSpinner.hide();
          }
  
          // auto-play when ready (if enabled)
          if (this.player.options_.autoplay === true) {
              // autoplay is only allowed when audio is muted
              this.setVolume(0);
  
              // try auto-play
              if (this.backend === WEBAUDIO) {
                  this.play();
              } else {
                  this.player.play().catch(e => {
                      this.onWaveError(e);
                  });
              }
          }
      }
  
      /**
       * Fires when audio playback completed.
       *
       * @fires playbackFinish
       * @private
       */
      onWaveFinish() {
          this.log('Finished playback');
  
          // notify listeners
          this.player.trigger(Event.PLAYBACK_FINISH);
  
          // check if loop is enabled
          if (this.player.options_.loop === true) {
              if (this.backend === WEBAUDIO) {
                  // reset waveform
                  this.surfer.stop();
                  this.play();
              }
          } else {
              // finished
              this.waveFinished = true;
  
              if (this.backend === WEBAUDIO) {
                  // pause player
                  this.pause();
  
                  // show the replay state of play toggle
                  this.player.trigger(Event.ENDED);
  
                  // this gets called once after the clip has ended and the user
                  // seeks so that we can change the replay button back to a play
                  // button
                  this.surfer.once(Event.SEEK, () => {
                      if (this.player.controlBar.playToggle !== undefined) {
                          this.player.controlBar.playToggle.removeClass('vjs-ended');
                      }
                      this.player.trigger(Event.PAUSE);
                  });
              }
          }
      }
  
      /**
       * Fires continuously during audio playback.
       *
       * @param {number} time - Current time/location of the playhead.
       * @private
       */
      onWaveProgress(time) {
          this.setCurrentTime();
      }
  
      /**
       * Fires during seeking of the waveform.
       *
       * @private
       */
      onWaveSeek() {
          this.setCurrentTime();
      }
  
      /**
       * Waveform error.
       *
       * @param {string} error - The wavesurfer error.
       * @private
       */
      onWaveError(error) {
          // notify listeners
          if (error.name && error.name === 'AbortError' ||
              error.name === 'DOMException' && error.message.startsWith('The operation was aborted'))
          {
              this.player.trigger(Event.ABORT, error);
          } else {
              this.player.trigger(Event.ERROR, error);
  
              this.log(error, 'error');
          }
      }
  
      /**
       * Fired when the play toggle is clicked.
       * @private
       */
      onPlayToggle() {
          if (this.player.controlBar.playToggle !== undefined &&
              this.player.controlBar.playToggle.hasClass('vjs-ended')) {
              this.player.controlBar.playToggle.removeClass('vjs-ended');
          }
          if (this.surfer.isPlaying()) {
              this.pause();
          } else {
              this.play();
          }
      }
  
      /**
       * Fired when the volume in the video.js player changes.
       * @private
       */
      onVolumeChange() {
          let volume = this.player.volume();
          if (this.player.muted()) {
              // muted volume
              volume = 0;
          }
  
          // update wavesurfer.js volume
          this.surfer.setVolume(volume);
      }
  
      /**
       * Fired when the video.js player switches in or out of fullscreen mode.
       * @private
       */
      onScreenChange() {
          // execute with tiny delay so the player element completes
          // rendering and correct dimensions are reported
          let fullscreenDelay = this.player.setInterval(() => {
              let isFullscreen = this.player.isFullscreen();
              let newWidth, newHeight;
              if (!isFullscreen) {
                  // restore original dimensions
                  newWidth = this.originalWidth;
                  newHeight = this.originalHeight;
              }
  
              if (this.waveReady) {
                  if (this.liveMode && !this.surfer.microphone.active) {
                      // we're in live mode but the microphone hasn't been
                      // started yet
                      return;
                  }
                  // redraw
                  this.redrawWaveform(newWidth, newHeight);
              }
  
              // stop fullscreenDelay interval
              this.player.clearInterval(fullscreenDelay);
  
          }, 100);
      }
  
      /**
       * Redraw waveform.
       *
       * @param {number} [newWidth] - New width for the waveform.
       * @param {number} [newHeight] - New height for the waveform.
       * @private
       */
      redrawWaveform(newWidth, newHeight) {
          if (!this.isDestroyed()) {
              if (this.player.el_) {
                  let rect = this.player.el_.getBoundingClientRect();
                  if (newWidth === undefined) {
                      // get player width
                      newWidth = rect.width;
                  }
                  if (newHeight === undefined) {
                      // get player height
                      newHeight = rect.height;
                  }
              }
  
              // destroy old drawing
              this.surfer.drawer.destroy();
  
              // set new dimensions
              this.surfer.params.width = newWidth;
              this.surfer.params.height = newHeight - this.player.controlBar.height();
  
              // redraw waveform
              this.surfer.createDrawer();
              this.surfer.drawer.wrapper.className = wavesurferClassName;
              this.surfer.drawBuffer();
  
              // make sure playhead is restored at right position
              this.surfer.drawer.progress(this.surfer.backend.getPlayedPercents());
          }
      }
  
      /**
       * Log message to console (if the debug option is enabled).
       *
       * @private
       * @param {Array} args - The arguments to be passed to the matching console
       *     method.
       * @param {string} logType - The name of the console method to use.
       */
      log(args, logType) {
          log(args, logType, this.debug);
      }
  
      /**
       * Replaces the default `formatTime` implementation with a custom implementation.
       *
       * @param {function} customImplementation - A function which will be used in place
       *     of the default `formatTime` implementation. Will receive the current time
       *     in seconds and the guide (in seconds) as arguments.
       */
      setFormatTime(customImplementation) {
          this._formatTime = customImplementation;
  
          videojs.setFormatTime(this._formatTime);
      }
  }
  
  // version nr is injected during build
  Wavesurfer.VERSION = __VERSION__;
  
  // register plugin once
  videojs.Wavesurfer = Wavesurfer;
  if (videojs.getPlugin(wavesurferPluginName) === undefined) {
      videojs.registerPlugin(wavesurferPluginName, Wavesurfer);
  }
  
  // register a star-middleware
  videojs.use('*', player => {
      // make player available on middleware
      WavesurferMiddleware.player = player;
  
      return WavesurferMiddleware;
  });
  
  export {Wavesurfer};