mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			260 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			260 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import Hls from 'hls.js'
 | 
						|
import EventEmitter from 'events'
 | 
						|
 | 
						|
export default class LocalVideoPlayer extends EventEmitter {
 | 
						|
  constructor(ctx) {
 | 
						|
    super()
 | 
						|
 | 
						|
    this.ctx = ctx
 | 
						|
    this.player = null
 | 
						|
 | 
						|
    this.libraryItem = null
 | 
						|
    this.videoTrack = null
 | 
						|
    this.isHlsTranscode = null
 | 
						|
    this.hlsInstance = null
 | 
						|
    this.usingNativeplayer = false
 | 
						|
    this.startTime = 0
 | 
						|
    this.playWhenReady = false
 | 
						|
    this.defaultPlaybackRate = 1
 | 
						|
 | 
						|
    this.playableMimeTypes = []
 | 
						|
 | 
						|
    this.initialize()
 | 
						|
  }
 | 
						|
 | 
						|
  initialize() {
 | 
						|
    if (document.getElementById('video-player')) {
 | 
						|
      document.getElementById('video-player').remove()
 | 
						|
    }
 | 
						|
    var videoEl = document.createElement('video')
 | 
						|
    videoEl.id = 'video-player'
 | 
						|
    // videoEl.style.display = 'none'
 | 
						|
    videoEl.className = 'absolute bg-black z-50'
 | 
						|
    videoEl.style.height = '216px'
 | 
						|
    videoEl.style.width = '384px'
 | 
						|
    videoEl.style.bottom = '80px'
 | 
						|
    videoEl.style.left = '16px'
 | 
						|
    document.body.appendChild(videoEl)
 | 
						|
    this.player = videoEl
 | 
						|
 | 
						|
    this.player.addEventListener('play', this.evtPlay.bind(this))
 | 
						|
    this.player.addEventListener('pause', this.evtPause.bind(this))
 | 
						|
    this.player.addEventListener('progress', this.evtProgress.bind(this))
 | 
						|
    this.player.addEventListener('ended', this.evtEnded.bind(this))
 | 
						|
    this.player.addEventListener('error', this.evtError.bind(this))
 | 
						|
    this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
 | 
						|
    this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
 | 
						|
 | 
						|
    var mimeTypes = ['video/mp4']
 | 
						|
    var mimeTypeCanPlayMap = {}
 | 
						|
    mimeTypes.forEach((mt) => {
 | 
						|
      var canPlay = this.player.canPlayType(mt)
 | 
						|
      mimeTypeCanPlayMap[mt] = canPlay
 | 
						|
      if (canPlay) this.playableMimeTypes.push(mt)
 | 
						|
    })
 | 
						|
    console.log(`[LocalVideoPlayer] Supported mime types`, mimeTypeCanPlayMap, this.playableMimeTypes)
 | 
						|
  }
 | 
						|
 | 
						|
  evtPlay() {
 | 
						|
    this.emit('stateChange', 'PLAYING')
 | 
						|
  }
 | 
						|
  evtPause() {
 | 
						|
    this.emit('stateChange', 'PAUSED')
 | 
						|
  }
 | 
						|
  evtProgress() {
 | 
						|
    var lastBufferTime = this.getLastBufferedTime()
 | 
						|
    this.emit('buffertimeUpdate', lastBufferTime)
 | 
						|
  }
 | 
						|
  evtEnded() {
 | 
						|
    console.log(`[LocalVideoPlayer] Ended`)
 | 
						|
    this.emit('finished')
 | 
						|
  }
 | 
						|
  evtError(error) {
 | 
						|
    console.error('Player error', error)
 | 
						|
    this.emit('error', error)
 | 
						|
  }
 | 
						|
  evtLoadedMetadata(data) {
 | 
						|
    if (!this.isHlsTranscode) {
 | 
						|
      this.player.currentTime = this.startTime
 | 
						|
    }
 | 
						|
 | 
						|
    this.emit('stateChange', 'LOADED')
 | 
						|
    if (this.playWhenReady) {
 | 
						|
      this.playWhenReady = false
 | 
						|
      this.play()
 | 
						|
    }
 | 
						|
  }
 | 
						|
  evtTimeupdate() {
 | 
						|
    if (this.player.paused) {
 | 
						|
      this.emit('timeupdate', this.getCurrentTime())
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  destroy() {
 | 
						|
    this.destroyHlsInstance()
 | 
						|
    if (this.player) {
 | 
						|
      this.player.remove()
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  set(libraryItem, videoTrack, isHlsTranscode, startTime, playWhenReady = false) {
 | 
						|
    this.libraryItem = libraryItem
 | 
						|
    this.videoTrack = videoTrack
 | 
						|
    this.isHlsTranscode = isHlsTranscode
 | 
						|
    this.playWhenReady = playWhenReady
 | 
						|
    this.startTime = startTime
 | 
						|
 | 
						|
    if (this.hlsInstance) {
 | 
						|
      this.destroyHlsInstance()
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.isHlsTranscode) {
 | 
						|
      this.setHlsStream()
 | 
						|
    } else {
 | 
						|
      this.setDirectPlay()
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  setHlsStream() {
 | 
						|
    // iOS does not support Media Elements but allows for HLS in the native video player
 | 
						|
    if (!Hls.isSupported()) {
 | 
						|
      console.warn('HLS is not supported - fallback to using video element')
 | 
						|
      this.usingNativeplayer = true
 | 
						|
      this.player.src = this.videoTrack.relativeContentUrl
 | 
						|
      this.player.currentTime = this.startTime
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    var hlsOptions = {
 | 
						|
      startPosition: this.startTime || -1
 | 
						|
      // No longer needed because token is put in a query string
 | 
						|
      // xhrSetup: (xhr) => {
 | 
						|
      //   xhr.setRequestHeader('Authorization', `Bearer ${this.token}`)
 | 
						|
      // }
 | 
						|
    }
 | 
						|
    this.hlsInstance = new Hls(hlsOptions)
 | 
						|
 | 
						|
    this.hlsInstance.attachMedia(this.player)
 | 
						|
    this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
 | 
						|
      this.hlsInstance.loadSource(this.videoTrack.relativeContentUrl)
 | 
						|
 | 
						|
      this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
 | 
						|
        console.log('[HLS] Manifest Parsed')
 | 
						|
      })
 | 
						|
 | 
						|
      this.hlsInstance.on(Hls.Events.ERROR, (e, data) => {
 | 
						|
        console.error('[HLS] Error', data.type, data.details, data)
 | 
						|
        if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
 | 
						|
          console.error('[HLS] BUFFER STALLED ERROR')
 | 
						|
        }
 | 
						|
      })
 | 
						|
      this.hlsInstance.on(Hls.Events.DESTROYING, () => {
 | 
						|
        console.log('[HLS] Destroying HLS Instance')
 | 
						|
      })
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  setDirectPlay() {
 | 
						|
    this.player.src = this.videoTrack.relativeContentUrl
 | 
						|
    console.log(`[LocalVideoPlayer] Loading track src ${this.videoTrack.relativeContentUrl}`)
 | 
						|
    this.player.load()
 | 
						|
  }
 | 
						|
 | 
						|
  destroyHlsInstance() {
 | 
						|
    if (!this.hlsInstance) return
 | 
						|
    if (this.hlsInstance.destroy) {
 | 
						|
      var temp = this.hlsInstance
 | 
						|
      temp.destroy()
 | 
						|
    }
 | 
						|
    this.hlsInstance = null
 | 
						|
  }
 | 
						|
 | 
						|
  async resetStream(startTime) {
 | 
						|
    this.destroyHlsInstance()
 | 
						|
    await new Promise((resolve) => setTimeout(resolve, 1000))
 | 
						|
    this.set(this.libraryItem, this.videoTrack, this.isHlsTranscode, startTime, true)
 | 
						|
  }
 | 
						|
 | 
						|
  playPause() {
 | 
						|
    if (!this.player) return
 | 
						|
    if (this.player.paused) this.play()
 | 
						|
    else this.pause()
 | 
						|
  }
 | 
						|
 | 
						|
  play() {
 | 
						|
    if (this.player) this.player.play()
 | 
						|
  }
 | 
						|
 | 
						|
  pause() {
 | 
						|
    if (this.player) this.player.pause()
 | 
						|
  }
 | 
						|
 | 
						|
  getCurrentTime() {
 | 
						|
    return this.player ? this.player.currentTime : 0
 | 
						|
  }
 | 
						|
 | 
						|
  getDuration() {
 | 
						|
    return this.videoTrack.duration
 | 
						|
  }
 | 
						|
 | 
						|
  setPlaybackRate(playbackRate) {
 | 
						|
    if (!this.player) return
 | 
						|
    this.defaultPlaybackRate = playbackRate
 | 
						|
    this.player.playbackRate = playbackRate
 | 
						|
  }
 | 
						|
 | 
						|
  seek(time) {
 | 
						|
    if (!this.player) return
 | 
						|
    this.player.currentTime = Math.max(0, time)
 | 
						|
  }
 | 
						|
 | 
						|
  setVolume(volume) {
 | 
						|
    if (!this.player) return
 | 
						|
    this.player.volume = volume
 | 
						|
  }
 | 
						|
 | 
						|
  // Utils
 | 
						|
  isValidDuration(duration) {
 | 
						|
    if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) {
 | 
						|
      return true
 | 
						|
    }
 | 
						|
    return false
 | 
						|
  }
 | 
						|
 | 
						|
  getBufferedRanges() {
 | 
						|
    if (!this.player) return []
 | 
						|
    const ranges = []
 | 
						|
    const seekable = this.player.buffered || []
 | 
						|
 | 
						|
    let offset = 0
 | 
						|
 | 
						|
    for (let i = 0, length = seekable.length; i < length; i++) {
 | 
						|
      let start = seekable.start(i)
 | 
						|
      let end = seekable.end(i)
 | 
						|
      if (!this.isValidDuration(start)) {
 | 
						|
        start = 0
 | 
						|
      }
 | 
						|
      if (!this.isValidDuration(end)) {
 | 
						|
        end = 0
 | 
						|
        continue
 | 
						|
      }
 | 
						|
 | 
						|
      ranges.push({
 | 
						|
        start: start + offset,
 | 
						|
        end: end + offset
 | 
						|
      })
 | 
						|
    }
 | 
						|
    return ranges
 | 
						|
  }
 | 
						|
 | 
						|
  getLastBufferedTime() {
 | 
						|
    var bufferedRanges = this.getBufferedRanges()
 | 
						|
    if (!bufferedRanges.length) return 0
 | 
						|
 | 
						|
    var buff = bufferedRanges.find((buff) => buff.start < this.player.currentTime && buff.end > this.player.currentTime)
 | 
						|
    if (buff) return buff.end
 | 
						|
 | 
						|
    var last = bufferedRanges[bufferedRanges.length - 1]
 | 
						|
    return last.end
 | 
						|
  }
 | 
						|
} |