diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt index 7c2b00f6..d8420940 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt @@ -370,26 +370,14 @@ data class BookChapter( val endMs get() = (end * 1000L).toLong() } -@JsonIgnoreProperties(ignoreUnknown = true) -class MediaProgress( - var id:String, - var libraryItemId:String, - var episodeId:String?, - var duration:Double, // seconds - progress:Double, // 0 to 1 - var currentTime:Double, - isFinished:Boolean, - var lastUpdate:Long, - var startedAt:Long, - var finishedAt:Long? -) : MediaProgressWrapper(isFinished, progress) - @JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION, defaultImpl = MediaProgress::class) @JsonSubTypes( JsonSubTypes.Type(MediaProgress::class), JsonSubTypes.Type(LocalMediaProgress::class) ) -open class MediaProgressWrapper(var isFinished:Boolean, var progress:Double) +open class MediaProgressWrapper(var isFinished:Boolean, var currentTime:Double, var progress:Double) { + open val mediaItemId get() = "" +} // Helper class data class LibraryItemWithEpisode( diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt index e6658fcc..ceae4f43 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt @@ -1,5 +1,6 @@ package com.audiobookshelf.app.data +import android.util.Log import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties import kotlin.math.roundToInt @@ -11,7 +12,7 @@ class LocalMediaProgress( var localEpisodeId:String?, var duration:Double, progress:Double, // 0 to 1 - var currentTime:Double, + currentTime:Double, isFinished:Boolean, var lastUpdate:Long, var startedAt:Long, @@ -22,9 +23,16 @@ class LocalMediaProgress( var serverUserId:String?, var libraryItemId:String?, var episodeId:String? -) : MediaProgressWrapper(isFinished, progress) { +) : MediaProgressWrapper(isFinished, currentTime, progress) { @get:JsonIgnore val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt() + @get:JsonIgnore + override val mediaItemId get() = if (libraryItemId != null) { + if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId" + } else { + if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId" + } + @JsonIgnore fun updateIsFinished(finished:Boolean) { @@ -41,8 +49,7 @@ class LocalMediaProgress( fun updateFromPlaybackSession(playbackSession:PlaybackSession) { currentTime = playbackSession.currentTime progress = playbackSession.progress - lastUpdate = System.currentTimeMillis() - + lastUpdate = playbackSession.updatedAt isFinished = playbackSession.progress >= 0.99 finishedAt = if (isFinished) lastUpdate else null } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt new file mode 100644 index 00000000..415ae2b0 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt @@ -0,0 +1,22 @@ +package com.audiobookshelf.app.data + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +class MediaProgress( + var id:String, + var libraryItemId:String, + var episodeId:String?, + var duration:Double, // seconds + progress:Double, // 0 to 1 + currentTime:Double, + isFinished:Boolean, + var lastUpdate:Long, + var startedAt:Long, + var finishedAt:Long? +) : MediaProgressWrapper(isFinished, currentTime, progress) { + + @get:JsonIgnore + override val mediaItemId get() = if (episodeId.isNullOrEmpty()) libraryItemId else "$libraryItemId-$episodeId" +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt index 72055cc5..046fd90d 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt @@ -1,9 +1,7 @@ package com.audiobookshelf.app.media import android.util.Log -import com.audiobookshelf.app.data.MediaItemEvent -import com.audiobookshelf.app.data.MediaItemHistory -import com.audiobookshelf.app.data.PlaybackSession +import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.player.PlayerNotificationService import com.audiobookshelf.app.player.SyncResult @@ -43,8 +41,36 @@ object MediaEventManager { addPlaybackEvent("Seek", playbackSession, syncResult) } + fun syncEvent(mediaProgress: MediaProgressWrapper, description: String) { + Log.i(tag, "Sync Event for media item id \"${mediaProgress.mediaItemId}\", currentTime=${mediaProgress.currentTime}") + addSyncEvent("Sync", mediaProgress, description) + } + + private fun addSyncEvent(eventName:String, mediaProgress:MediaProgressWrapper, description: String) { + val mediaItemHistory = getMediaItemHistoryMediaItem(mediaProgress.mediaItemId) + if (mediaItemHistory == null) { + Log.w(tag, "addSyncEvent: Media Item History not created yet for media item id ${mediaProgress.mediaItemId}") + return + } + + val mediaItemEvent = MediaItemEvent( + name = eventName, + type = "Sync", + description = description, + currentTime = mediaProgress.currentTime, + serverSyncAttempted = false, + serverSyncSuccess = null, + serverSyncMessage = null, + timestamp = System.currentTimeMillis() + ) + mediaItemHistory.events.add(mediaItemEvent) + DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory) + + clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory) + } + private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult:SyncResult?) { - val mediaItemHistory = getMediaItemHistoryForSession(playbackSession) ?: createMediaItemHistoryForSession(playbackSession) + val mediaItemHistory = getMediaItemHistoryMediaItem(playbackSession.mediaItemId) ?: createMediaItemHistoryForSession(playbackSession) val mediaItemEvent = MediaItemEvent( name = eventName, @@ -62,8 +88,8 @@ object MediaEventManager { clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory) } - private fun getMediaItemHistoryForSession(playbackSession: PlaybackSession) : MediaItemHistory? { - return DeviceManager.dbManager.getMediaItemHistory(playbackSession.mediaItemId) + private fun getMediaItemHistoryMediaItem(mediaItemId: String) : MediaItemHistory? { + return DeviceManager.dbManager.getMediaItemHistory(mediaItemId) } private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession):MediaItemHistory { diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt index 3f7de691..634ef419 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt @@ -6,6 +6,7 @@ import android.util.Log import com.audiobookshelf.app.data.LocalMediaProgress import com.audiobookshelf.app.data.MediaProgress import com.audiobookshelf.app.data.PlaybackSession +import com.audiobookshelf.app.data.Podcast import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.media.MediaEventManager import com.audiobookshelf.app.server.ApiHandler @@ -190,6 +191,9 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic currentPlaybackSession?.let { it.updatedAt = mediaProgress.lastUpdate it.currentTime = mediaProgress.currentTime + + MediaEventManager.syncEvent(mediaProgress, "Received from server get media progress request while playback session open") + DeviceManager.dbManager.saveLocalPlaybackSession(it) saveLocalProgress(it) } @@ -279,17 +283,18 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress() } else { currentLocalMediaProgress = mediaProgress + currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession) } } else { currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession) } + currentLocalMediaProgress?.let { if (it.progress.isNaN()) { Log.e(tag, "Invalid progress on local media progress") } else { DeviceManager.dbManager.saveLocalMediaProgress(it) playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it) - Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%") } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt index a44a7ffb..7039fcf3 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt @@ -4,7 +4,6 @@ import android.util.Log import com.audiobookshelf.app.data.PlaybackSession import com.audiobookshelf.app.data.PlayerState import com.audiobookshelf.app.device.DeviceManager -import com.audiobookshelf.app.media.MediaEventManager import com.google.android.exoplayer2.PlaybackException import com.google.android.exoplayer2.Player @@ -17,8 +16,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) : var lastPauseTime: Long = 0 //ms } - private var onSeekBack: Boolean = false - private var isSeeking: Boolean = false + private var lazyIsPlaying: Boolean = false override fun onPlayerError(error: PlaybackException) { val errorMessage = error.message ?: "Unknown Error" @@ -33,51 +31,48 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) : ) { if (reason == Player.DISCONTINUITY_REASON_SEEK) { // If playing set seeking flag - isSeeking = playerNotificationService.currentPlayer.isPlaying - Log.d(tag, "onPositionDiscontinuity: oldPosition=${oldPosition.positionMs}/${oldPosition.mediaItemIndex}, newPosition=${newPosition.positionMs}/${newPosition.mediaItemIndex}, isPlaying=${playerNotificationService.currentPlayer.isPlaying} reason=SEEK") playerNotificationService.mediaProgressSyncer.seek() + lastPauseTime = 0 // When seeking while paused reset the auto-rewind } else { Log.d(tag, "onPositionDiscontinuity: oldPosition=${oldPosition.positionMs}/${oldPosition.mediaItemIndex}, newPosition=${newPosition.positionMs}/${newPosition.mediaItemIndex}, isPlaying=${playerNotificationService.currentPlayer.isPlaying}, reason=$reason") } } override fun onIsPlayingChanged(isPlaying: Boolean) { - Log.d(tag, "onIsPlayingChanged to $isPlaying | ${playerNotificationService.getMediaPlayer()}") - - if (isSeeking) { - if (isPlaying) { - isSeeking = false - Log.d(tag, "onIsPlayingChanged isSeeking seek complete") - } else { - Log.d(tag, "onIsPlayingChanged isSeeking skipping pause") - } - return - } + Log.d(tag, "onIsPlayingChanged to $isPlaying | ${playerNotificationService.getMediaPlayer()} | playbackState=${playerNotificationService.currentPlayer.playbackState}") val player = playerNotificationService.currentPlayer - if (player.isPlaying) { + // Goal of these 2 if statements and the lazyIsPlaying is to ignore this event when it is triggered by a seek + // When a seek occurs the player is paused and buffering, then plays again right afterwards. + if (!isPlaying && player.playbackState == Player.STATE_BUFFERING) { + Log.d(tag, "onIsPlayingChanged: Pause event when buffering is ignored") + return + } + if (lazyIsPlaying == isPlaying) { + Log.d(tag, "onIsPlayingChanged: Lazy is playing $lazyIsPlaying is already set to this so ignoring") + return + } + + lazyIsPlaying = isPlaying + + if (isPlaying) { Log.d(tag, "SeekBackTime: Player is playing") if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) { - var seekBackTime = 0L - if (onSeekBack) onSeekBack = false - else { - Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime") - seekBackTime = calcPauseSeekBackTime() - if (seekBackTime > 0) { - // Current chapter is used so that seek back does not go back to the previous chapter - val currentChapter = playerNotificationService.getCurrentBookChapter() - val minSeekBackTime = currentChapter?.startMs ?: 0 + Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime") + var seekBackTime = calcPauseSeekBackTime() + if (seekBackTime > 0) { + // Current chapter is used so that seek back does not go back to the previous chapter + val currentChapter = playerNotificationService.getCurrentBookChapter() + val minSeekBackTime = currentChapter?.startMs ?: 0 - val currentTime = playerNotificationService.getCurrentTime() - val newTime = currentTime - seekBackTime - if (newTime < minSeekBackTime) { - seekBackTime = currentTime - minSeekBackTime - } - Log.d(tag, "SeekBackTime $seekBackTime") - onSeekBack = true + val currentTime = playerNotificationService.getCurrentTime() + val newTime = currentTime - seekBackTime + if (newTime < minSeekBackTime) { + seekBackTime = currentTime - minSeekBackTime } + Log.d(tag, "SeekBackTime $seekBackTime") } // Check if playback session still exists or sync media progress if updated @@ -92,12 +87,12 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) : } } } else { - Log.d(tag, "SeekBackTime: Player not playing set last pause time") + Log.d(tag, "SeekBackTime: Player not playing set last pause time | playbackState=${player.playbackState}") lastPauseTime = System.currentTimeMillis() } // Start/stop progress sync interval - if (player.isPlaying) { + if (isPlaying) { player.volume = 1F // Volume on sleep timer might have decreased this val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession playbackSession?.let { playerNotificationService.mediaProgressSyncer.play(it) } @@ -107,7 +102,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) : } } - playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying) + playerNotificationService.clientEventEmitter?.onPlayingUpdate(isPlaying) DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index e4e3ba13..1975fd66 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -823,6 +823,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } currentPlaybackSession = null + mediaProgressSyncer.reset() clientEventEmitter?.onPlaybackClosed() PlayerListener.lastPauseTime = 0 isClosed = true diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt index 824362b7..da9d5dd9 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt @@ -3,6 +3,7 @@ package com.audiobookshelf.app.data import android.util.Log import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.device.DeviceManager +import com.audiobookshelf.app.media.MediaEventManager import com.audiobookshelf.app.server.ApiHandler import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -188,6 +189,24 @@ class AbsDatabase : Plugin() { } } + @PluginMethod + fun getLocalMediaProgressForServerItem(call:PluginCall) { + val libraryItemId = call.getString("libraryItemId", "").toString() + var episodeId:String? = call.getString("episodeId", "").toString() + if (episodeId == "") episodeId = null + + GlobalScope.launch(Dispatchers.IO) { + val allLocalMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress() + val localMediaProgress = allLocalMediaProgress.find { libraryItemId == it.libraryItemId && (episodeId == null || it.episodeId == episodeId) } + + if (localMediaProgress == null) { + call.resolve() + } else { + call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress))) + } + } + } + @PluginMethod fun removeLocalMediaProgress(call:PluginCall) { val localMediaProgressId = call.getString("localMediaProgressId", "").toString() @@ -256,6 +275,8 @@ class AbsDatabase : Plugin() { Log.w(tag, "syncServerMediaProgressWithLocalMediaProgress Local media progress not found $localMediaProgressId") call.resolve() } else { + MediaEventManager.syncEvent(mediaProgress, "Received from webhook event") + localMediaProgress.updateFromServerMediaProgress(mediaProgress) DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress) call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress))) diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index 49025a94..900584f8 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -6,6 +6,7 @@ import android.net.NetworkCapabilities import android.util.Log import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager +import com.audiobookshelf.app.media.MediaEventManager import com.audiobookshelf.app.player.MediaProgressSyncData import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.core.json.JsonReadFeature @@ -283,6 +284,8 @@ class ApiHandler(var ctx:Context) { if (progressSyncResponsePayload.localProgressUpdates.isNotEmpty()) { // Update all local media progress progressSyncResponsePayload.localProgressUpdates.forEach { localMediaProgress -> + MediaEventManager.syncEvent(localMediaProgress, "Received from server sync local API request") + DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress) } } diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index 618e8865..6fccdb9e 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -184,6 +184,7 @@ export default { async playLibraryItem(payload) { const libraryItemId = payload.libraryItemId const episodeId = payload.episodeId + const startTime = payload.startTime // When playing local library item and can also play this item from the server // then store the server library item id so it can be used if a cast is made @@ -200,6 +201,16 @@ export default { } } + // if already playing this item then jump to start time + if (this.$store.getters['getIsMediaStreaming'](libraryItemId, episodeId)) { + console.log('Already streaming item', startTime) + if (startTime !== undefined && startTime !== null) { + // seek to start time + AbsAudioPlayer.seek({ value: Math.floor(startTime) }) + } + return + } + this.serverLibraryItemId = null this.serverEpisodeId = null @@ -209,7 +220,9 @@ export default { } console.log('Called playLibraryItem', libraryItemId) - AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true, playbackRate }) + const preparePayload = { libraryItemId, episodeId, playWhenReady: true, playbackRate } + if (startTime !== undefined && startTime !== null) preparePayload.startTime = startTime + AbsAudioPlayer.prepareLibraryItem(preparePayload) .then((data) => { if (data.error) { const errorMsg = data.error || 'Failed to play' diff --git a/layouts/default.vue b/layouts/default.vue index e40f6f42..4ef44e8b 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -208,11 +208,19 @@ export default { console.log(`[default] userMediaProgressUpdate checking for local media progress ${prog.id}`) // Update local media progress if exists - var localProg = this.$store.getters['globals/getLocalMediaProgressByServerItemId'](prog.libraryItemId, prog.episodeId) + const localProg = await this.$db.getLocalMediaProgressForServerItem({ libraryItemId: prog.libraryItemId, episodeId: prog.episodeId }) + var newLocalMediaProgress = null if (localProg && localProg.lastUpdate < prog.lastUpdate) { + if (localProg.currentTime == prog.currentTime && localProg.isFinished == prog.isFinished) { + console.log('[default] syncing progress server lastUpdate > local lastUpdate but currentTime and isFinished is equal') + return + } else { + console.log(`[default] syncing progress server lastUpdate > local lastUpdate. server currentTime=${prog.currentTime} local currentTime=${localProg.currentTime} | server/local isFinished=${prog.isFinished}/${localProg.isFinished}`) + } + // Server progress is more up-to-date - console.log(`[default] syncing progress from server with local item for "${prog.libraryItemId}" ${prog.episodeId ? `episode ${prog.episodeId}` : ''}`) + console.log(`[default] syncing progress from server with local item for "${prog.libraryItemId}" ${prog.episodeId ? `episode ${prog.episodeId}` : ''} | server lastUpdate=${prog.lastUpdate} > local lastUpdate=${localProg.lastUpdate}`) const payload = { localMediaProgressId: localProg.id, mediaProgress: prog diff --git a/pages/media/_id/history.vue b/pages/media/_id/history.vue index 67b0d70e..b013fbd4 100644 --- a/pages/media/_id/history.vue +++ b/pages/media/_id/history.vue @@ -10,16 +10,16 @@
{{ name }}
{{ $formatDate(evt.timestamp, 'HH:mm') }}
- -{{ evt.name }}
+ +{{ evt.name }}
- - + + -+{{ evt.num }}
++{{ evt.num }}
-{{ $secondsToTimestampFull(evt.currentTime) }}
+{{ $secondsToTimestampFull(evt.currentTime) }}