This commit is contained in:
Nate Adams 2026-02-22 20:49:33 -07:00 committed by GitHub
commit b0ce5d41ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 227 additions and 3 deletions

View file

@ -148,6 +148,10 @@ export default {
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'
@ -291,6 +295,8 @@ export default {
setPlaybackRate(playbackRate) {
this.currentPlaybackRate = playbackRate
this.playerHandler.setPlaybackRate(playbackRate)
// Update position state with new playback rate
this.updateMediaSessionPositionState()
},
seek(time) {
this.playerHandler.seek(time)
@ -300,6 +306,7 @@ export default {
this.playerHandler.seek(time, false)
},
setCurrentTime(time) {
const previousChapterId = this.currentChapter?.id
this.currentTime = time
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.setCurrentTime(time)
@ -308,6 +315,14 @@ export default {
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
@ -355,7 +370,20 @@ export default {
mediaSessionSeekTo(e) {
console.log('Media session seek to', e)
if (e.seekTime !== null && !isNaN(e.seekTime)) {
this.playerHandler.seek(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() {
@ -373,6 +401,91 @@ export default {
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')

View file

@ -56,7 +56,8 @@ export default {
listeningTimeSinceSync: 0,
coverRgb: null,
coverBgIsLight: false,
currentTime: 0
currentTime: 0,
currentPlaybackRate: 1
}
},
computed: {
@ -88,6 +89,10 @@ export default {
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
},
coverAspectRatio() {
const coverAspectRatio = this.playbackSession.coverAspectRatio
return coverAspectRatio === this.$constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1
@ -133,7 +138,20 @@ export default {
mediaSessionSeekTo(e) {
console.log('Media session seek to', e)
if (e.seekTime !== null && !isNaN(e.seekTime)) {
this.seek(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.seek(absoluteTime)
} else {
// "Use chapter track" disabled or no chapters - use full-file seek
this.seek(e.seekTime)
}
}
},
mediaSessionPreviousTrack() {
@ -151,6 +169,86 @@ export default {
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
}
const baseTitle = this.mediaItemShare.playbackSession.displayTitle || 'No title'
const chapterTitle = this.currentChapter.title
navigator.mediaSession.metadata = new MediaMetadata({
title: chapterTitle || baseTitle,
artist: this.mediaItemShare.playbackSession.displayAuthor || 'Unknown',
album: baseTitle,
artwork: [
{
src: this.coverUrl
}
]
})
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
let chapterPosition = this.currentTime - chapterStart
chapterPosition = Math.max(0, Math.min(chapterPosition, chapterDuration))
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() {
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API
if ('mediaSession' in navigator) {
@ -237,7 +335,10 @@ export default {
},
setPlaybackRate(playbackRate) {
if (!this.localAudioPlayer || !this.hasLoaded) return
this.currentPlaybackRate = playbackRate
this.localAudioPlayer.setPlaybackRate(playbackRate)
// Update position state with new playback rate
this.updateMediaSessionPositionState()
},
seek(time) {
if (!this.localAudioPlayer || !this.hasLoaded) return
@ -248,9 +349,19 @@ export default {
setCurrentTime(time) {
if (!this.$refs.audioPlayer) return
const previousChapterId = this.currentChapter?.id
// Update UI
this.$refs.audioPlayer.setCurrentTime(time)
this.currentTime = time
// 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() {
if (!this.localAudioPlayer) return