mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-03-01 05:29:41 +00:00
When "Use chapter track" is enabled, the Media Session API now reports duration and position relative to the current chapter instead of the full audiobook. This makes the OS lock screen/notification scrubber span only the current chapter. - Call setPositionState() with chapter-relative values - Map OS seek requests back to absolute file position - Update metadata with chapter title on chapter change - Fall back to full-file behavior when setting is disabled
684 lines
25 KiB
Vue
684 lines
25 KiB
Vue
<template>
|
|
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 lg:h-40 z-50 bg-primary px-2 lg:px-4 pb-1 lg:pb-4 pt-2">
|
|
<div class="absolute left-2 top-2 lg:left-4 cursor-pointer">
|
|
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
|
</div>
|
|
<div class="flex items-start mb-6 lg:mb-0" :class="isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
|
<div class="min-w-0 w-full">
|
|
<div class="flex items-center">
|
|
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
|
|
{{ title }}
|
|
</nuxt-link>
|
|
<widgets-explicit-indicator v-if="isExplicit" />
|
|
</div>
|
|
<div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
|
<span class="material-symbols text-sm">person</span>
|
|
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">{{ podcastAuthor }}</div>
|
|
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
|
|
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
|
</div>
|
|
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
|
|
</div>
|
|
|
|
<div class="text-gray-400 flex items-center">
|
|
<span class="material-symbols text-xs">schedule</span>
|
|
<p class="font-mono text-xs sm:text-sm pl-1 sm:pl-1.5 pb-px">{{ totalDurationPretty }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="grow" />
|
|
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
|
<button :aria-label="$strings.LabelClosePlayer" class="material-symbols sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
|
|
</ui-tooltip>
|
|
</div>
|
|
<player-ui
|
|
ref="audioPlayer"
|
|
:chapters="chapters"
|
|
:current-chapter="currentChapter"
|
|
:paused="!isPlaying"
|
|
:loading="playerLoading"
|
|
:bookmarks="bookmarks"
|
|
:sleep-timer-set="sleepTimerSet"
|
|
:sleep-timer-remaining="sleepTimerRemaining"
|
|
:sleep-timer-type="sleepTimerType"
|
|
:is-podcast="isPodcast"
|
|
:hasNextItemInQueue="hasNextItemInQueue"
|
|
@playPause="playPause"
|
|
@jumpForward="jumpForward"
|
|
@jumpBackward="jumpBackward"
|
|
@setVolume="setVolume"
|
|
@setPlaybackRate="setPlaybackRate"
|
|
@seek="seek"
|
|
@nextItemInQueue="playNextItemInQueue"
|
|
@close="closePlayer"
|
|
@showBookmarks="showBookmarks"
|
|
@showSleepTimer="showSleepTimerModal = true"
|
|
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
|
/>
|
|
|
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :playback-rate="currentPlaybackRate" :library-item-id="libraryItemId" @select="selectBookmark" />
|
|
|
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
|
|
|
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" />
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import PlayerHandler from '@/players/PlayerHandler'
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
playerHandler: new PlayerHandler(this),
|
|
totalDuration: 0,
|
|
showBookmarksModal: false,
|
|
bookmarkCurrentTime: 0,
|
|
playerLoading: false,
|
|
isPlaying: false,
|
|
currentTime: 0,
|
|
showSleepTimerModal: false,
|
|
showPlayerQueueItemsModal: false,
|
|
sleepTimerSet: false,
|
|
sleepTimerRemaining: 0,
|
|
sleepTimerType: null,
|
|
sleepTimer: null,
|
|
displayTitle: null,
|
|
currentPlaybackRate: 1,
|
|
syncFailedToast: null,
|
|
coverAspectRatio: 1,
|
|
lastChapterId: null
|
|
}
|
|
},
|
|
computed: {
|
|
isSquareCover() {
|
|
return this.coverAspectRatio === 1
|
|
},
|
|
isMobile() {
|
|
return this.$store.state.globals.isMobile
|
|
},
|
|
bookCoverWidth() {
|
|
if (this.isMobile) return 64 / this.coverAspectRatio
|
|
return 77 / this.coverAspectRatio
|
|
},
|
|
cover() {
|
|
if (this.media.coverPath) return this.media.coverPath
|
|
return 'Logo.png'
|
|
},
|
|
user() {
|
|
return this.$store.state.user.user
|
|
},
|
|
userMediaProgress() {
|
|
if (!this.libraryItemId) return
|
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
|
},
|
|
userItemCurrentTime() {
|
|
return this.userMediaProgress ? this.userMediaProgress.currentTime || 0 : 0
|
|
},
|
|
bookmarks() {
|
|
if (!this.libraryItemId) return []
|
|
return this.$store.getters['user/getUserBookmarksForItem'](this.libraryItemId)
|
|
},
|
|
streamLibraryItem() {
|
|
return this.$store.state.streamLibraryItem
|
|
},
|
|
streamEpisode() {
|
|
if (!this.$store.state.streamEpisodeId) return null
|
|
const episodes = this.streamLibraryItem.media.episodes || []
|
|
return episodes.find((ep) => ep.id === this.$store.state.streamEpisodeId)
|
|
},
|
|
libraryItemId() {
|
|
return this.streamLibraryItem?.id || null
|
|
},
|
|
media() {
|
|
return this.streamLibraryItem?.media || {}
|
|
},
|
|
isPodcast() {
|
|
return this.streamLibraryItem?.mediaType === 'podcast'
|
|
},
|
|
isExplicit() {
|
|
return !!this.mediaMetadata.explicit
|
|
},
|
|
mediaMetadata() {
|
|
return this.media.metadata || {}
|
|
},
|
|
chapters() {
|
|
if (this.streamEpisode) return this.streamEpisode.chapters || []
|
|
return this.media.chapters || []
|
|
},
|
|
currentChapter() {
|
|
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
|
},
|
|
useChapterTrack() {
|
|
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
|
return this.chapters.length ? _useChapterTrack : false
|
|
},
|
|
title() {
|
|
if (this.playerHandler.displayTitle) return this.playerHandler.displayTitle
|
|
return this.mediaMetadata.title || 'No Title'
|
|
},
|
|
authors() {
|
|
return this.mediaMetadata.authors || []
|
|
},
|
|
libraryId() {
|
|
return this.streamLibraryItem?.libraryId || null
|
|
},
|
|
totalDurationPretty() {
|
|
// Adjusted by playback rate
|
|
return this.$secondsToTimestamp(this.totalDuration / this.currentPlaybackRate)
|
|
},
|
|
podcastAuthor() {
|
|
if (!this.isPodcast) return null
|
|
return this.mediaMetadata.author || this.$strings.LabelUnknown
|
|
},
|
|
hasNextItemInQueue() {
|
|
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
|
|
},
|
|
currentPlayerQueueIndex() {
|
|
if (!this.libraryItemId) return -1
|
|
return this.playerQueueItems.findIndex((i) => {
|
|
if (this.streamEpisode?.id) return i.episodeId === this.streamEpisode.id
|
|
return i.libraryItemId === this.libraryItemId
|
|
})
|
|
},
|
|
playerQueueItems() {
|
|
return this.$store.state.playerQueueItems || []
|
|
}
|
|
},
|
|
methods: {
|
|
mediaFinished(libraryItemId, episodeId) {
|
|
// Play next item in queue
|
|
if (!this.playerQueueItems.length || !this.$store.state.playerQueueAutoPlay) {
|
|
// TODO: Set media finished flag so play button will play next queue item
|
|
return
|
|
}
|
|
var currentQueueIndex = this.playerQueueItems.findIndex((i) => {
|
|
if (episodeId) return i.libraryItemId === libraryItemId && i.episodeId === episodeId
|
|
return i.libraryItemId === libraryItemId
|
|
})
|
|
if (currentQueueIndex < 0) {
|
|
console.error('Media finished not found in queue - using first in queue', this.playerQueueItems)
|
|
currentQueueIndex = -1
|
|
}
|
|
if (currentQueueIndex === this.playerQueueItems.length - 1) {
|
|
console.log('Finished last item in queue')
|
|
return
|
|
}
|
|
const nextItemInQueue = this.playerQueueItems[currentQueueIndex + 1]
|
|
if (nextItemInQueue) {
|
|
this.playLibraryItem({
|
|
libraryItemId: nextItemInQueue.libraryItemId,
|
|
episodeId: nextItemInQueue.episodeId || null,
|
|
queueItems: this.playerQueueItems
|
|
})
|
|
}
|
|
},
|
|
setPlaying(isPlaying) {
|
|
this.isPlaying = isPlaying
|
|
this.$store.commit('setIsPlaying', isPlaying)
|
|
this.updateMediaSessionPlaybackState()
|
|
},
|
|
setSleepTimer(time) {
|
|
this.sleepTimerSet = true
|
|
this.showSleepTimerModal = false
|
|
|
|
this.sleepTimerType = time.timerType
|
|
if (this.sleepTimerType === this.$constants.SleepTimerTypes.COUNTDOWN) {
|
|
this.runSleepTimer(time)
|
|
}
|
|
},
|
|
runSleepTimer(time) {
|
|
this.sleepTimerRemaining = time.seconds
|
|
|
|
var lastTick = Date.now()
|
|
clearInterval(this.sleepTimer)
|
|
this.sleepTimer = setInterval(() => {
|
|
var elapsed = Date.now() - lastTick
|
|
lastTick = Date.now()
|
|
this.sleepTimerRemaining -= elapsed / 1000
|
|
|
|
if (this.sleepTimerRemaining <= 0) {
|
|
this.sleepTimerEnd()
|
|
}
|
|
}, 1000)
|
|
},
|
|
checkChapterEnd() {
|
|
if (!this.currentChapter) return
|
|
|
|
// Track chapter transitions by comparing current chapter with last chapter
|
|
if (this.lastChapterId !== this.currentChapter.id) {
|
|
// Chapter changed - if we had a previous chapter, this means we crossed a boundary
|
|
if (this.lastChapterId) {
|
|
this.sleepTimerEnd()
|
|
}
|
|
this.lastChapterId = this.currentChapter.id
|
|
}
|
|
},
|
|
sleepTimerEnd() {
|
|
this.clearSleepTimer()
|
|
this.playerHandler.pause()
|
|
this.$toast.info(this.$strings.ToastSleepTimerDone)
|
|
},
|
|
cancelSleepTimer() {
|
|
this.showSleepTimerModal = false
|
|
this.clearSleepTimer()
|
|
},
|
|
clearSleepTimer() {
|
|
clearInterval(this.sleepTimer)
|
|
this.sleepTimerRemaining = 0
|
|
this.sleepTimer = null
|
|
this.sleepTimerSet = false
|
|
this.sleepTimerType = null
|
|
},
|
|
incrementSleepTimer(amount) {
|
|
if (!this.sleepTimerSet) return
|
|
this.sleepTimerRemaining += amount
|
|
},
|
|
decrementSleepTimer(amount) {
|
|
if (this.sleepTimerRemaining < amount) {
|
|
this.sleepTimerRemaining = 3
|
|
return
|
|
}
|
|
this.sleepTimerRemaining = Math.max(0, this.sleepTimerRemaining - amount)
|
|
},
|
|
playPause() {
|
|
this.playerHandler.playPause()
|
|
},
|
|
jumpForward() {
|
|
this.playerHandler.jumpForward()
|
|
},
|
|
jumpBackward() {
|
|
this.playerHandler.jumpBackward()
|
|
},
|
|
setVolume(volume) {
|
|
this.playerHandler.setVolume(volume)
|
|
},
|
|
setPlaybackRate(playbackRate) {
|
|
this.currentPlaybackRate = playbackRate
|
|
this.playerHandler.setPlaybackRate(playbackRate)
|
|
// Update position state with new playback rate
|
|
this.updateMediaSessionPositionState()
|
|
},
|
|
seek(time) {
|
|
this.playerHandler.seek(time)
|
|
},
|
|
playbackTimeUpdate(time) {
|
|
// When updating progress from another session
|
|
this.playerHandler.seek(time, false)
|
|
},
|
|
setCurrentTime(time) {
|
|
const previousChapterId = this.currentChapter?.id
|
|
this.currentTime = time
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.setCurrentTime(time)
|
|
}
|
|
|
|
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
|
|
this.checkChapterEnd()
|
|
}
|
|
|
|
// Update MediaSession position state (chapter-relative when useChapterTrack enabled)
|
|
this.updateMediaSessionPositionState()
|
|
|
|
// If chapter changed and useChapterTrack enabled, update MediaSession metadata
|
|
if (this.useChapterTrack && this.currentChapter?.id !== previousChapterId && this.currentChapter) {
|
|
this.updateMediaSessionForChapter()
|
|
}
|
|
},
|
|
setDuration(duration) {
|
|
this.totalDuration = duration
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.setDuration(duration)
|
|
}
|
|
},
|
|
setBufferTime(buffertime) {
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.setBufferTime(buffertime)
|
|
}
|
|
},
|
|
showBookmarks() {
|
|
this.bookmarkCurrentTime = this.currentTime
|
|
this.showBookmarksModal = true
|
|
},
|
|
selectBookmark(bookmark) {
|
|
this.seek(bookmark.time)
|
|
this.showBookmarksModal = false
|
|
},
|
|
closePlayer() {
|
|
this.playerHandler.closePlayer()
|
|
this.$store.commit('setMediaPlaying', null)
|
|
},
|
|
mediaSessionPlay() {
|
|
console.log('Media session play')
|
|
this.playerHandler.play()
|
|
},
|
|
mediaSessionPause() {
|
|
console.log('Media session pause')
|
|
this.playerHandler.pause()
|
|
},
|
|
mediaSessionStop() {
|
|
console.log('Media session stop')
|
|
this.playerHandler.pause()
|
|
},
|
|
mediaSessionSeekBackward() {
|
|
console.log('Media session seek backward')
|
|
this.playerHandler.jumpBackward()
|
|
},
|
|
mediaSessionSeekForward() {
|
|
console.log('Media session seek forward')
|
|
this.playerHandler.jumpForward()
|
|
},
|
|
mediaSessionSeekTo(e) {
|
|
console.log('Media session seek to', e)
|
|
if (e.seekTime !== null && !isNaN(e.seekTime)) {
|
|
// When "Use chapter track" is enabled and chapters exist, seekTime is
|
|
// relative to current chapter start. Map it back to absolute position.
|
|
if (this.useChapterTrack && this.currentChapter) {
|
|
const chapterStart = this.currentChapter.start
|
|
const chapterEnd = this.currentChapter.end
|
|
const chapterDuration = chapterEnd - chapterStart
|
|
// Clamp seekTime to chapter bounds to prevent seeking outside chapter
|
|
const clampedSeekTime = Math.max(0, Math.min(e.seekTime, chapterDuration))
|
|
const absoluteTime = chapterStart + clampedSeekTime
|
|
this.playerHandler.seek(absoluteTime)
|
|
} else {
|
|
// "Use chapter track" disabled or no chapters - use full-file seek
|
|
this.playerHandler.seek(e.seekTime)
|
|
}
|
|
}
|
|
},
|
|
mediaSessionPreviousTrack() {
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.prevChapter()
|
|
}
|
|
},
|
|
mediaSessionNextTrack() {
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.nextChapter()
|
|
}
|
|
},
|
|
updateMediaSessionPlaybackState() {
|
|
if ('mediaSession' in navigator) {
|
|
navigator.mediaSession.playbackState = this.isPlaying ? 'playing' : 'paused'
|
|
}
|
|
},
|
|
/**
|
|
* Update MediaSession metadata when chapter changes (only if useChapterTrack is enabled).
|
|
* Updates the title to show chapter info and resets position state.
|
|
*/
|
|
updateMediaSessionForChapter() {
|
|
if (!('mediaSession' in navigator) || !this.useChapterTrack || !this.currentChapter) {
|
|
return
|
|
}
|
|
|
|
// Update metadata with chapter title
|
|
const baseTitle = this.title
|
|
const chapterTitle = this.currentChapter.title
|
|
|
|
navigator.mediaSession.metadata = new MediaMetadata({
|
|
title: chapterTitle || baseTitle,
|
|
artist: this.playerHandler.displayAuthor || this.mediaMetadata.authorName || 'Unknown',
|
|
album: baseTitle,
|
|
artwork: [
|
|
{
|
|
src: this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
|
|
}
|
|
]
|
|
})
|
|
|
|
// Immediately update position state for new chapter
|
|
this.updateMediaSessionPositionState()
|
|
},
|
|
/**
|
|
* Update MediaSession position state.
|
|
* When "Use chapter track" is enabled and a chapter is active, reports
|
|
* duration/position relative to chapter bounds so the OS scrubber spans
|
|
* only the current chapter. Otherwise uses full-file duration/position.
|
|
*/
|
|
updateMediaSessionPositionState() {
|
|
if (!('mediaSession' in navigator) || !navigator.mediaSession.setPositionState) {
|
|
return
|
|
}
|
|
|
|
const playbackRate = this.currentPlaybackRate || 1
|
|
|
|
if (this.useChapterTrack && this.currentChapter) {
|
|
// "Use chapter track" enabled - report chapter-relative values
|
|
const chapterStart = this.currentChapter.start
|
|
const chapterEnd = this.currentChapter.end
|
|
const chapterDuration = chapterEnd - chapterStart
|
|
|
|
// Calculate position relative to chapter start
|
|
// Clamp to valid range to handle slight timing drift
|
|
let chapterPosition = this.currentTime - chapterStart
|
|
chapterPosition = Math.max(0, Math.min(chapterPosition, chapterDuration))
|
|
|
|
// Validate values to prevent NaN or invalid states
|
|
if (isNaN(chapterDuration) || chapterDuration <= 0 || isNaN(chapterPosition)) {
|
|
console.warn('Invalid chapter position state values, skipping update')
|
|
return
|
|
}
|
|
|
|
try {
|
|
navigator.mediaSession.setPositionState({
|
|
duration: chapterDuration,
|
|
position: chapterPosition,
|
|
playbackRate: playbackRate
|
|
})
|
|
} catch (e) {
|
|
console.error('Error setting media session position state:', e)
|
|
}
|
|
} else if (this.totalDuration > 0) {
|
|
// "Use chapter track" disabled or no chapters - use full-file values
|
|
const position = Math.max(0, Math.min(this.currentTime, this.totalDuration))
|
|
|
|
if (isNaN(this.totalDuration) || isNaN(position)) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
navigator.mediaSession.setPositionState({
|
|
duration: this.totalDuration,
|
|
position: position,
|
|
playbackRate: playbackRate
|
|
})
|
|
} catch (e) {
|
|
console.error('Error setting media session position state:', e)
|
|
}
|
|
}
|
|
},
|
|
setMediaSession() {
|
|
if (!this.streamLibraryItem) {
|
|
console.error('setMediaSession: No library item set')
|
|
return
|
|
}
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API
|
|
if ('mediaSession' in navigator) {
|
|
const chapterInfo = []
|
|
if (this.chapters.length) {
|
|
this.chapters.forEach((chapter) => {
|
|
chapterInfo.push({
|
|
title: chapter.title,
|
|
startTime: chapter.start
|
|
})
|
|
})
|
|
}
|
|
|
|
navigator.mediaSession.metadata = new MediaMetadata({
|
|
title: this.title,
|
|
artist: this.playerHandler.displayAuthor || this.mediaMetadata.authorName || 'Unknown',
|
|
album: this.mediaMetadata.seriesName || '',
|
|
artwork: [
|
|
{
|
|
src: this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
|
|
}
|
|
],
|
|
chapterInfo
|
|
})
|
|
console.log('Set media session metadata', navigator.mediaSession.metadata)
|
|
|
|
navigator.mediaSession.setActionHandler('play', this.mediaSessionPlay)
|
|
navigator.mediaSession.setActionHandler('pause', this.mediaSessionPause)
|
|
navigator.mediaSession.setActionHandler('stop', this.mediaSessionStop)
|
|
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
|
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
|
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
|
navigator.mediaSession.setActionHandler('previoustrack', this.mediaSessionSeekBackward)
|
|
navigator.mediaSession.setActionHandler('nexttrack', this.mediaSessionSeekForward)
|
|
} else {
|
|
console.warn('Media session not available')
|
|
}
|
|
},
|
|
streamProgress(data) {
|
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) {
|
|
if (!data.numSegments) return
|
|
var chunks = data.chunks
|
|
console.log(`[MediaPlayerContainer] Stream Progress ${data.percent}`)
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
|
} else {
|
|
console.error('No Audio Ref')
|
|
}
|
|
}
|
|
},
|
|
sessionOpen(session) {
|
|
// For opening session on init (temporarily unused)
|
|
this.$store.commit('setMediaPlaying', {
|
|
libraryItem: session.libraryItem,
|
|
episodeId: session.episodeId
|
|
})
|
|
this.playerHandler.prepareOpenSession(session, this.currentPlaybackRate)
|
|
},
|
|
streamOpen(session) {
|
|
console.log(`[MediaPlayerContainer] Stream session open`, session)
|
|
},
|
|
streamClosed(streamId) {
|
|
// Stream was closed from the server
|
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
|
|
console.warn('[MediaPlayerContainer] Closing stream due to request from server')
|
|
this.playerHandler.closePlayer()
|
|
}
|
|
},
|
|
streamReady() {
|
|
console.log(`[MediaPlayerContainer] Stream Ready`)
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.setStreamReady()
|
|
} else {
|
|
console.error('No Audio Ref')
|
|
}
|
|
},
|
|
streamError(streamId) {
|
|
// Stream had critical error from the server
|
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
|
|
console.warn('[MediaPlayerContainer] Closing stream due to stream error from server')
|
|
this.playerHandler.closePlayer()
|
|
}
|
|
},
|
|
streamReset({ startTime, streamId }) {
|
|
this.playerHandler.resetStream(startTime, streamId)
|
|
},
|
|
castSessionActive(isActive) {
|
|
if (isActive && this.playerHandler.isPlayingLocalItem) {
|
|
// Cast session started switch to cast player
|
|
this.playerHandler.switchPlayer()
|
|
} else if (!isActive && this.playerHandler.isPlayingCastedItem) {
|
|
// Cast session ended switch to local player
|
|
this.playerHandler.switchPlayer()
|
|
}
|
|
},
|
|
playNextItemInQueue() {
|
|
if (this.hasNextItemInQueue) {
|
|
this.playQueueItem({ index: this.currentPlayerQueueIndex + 1 })
|
|
}
|
|
},
|
|
/**
|
|
* @param {{ index: number }} payload
|
|
*/
|
|
playQueueItem(payload) {
|
|
if (payload?.index === undefined) {
|
|
console.error('playQueueItem: No index provided')
|
|
return
|
|
}
|
|
if (!this.playerQueueItems[payload.index]) {
|
|
console.error('playQueueItem: No item found at index', payload.index)
|
|
return
|
|
}
|
|
const item = this.playerQueueItems[payload.index]
|
|
this.playLibraryItem({
|
|
libraryItemId: item.libraryItemId,
|
|
episodeId: item.episodeId || null,
|
|
queueItems: this.playerQueueItems
|
|
})
|
|
},
|
|
async playLibraryItem(payload) {
|
|
const libraryItemId = payload.libraryItemId
|
|
const episodeId = payload.episodeId || null
|
|
|
|
if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) {
|
|
if (payload.startTime !== null && !isNaN(payload.startTime)) {
|
|
this.seek(payload.startTime)
|
|
} else {
|
|
this.playerHandler.play()
|
|
}
|
|
return
|
|
}
|
|
|
|
const libraryItem = await this.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
|
console.error('Failed to fetch full item', error)
|
|
return null
|
|
})
|
|
if (!libraryItem) return
|
|
|
|
this.$store.commit('setMediaPlaying', {
|
|
libraryItem,
|
|
episodeId,
|
|
queueItems: payload.queueItems || []
|
|
})
|
|
// Set cover aspect ratio for this item's library since the library may change
|
|
this.coverAspectRatio = this.$store.getters['libraries/getBookCoverAspectRatio']
|
|
|
|
this.$nextTick(() => {
|
|
if (this.$refs.audioPlayer) this.$refs.audioPlayer.checkUpdateChapterTrack()
|
|
})
|
|
|
|
this.playerHandler.load(libraryItem, episodeId, true, this.currentPlaybackRate, payload.startTime)
|
|
},
|
|
pauseItem() {
|
|
this.playerHandler.pause()
|
|
},
|
|
showFailedProgressSyncs() {
|
|
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
|
|
this.syncFailedToast = this.$toast(this.$strings.ToastProgressIsNotBeingSynced, { timeout: false, type: 'error' })
|
|
},
|
|
sessionClosedEvent(sessionId) {
|
|
if (this.playerHandler.currentSessionId === sessionId) {
|
|
console.log('sessionClosedEvent closing current session', sessionId)
|
|
this.playerHandler.resetPlayer() // Closes player without reporting to server
|
|
this.$store.commit('setMediaPlaying', null)
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
this.$eventBus.$on('cast-session-active', this.castSessionActive)
|
|
this.$eventBus.$on('playback-seek', this.seek)
|
|
this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate)
|
|
this.$eventBus.$on('play-queue-item', this.playQueueItem)
|
|
this.$eventBus.$on('play-item', this.playLibraryItem)
|
|
this.$eventBus.$on('pause-item', this.pauseItem)
|
|
},
|
|
beforeDestroy() {
|
|
this.$eventBus.$off('cast-session-active', this.castSessionActive)
|
|
this.$eventBus.$off('playback-seek', this.seek)
|
|
this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate)
|
|
this.$eventBus.$off('play-queue-item', this.playQueueItem)
|
|
this.$eventBus.$off('play-item', this.playLibraryItem)
|
|
this.$eventBus.$off('pause-item', this.pauseItem)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
#mediaPlayerContainer {
|
|
box-shadow: 0px -6px 8px #1111113f;
|
|
}
|
|
</style>
|