mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-03 10:28:51 +02:00
Fix regain focus check server progress for local media items open in player & sync more recent
This commit is contained in:
@@ -69,22 +69,22 @@
|
||||
|
||||
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
|
||||
<div class="flex items-center max-w-full" :class="playerSettings.lockUi ? 'justify-center' : 'justify-between'">
|
||||
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
||||
<div v-show="!playerSettings.lockUi" class="jump-icon text-fg cursor-pointer flex flex-col items-center" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">
|
||||
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="showLoadingState ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
||||
<div v-show="!playerSettings.lockUi" class="jump-icon text-fg cursor-pointer flex flex-col items-center" :class="showLoadingState ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">
|
||||
<span class="material-symbols text-3xl leading-none">replay</span>
|
||||
<span v-if="showFullscreen" class="jump-label text-[10px] font-semibold leading-tight">{{ jumpBackwardsLabel }}</span>
|
||||
</div>
|
||||
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
||||
<div v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" />
|
||||
|
||||
<span v-if="!isLoading" class="material-symbols fill" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
||||
<span v-if="!showLoadingState" class="material-symbols fill" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
||||
<widgets-spinner-icon v-else class="h-8 w-8" />
|
||||
</div>
|
||||
<div v-show="!playerSettings.lockUi" class="jump-icon text-fg cursor-pointer flex flex-col items-center" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">
|
||||
<div v-show="!playerSettings.lockUi" class="jump-icon text-fg cursor-pointer flex flex-col items-center" :class="showLoadingState ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">
|
||||
<span class="material-symbols text-3xl leading-none">forward_media</span>
|
||||
<span v-if="showFullscreen" class="jump-label text-[10px] font-semibold leading-tight">{{ jumpForwardLabel }}</span>
|
||||
</div>
|
||||
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
||||
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="nextChapter && !showLoadingState ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<div class="flex-grow" />
|
||||
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
|
||||
</div>
|
||||
<div ref="track" class="h-1.5 w-full bg-track/50 relative rounded-full" :class="{ 'animate-pulse': isLoading }" @click.stop>
|
||||
<div ref="track" class="h-1.5 w-full bg-track/50 relative rounded-full" :class="{ 'animate-pulse': showLoadingState }" @click.stop>
|
||||
<div ref="readyTrack" class="h-full bg-track-buffered absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||
<div ref="bufferedTrack" class="h-full bg-track absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||
<div ref="playedTrack" class="h-full bg-track-cursor absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||
@@ -157,6 +157,7 @@ export default {
|
||||
lockUi: false
|
||||
},
|
||||
isLoading: false,
|
||||
isCheckingServerProgress: false,
|
||||
isDraggingCursor: false,
|
||||
draggingTouchStartX: 0,
|
||||
draggingTouchStartTime: 0,
|
||||
@@ -270,6 +271,9 @@ export default {
|
||||
return 190 * heightScale
|
||||
}
|
||||
},
|
||||
showLoadingState() {
|
||||
return this.isLoading || this.isCheckingServerProgress
|
||||
},
|
||||
showCastBtn() {
|
||||
return this.$store.state.isCastAvailable
|
||||
},
|
||||
@@ -461,13 +465,13 @@ export default {
|
||||
},
|
||||
async jumpNextChapter() {
|
||||
await this.$hapticsImpact()
|
||||
if (this.isLoading) return
|
||||
if (this.showLoadingState) return
|
||||
if (!this.nextChapter) return
|
||||
this.seek(this.nextChapter.start)
|
||||
},
|
||||
async jumpChapterStart() {
|
||||
await this.$hapticsImpact()
|
||||
if (this.isLoading) return
|
||||
if (this.showLoadingState) return
|
||||
if (!this.currentChapter) {
|
||||
return this.restart()
|
||||
}
|
||||
@@ -497,12 +501,12 @@ export default {
|
||||
},
|
||||
async jumpBackwards() {
|
||||
await this.$hapticsImpact()
|
||||
if (this.isLoading) return
|
||||
if (this.showLoadingState) return
|
||||
AbsAudioPlayer.seekBackward({ value: this.jumpBackwardsTime })
|
||||
},
|
||||
async jumpForward() {
|
||||
await this.$hapticsImpact()
|
||||
if (this.isLoading) return
|
||||
if (this.showLoadingState) return
|
||||
AbsAudioPlayer.seekForward({ value: this.jumpForwardTime })
|
||||
},
|
||||
setStreamReady() {
|
||||
@@ -606,7 +610,7 @@ export default {
|
||||
}
|
||||
},
|
||||
seek(time) {
|
||||
if (this.isLoading) return
|
||||
if (this.showLoadingState) return
|
||||
if (this.seekLoading) {
|
||||
console.error('Already seek loading', this.seekedTime)
|
||||
return
|
||||
@@ -638,11 +642,14 @@ export default {
|
||||
},
|
||||
async playPauseClick() {
|
||||
await this.$hapticsImpact()
|
||||
if (this.isLoading) return
|
||||
if (this.showLoadingState) return
|
||||
|
||||
this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing
|
||||
this.isEnded = false
|
||||
},
|
||||
setIsCheckingServerProgress(value) {
|
||||
this.isCheckingServerProgress = !!value
|
||||
},
|
||||
play() {
|
||||
AbsAudioPlayer.playPlayer()
|
||||
this.startPlayInterval()
|
||||
|
||||
@@ -304,48 +304,102 @@ export default {
|
||||
this.$refs.audioPlayer?.seek(currentTime)
|
||||
},
|
||||
/**
|
||||
* When device gains focus then refresh the timestamps in the audio player
|
||||
* Fetch the current user's media progress from the server for a given library item / episode.
|
||||
* Returns the server media progress object, or null if the request fails, times out, or the
|
||||
* response doesn't match the requested library item.
|
||||
*
|
||||
* The audio player's loading state is shown while the request is in flight so the user
|
||||
* doesn't tap play before we have a chance to update the timestamps. The request timeout
|
||||
* is 7 seconds so a slow/unresponsive server doesn't block the user for long.
|
||||
*/
|
||||
deviceFocused(hasFocus) {
|
||||
if (!this.$store.state.currentPlaybackSession) return
|
||||
async getServerMediaProgress({ libraryItemId, episodeId }) {
|
||||
if (!libraryItemId) return null
|
||||
const url = episodeId ? `/api/me/progress/${libraryItemId}/${episodeId}` : `/api/me/progress/${libraryItemId}`
|
||||
|
||||
if (hasFocus) {
|
||||
this.$refs.audioPlayer?.setIsCheckingServerProgress(true)
|
||||
try {
|
||||
const data = await this.$nativeHttp.get(url, { connectTimeout: 7000, readTimeout: 7000 })
|
||||
if (!data || data.libraryItemId !== libraryItemId) return null
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('[AudioPlayerContainer] Failed to get server media progress', error)
|
||||
return null
|
||||
} finally {
|
||||
this.$refs.audioPlayer?.setIsCheckingServerProgress(false)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* When device gains focus then refresh the timestamps in the audio player
|
||||
* if local item is open then fetch the server media progress and update if more recent
|
||||
*/
|
||||
async deviceFocused(hasFocus) {
|
||||
if (!this.$store.state.currentPlaybackSession) return
|
||||
if (!hasFocus) return
|
||||
// dont refresh timestamps if player is playing
|
||||
if (this.$refs.audioPlayer?.isPlaying) return
|
||||
|
||||
const playbackSession = this.$store.state.currentPlaybackSession
|
||||
if (this.$refs.audioPlayer.isLocalPlayMethod) {
|
||||
const localLibraryItemId = playbackSession.localLibraryItem?.id
|
||||
const localEpisodeId = playbackSession.localEpisodeId
|
||||
if (!localLibraryItemId) {
|
||||
console.error('[AudioPlayerContainer] device visibility: no local library item for session', JSON.stringify(playbackSession))
|
||||
return
|
||||
}
|
||||
const localMediaProgress = this.$store.state.globals.localMediaProgress.find((mp) => {
|
||||
if (localEpisodeId) return mp.localEpisodeId === localEpisodeId
|
||||
return mp.localLibraryItemId === localLibraryItemId
|
||||
})
|
||||
if (!localMediaProgress) {
|
||||
console.error('[AudioPlayerContainer] device visibility: Local media progress not found')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[AudioPlayerContainer] device visibility: found local media progress', localMediaProgress.currentTime, 'last time in player is', this.currentTime)
|
||||
this.$refs.audioPlayer.currentTime = localMediaProgress.currentTime
|
||||
this.$refs.audioPlayer.timeupdate()
|
||||
|
||||
// If the local item came from a server and we're connected, check whether the
|
||||
// server's progress is more recent (e.g. user kept listening on the server)
|
||||
// and if so, update the player time and sync the server progress to local DB.
|
||||
const serverLibraryItemId = playbackSession.libraryItemId
|
||||
const serverEpisodeId = playbackSession.episodeId
|
||||
if (!serverLibraryItemId || !this.$store.state.user.user || !this.$store.state.networkConnected) return
|
||||
|
||||
console.log('[AudioPlayerContainer] device visibility: checking server media progress for local item', serverLibraryItemId, serverEpisodeId)
|
||||
const data = await this.getServerMediaProgress({ libraryItemId: serverLibraryItemId, episodeId: serverEpisodeId })
|
||||
if (!data || !data.lastUpdate || data.lastUpdate <= localMediaProgress.lastUpdate) return
|
||||
|
||||
console.log('[AudioPlayerContainer] device visibility: server progress is more recent for local item', data.currentTime, 'vs local', localMediaProgress.currentTime, `(server lastUpdate=${data.lastUpdate} > local lastUpdate=${localMediaProgress.lastUpdate})`)
|
||||
if (!this.$refs.audioPlayer?.isPlaying && data.currentTime !== localMediaProgress.currentTime) {
|
||||
// Use seek() so the native audio player's current session is updated
|
||||
this.$refs.audioPlayer.seek(data.currentTime)
|
||||
}
|
||||
|
||||
try {
|
||||
const newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress({
|
||||
localMediaProgressId: localMediaProgress.id,
|
||||
mediaProgress: data
|
||||
})
|
||||
if (newLocalMediaProgress?.id) {
|
||||
this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AudioPlayerContainer] device visibility: Failed to sync server progress to local', error)
|
||||
}
|
||||
} else {
|
||||
// server item so fetch server media progress and update player time
|
||||
const libraryItemId = playbackSession.libraryItemId
|
||||
const episodeId = playbackSession.episodeId
|
||||
console.log('[AudioPlayerContainer] device visibility: checking server media progress for server item', libraryItemId, episodeId)
|
||||
const data = await this.getServerMediaProgress({ libraryItemId, episodeId })
|
||||
if (!data) return
|
||||
if (!this.$refs.audioPlayer?.isPlaying) {
|
||||
const playbackSession = this.$store.state.currentPlaybackSession
|
||||
if (this.$refs.audioPlayer.isLocalPlayMethod) {
|
||||
const localLibraryItemId = playbackSession.localLibraryItem?.id
|
||||
const localEpisodeId = playbackSession.localEpisodeId
|
||||
if (!localLibraryItemId) {
|
||||
console.error('[AudioPlayerContainer] device visibility: no local library item for session', JSON.stringify(playbackSession))
|
||||
return
|
||||
}
|
||||
const localMediaProgress = this.$store.state.globals.localMediaProgress.find((mp) => {
|
||||
if (localEpisodeId) return mp.localEpisodeId === localEpisodeId
|
||||
return mp.localLibraryItemId === localLibraryItemId
|
||||
})
|
||||
if (localMediaProgress) {
|
||||
console.log('[AudioPlayerContainer] device visibility: found local media progress', localMediaProgress.currentTime, 'last time in player is', this.currentTime)
|
||||
this.$refs.audioPlayer.currentTime = localMediaProgress.currentTime
|
||||
this.$refs.audioPlayer.timeupdate()
|
||||
} else {
|
||||
console.error('[AudioPlayerContainer] device visibility: Local media progress not found')
|
||||
}
|
||||
} else {
|
||||
const libraryItemId = playbackSession.libraryItemId
|
||||
const episodeId = playbackSession.episodeId
|
||||
const url = episodeId ? `/api/me/progress/${libraryItemId}/${episodeId}` : `/api/me/progress/${libraryItemId}`
|
||||
this.$nativeHttp
|
||||
.get(url)
|
||||
.then((data) => {
|
||||
if (!this.$refs.audioPlayer?.isPlaying && data.libraryItemId === libraryItemId) {
|
||||
console.log('[AudioPlayerContainer] device visibility: got server media progress', data.currentTime, 'last time in player is', this.currentTime)
|
||||
this.$refs.audioPlayer.currentTime = data.currentTime
|
||||
this.$refs.audioPlayer.timeupdate()
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[AudioPlayerContainer] device visibility: Failed to get progress', error)
|
||||
})
|
||||
console.log('[AudioPlayerContainer] device visibility: got server media progress', data.currentTime, 'last time in player is', this.currentTime)
|
||||
// Only seek if the difference is greater than 1 second
|
||||
if (Math.abs(data.currentTime - this.currentTime) > 1) {
|
||||
// Use seek() so the native audio player's current session is updated
|
||||
this.$refs.audioPlayer.seek(data.currentTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ class ServerSocket extends EventEmitter {
|
||||
}
|
||||
|
||||
onAuthFailed(data) {
|
||||
console.log('[SOCKET] Auth failed', data)
|
||||
console.log('[SOCKET] Auth failed: ' + (data?.message || 'Unknown reason'))
|
||||
this.isAuthenticated = false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user