diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index fa369072..d40ce0da 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -268,6 +268,10 @@ export default { seek(time) { 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) { @@ -477,12 +481,14 @@ export default { 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-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-item', this.playLibraryItem) this.$eventBus.$off('pause-item', this.pauseItem) } diff --git a/client/layouts/default.vue b/client/layouts/default.vue index b201fc4a..83e7e5c2 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -35,7 +35,9 @@ export default { isSocketConnected: false, isFirstSocketConnection: true, socketConnectionToastId: null, - currentLang: null + currentLang: null, + multiSessionOtherSessionId: null, // Used for multiple sessions open warning toast + multiSessionCurrentSessionId: null // Used for multiple sessions open warning toast } }, watch: { @@ -300,14 +302,27 @@ export default { this.$store.commit('users/updateUserOnline', user) }, userSessionClosed(sessionId) { + // If this session or other session is closed then dismiss multiple sessions warning toast + if (sessionId === this.multiSessionOtherSessionId || this.multiSessionCurrentSessionId === sessionId) { + this.multiSessionOtherSessionId = null + this.multiSessionCurrentSessionId = null + this.$toast.dismiss('multiple-sessions') + } if (this.$refs.streamContainer) this.$refs.streamContainer.sessionClosedEvent(sessionId) }, userMediaProgressUpdate(payload) { this.$store.commit('user/updateMediaProgress', payload) if (payload.data) { - if (this.$store.getters['getIsMediaStreaming'](payload.data.libraryItemId, payload.data.episodeId)) { - // TODO: Update currently open session if being played from another device + if (this.$store.getters['getIsMediaStreaming'](payload.data.libraryItemId, payload.data.episodeId) && this.$store.state.playbackSessionId !== payload.sessionId) { + this.multiSessionOtherSessionId = payload.sessionId + this.multiSessionCurrentSessionId = this.$store.state.playbackSessionId + console.log(`Media progress was updated from another session (${this.multiSessionOtherSessionId}) for currently open media. Device description=${payload.deviceDescription}. Current session id=${this.multiSessionCurrentSessionId}`) + if (this.$store.state.streamIsPlaying) { + this.$toast.update('multiple-sessions', { content: `Another session is open for this item on device ${payload.deviceDescription}`, options: { timeout: 20000, type: 'warning', pauseOnFocusLoss: false } }, true) + } else { + this.$eventBus.$emit('playback-time-update', payload.data.currentTime) + } } } }, diff --git a/client/players/PlayerHandler.js b/client/players/PlayerHandler.js index 748dfc60..57af1988 100644 --- a/client/players/PlayerHandler.js +++ b/client/players/PlayerHandler.js @@ -52,6 +52,11 @@ export default class PlayerHandler { return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId) } + setSessionId(sessionId) { + this.currentSessionId = sessionId + this.ctx.$store.commit('setPlaybackSessionId', sessionId) + } + load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) { this.libraryItem = libraryItem this.isVideo = libraryItem.mediaType === 'video' @@ -182,7 +187,7 @@ export default class PlayerHandler { } async prepare(forceTranscode = false) { - this.currentSessionId = null // Reset session + this.setSessionId(null) // Reset session const payload = { deviceInfo: { @@ -218,7 +223,7 @@ export default class PlayerHandler { prepareSession(session) { this.failedProgressSyncs = 0 this.startTime = this.startTimeOverride !== undefined ? this.startTimeOverride : session.currentTime - this.currentSessionId = session.id + this.setSessionId(session.id) this.displayTitle = session.displayTitle this.displayAuthor = session.displayAuthor @@ -263,7 +268,7 @@ export default class PlayerHandler { this.player = null this.playerState = 'IDLE' this.libraryItem = null - this.currentSessionId = null + this.setSessionId(null) this.startTime = 0 this.stopPlayInterval() } @@ -300,7 +305,7 @@ export default class PlayerHandler { if (this.player) { const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync)) // When opening player and quickly closing dont save progress - if (listeningTimeToAdd > 20 || this.lastSyncTime > 0) { + if (listeningTimeToAdd > 20) { syncData = { timeListened: listeningTimeToAdd, duration: this.getDuration(), @@ -390,13 +395,13 @@ export default class PlayerHandler { this.player.setPlaybackRate(playbackRate) } - seek(time) { + seek(time, shouldSync = true) { if (!this.player) return this.player.seek(time, this.playerPlaying) this.ctx.setCurrentTime(time) // Update progress if paused - if (!this.playerPlaying) { + if (!this.playerPlaying && shouldSync) { this.sendProgressSync(time) } } diff --git a/client/store/index.js b/client/store/index.js index 5247030b..89ff486c 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -6,6 +6,7 @@ export const state = () => ({ Source: null, versionData: null, serverSettings: null, + playbackSessionId: null, streamLibraryItem: null, streamEpisodeId: null, streamIsPlaying: false, @@ -150,6 +151,9 @@ export const mutations = { if (!settings) return state.serverSettings = settings }, + setPlaybackSessionId(state, playbackSessionId) { + state.playbackSessionId = playbackSessionId + }, setMediaPlaying(state, payload) { if (!payload) { state.streamLibraryItem = null diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index d5d204cb..d63d6249 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -130,6 +130,8 @@ class PlaybackSessionManager { const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { id: itemProgress.id, + sessionId: session.id, + deviceDescription: session.deviceDescription, data: itemProgress.toJSON() }) } @@ -239,6 +241,8 @@ class PlaybackSessionManager { const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { id: itemProgress.id, + sessionId: session.id, + deviceDescription: session.deviceDescription, data: itemProgress.toJSON() }) } @@ -306,7 +310,7 @@ class PlaybackSessionManager { // See https://github.com/advplyr/audiobookshelf/issues/868 // Remove playback sessions with listening time too high async removeInvalidSessions() { - const selectFunc = (session) => isNaN(session.timeListening) || Number(session.timeListening) > 3600000000 + const selectFunc = (session) => isNaN(session.timeListening) || Number(session.timeListening) > 36000000 const numSessionsRemoved = await this.db.removeEntities('session', selectFunc, true) if (numSessionsRemoved) { Logger.info(`[PlaybackSessionManager] Removed ${numSessionsRemoved} invalid playback sessions`)