mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			310 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			310 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import Hls from 'hls.js'
 | 
						|
import EventEmitter from 'events'
 | 
						|
 | 
						|
export default class LocalPlayer extends EventEmitter {
 | 
						|
  constructor(ctx) {
 | 
						|
    super()
 | 
						|
 | 
						|
    this.ctx = ctx
 | 
						|
    this.player = null
 | 
						|
 | 
						|
    this.libraryItem = null
 | 
						|
    this.audioTracks = []
 | 
						|
    this.currentTrackIndex = 0
 | 
						|
    this.isHlsTranscode = null
 | 
						|
    this.hlsInstance = null
 | 
						|
    this.usingNativeplayer = false
 | 
						|
    this.startTime = 0
 | 
						|
    this.trackStartTime = 0
 | 
						|
    this.playWhenReady = false
 | 
						|
    this.defaultPlaybackRate = 1
 | 
						|
 | 
						|
    this.playableMimeTypes = []
 | 
						|
 | 
						|
    this.initialize()
 | 
						|
  }
 | 
						|
 | 
						|
  get currentTrack() {
 | 
						|
    return this.audioTracks[this.currentTrackIndex] || {}
 | 
						|
  }
 | 
						|
 | 
						|
  initialize() {
 | 
						|
    if (document.getElementById('audio-player')) {
 | 
						|
      document.getElementById('audio-player').remove()
 | 
						|
    }
 | 
						|
    var audioEl = document.createElement('audio')
 | 
						|
    audioEl.id = 'audio-player'
 | 
						|
    audioEl.style.display = 'none'
 | 
						|
    document.body.appendChild(audioEl)
 | 
						|
    this.player = audioEl
 | 
						|
 | 
						|
    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 = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', 'audio/x-ms-wma', 'audio/x-aiff']
 | 
						|
    var mimeTypeCanPlayMap = {}
 | 
						|
    mimeTypes.forEach((mt) => {
 | 
						|
      var canPlay = this.player.canPlayType(mt)
 | 
						|
      mimeTypeCanPlayMap[mt] = canPlay
 | 
						|
      if (canPlay) this.playableMimeTypes.push(mt)
 | 
						|
    })
 | 
						|
    console.log(`[LocalPlayer] 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() {
 | 
						|
    if (this.currentTrackIndex < this.audioTracks.length - 1) {
 | 
						|
      console.log(`[LocalPlayer] Track ended - loading next track ${this.currentTrackIndex + 1}`)
 | 
						|
      // Has next track
 | 
						|
      this.currentTrackIndex++
 | 
						|
      this.playWhenReady = true
 | 
						|
      this.startTime = this.currentTrack.startOffset
 | 
						|
      this.loadCurrentTrack()
 | 
						|
    } else {
 | 
						|
      console.log(`[LocalPlayer] Ended`)
 | 
						|
    }
 | 
						|
  }
 | 
						|
  evtError(error) {
 | 
						|
    console.error('Player error', error)
 | 
						|
    this.emit('error', error)
 | 
						|
  }
 | 
						|
  evtLoadedMetadata(data) {
 | 
						|
    if (!this.isHlsTranscode) {
 | 
						|
      this.player.currentTime = this.trackStartTime
 | 
						|
    }
 | 
						|
 | 
						|
    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, tracks, isHlsTranscode, startTime, playWhenReady = false) {
 | 
						|
    this.libraryItem = libraryItem
 | 
						|
    this.audioTracks = tracks
 | 
						|
    this.isHlsTranscode = isHlsTranscode
 | 
						|
    this.playWhenReady = playWhenReady
 | 
						|
    this.startTime = startTime
 | 
						|
 | 
						|
    if (this.hlsInstance) {
 | 
						|
      this.destroyHlsInstance()
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.isHlsTranscode) {
 | 
						|
      this.setHlsStream()
 | 
						|
    } else {
 | 
						|
      this.setDirectPlay()
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  setHlsStream() {
 | 
						|
    this.trackStartTime = 0
 | 
						|
 | 
						|
    // iOS does not support Media Elements but allows for HLS in the native audio player
 | 
						|
    if (!Hls.isSupported()) {
 | 
						|
      console.warn('HLS is not supported - fallback to using audio element')
 | 
						|
      this.usingNativeplayer = true
 | 
						|
      this.player.src = this.currentTrack.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.currentTrack.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() {
 | 
						|
    // Set initial track and track time offset
 | 
						|
    var trackIndex = this.audioTracks.findIndex(t => this.startTime >= t.startOffset && this.startTime < (t.startOffset + t.duration))
 | 
						|
    this.currentTrackIndex = trackIndex >= 0 ? trackIndex : 0
 | 
						|
 | 
						|
    this.loadCurrentTrack()
 | 
						|
  }
 | 
						|
 | 
						|
  loadCurrentTrack() {
 | 
						|
    if (!this.currentTrack) return
 | 
						|
    // When direct play track is loaded current time needs to be set
 | 
						|
    this.trackStartTime = Math.max(0, this.startTime - (this.currentTrack.startOffset || 0))
 | 
						|
    this.player.src = this.currentTrack.relativeContentUrl
 | 
						|
    console.log(`[LocalPlayer] Loading track src ${this.currentTrack.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.audioTracks, 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() {
 | 
						|
    var currentTrackOffset = this.currentTrack.startOffset || 0
 | 
						|
    return this.player ? currentTrackOffset + this.player.currentTime : 0
 | 
						|
  }
 | 
						|
 | 
						|
  getDuration() {
 | 
						|
    if (!this.audioTracks.length) return 0
 | 
						|
    var lastTrack = this.audioTracks[this.audioTracks.length - 1]
 | 
						|
    return lastTrack.startOffset + lastTrack.duration
 | 
						|
  }
 | 
						|
 | 
						|
  setPlaybackRate(playbackRate) {
 | 
						|
    if (!this.player) return
 | 
						|
    this.defaultPlaybackRate = playbackRate
 | 
						|
    this.player.playbackRate = playbackRate
 | 
						|
  }
 | 
						|
 | 
						|
  seek(time) {
 | 
						|
    if (!this.player) return
 | 
						|
    if (this.isHlsTranscode) {
 | 
						|
      // Seeking HLS stream
 | 
						|
      var offsetTime = time - (this.currentTrack.startOffset || 0)
 | 
						|
      this.player.currentTime = Math.max(0, offsetTime)
 | 
						|
    } else {
 | 
						|
      // Seeking Direct play
 | 
						|
      if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) {
 | 
						|
        // Change Track
 | 
						|
        var trackIndex = this.audioTracks.findIndex(t => time >= t.startOffset && time < (t.startOffset + t.duration))
 | 
						|
        if (trackIndex >= 0) {
 | 
						|
          this.startTime = time
 | 
						|
          this.currentTrackIndex = trackIndex
 | 
						|
 | 
						|
          if (!this.player.paused) {
 | 
						|
            // audio player playing so play when track loads
 | 
						|
            this.playWhenReady = true
 | 
						|
          }
 | 
						|
          this.loadCurrentTrack()
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        var offsetTime = time - (this.currentTrack.startOffset || 0)
 | 
						|
        this.player.currentTime = Math.max(0, offsetTime)
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
  }
 | 
						|
 | 
						|
  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
 | 
						|
  }
 | 
						|
} |