mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			361 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			361 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import LocalAudioPlayer from './LocalAudioPlayer'
 | |
| import LocalVideoPlayer from './LocalVideoPlayer'
 | |
| import CastPlayer from './CastPlayer'
 | |
| import AudioTrack from './AudioTrack'
 | |
| import VideoTrack from './VideoTrack'
 | |
| 
 | |
| export default class PlayerHandler {
 | |
|   constructor(ctx) {
 | |
|     this.ctx = ctx
 | |
|     this.libraryItem = null
 | |
|     this.episodeId = null
 | |
|     this.displayTitle = null
 | |
|     this.displayAuthor = null
 | |
|     this.playWhenReady = false
 | |
|     this.initialPlaybackRate = 1
 | |
|     this.player = null
 | |
|     this.playerState = 'IDLE'
 | |
|     this.isHlsTranscode = false
 | |
|     this.isVideo = false
 | |
|     this.currentSessionId = null
 | |
|     this.startTime = 0
 | |
| 
 | |
|     this.failedProgressSyncs = 0
 | |
|     this.lastSyncTime = 0
 | |
|     this.lastSyncedAt = 0
 | |
|     this.listeningTimeSinceSync = 0
 | |
| 
 | |
|     this.playInterval = null
 | |
|   }
 | |
| 
 | |
|   get isCasting() {
 | |
|     return this.ctx.$store.state.globals.isCasting
 | |
|   }
 | |
|   get libraryItemId() {
 | |
|     return this.libraryItem ? this.libraryItem.id : null
 | |
|   }
 | |
|   get isPlayingCastedItem() {
 | |
|     return this.libraryItem && (this.player instanceof CastPlayer)
 | |
|   }
 | |
|   get isPlayingLocalItem() {
 | |
|     return this.libraryItem && (this.player instanceof LocalAudioPlayer)
 | |
|   }
 | |
|   get userToken() {
 | |
|     return this.ctx.$store.getters['user/getToken']
 | |
|   }
 | |
|   get playerPlaying() {
 | |
|     return this.playerState === 'PLAYING'
 | |
|   }
 | |
|   get episode() {
 | |
|     if (!this.episodeId) return null
 | |
|     return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
 | |
|   }
 | |
| 
 | |
|   load(libraryItem, episodeId, playWhenReady, playbackRate) {
 | |
|     this.libraryItem = libraryItem
 | |
|     this.episodeId = episodeId
 | |
|     this.playWhenReady = playWhenReady
 | |
|     this.initialPlaybackRate = playbackRate
 | |
|     this.isVideo = libraryItem.mediaType === 'video'
 | |
| 
 | |
|     if (!this.player) this.switchPlayer(playWhenReady)
 | |
|     else this.prepare()
 | |
|   }
 | |
| 
 | |
|   switchPlayer(playWhenReady) {
 | |
|     if (this.isCasting && !(this.player instanceof CastPlayer)) {
 | |
|       console.log('[PlayerHandler] Switching to cast player')
 | |
| 
 | |
|       this.stopPlayInterval()
 | |
|       this.playerStateChange('LOADING')
 | |
| 
 | |
|       this.startTime = this.player ? this.player.getCurrentTime() : this.startTime
 | |
|       if (this.player) {
 | |
|         this.player.destroy()
 | |
|       }
 | |
|       this.player = new CastPlayer(this.ctx)
 | |
|       this.setPlayerListeners()
 | |
| 
 | |
|       if (this.libraryItem) {
 | |
|         // libraryItem was already loaded - prepare for cast
 | |
|         this.playWhenReady = playWhenReady
 | |
|         this.prepare()
 | |
|       }
 | |
|     } else if (!this.isCasting && !(this.player instanceof LocalAudioPlayer) && !(this.player instanceof LocalVideoPlayer)) {
 | |
|       console.log('[PlayerHandler] Switching to local player')
 | |
| 
 | |
|       this.stopPlayInterval()
 | |
|       this.playerStateChange('LOADING')
 | |
| 
 | |
|       if (this.player) {
 | |
|         this.player.destroy()
 | |
|       }
 | |
| 
 | |
|       if (this.isVideo) {
 | |
|         this.player = new LocalVideoPlayer(this.ctx)
 | |
|       } else {
 | |
|         this.player = new LocalAudioPlayer(this.ctx)
 | |
|       }
 | |
| 
 | |
|       this.setPlayerListeners()
 | |
| 
 | |
|       if (this.libraryItem) {
 | |
|         // libraryItem was already loaded - prepare for local play
 | |
|         this.playWhenReady = playWhenReady
 | |
|         this.prepare()
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   setPlayerListeners() {
 | |
|     this.player.on('stateChange', this.playerStateChange.bind(this))
 | |
|     this.player.on('timeupdate', this.playerTimeupdate.bind(this))
 | |
|     this.player.on('buffertimeUpdate', this.playerBufferTimeUpdate.bind(this))
 | |
|     this.player.on('error', this.playerError.bind(this))
 | |
|     this.player.on('finished', this.playerFinished.bind(this))
 | |
|   }
 | |
| 
 | |
|   playerError() {
 | |
|     // Switch to HLS stream on error
 | |
|     if (!this.isCasting && !this.currentStreamId && (this.player instanceof LocalAudioPlayer)) {
 | |
|       console.log(`[PlayerHandler] Audio player error switching to HLS stream`)
 | |
|       this.prepare(true)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   playerFinished() {
 | |
|     this.stopPlayInterval()
 | |
| 
 | |
|     var currentTime = this.player.getCurrentTime()
 | |
|     this.ctx.setCurrentTime(currentTime)
 | |
| 
 | |
|     // TODO: Add listening time between last sync and now?
 | |
|     this.sendProgressSync(currentTime)
 | |
|   }
 | |
| 
 | |
|   playerStateChange(state) {
 | |
|     console.log('[PlayerHandler] Player state change', state)
 | |
|     this.playerState = state
 | |
|     if (this.playerState === 'PLAYING') {
 | |
|       this.setPlaybackRate(this.initialPlaybackRate)
 | |
|       this.startPlayInterval()
 | |
|     } else {
 | |
|       this.stopPlayInterval()
 | |
|     }
 | |
|     if (this.playerState === 'LOADED' || this.playerState === 'PLAYING') {
 | |
|       this.ctx.setDuration(this.getDuration())
 | |
|     }
 | |
|     if (this.playerState !== 'LOADING') {
 | |
|       this.ctx.setCurrentTime(this.player.getCurrentTime())
 | |
|     }
 | |
| 
 | |
|     this.ctx.setPlaying(this.playerState === 'PLAYING')
 | |
|     this.ctx.playerLoading = this.playerState === 'LOADING'
 | |
|   }
 | |
| 
 | |
|   playerTimeupdate(time) {
 | |
|     this.ctx.setCurrentTime(time)
 | |
|   }
 | |
| 
 | |
|   playerBufferTimeUpdate(buffertime) {
 | |
|     this.ctx.setBufferTime(buffertime)
 | |
|   }
 | |
| 
 | |
|   async prepare(forceTranscode = false) {
 | |
|     var payload = {
 | |
|       supportedMimeTypes: this.player.playableMimeTypes,
 | |
|       mediaPlayer: this.isCasting ? 'chromecast' : 'html5',
 | |
|       forceTranscode,
 | |
|       forceDirectPlay: this.isCasting || this.isVideo // TODO: add transcode support for chromecast
 | |
|     }
 | |
| 
 | |
|     var path = this.episodeId ? `/api/items/${this.libraryItem.id}/play/${this.episodeId}` : `/api/items/${this.libraryItem.id}/play`
 | |
|     var session = await this.ctx.$axios.$post(path, payload).catch((error) => {
 | |
|       console.error('Failed to start stream', error)
 | |
|     })
 | |
|     this.prepareSession(session)
 | |
|   }
 | |
| 
 | |
|   prepareOpenSession(session, playbackRate) { // Session opened on init socket
 | |
|     this.libraryItem = session.libraryItem
 | |
|     this.isVideo = session.libraryItem.mediaType === 'video'
 | |
|     this.playWhenReady = false
 | |
|     this.initialPlaybackRate = playbackRate
 | |
| 
 | |
|     if (!this.player) this.switchPlayer()
 | |
|     this.prepareSession(session)
 | |
|   }
 | |
| 
 | |
|   prepareSession(session) {
 | |
|     this.failedProgressSyncs = 0
 | |
|     this.startTime = session.currentTime
 | |
|     this.currentSessionId = session.id
 | |
|     this.displayTitle = session.displayTitle
 | |
|     this.displayAuthor = session.displayAuthor
 | |
| 
 | |
|     console.log('[PlayerHandler] Preparing Session', session)
 | |
| 
 | |
|     if (session.videoTrack) {
 | |
|       var videoTrack = new VideoTrack(session.videoTrack, this.userToken)
 | |
| 
 | |
|       this.ctx.playerLoading = true
 | |
|       this.isHlsTranscode = true
 | |
|       if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
 | |
|         this.isHlsTranscode = false
 | |
|       }
 | |
| 
 | |
|       this.player.set(this.libraryItem, videoTrack, this.isHlsTranscode, this.startTime, this.playWhenReady)
 | |
|     } else {
 | |
|       var audioTracks = session.audioTracks.map(at => new AudioTrack(at, this.userToken))
 | |
| 
 | |
|       this.ctx.playerLoading = true
 | |
|       this.isHlsTranscode = true
 | |
|       if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
 | |
|         this.isHlsTranscode = false
 | |
|       }
 | |
| 
 | |
|       this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
 | |
|     }
 | |
| 
 | |
|     // browser media session api
 | |
|     this.ctx.setMediaSession()
 | |
|   }
 | |
| 
 | |
|   closePlayer() {
 | |
|     console.log('[PlayerHandler] Close Player')
 | |
|     this.sendCloseSession()
 | |
|     if (this.player) {
 | |
|       this.player.destroy()
 | |
|     }
 | |
|     this.player = null
 | |
|     this.playerState = 'IDLE'
 | |
|     this.libraryItem = null
 | |
|     this.startTime = 0
 | |
|     this.stopPlayInterval()
 | |
|   }
 | |
| 
 | |
|   resetStream(startTime, streamId) {
 | |
|     if (this.isHlsTranscode && this.currentSessionId === streamId) {
 | |
|       this.player.resetStream(startTime)
 | |
|     } else {
 | |
|       console.warn('resetStream mismatch streamId', this.currentSessionId, streamId)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   startPlayInterval() {
 | |
|     clearInterval(this.playInterval)
 | |
|     var lastTick = Date.now()
 | |
|     this.playInterval = setInterval(() => {
 | |
|       // Update UI
 | |
|       if (!this.player) return
 | |
|       var currentTime = this.player.getCurrentTime()
 | |
|       this.ctx.setCurrentTime(currentTime)
 | |
| 
 | |
|       var exactTimeElapsed = ((Date.now() - lastTick) / 1000)
 | |
|       lastTick = Date.now()
 | |
|       this.listeningTimeSinceSync += exactTimeElapsed
 | |
|       if (this.listeningTimeSinceSync >= 5) {
 | |
|         this.sendProgressSync(currentTime)
 | |
|       }
 | |
|     }, 1000)
 | |
|   }
 | |
| 
 | |
|   sendCloseSession() {
 | |
|     var syncData = null
 | |
|     if (this.player) {
 | |
|       var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
 | |
|       syncData = {
 | |
|         timeListened: listeningTimeToAdd,
 | |
|         duration: this.getDuration(),
 | |
|         currentTime: this.getCurrentTime()
 | |
|       }
 | |
|     }
 | |
|     this.listeningTimeSinceSync = 0
 | |
|     return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 1000 }).catch((error) => {
 | |
|       console.error('Failed to close session', error)
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   sendProgressSync(currentTime) {
 | |
|     var diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime)
 | |
|     if (diffSinceLastSync < 1) return
 | |
| 
 | |
|     this.lastSyncTime = currentTime
 | |
|     var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
 | |
|     var syncData = {
 | |
|       timeListened: listeningTimeToAdd,
 | |
|       duration: this.getDuration(),
 | |
|       currentTime
 | |
|     }
 | |
|     this.listeningTimeSinceSync = 0
 | |
|     this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 1000 }).then(() => {
 | |
|       this.failedProgressSyncs = 0
 | |
|     }).catch((error) => {
 | |
|       console.error('Failed to update session progress', error)
 | |
|       this.failedProgressSyncs++
 | |
|       if (this.failedProgressSyncs >= 2) {
 | |
|         this.ctx.showFailedProgressSyncs()
 | |
|         this.failedProgressSyncs = 0
 | |
|       }
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   stopPlayInterval() {
 | |
|     clearInterval(this.playInterval)
 | |
|     this.playInterval = null
 | |
|   }
 | |
| 
 | |
|   playPause() {
 | |
|     if (this.player) this.player.playPause()
 | |
|   }
 | |
| 
 | |
|   play() {
 | |
|     if (this.player) this.player.play()
 | |
|   }
 | |
| 
 | |
|   pause() {
 | |
|     if (this.player) this.player.pause()
 | |
|   }
 | |
| 
 | |
|   getCurrentTime() {
 | |
|     return this.player ? this.player.getCurrentTime() : 0
 | |
|   }
 | |
| 
 | |
|   getDuration() {
 | |
|     return this.player ? this.player.getDuration() : 0
 | |
|   }
 | |
| 
 | |
|   jumpBackward() {
 | |
|     if (!this.player) return
 | |
|     var currentTime = this.getCurrentTime()
 | |
|     this.seek(Math.max(0, currentTime - 10))
 | |
|   }
 | |
| 
 | |
|   jumpForward() {
 | |
|     if (!this.player) return
 | |
|     var currentTime = this.getCurrentTime()
 | |
|     this.seek(Math.min(currentTime + 10, this.getDuration()))
 | |
|   }
 | |
| 
 | |
|   setVolume(volume) {
 | |
|     if (!this.player) return
 | |
|     this.player.setVolume(volume)
 | |
|   }
 | |
| 
 | |
|   setPlaybackRate(playbackRate) {
 | |
|     this.initialPlaybackRate = playbackRate // Might be loaded from settings before player is started
 | |
|     if (!this.player) return
 | |
|     this.player.setPlaybackRate(playbackRate)
 | |
|   }
 | |
| 
 | |
|   seek(time) {
 | |
|     if (!this.player) return
 | |
|     this.player.seek(time, this.playerPlaying)
 | |
|     this.ctx.setCurrentTime(time)
 | |
| 
 | |
|     // Update progress if paused
 | |
|     if (!this.playerPlaying) {
 | |
|       this.sendProgressSync(time)
 | |
|     }
 | |
|   }
 | |
| } |