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
 | |
|   }
 | |
| } |