diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index 69c1188b..907a4587 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -69,22 +69,22 @@
- first_page -
+ first_page +
replay {{ jumpBackwardsLabel }}
- {{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }} + {{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}
-
+
forward_media {{ jumpForwardLabel }}
- last_page + last_page
@@ -94,7 +94,7 @@

{{ timeRemainingPretty }}

-
+
@@ -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() diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index 1040e61b..d15e7179 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -48,6 +48,9 @@ export default { }, isIos() { return this.$platform === 'ios' + }, + currentPlaybackSession() { + return this.$store.state.currentPlaybackSession } }, methods: { @@ -304,48 +307,129 @@ 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 getServerMediaProgressForCurrentSession() { + if (!this.$store.state.user.user || !this.$store.state.networkConnected) return null + const libraryItemId = this.currentPlaybackSession?.libraryItemId + const episodeId = this.currentPlaybackSession?.episodeId + if (!libraryItemId) return null - if (hasFocus) { + if (this.$refs.audioPlayer?.isCheckingServerProgress) { + console.log('[AudioPlayerContainer] getServerMediaProgressForCurrentSession: already checking server progress') + return null + } + + const url = episodeId ? `/api/me/progress/${libraryItemId}/${episodeId}` : `/api/me/progress/${libraryItemId}` + + 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) + } + }, + getLocalMediaProgressForCurrentSession() { + if (!this.currentPlaybackSession) return null + return this.$store.getters['globals/getLocalMediaProgressById'](this.currentPlaybackSession.localLibraryItem?.id, this.currentPlaybackSession.localEpisodeId) + }, + /** + * Sync the server media progress with the local media progress + */ + async syncServerMediaProgressWithLocalMediaProgress(localMediaProgressId, serverMediaProgress) { + try { + const newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress({ + localMediaProgressId, + mediaProgress: serverMediaProgress + }) + if (newLocalMediaProgress?.id) { + this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress) + } + } catch (error) { + console.error('[AudioPlayerContainer] Failed to sync server progress with local media progress', error) + } + }, + /** + * Check if the server media progress is more recent than the local media progress and sync if so + */ + async checkSyncServerProgressWithLocalProgress(localMediaProgress) { + if (!localMediaProgress) return + console.log('[AudioPlayerContainer] checkSyncServerProgressWithLocalProgress: checking server media progress for local media item open in player') + const serverMediaProgress = await this.getServerMediaProgressForCurrentSession() + if (!serverMediaProgress?.lastUpdate || serverMediaProgress.lastUpdate <= localMediaProgress.lastUpdate) return + + console.log('[AudioPlayerContainer] checkSyncServerProgressWithLocalProgress: server progress is more recent than local progress. Server current time:', serverMediaProgress.currentTime, 'vs local', localMediaProgress.currentTime, `(server lastUpdate=${serverMediaProgress.lastUpdate} > local lastUpdate=${localMediaProgress.lastUpdate})`) + if (!this.$refs.audioPlayer?.isPlaying && serverMediaProgress.currentTime !== localMediaProgress.currentTime) { + // Use seek() so the native audio player's current session is updated + this.$refs.audioPlayer.seek(serverMediaProgress.currentTime) + } + + await this.syncServerMediaProgressWithLocalMediaProgress(localMediaProgress.id, serverMediaProgress) + }, + /** + * When socket is reconnected after a delay, if a local media item is open in the player (paused) + * we fetch the server media progress and sync it if it is more recent than the local progress + * + * If there is no socket connection we may have missed external progress updates + */ + async socketReconnected() { + if (!this.currentPlaybackSession) return + // dont update timestamps if player is playing + if (this.$refs.audioPlayer?.isPlaying) return + + if (this.$refs.audioPlayer.isLocalPlayMethod) { + const localMediaProgress = this.getLocalMediaProgressForCurrentSession() + if (!localMediaProgress) { + console.error('[AudioPlayerContainer] socket reconnected: Local media progress not found') + return + } + + await this.checkSyncServerProgressWithLocalProgress(localMediaProgress) + } + }, + /** + * When device re-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.currentPlaybackSession || !hasFocus) return + // dont update timestamps if player is playing + if (this.$refs.audioPlayer?.isPlaying) return + + if (this.$refs.audioPlayer.isLocalPlayMethod) { + const localMediaProgress = this.getLocalMediaProgressForCurrentSession() + 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() + + await this.checkSyncServerProgressWithLocalProgress(localMediaProgress) + } else { + // server item so fetch server media progress and update player time + console.log('[AudioPlayerContainer] device visibility: checking server media progress for server media item open in player') + const data = await this.getServerMediaProgressForCurrentSession() + 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) } } } @@ -368,6 +452,7 @@ export default { this.$eventBus.$on('user-settings', this.settingsUpdated) this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate) this.$eventBus.$on('device-focus-update', this.deviceFocused) + this.$eventBus.$on('socket-reconnected', this.socketReconnected) }, beforeDestroy() { this.onLocalMediaProgressUpdateListener?.remove() @@ -383,6 +468,7 @@ export default { this.$eventBus.$off('user-settings', this.settingsUpdated) this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate) this.$eventBus.$off('device-focus-update', this.deviceFocused) + this.$eventBus.$off('socket-reconnected', this.socketReconnected) } } diff --git a/layouts/default.vue b/layouts/default.vue index aa621721..bed658ea 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -24,6 +24,7 @@ export default { inittingLibraries: false, hasMounted: false, disconnectTime: 0, + socketDisconnectedTime: 0, timeLostFocus: 0, currentLang: null } @@ -44,7 +45,7 @@ export default { } else { var timeSinceDisconnect = Date.now() - this.disconnectTime if (timeSinceDisconnect > 5000) { - console.log('Time since disconnect was', timeSinceDisconnect, 'sync with server') + console.log('[default] Time since disconnect was', timeSinceDisconnect, 'sync with server') setTimeout(() => { this.syncLocalSessions(false) }, 4000) @@ -55,6 +56,28 @@ export default { this.disconnectTime = Date.now() } } + }, + socketConnected: { + handler(newVal, oldVal) { + if (!this.hasMounted) { + // watcher runs before mount, handling libraries/connection should be handled in mount + return + } + if (newVal) { + // if we havent been receiving socket events then external progress updates may have been missed + const timeSinceDisconnect = Date.now() - this.socketDisconnectedTime + if (timeSinceDisconnect > 30000 && this.isPlayerOpen) { + console.log('[default] socket reconnected after ' + timeSinceDisconnect + 'ms and player is open, triggering server media progress sync') + // used for triggering a server media progress sync if local media item is open in player + this.$eventBus.$emit('socket-reconnected') + } else { + console.log('[default] socket reconnected after ' + timeSinceDisconnect + 'ms') + } + } else { + console.log('[default] socket disconnected') + this.socketDisconnectedTime = Date.now() + } + } } }, computed: { @@ -67,6 +90,9 @@ export default { networkConnected() { return this.$store.state.networkConnected }, + socketConnected() { + return this.$store.state.socketConnected + }, user() { return this.$store.state.user.user }, diff --git a/plugins/server.js b/plugins/server.js index 90f6f715..fccc06b9 100644 --- a/plugins/server.js +++ b/plugins/server.js @@ -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 }