Add:Experimental direct play support

This commit is contained in:
advplyr 2022-03-05 15:37:30 -06:00
parent eb82d9c300
commit ecf62c5443
5 changed files with 155 additions and 32 deletions

View File

@ -114,7 +114,8 @@ export default {
showChaptersModal: false,
currentTime: 0,
trackOffsetLeft: 16, // Track is 16px from edge
duration: 0
duration: 0,
chapterTicks: []
}
},
computed: {
@ -138,7 +139,6 @@ export default {
},
timeRemainingPretty() {
if (this.timeRemaining < 0) {
console.warn('Time remaining < 0', this.duration, this.currentTime, this.timeRemaining)
return this.$secondsToTimestamp(this.timeRemaining * -1)
}
return '-' + this.$secondsToTimestamp(this.timeRemaining)
@ -147,15 +147,6 @@ export default {
if (!this.duration) return 0
return Math.round((100 * this.currentTime) / this.duration)
},
chapterTicks() {
return this.chapters.map((chap) => {
var perc = chap.start / this.duration
return {
title: chap.title,
left: perc * this.trackWidth
}
})
},
currentChapter() {
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
},
@ -169,6 +160,14 @@ export default {
methods: {
setDuration(duration) {
this.duration = duration
this.chapterTicks = this.chapters.map((chap) => {
var perc = chap.start / this.duration
return {
title: chap.title,
left: perc * this.trackWidth
}
})
},
setCurrentTime(time) {
this.currentTime = time

View File

@ -72,6 +72,9 @@ export default {
}
},
computed: {
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
coverAspectRatio() {
return this.$store.getters['getServerSetting']('coverAspectRatio')
},

View File

@ -17,6 +17,8 @@ export default class CastPlayer extends EventEmitter {
this.playWhenReady = false
this.defaultPlaybackRate = 1
this.playableMimetypes = {}
this.coverUrl = ''
this.castPlayerState = 'IDLE'

View File

@ -14,10 +14,13 @@ export default class LocalPlayer extends EventEmitter {
this.hlsStreamId = null
this.hlsInstance = null
this.usingNativeplayer = false
this.currentTime = 0
this.startTime = 0
this.trackStartTime = 0
this.playWhenReady = false
this.defaultPlaybackRate = 1
this.playableMimetypes = {}
this.initialize()
}
@ -38,9 +41,16 @@ export default class LocalPlayer extends EventEmitter {
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']
mimeTypes.forEach((mt) => {
this.playableMimetypes[mt] = this.player.canPlayType(mt)
})
console.log(`[LocalPlayer] Supported mime types`, this.playableMimetypes)
}
evtPlay() {
@ -53,11 +63,27 @@ export default class LocalPlayer extends EventEmitter {
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) {
console.log('Audio Loaded Metadata', data)
if (!this.hlsStreamId) {
this.player.currentTime = this.trackStartTime
}
this.emit('stateChange', 'LOADED')
if (this.playWhenReady) {
this.playWhenReady = false
@ -89,23 +115,33 @@ export default class LocalPlayer extends EventEmitter {
this.audioTracks = tracks
this.hlsStreamId = hlsStreamId
this.playWhenReady = playWhenReady
this.startTime = startTime
if (this.hlsInstance) {
this.destroyHlsInstance()
}
this.currentTime = startTime
if (this.hlsStreamId) {
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.currentTime
this.player.currentTime = this.startTime
return
}
var hlsOptions = {
startPosition: this.currentTime || -1
startPosition: this.startTime || -1
// No longer needed because token is put in a query string
// xhrSetup: (xhr) => {
// xhr.setRequestHeader('Authorization', `Bearer ${this.token}`)
@ -133,6 +169,23 @@ export default class LocalPlayer extends EventEmitter {
})
}
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) {
@ -181,8 +234,31 @@ export default class LocalPlayer extends EventEmitter {
seek(time) {
if (!this.player) return
var offsetTime = time - (this.currentTrack.startOffset || 0)
this.player.currentTime = Math.max(0, offsetTime)
if (this.hlsStreamId) {
// 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) {

View File

@ -38,7 +38,6 @@ export default class PlayerHandler {
load(audiobook, playWhenReady, startTime = 0) {
if (!this.player) this.switchPlayer()
console.log('Load audiobook', audiobook)
this.audiobook = audiobook
this.startTime = startTime
this.playWhenReady = playWhenReady
@ -88,6 +87,15 @@ export default class PlayerHandler {
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))
}
playerError() {
// Switch to HLS stream on error
if (!this.isCasting && !this.currentStreamId && (this.player instanceof LocalPlayer)) {
console.log(`[PlayerHandler] Audio player error switching to HLS stream`)
this.prepare(true)
}
}
playerStateChange(state) {
@ -117,8 +125,36 @@ export default class PlayerHandler {
this.ctx.setBufferTime(buffertime)
}
async prepare() {
var useHls = !this.isCasting
async prepare(forceHls = false) {
var useHls = false
var runningTotal = 0
var audioTracks = (this.audiobook.tracks || []).map((track) => {
var audioTrack = new AudioTrack(track)
audioTrack.startOffset = runningTotal
audioTrack.contentUrl = `/lib/${this.audiobook.libraryId}/${this.audiobook.folderId}/${track.path}?token=${this.userToken}`
audioTrack.mimeType = this.getMimeTypeForTrack(track)
audioTrack.canDirectPlay = !!this.player.playableMimetypes[audioTrack.mimeType]
runningTotal += audioTrack.duration
return audioTrack
})
// 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/books/${this.audiobook.id}/stream`).catch((error) => {
console.error('Failed to start stream', error)
@ -126,23 +162,30 @@ export default class PlayerHandler {
if (stream) {
console.log(`[PlayerHandler] prepare hls stream`, stream)
this.setHlsStream(stream)
} else {
console.error(`[PlayerHandler] Failed to start HLS stream`)
}
} else {
// Setup tracks
var runningTotal = 0
var audioTracks = (this.audiobook.tracks || []).map((track) => {
var audioTrack = new AudioTrack(track)
audioTrack.startOffset = runningTotal
audioTrack.contentUrl = `/lib/${this.audiobook.libraryId}/${this.audiobook.folderId}/${track.path}?token=${this.userToken}`
audioTrack.mimeType = (track.codec === 'm4b' || track.codec === 'm4a') ? 'audio/mp4' : `audio/${track.codec}`
runningTotal += audioTrack.duration
return audioTrack
})
this.setDirectPlay(audioTracks)
}
}
getMimeTypeForTrack(track) {
var ext = track.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'
}
return 'audio/mpeg'
}
closePlayer() {
console.log('[PlayerHandler] CLose Player')
if (this.player) {