Source: youtubePlayer.js

const EventEmitter2 = require('eventemitter2').EventEmitter2
const qs = require('qs')
const {scaleLinear} = require('d3-scale')

const PlayerEventTypes = require('./constants/PlayerEventTypes')

const ee = new EventEmitter2({
  wildcard: true
})

/**
 * Class reprensenting a YoutubePlayer
 * @namespace YoutubePlayer
 * @example
 * const player = new YoutubePlayer()
 */
class YoutubePlayer {
  /**
   * @desc Create a YoutubePlayer
   */
  constructor() {
    this.emit = ee.emit.bind(ee)

    /**
     * @method on
     * @memberof YoutubePlayer
     * @instance
     * @desc Listen for player events
     * @param {String} eventName - name of even
     * @param {Function} callback - called when the event occurs
     * @example
     * player.on(Player.EventTypes.PLAY, () => {
     *
     * })
     * @example
     * player.on(Player.EventTypes.PLAYBACK_RATE, () => {
     *
     * })
     */
    this.on = ee.on.bind(ee)

    this._player = null

    this._playbackRate = 1
    this._playbackRateScale = scaleLinear()
    .domain([0.75, 2])
    .range([0.75, 2])
    .clamp(true)

    this._volume = 100 // scaled
    this._maxVolume = 100 // scaled
    this._volumeScale = scaleLinear()
    .domain([0, 1])
    .range([0, this._maxVolume])
    .clamp(true)
    this._maxVolumeScale = scaleLinear()
    .domain([0, 1])
    .range([0, 100])
    .clamp(true)

    this._isApiReady = false
    this.isReady = false

    this.random = false
    this.repeat = false

    if (!window.onYouTubeIframeAPIReady) {
      const tag = document.createElement('script')

      tag.src = 'https://www.youtube.com/iframe_api'

      const firstScriptTag = document.getElementsByTagName('script')[0]

      firstScriptTag.parentNode.insertBefore(tag, firstScriptTag)
    }

    window.onYouTubeIframeAPIReady = () => {
      this._onYoutubeApiReady()
    }

    // if already api loaded
    if (window.YT) {
      this._onYoutubeApiReady()
    }
  }

  _destroyPlayer() {
    return new Promise((resolve, reject) => {
      if (this._player && this._player.destroy) {
        this._player.destroy()
      }

      if (this._player) {
        this._playerEl.remove()
      }

      return resolve()
    })
  }

  _setPlayer(props) {
    if (!props) {
      props = {}
    }

    const {videoId, playlistId} = props

    return new Promise((resolve, reject) => {
      this._destroyPlayer()

      this._playerEl = document.createElement('div')
      this._playerEl.cssText = 'position:absolute;left:-1000px;bottom:-1000px;visibility:hidden;pointer-events:none'
      this._playerEl.id = `player_${Math.random()*1e10|0}`
      document.body.appendChild(this._playerEl)

      if (window.YT) {
        this._player = new window.YT.Player(this._playerEl.id, {
          height: 1,
          width: 1,
          events: {
            onReady: this._onPlayerReady.bind(this),
            onStateChange: this._onPlayerStateChange.bind(this)
          },
          videoId: videoId,
          playerVars: {
            listType: 'playlist',
            list: playlistId
          }
        })
      }
    })
  }

  _onYoutubeApiReady() {
    this._isApiReady = true;
  }

  _onPlayerReady(event) {
    this.setPlaybackRate(this._playbackRate)
    this.setVolume(this._volume)

    this.isReady = true
    this._log('Player ready');
    this.emit(PlayerEventTypes.READY);
  }

  _onPlayerStateChange(event) {
    this._log(`Player state change: ${event.data}`);
    this.emit(PlayerEventTypes.STATE_CHANGE, event.data);
  }

  _log(type, message) {
    if (type && !message) {
      message = type
      type = 'log'
    }

    setTimeout(() => {
      this.emit(PlayerEventTypes.LOG, message)
    }, 0)

    if (this._debug) {
      console[type](message)
    }
  }

  /**
   * getCurrentUrl
   * @desc Get currently playing YouTube video url
   * @return {String}
   * @example
   * const url = player.getCurrentUrl()
   */
  getCurrentUrl() {
    if (this._player) {
      return this._player.getVideoUrl()
    }

    return null
  }

  /**
   * emptyQueue
   * @desc Empties queue
   * @return {Promise}
   * @example
   * player.emptyQueue()
   */
  emptyQueue() {
    return new Promise((resolve, reject) => {
      this.stop()

      this._log('Empty queue');
      this.emit(PlayerEventTypes.EMPTY_QUEUE);

      resolve()
    })
  }

  /**
   * enqueue
   * @desc Enqueue a YouTube url to play
   * @param {String} url - a YouTube url
   * @return {Promise}
   * @example
   * const url = 'https://www.youtube.com/watch?v=qk1nnAHI1mI'
   *
   * player.enqueue(url)
   */
  enqueue(url) {
    return new Promise((resolve, reject) => {
      if (Array.isArray(url)) {
        url = url[0]
      }

      const query = qs.parse(url.replace(/.*\?/gi, ''))
      const videoId = query.v
      const playlistId = query.list

      if (!videoId) {
        return reject(new Error('videoId not found'))
      }

      this._log('Enqueue audio');
      this.emit(PlayerEventTypes.ENQUEUE);

      if (this._isApiReady) {
        return this._setPlayer({
          videoId,
          playlistId
        })
      } else {
        setTimeout(() => {
          this._setPlayer({
            videoId,
            playlistId
          })
          .then(() => resolve())
          .catch(() => reject())
        }, 1e3)
      }
    })
  }

  /**
   * deqeue
   * @desc Deques an YouTube video from queue
   * @return {Promise}
   * @example
   * player.deque()
   */
  deque() {
    return new Promise((resolve, reject) => {
      this.next()

      this._log('Deque audio');
      this.emit(PlayerEventTypes.DEQUE);
      resolve()
    })
  }

  /**
   * play
   * @desc Plays current YouTube video in queue
   * @return {Promise}
   * @example
   * player.play()
   */
  play() {
    return new Promise((resolve, reject) => {
      if (this._player && this._player.playVideo) {
        this._player.playVideo()

        this._log('Play audio');
        this.emit(PlayerEventTypes.PLAY);
        resolve()
      } else {
        reject(new Error('player not ready'))
      }
    })
  }

  /**
   * playQueue
   * @desc Start playing YouTube video in queue
   * @return {Promise}
   * @example
   * player.playQueue()
   */
  playQueue() {
    return this.play()
  }

  /**
   * stop
   * @desc Stop playing current YouTube video
   * @return {Promise}
   * @example
   * player.stop()
   */
  stop() {
    return new Promise((resolve, reject) => {
      if (this._player && this._player.stopVideo) {
        this._player.stopVideo()
      }

      this._log('Stop audio');
      this.emit(PlayerEventTypes.STOP);
      resolve()
    })
  }

  /**
   * pause
   * @desc Pause playing current YouTube video
   * @return {Promise}
   * @example
   * player.pause()
   */
  pause() {
    return new Promise((resolve, reject) => {
      if (this._player && this._player.pauseVideo) {
        this._player.pauseVideo()
      }

      this._log('Pause audio');
      this.emit(PlayerEventTypes.PAUSE);
      resolve()
    })
  }

  /**
   * replay
   * @desc Replay current YouTube video
   * @return {Promise}
   * @example
   * player.replay()
   */
  replay() {
    return new Promise((resolve, reject) => {
      if (this._player && this._player.seekTo) {
        this._player.seekTo(0)
        this.play()

        this._log('Replay audio');
        this.emit(PlayerEventTypes.REPLAY);
        resolve()
      } else {
        reject(new Error('player not ready'))
      }
    })
  }

  /**
   * playUrl
   * @desc Play YouTube url
   * @param {String} url - YouTube url
   * @return {Promise}
   * @example
   * const url = 'https://www.youtube.com/watch?v=qk1nnAHI1mI'
   *
   * player.playUrl(url)
   */
  playUrl(url) {
    return this.emptyQueue()
    .then(() => {
      return this.enqueue(url)
    })
  }

  /**
   * next
   * @desc Play next YouTube video in queue
   * @return {Promise}
   * @example
   * player.next()
   */
  next() {
    return new Promise((resolve, reject) => {
      if (this._player && this._player.nextVideo) {
        this._player.nextVideo()

        this._log('Next audio');
        this.emit(PlayerEventTypes.NEXT);
        resolve()
      } else {
        reject(new Error('player not ready'))
      }
    })
  }

  /**
   * previous
   * @desc Play previous YouTube video in queue
   * @return {Promise}
   * @example
   * player.previous()
   */
  previous() {
    return new Promise((resolve, reject) => {
      if (this._player && this._player.previousVideo) {
        this._player.previousVideo()

        this._log('Previous audio');
        this.emit(PlayerEventTypes.PREVIOUS);
        resolve()
      } else {
        reject(new Error('player not ready'))
      }
    })
  }

  /**
   * setRandom
   * @desc Enable to disable random playback
   * @param {Boolean} enabled - boolean to enable random playback
   * @return {Promise}
   * @example
   * player.setRandom(true)
   */
  setRandom(enabled) {
    this.random = enabled
    this._log(`Set random: ${enabled}`)
    this.emit(PlayerEventTypes.RANDOM)
    return Promise.resolve(true)
  }

  /**
   * setRepeat
   * @desc Enable to disable repeat mode. Repeat mode replays the YouTube videos once the entire queue has finished playing.
   * @param {Boolean} enabled - boolean to enable repeat mode
   * @return {Promise}
   * @example
   * player.setRepeat(true)
   */
  setRepeat(enabled) {
    this.repeat = enabled
    this._log(`Set repeat: ${enabled}`)
    this.emit(PlayerEventTypes.REPEAT)
    return Promise.resolve(true)
  }

  /**
   * hasNext
   * @desc Return true if there's a YouTube video to play next in queue
   * @return {Boolean} hasNext
   * @example
   * const hasNext = player.hasNext()
   */
  hasNext() {
    // TODO
    return true
  }

  /**
   * hasPrevious
   * @desc Return true if there's a previous YouTube video in queue
   * @return {Boolean} hasPrevious
   * @example
   * const hasPrevious = player.hasPrevious()
   */
  hasPrevious() {
    // TODO
    return true
  }

  /**
   * setPlaybackRate
   * @desc Set the plaback rate speed, range 0.75-2
   * @param {Number} playbackRate - new playback rate
   * @return {Promise}
   * @example
   * player.setPlaybackRate(2)
   */
  setPlaybackRate(rate) {
    return new Promise((resolve, reject) => {
      this._playbackRate = this._playbackRateScale(rate)

      if (this._player && this._player.setPlaybackRate) {
        this._player.setPlaybackRate(this._playbackRate)
      }

      this._log(`Set playback rate: ${this._playbackRate}`);
      this.emit(PlayerEventTypes.PLAYBACK_RATE, this._playbackRate);
      resolve()
    })
  }

  /**
   * getPlaybackRate
   * @desc Get the current plaback rate speed
   * @return {Number} playback rate speed
   * @example
   * const playbackRate = player.getPlaybackRate()
   */
  getPlaybackRate() {
    return this._playbackRate
  }

  /**
   * setVolume
   * @desc Set volume, range 0-1
   * @param {Number} volume - volume value
   * @return {Promise}
   * @example
   * player.setVolume(0.9)
   */
  setVolume(volume) {
    return new Promise((resolve, reject) => {
      this._volume = this._volumeScale(volume)

      if (this._player && this._player.setVolume) {
        this._player.setVolume(this._volume)
      }

      this._log(`Set volume: ${this._volume}`);
      this.emit(PlayerEventTypes.VOLUME, this._volume);
      resolve()
    })
  }

  /**
   * getVolume
   * @desc Get current volume value
   * @return {Number} volume - current volume value
   * @example
   * player.getVolume()
   */
  getVolume() {
    return this._volume / 100
  }

  /**
   * setMaxVolume
   * @desc Set the maximum volume limit. For example if max volume is set to 0.6, then when volume is scaled from 0-0.6, meaning that volume level at 1 will play at 0.6
   * @param {Number} maxVolume - max volume, range 0-1
   * @return {Promise}
   * @example
   * player.setMaxVolume(0.8)
   */
  setMaxVolume(maxVolume) {
    return new Promise((resolve, reject) => {
      this._maxVolume = this._maxVolumeScale(maxVolume)
      this._volumeScale = scaleLinear()
      .domain([0, 1])
      .range([0, this._maxVolume])
      .clamp(true)

      this.setVolume(this._volume)

      this._log(`Set max volume: ${this._volume}`);
      this.emit(PlayerEventTypes.MAX_VOLUME, this._maxVolume);
      resolve()
    })
  }

  /**
   * getMaxVolume
   * @desc Get max volume value
   * @return {Number} maxVolume - max volume value
   * @example
   * player.getMaxVolume()
   */
  getMaxVolume() {
    return this._maxVolume / 100
  }

  /**
   * setMuted
   * @desc Set volume level to muted
   * @param {Boolean} enabled - boolean to enable mute
   * @return {Promise}
   * @example
   * player.setMuted(true)
   */
  // TODO
  setMuted(enabled) {
    this.muted = !!enabled

    return Promise.resolve(this.muted)
  }

  /**
   * getCurrentTime
   * @desc Return elapsed time in seconds since the YouTube video started playing
   * @return {Number} time - current time
   * @example
   * player.getCurrentTime()
   */
  getCurrentTime() {
    if (this._player && this._player.getCurrentTime) {
      return this._player.getCurrentTime()
    }

    return 0
  }

  /**
   * seekTo
   * @desc Seek to a specified time in YouTube video
   * @param {Number} seconds - number of seconds to seek to
   * @return {Promise}
   * @example
   * const seconds = 45
   *
   * player.seekTo(seconds)
   */
  seekTo(seconds) {
    if (this._player) {
      return this._player.seekTo(seconds)
    }

    return Promise.resolve()
  }

  /**
   * getDuration
   * @desc Get duration of YouTube video
   * @return {Number} duration - duration of YouTube video
   * @example
   * player.getDuration()
   */
  getDuration() {
    if (this._player) {
      return this._player.getDuration()
    }

    return 0
  }

  /**
   * @static EventTypes
   * @type {Object}
   * @desc Return event types
   * @return {Object} eventTypes - all player event types
   * @example
   * const EventTypes = Player.EventTypes
   *
   * {
      LOG: 'log',
      ERROR: 'error',
      READY: 'ready',
      PLAY: 'play',
      REPLAY: 'replay',
      PAUSE: 'pause',
      STOP: 'pause',
      NEXT: 'next',
      PREVIOUS: 'previous',
      RANDOM: 'random',
      REPEAT: 'repeat',
      PLAYBACK_RATE: 'playbackRate',
      VOLUME: 'volume',
      MAX_VOLUME: 'maxVolume',
      MUTED: 'muted',
      ENDED: 'ended',
      ENQUEUE: 'enqueue',
      DEQUE: 'deque',
      EMPTY_QUEUE: 'emptyQueue',
      STATE_CHANGE: 'stateChange'
   * }
   */
  static get EventTypes() {
    return PlayerEventTypes
  }
}

module.exports = YoutubePlayer