mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-04-20 22:19:44 +00:00
New data model play media entity, PlaybackSessionManager
This commit is contained in:
parent
1cf9e85272
commit
099ae7c776
54 changed files with 841 additions and 902 deletions
|
|
@ -1,29 +1,31 @@
|
|||
export default class AudioTrack {
|
||||
constructor(track) {
|
||||
constructor(track, userToken) {
|
||||
this.index = track.index || 0
|
||||
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
|
||||
this.duration = track.duration || 0
|
||||
this.title = track.metadata ? track.metadata.filename || '' : ''
|
||||
this.title = track.title || ''
|
||||
this.contentUrl = track.contentUrl || null
|
||||
this.mimeType = track.mimeType
|
||||
|
||||
this.userToken = userToken
|
||||
}
|
||||
|
||||
get fullContentUrl() {
|
||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `${process.env.serverUrl}${this.contentUrl}`
|
||||
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
return `${window.location.origin}${this.contentUrl}`
|
||||
return `${window.location.origin}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
|
||||
get relativeContentUrl() {
|
||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `${process.env.serverUrl}${this.contentUrl}`
|
||||
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
|
||||
return this.contentUrl
|
||||
return this.contentUrl + '?token=${this.userToken}'
|
||||
}
|
||||
}
|
||||
|
|
@ -12,17 +12,19 @@ export default class CastPlayer extends EventEmitter {
|
|||
this.audiobook = null
|
||||
this.audioTracks = []
|
||||
this.currentTrackIndex = 0
|
||||
this.hlsStreamId = null
|
||||
this.isHlsTranscode = null
|
||||
this.currentTime = 0
|
||||
this.playWhenReady = false
|
||||
this.defaultPlaybackRate = 1
|
||||
|
||||
this.playableMimetypes = {}
|
||||
// TODO: Use canDisplayType on receiver to check mime types
|
||||
this.playableMimeTypes = {}
|
||||
|
||||
this.coverUrl = ''
|
||||
this.castPlayerState = 'IDLE'
|
||||
|
||||
// Supported audio codecs for chromecast
|
||||
|
||||
this.supportedAudioCodecs = ['opus', 'mp3', 'aac', 'flac', 'webma', 'wav']
|
||||
|
||||
this.initialize()
|
||||
|
|
@ -68,10 +70,10 @@ export default class CastPlayer extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
async set(audiobook, tracks, hlsStreamId, startTime, playWhenReady = false) {
|
||||
async set(audiobook, tracks, isHlsTranscode, startTime, playWhenReady = false) {
|
||||
this.audiobook = audiobook
|
||||
this.audioTracks = tracks
|
||||
this.hlsStreamId = hlsStreamId
|
||||
this.isHlsTranscode = isHlsTranscode
|
||||
this.playWhenReady = playWhenReady
|
||||
|
||||
this.currentTime = startTime
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
this.libraryItem = null
|
||||
this.audioTracks = []
|
||||
this.currentTrackIndex = 0
|
||||
this.hlsStreamId = null
|
||||
this.isHlsTranscode = null
|
||||
this.hlsInstance = null
|
||||
this.usingNativeplayer = false
|
||||
this.startTime = 0
|
||||
|
|
@ -19,7 +19,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
this.playWhenReady = false
|
||||
this.defaultPlaybackRate = 1
|
||||
|
||||
this.playableMimetypes = {}
|
||||
this.playableMimeTypes = {}
|
||||
|
||||
this.initialize()
|
||||
}
|
||||
|
|
@ -48,9 +48,9 @@ export default class LocalPlayer extends EventEmitter {
|
|||
|
||||
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac']
|
||||
mimeTypes.forEach((mt) => {
|
||||
this.playableMimetypes[mt] = this.player.canPlayType(mt)
|
||||
this.playableMimeTypes[mt] = this.player.canPlayType(mt)
|
||||
})
|
||||
console.log(`[LocalPlayer] Supported mime types`, this.playableMimetypes)
|
||||
console.log(`[LocalPlayer] Supported mime types`, this.playableMimeTypes)
|
||||
}
|
||||
|
||||
evtPlay() {
|
||||
|
|
@ -80,7 +80,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
this.emit('error', error)
|
||||
}
|
||||
evtLoadedMetadata(data) {
|
||||
if (!this.hlsStreamId) {
|
||||
if (!this.isHlsTranscode) {
|
||||
this.player.currentTime = this.trackStartTime
|
||||
}
|
||||
|
||||
|
|
@ -97,23 +97,16 @@ export default class LocalPlayer extends EventEmitter {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
if (this.hlsStreamId) {
|
||||
// Close HLS Stream
|
||||
console.log('Closing HLS Streams', this.hlsStreamId)
|
||||
this.ctx.$axios.$post(`/api/streams/${this.hlsStreamId}/close`).catch((error) => {
|
||||
console.error('Failed to request close hls stream', this.hlsStreamId, error)
|
||||
})
|
||||
}
|
||||
this.destroyHlsInstance()
|
||||
if (this.player) {
|
||||
this.player.remove()
|
||||
}
|
||||
}
|
||||
|
||||
set(libraryItem, tracks, hlsStreamId, startTime, playWhenReady = false) {
|
||||
set(libraryItem, tracks, isHlsTranscode, startTime, playWhenReady = false) {
|
||||
this.libraryItem = libraryItem
|
||||
this.audioTracks = tracks
|
||||
this.hlsStreamId = hlsStreamId
|
||||
this.isHlsTranscode = isHlsTranscode
|
||||
this.playWhenReady = playWhenReady
|
||||
this.startTime = startTime
|
||||
|
||||
|
|
@ -121,7 +114,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
this.destroyHlsInstance()
|
||||
}
|
||||
|
||||
if (this.hlsStreamId) {
|
||||
if (this.isHlsTranscode) {
|
||||
this.setHlsStream()
|
||||
} else {
|
||||
this.setDirectPlay()
|
||||
|
|
@ -198,7 +191,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
async resetStream(startTime) {
|
||||
this.destroyHlsInstance()
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
this.set(this.libraryItem, this.audioTracks, this.hlsStreamId, startTime, true)
|
||||
this.set(this.libraryItem, this.audioTracks, this.isHlsTranscode, startTime, true)
|
||||
}
|
||||
|
||||
playPause() {
|
||||
|
|
@ -234,7 +227,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
|
||||
seek(time) {
|
||||
if (!this.player) return
|
||||
if (this.hlsStreamId) {
|
||||
if (this.isHlsTranscode) {
|
||||
// Seeking HLS stream
|
||||
var offsetTime = time - (this.currentTrack.startOffset || 0)
|
||||
this.player.currentTime = Math.max(0, offsetTime)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ export default class PlayerHandler {
|
|||
this.playWhenReady = false
|
||||
this.player = null
|
||||
this.playerState = 'IDLE'
|
||||
this.currentStreamId = null
|
||||
this.isHlsTranscode = false
|
||||
this.currentSessionId = null
|
||||
this.startTime = 0
|
||||
|
||||
this.lastSyncTime = 0
|
||||
|
|
@ -35,11 +36,10 @@ export default class PlayerHandler {
|
|||
return this.playerState === 'PLAYING'
|
||||
}
|
||||
|
||||
load(libraryItem, playWhenReady, startTime = 0) {
|
||||
load(libraryItem, playWhenReady) {
|
||||
if (!this.player) this.switchPlayer()
|
||||
|
||||
this.libraryItem = libraryItem
|
||||
this.startTime = startTime
|
||||
this.playWhenReady = playWhenReady
|
||||
this.prepare()
|
||||
}
|
||||
|
|
@ -125,118 +125,61 @@ export default class PlayerHandler {
|
|||
this.ctx.setBufferTime(buffertime)
|
||||
}
|
||||
|
||||
async prepare(forceHls = false) {
|
||||
var useHls = false
|
||||
|
||||
var runningTotal = 0
|
||||
|
||||
var audioTracks = (this.libraryItem.media.tracks || []).map((track) => {
|
||||
if (!track.metadata) {
|
||||
console.error('INVALID TRACK', track)
|
||||
return null
|
||||
}
|
||||
var audioTrack = new AudioTrack(track)
|
||||
audioTrack.startOffset = runningTotal
|
||||
audioTrack.contentUrl = `/s/item/${this.libraryItem.id}/${this.ctx.$encodeUriPath(track.metadata.relPath.replace(/^\//, ''))}?token=${this.userToken}`
|
||||
audioTrack.mimeType = this.getMimeTypeForTrack(track)
|
||||
audioTrack.canDirectPlay = !!this.player.playableMimetypes[audioTrack.mimeType]
|
||||
|
||||
runningTotal += audioTrack.duration
|
||||
return audioTrack
|
||||
async prepare(forceTranscode = false) {
|
||||
var payload = {
|
||||
supportedMimeTypes: Object.keys(this.player.playableMimeTypes),
|
||||
mediaPlayer: this.isCasting ? 'chromecast' : 'html5',
|
||||
forceTranscode,
|
||||
forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
|
||||
}
|
||||
var session = await this.ctx.$axios.$post(`/api/items/${this.libraryItem.id}/play`, payload).catch((error) => {
|
||||
console.error('Failed to start stream', error)
|
||||
})
|
||||
|
||||
// All html5 audio player plays use HLS unless experimental features is on
|
||||
if (!this.isCasting) {
|
||||
if (forceHls || !this.ctx.showExperimentalFeatures) {
|
||||
useHls = true
|
||||
} else {
|
||||
// Use HLS if any audio track cannot be direct played
|
||||
useHls = !!audioTracks.find(at => !at.canDirectPlay)
|
||||
|
||||
if (useHls) {
|
||||
console.warn(`[PlayerHandler] An audio track cannot be direct played`, audioTracks.find(at => !at.canDirectPlay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (useHls) {
|
||||
var stream = await this.ctx.$axios.$get(`/api/items/${this.libraryItem.id}/stream`).catch((error) => {
|
||||
console.error('Failed to start stream', error)
|
||||
})
|
||||
if (stream) {
|
||||
console.log(`[PlayerHandler] prepare hls stream`, stream)
|
||||
this.setHlsStream(stream)
|
||||
} else {
|
||||
console.error(`[PlayerHandler] Failed to start HLS stream`)
|
||||
}
|
||||
} else {
|
||||
this.setDirectPlay(audioTracks)
|
||||
}
|
||||
this.prepareSession(session)
|
||||
}
|
||||
|
||||
getMimeTypeForTrack(track) {
|
||||
var ext = track.metadata.ext
|
||||
if (ext === '.mp3' || ext === '.m4b' || ext === '.m4a') {
|
||||
return 'audio/mpeg'
|
||||
} else if (ext === '.mp4') {
|
||||
return 'audio/mp4'
|
||||
} else if (ext === '.ogg') {
|
||||
return 'audio/ogg'
|
||||
} else if (ext === '.aac' || ext === '.m4p') {
|
||||
return 'audio/aac'
|
||||
} else if (ext === '.flac') {
|
||||
return 'audio/flac'
|
||||
prepareOpenSession(session) { // Session opened on init socket
|
||||
if (!this.player) this.switchPlayer()
|
||||
|
||||
this.libraryItem = session.libraryItem
|
||||
this.playWhenReady = false
|
||||
this.prepareSession(session)
|
||||
}
|
||||
|
||||
prepareSession(session) {
|
||||
this.startTime = session.currentTime
|
||||
this.currentSessionId = session.id
|
||||
|
||||
console.log('[PlayerHandler] Preparing Session', session)
|
||||
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
|
||||
}
|
||||
return 'audio/mpeg'
|
||||
|
||||
this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
|
||||
}
|
||||
|
||||
closePlayer() {
|
||||
console.log('[PlayerHandler] Close Player')
|
||||
this.sendCloseSession()
|
||||
if (this.player) {
|
||||
this.player.destroy()
|
||||
}
|
||||
this.player = null
|
||||
this.playerState = 'IDLE'
|
||||
this.libraryItem = null
|
||||
this.currentStreamId = null
|
||||
this.startTime = 0
|
||||
this.stopPlayInterval()
|
||||
}
|
||||
|
||||
prepareStream(stream) {
|
||||
if (!this.player) this.switchPlayer()
|
||||
this.libraryItem = stream.libraryItem
|
||||
this.setHlsStream({
|
||||
streamId: stream.id,
|
||||
streamUrl: stream.clientPlaylistUri,
|
||||
startTime: stream.clientCurrentTime
|
||||
})
|
||||
}
|
||||
|
||||
setHlsStream(stream) {
|
||||
this.currentStreamId = stream.streamId
|
||||
var audioTrack = new AudioTrack({
|
||||
duration: this.libraryItem.media.duration,
|
||||
contentUrl: stream.streamUrl + '?token=' + this.userToken,
|
||||
mimeType: 'application/vnd.apple.mpegurl'
|
||||
})
|
||||
this.startTime = stream.startTime
|
||||
this.ctx.playerLoading = true
|
||||
this.player.set(this.libraryItem, [audioTrack], this.currentStreamId, stream.startTime, this.playWhenReady)
|
||||
}
|
||||
|
||||
setDirectPlay(audioTracks) {
|
||||
this.currentStreamId = null
|
||||
this.ctx.playerLoading = true
|
||||
this.player.set(this.libraryItem, audioTracks, null, this.startTime, this.playWhenReady)
|
||||
}
|
||||
|
||||
resetStream(startTime, streamId) {
|
||||
if (this.currentStreamId === streamId) {
|
||||
if (this.isHlsTranscode && this.currentSessionId === streamId) {
|
||||
this.player.resetStream(startTime)
|
||||
} else {
|
||||
console.warn('resetStream mismatch streamId', this.currentStreamId, streamId)
|
||||
console.warn('resetStream mismatch streamId', this.currentSessionId, streamId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,43 +197,39 @@ export default class PlayerHandler {
|
|||
this.listeningTimeSinceSync += exactTimeElapsed
|
||||
if (this.listeningTimeSinceSync >= 5) {
|
||||
this.sendProgressSync(currentTime)
|
||||
this.listeningTimeSinceSync = 0
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
sendCloseSession() {
|
||||
var syncData = null
|
||||
if (this.player) {
|
||||
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||
syncData = {
|
||||
timeListened: listeningTimeToAdd,
|
||||
currentTime: this.player.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
|
||||
if (this.currentStreamId) { // Updating stream progress (HLS stream)
|
||||
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||
var syncData = {
|
||||
timeListened: listeningTimeToAdd,
|
||||
currentTime,
|
||||
streamId: this.currentStreamId,
|
||||
audiobookId: this.libraryItem.id
|
||||
}
|
||||
this.ctx.$axios.$post('/api/syncStream', syncData, { timeout: 1000 }).catch((error) => {
|
||||
console.error('Failed to update stream progress', error)
|
||||
})
|
||||
} else {
|
||||
// Direct play via chromecast does not yet have backend stream session model
|
||||
// so the progress update for the libraryItem is updated this way (instead of through the stream)
|
||||
var duration = this.getDuration()
|
||||
var syncData = {
|
||||
totalDuration: duration,
|
||||
currentTime,
|
||||
progress: duration > 0 ? currentTime / duration : 0,
|
||||
isRead: false,
|
||||
audiobookId: this.libraryItem.id,
|
||||
lastUpdate: Date.now()
|
||||
}
|
||||
this.ctx.$axios.$post('/api/syncLocal', syncData, { timeout: 1000 }).catch((error) => {
|
||||
console.error('Failed to update local progress', error)
|
||||
})
|
||||
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||
var syncData = {
|
||||
timeListened: listeningTimeToAdd,
|
||||
currentTime
|
||||
}
|
||||
this.listeningTimeSinceSync = 0
|
||||
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 1000 }).catch((error) => {
|
||||
console.error('Failed to update session progress', error)
|
||||
})
|
||||
}
|
||||
|
||||
stopPlayInterval() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue