Source: Player.js

/**
 * Copyright (C) 2019.
 * All Rights Reserved.
 * @file Player.js
 * @desc
 * the player main stream
 * load data by url -> get the data -> ts demux -> get h265 data -> Webassmebly decode -> YUV data -> draw yuv to Canvas -> image and audio synv -> audio stream -> AudioContext decode and play
 * @author Jarry
 */

import getEvents from './toolkit/Events.js'
import Element from './toolkit/Element.js'
import AlertError from './error/AlertError'
import { throwError } from './error/ThrowError'
import BaseClass from './base/BaseClass.js'
import LoaderController from './loader/LoaderController'
import DataController from './data/DataController'
import Action from './action/Action.js'
import AudioPlayer from './audio/AudioPlayer.js'
import { Config, READY } from './config/Config'
import PlayerUtil from './utils/PlayerUtil'
import ComponentsController from './components/ComponentsController'
import ControlBarController from './control-bar/ControlBarController'
import DataProcessorController from './data-processor/dataProcessorController'
import StreamController from './action/StreamController.js'
import ImagePlayer from './ImagePlayer/ImagePlayer.js'
import webworkify from 'webworkify-webpack'
import Events from './config/EventsConfig'

class Player extends BaseClass {
  mode = Config.mode
  $container = null
  componentsController = null
  controlBarController = null
  controlBarHeight = 50
  alertError = null
  dataController = null
  demuxer = null
  decoder = null
  preload = true
  startTime = 0
  screenWidth = null
  screenHeight = null
  options = {}
  #libPath = Config.libPath
  readyStatus = { dataProcessor: false, firstData: false, audioPlayer: false }
  reseting = false
  #currentTime = 0
  #seek = false
  #playbackRate = 1
  #muted = false
  maxBufferLength = READY.MAXBUFFERLENGTH
  seekSegmentNo = -1
  status = 'init'
  loader = null
  currentIndex = null
  startIndex = 1
  loadData = null
  paused = false
  autoPlay = true
  duration = 0
  tsNumber = 0
 /**
  * @property {string} sourceURL - The url of the video to play
  * @property {string} source - The url of the video to play
  * @property {Object} defaultRate - The default value of playing rate
  * @property {string} type - The type of the video, such as HLS
  * @property {Object[]} rateList - The rate list of the player
  * @example <caption>Example of rateList.</caption>
  * // let options = {
*        rateList:[
           {"url":"http://localhost/20190902/01/a5/029f8fad8868a7116f20d8ae5b075996.m3u8","id":51,"name":"720P","value":"720"},
           {"url":"http://localhost/20190902/f0/db/ee545466ced38973a9d60fe7f24ed409.m3u8","id":51,"name":"高清","value":"600"},
           {"url":"http://localhost/20190902/78/6c/5a5a99476f4f792e2e7a701ba1f6d5ad.m3u8","id":264,"name":"极速","value":"jisu"},
           {"url":"http://localhost/20190902/54/05/7e714a321d9e7d92c937582b2e439833.m3u8","id":265,"name":"流畅","value":"300"}]
       }
  * @property {Function} processURL - process the url of video source 
  * @property {Number} maxBufferLength - The maximum value of the buffer, its default value is 5000(ms)
  * @property {Boolean} autoPlay - If auto play after initializing the Player
  * @property {string} libPath - The path of decoder
  * @property {Boolean} preload - If pre load video before playing
  * @property {Number} startTime - Start time to play the video
  * @property {string} playbackRate - Playback speed
  * @property {Number} controlBarHeight - The height of the control bar
  * @property {AlertError} alertError - The alert info when error happens
  * @property {Worker} httpWorker - set User's web worker
  * @property {Function} afterLoadPlaylist - To handle operations after playlist is loaded
 */
  constructor (el, options = {}) {
    super()
    if (!el) {
      this.logger.error('Please pass in a dom object as the display container')
    }
    this.el = el
    Object.assign(this.options, options)
    this.options.sourceURL = this.options.sourceURL || this.options.source
    this.options.streamList = this.options.streamList || []
    this.options.events = getEvents()
    this.maxBufferLength = options.maxBufferLength !== undefined ? options.maxBufferLength : this.maxBufferLength
    this.autoPlay = options.autoPlay !== undefined ? options.autoPlay : this.autoPlay
    this.#libPath =
      options.libPath !== undefined ? options.libPath : this.#libPath
    this.preload =
      options.preload === undefined ? this.preload : options.preload
    this.startTime = options.startTime === undefined ? this.startTime : options.startTime
    this.originStartTime = this.startTime
    this.playbackRate = options.playbackRate === undefined ? this.playbackRate : options.playbackRate
  }
  setAlertError () {
    this.options.alertError = this.alertError = AlertError.getInstance({
      player: this,
      component: this.componentsController.alertBox,
      events: this.options.events
    })
  }
  setDataController () {
    this.dataController = DataController.getInstance({
      player: this,
      events: this.options.events
    })
  }
  setLoadData () {
    this.dataController.setLoadData({
      player: this,
      events: this.options.events
    })
    this.loadData = this.dataController.loadData
  }
  setComponentsController () {
    this.componentsController = ComponentsController.getInstance({
      $container: this.$container,
      $screenContainer: this.$screenContainer,
      $canvas: this.$canvas,
      $audioContainer: this.$audioContainer,
      $audio: this.$audio,
      loadData: this.loadData,
      bigPlayButtonColor: this.bigPlayButtonColor,
      player: this,
      events: this.options.events
    })
  }
  setControlBarController () {
    const options = Object.assign({}, this.options, {
      $container: this.$container,
      $screenContainer: this.$screenContainer,
      controlBarAutoHide: this.controlBarAutoHide,
      player: this
    })
    this.controlBarController = ControlBarController.getInstance(options)
  }
  setLoadController () {
    this.loaderController = LoaderController.getInstance(this.options.type, {
      player: this,
      events: this.options.events
    })
  }
  setProcessorController() {
    this.processController = new DataProcessorController({
      type: 'ts',
      libPath: this.#libPath,
      events: this.options.events,
      player: this
    })
  }
  setStreamController () {
    this.streamController = new StreamController({
      events: this.options.events,
      loadData: this.loadData,
      imagePlayer: this.imagePlayer,
      audioPlayer: this.audioPlayer,
      player: this
    })
  }
  setImagerPlayer () {
    this.imagePlayer = new ImagePlayer({
      events: this.options.events,
      canvas: this.$canvas,
      maxBufferLength: this.maxBufferLength,
      player: this
    })
  }
  setAudioPlayer () {
    this.audioPlayer = new AudioPlayer({
      player: this,
      events: this.options.events,
      audioNode: this.$audio
    })
  }
  setAction () {
    this.action = new Action({
      player: this,
      screen: this.screen,
      imagePlayer: this.imagePlayer,
      loadData: this.loadData,
      audioPlayer: this.audioPlayer,
      events: this.options.events
    })
  }
  init () {
    this.controlBarHeight = this.options.controlBarHeight || this.controlBarHeight
    this.options.httpWorker = webworkify(require.resolve('./toolkit/HTTP.js'), {
      name: 'httpWorker'
    })
    this.currentTime = this.startTime
    this.addEl()
    this.setDataController()
    this.setLoadData()
    this.setComponentsController()
    this.setLoadController()
    this.setControlBarController()
    this.componentsController.setControlBarController(this.controlBarController)
    this.setAlertError()
    this.setProcessorController()
    this.setImagerPlayer()
    this.setAudioPlayer()
    this.setAction()
    this.setStreamController()

    this.componentsController.drawPoster()
    if (this.preload) {
      this.run()
    }
    this.bindEvent()
  }
  bindEvent () {
    this.events.on(Events.PlayerOnPlay, () => {
      this.play()
    })
    this.events.on(Events.PlayerOnPause, () => {
      this.pause()
    })
    this.events.on(Events.PlayerOnVolume, (value) => {
      this.volume = value
    })
    this.events.on(Events.DataProcessorReady, () => {
      this.logger.info('bindEvent', 'decoder ready')
      this.checkReady('dataProcessor')
    })
    this.events.on(Events.AudioPlayerReady, () => {
      this.logger.info('bindEvent', 'audioPlayer ready')
      if (!this.seeking) {
        this.checkReady('audioPlayer')
      }
    })
    this.events.on(Events.PlayerReady, () => {
      this.logger.info('bindEvent', 'player ready')
      this.onReady()
    })
    this.events.on(Events.PlayerSpeedTo, (data) => {
      this.changeSpeed(data)
    })
    this.events.on(Events.PlayerChangeRate, (data) => {
      this.changeRate(data)
    })
    this.events.on(Events.PlayerWait, () => {
      this.onWait() 
    })
    this.events.on(Events.PlayerPlay, () => {
      this.onPlay()
    })
    this.events.on(Events.LoaderPlayListLoaded, data => {
      if (typeof this.options.afterLoadPlaylist == 'function') {
        this.options.afterLoadPlaylist(this.laodData.sourceData)
      }
      let sourceData = data.loadData.sourceData
      this.duration = sourceData.duration
      this.tsNumber = sourceData.length
      this.streamController.setBaseInfo({
        duration: this.duration,
        tsNumber: this.tsNumber
      })
      this.dataController.startLoad(this.startTime)
      this.setStartTime(this.originStartTime)
    })
    this.events.on(Events.LoadDataFirstLoaded, buffer => {
      this.logger.info('bindEvent', 'first data ready')
      this.startIndex = buffer.no
      this.streamController.currentIndex = buffer.no
      this.checkReady('firstData')
    })
    this.events.on(Events.StreamDataReady, () => {
      this.logger.info('bindEvent', 'dataReady')
      this.onDataReady()
    })

    this.events.on(Events.PlayerLoadedMetaData, (width, height) => {
      this.setCanvas()
      this.resizeScreen(width, height)
    })
    this.events.on(Events.PlayerEnd, () => {
      this.status = 'end'
    })
    this.events.on(Events.ImagePlayerBuffeUpdate, () => {
      let buffer = this.buffer()
      this.events.emit(Events.PlayerbufferUpdate, buffer)
    })
    this.events.on(Events.PlayerOnSeek, (time) => {
      this.seek(Math.floor(time))
    })
    this.events.on(Events.PlayerAlert, (content) => {
      this.alertError.show(content)
    })
    this.events.on(Events.PlayerThrowError, (errors) => {
      throwError.apply(this, errors)
    })
  }
  reset (value) {
    if (this.action) {
      this.action.reset(value)
    }
  }
  switchPlaylist (url, callback) {
    this.loaderController.switchPlaylist(url, callback)
  }
  /**
   * @method
   * @name changeSpeed
   * @param {Object} data - data.value, The value of playback speed
   * @description Change the playback speed, such as 1, 0.5, 1.5, 2...*/
  changeSpeed (data = {}) {
    this.playbackRate = data.value || 1
  }

  setStartTime (time) {
    this.startTime = time
  }
  /**
   * @method
   * @name changeRate
   * @param {Object} data - The url of the video source
   * @param {function} callback - Function to handle after changing the video source 
   * @description Change the rate of the video source, such as 720P, HD...*/
  changeRate (data) {
    this.pause()
    this.events.emit(Events.ControlBarPauseLoading, this)
    this.setStartTime((this.currentTime - 5000) / 1000)
    this.changing = true
    this.seeking = false
    this.imagePlayer.firstRender = false
    this.readyStatus = { dataProcessor: false, firstData: false, audioPlayer: false }
    this.reset(true)
    this.switchPlaylist(data.url)
  }
  /**
   * @method
   * @name changeSrc
   * @param {string} url - The url of the video source
   * @param {function} callback - Function to handle after changing the video source 
   * @description Change the source of the video to play*/
  changeSrc (url, callback) {
    this.pause()
    this.events.emit(Events.ControlBarPauseLoading, this)
    this.events.emit(Events.PlayerChangeSrc, this)
    this.changing = true
    this.seeking = false
    this.imagePlayer.firstRender = false
    this.readyStatus = { dataProcessor: false, firstData: false, audioPlayer: false }
    this.currentTime = this.startTime
    this.imagePlayer.clear()
    this.reset(true)
    this.switchPlaylist(url, callback)
  }
  setCanvas () {
    const $canvas = PlayerUtil.createCanvas(this)
    if (Element.isElement(this.$canvas)) {
      if (this.$canvas.width && this.$canvas.height) {
        this.imagePlayer.setScreenRender($canvas)
        this.$canvas.replaceWith($canvas)
        this.$canvas = $canvas
        return
      }
    } else {
      this.$screen.appendChild($canvas)
      this.$canvas = $canvas
    }
  }
  /**
   * @method
   * @name destroy
   * @description destroy the instance of Class Player*/
  destroy () {
    if (this.controlBarController) {
      this.controlBarController.destroy()
      delete this.controlBarController
    }
    if (this.$container) {
      this.el.removeChild(this.$container)
      delete this.$container
    }
    this.decodeController.destroy()
    this.demuxController.destroy()
    this.loaderController.destroy()
  }

  run () {
    this.loaderController.run()
  }

  checkReady (type) {
    let readyStatus = this.readyStatus
    if (type && typeof type === 'string') {
      readyStatus[type] = true
      let keys = Object.keys(readyStatus)
      for (let i = 0; i < keys.length; i++) {
        if (!readyStatus[keys[i]]) {
          return false
        }
      }
      this.logger.info('checkReady', 'player ready')
      this.events.emit(Events.PlayerReady)
      return true
    } else {
      this.logger.error('checkReady', 'check ready', 'type is no correct, type:', type)
      return false
    }
  }

  addEl () {
    let $container, $screen, $audioContainer, $audio
    if (!this.el) {
      this.logger.error('addEl', 'not found el.', 'el:', this.options.el)
      return
    }

    $container = PlayerUtil.createContainer(this)
    this.el.appendChild($container)
    this.$container = $container

    $screen = PlayerUtil.createScreenContainer(this)
    this.$screen = $screen
    $container.appendChild($screen)
    this.$screenContainer = $screen

    this.setCanvas()

    $audio = PlayerUtil.createAudio(this)
    this.$audio = $audio
    $audioContainer = PlayerUtil.createAudioContainer(this)
    $audioContainer.appendChild($audio)
    this.$audioContainer = $audioContainer
    $container.appendChild($audioContainer)


  }
  onWait () {
    this.logger.info('onWait', 'wait,wait,wait')
  }
  onPlay () {
    this.logger.info('onPlay', 'play, play, play')
  }
  onReady () {
    if (!this.changing) {
      this.componentsController.run()
    }
    if (this.changing) {
      this.logger.info('onReady', 'change ready', 'startIndex:', this.startIndex)
    }
    this.streamController.startLoad(this.startIndex)
  }

  onDataReady () {
    if (this.changing) {
      this.changing = false
      this.play()
    }
    if (this.seeking || (this.autoPlay && !this.paused)) {
      this.play()
    }
  }

  buffer () {
    let videoBuffered = this.imagePlayer.buffer()
    let audioPlayerBuffered = this.audioPlayer.buffer()
    let sTime = Math.max(videoBuffered.start, audioPlayerBuffered.start)
    let eTime = Math.min(videoBuffered.end, audioPlayerBuffered.end)

    if (sTime < eTime) {
      return [sTime, eTime]
    }
    return [0, 0]
  }
  /**
   * @method
   * @name play
   * @description play the video*/
  play() {
    this.logger.info('play', this.status)
    if (this.status !== 'playing') {
      this.logger.info('start play')
      this.status = 'playing'
      this.paused = false
      this.action.play(this.currentTime)
      this.events.emit(Events.PlayerPlay, this)
    }
  }
  on (name, callback) {
    this.events.on('Player.' + name, callback)
  }
  off (name, callback) {
    this.events.off('Player.' + name, callback)
  }
  once (name, callback) {
    this.events.once('Player.' + name, callback)
  }
  /**
   * @method
   * @name pause
   * @description pause the video*/
  pause () {
    if (this.status != 'pause') {
      this.logger.info('pause')
      this.status = 'pause'
      this.action.pause()
      this.paused = true
      this.events.emit(Events.PlayerPause, this)
    }
  }
  /**
   * @method
   * @name seek
   * @param {number} time - the duration of seeking
   * @description seek time to play*/
  seek (time) {
    if (time < this.startTime) {
      return
    }
    if (time >= this.duration * 1000) {
      this.logger.info('seek', 'seek to time:', time)
      return
    }
    this.seekTime = Date.now()
    this.seeking = true
    this.currentTime = time
    this.action.seek(time)
  }
  get muted () {
    return this.#muted
  }
  set muted (value) {
    this.#muted = !!value
  }
  set playbackRate (value) {
    this.#playbackRate = value
  }
  get playbackRate () {
    return this.#playbackRate
  }
  set seeking (value) {
    this.#seek = value
    if (value) {
      this.events.emit(Events.PlayerSeeking)
    } else {
      this.events.emit(Events.PlayerSeekEnd)
    }
  }
  get seeking () {
    return this.#seek
  }
  get currentTime () {
    return this.#currentTime
  }
  set currentTime (time) {
    this.#currentTime = time
  }
  get volume () {
    return this.audioPlayer.volume
  }
  set volume (value) {
    this.audioPlayer.volume = value
  }
  resizeScreen (width, height) {
    Element.adaptSizeElement(width, height, this.$screenContainer, this.$canvas)
  }
}

export default Player