mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-03-01 13:39:41 +00:00
Prevents intro/outro skip from immediately triggering when user uses jump back, prev chapter, or seeks manually into a skip zone.
658 lines
23 KiB
Vue
658 lines
23 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)
|
||
},
|
||
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._manualSeekTime = Date.now()
|
||
this.playerHandler.jumpForward()
|
||
},
|
||
jumpBackward() {
|
||
this._manualSeekTime = Date.now()
|
||
this.playerHandler.jumpBackward()
|
||
},
|
||
setVolume(volume) {
|
||
this.playerHandler.setVolume(volume)
|
||
},
|
||
setPlaybackRate(playbackRate) {
|
||
this.currentPlaybackRate = playbackRate
|
||
this.playerHandler.setPlaybackRate(playbackRate)
|
||
},
|
||
seek(time) {
|
||
this._manualSeekTime = Date.now()
|
||
this.playerHandler.seek(time)
|
||
},
|
||
playbackTimeUpdate(time) {
|
||
// When updating progress from another session
|
||
this.playerHandler.seek(time, false)
|
||
},
|
||
setCurrentTime(time) {
|
||
this.currentTime = time
|
||
if (this.$refs.audioPlayer) {
|
||
this.$refs.audioPlayer.setCurrentTime(time)
|
||
}
|
||
|
||
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
|
||
this.checkChapterEnd()
|
||
}
|
||
|
||
// 检查章节intro/outro跳过
|
||
this.checkAndSkipIntroOutro(time)
|
||
},
|
||
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)) {
|
||
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'
|
||
}
|
||
},
|
||
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)
|
||
}
|
||
},
|
||
|
||
// 获取跳过设置
|
||
getSkipSettings() {
|
||
return {
|
||
skipIntro: this.$store.getters['user/getUserSetting']('skipIntro'),
|
||
introDuration: this.$store.getters['user/getUserSetting']('introDuration'),
|
||
skipOutro: this.$store.getters['user/getUserSetting']('skipOutro'),
|
||
outroDuration: this.$store.getters['user/getUserSetting']('outroDuration')
|
||
}
|
||
},
|
||
|
||
// 检查并执行章节intro/outro跳过
|
||
checkAndSkipIntroOutro(currentTime) {
|
||
const skipSettings = this.getSkipSettings()
|
||
if (!skipSettings) return
|
||
|
||
const doSkipIntro = skipSettings.skipIntro && skipSettings.introDuration > 0
|
||
const doSkipOutro = skipSettings.skipOutro && skipSettings.outroDuration > 0
|
||
if (!doSkipIntro && !doSkipOutro) return
|
||
if (!this.isPlaying || !this.chapters.length) return
|
||
|
||
// 用户手动seek后2秒内不触发跳过
|
||
if (this._manualSeekTime && Date.now() - this._manualSeekTime < 2000) return
|
||
|
||
// 防重入:正在跳过时等待到达目标位置后再解除
|
||
if (this._isSkipping) {
|
||
if (this._skipTarget != null && currentTime >= this._skipTarget - 0.5) {
|
||
this._isSkipping = false
|
||
this._skipTarget = null
|
||
}
|
||
return
|
||
}
|
||
|
||
const chapter = this.chapters.find((ch) => ch.start <= currentTime && currentTime < ch.end)
|
||
if (!chapter) return
|
||
|
||
const introDuration = doSkipIntro ? skipSettings.introDuration : 0
|
||
const outroDuration = doSkipOutro ? skipSettings.outroDuration : 0
|
||
|
||
const introEndTime = Math.min(chapter.start + introDuration, chapter.end)
|
||
const outroStartTime = Math.max(chapter.end - outroDuration, chapter.start)
|
||
|
||
// 短章节:intro和outro区间重叠则不跳
|
||
if (doSkipIntro && doSkipOutro && introEndTime > outroStartTime) return
|
||
|
||
// 检查是否在intro区间
|
||
if (doSkipIntro && currentTime < introEndTime) {
|
||
const target = introEndTime + 0.5
|
||
this._isSkipping = true
|
||
this._skipTarget = target
|
||
this.seek(target)
|
||
return
|
||
}
|
||
|
||
// 检查是否在outro区间
|
||
if (doSkipOutro && currentTime >= outroStartTime) {
|
||
const chapterIndex = this.chapters.indexOf(chapter)
|
||
const nextChapter = this.chapters[chapterIndex + 1]
|
||
|
||
if (nextChapter) {
|
||
// 有下一章:跳到下一章开头(如果同时开了skipIntro则跳过intro)
|
||
let target = nextChapter.start
|
||
if (doSkipIntro) {
|
||
const nextIntroEnd = Math.min(nextChapter.start + introDuration, nextChapter.end)
|
||
const nextOutroStart = Math.max(nextChapter.end - outroDuration, nextChapter.start)
|
||
// 确保下一章intro/outro不重叠
|
||
if (nextIntroEnd <= nextOutroStart) {
|
||
target = nextIntroEnd + 0.5
|
||
}
|
||
}
|
||
this._isSkipping = true
|
||
this._skipTarget = target
|
||
this.seek(target)
|
||
} else {
|
||
// 最后一章:跳到结尾,触发播放完成
|
||
this._isSkipping = true
|
||
this._skipTarget = chapter.end
|
||
this.seek(chapter.end)
|
||
}
|
||
}
|
||
}
|
||
},
|
||
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>
|