Local media sync events

This commit is contained in:
advplyr 2023-01-15 14:58:26 -06:00
parent 297eca6a86
commit 8f6dd72df2
15 changed files with 234 additions and 80 deletions

View file

@ -370,26 +370,14 @@ data class BookChapter(
val endMs get() = (end * 1000L).toLong() 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) @JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION, defaultImpl = MediaProgress::class)
@JsonSubTypes( @JsonSubTypes(
JsonSubTypes.Type(MediaProgress::class), JsonSubTypes.Type(MediaProgress::class),
JsonSubTypes.Type(LocalMediaProgress::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 // Helper class
data class LibraryItemWithEpisode( data class LibraryItemWithEpisode(

View file

@ -1,5 +1,6 @@
package com.audiobookshelf.app.data package com.audiobookshelf.app.data
import android.util.Log
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -11,7 +12,7 @@ class LocalMediaProgress(
var localEpisodeId:String?, var localEpisodeId:String?,
var duration:Double, var duration:Double,
progress:Double, // 0 to 1 progress:Double, // 0 to 1
var currentTime:Double, currentTime:Double,
isFinished:Boolean, isFinished:Boolean,
var lastUpdate:Long, var lastUpdate:Long,
var startedAt:Long, var startedAt:Long,
@ -22,9 +23,16 @@ class LocalMediaProgress(
var serverUserId:String?, var serverUserId:String?,
var libraryItemId:String?, var libraryItemId:String?,
var episodeId:String? var episodeId:String?
) : MediaProgressWrapper(isFinished, progress) { ) : MediaProgressWrapper(isFinished, currentTime, progress) {
@get:JsonIgnore @get:JsonIgnore
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt() 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 @JsonIgnore
fun updateIsFinished(finished:Boolean) { fun updateIsFinished(finished:Boolean) {
@ -41,8 +49,7 @@ class LocalMediaProgress(
fun updateFromPlaybackSession(playbackSession:PlaybackSession) { fun updateFromPlaybackSession(playbackSession:PlaybackSession) {
currentTime = playbackSession.currentTime currentTime = playbackSession.currentTime
progress = playbackSession.progress progress = playbackSession.progress
lastUpdate = System.currentTimeMillis() lastUpdate = playbackSession.updatedAt
isFinished = playbackSession.progress >= 0.99 isFinished = playbackSession.progress >= 0.99
finishedAt = if (isFinished) lastUpdate else null finishedAt = if (isFinished) lastUpdate else null
} }

View file

@ -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"
}

View file

@ -1,9 +1,7 @@
package com.audiobookshelf.app.media package com.audiobookshelf.app.media
import android.util.Log import android.util.Log
import com.audiobookshelf.app.data.MediaItemEvent import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.data.MediaItemHistory
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.PlayerNotificationService import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.player.SyncResult import com.audiobookshelf.app.player.SyncResult
@ -43,8 +41,36 @@ object MediaEventManager {
addPlaybackEvent("Seek", playbackSession, syncResult) 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?) { 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( val mediaItemEvent = MediaItemEvent(
name = eventName, name = eventName,
@ -62,8 +88,8 @@ object MediaEventManager {
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory) clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
} }
private fun getMediaItemHistoryForSession(playbackSession: PlaybackSession) : MediaItemHistory? { private fun getMediaItemHistoryMediaItem(mediaItemId: String) : MediaItemHistory? {
return DeviceManager.dbManager.getMediaItemHistory(playbackSession.mediaItemId) return DeviceManager.dbManager.getMediaItemHistory(mediaItemId)
} }
private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession):MediaItemHistory { private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession):MediaItemHistory {

View file

@ -6,6 +6,7 @@ import android.util.Log
import com.audiobookshelf.app.data.LocalMediaProgress import com.audiobookshelf.app.data.LocalMediaProgress
import com.audiobookshelf.app.data.MediaProgress import com.audiobookshelf.app.data.MediaProgress
import com.audiobookshelf.app.data.PlaybackSession import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.data.Podcast
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaEventManager import com.audiobookshelf.app.media.MediaEventManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
@ -190,6 +191,9 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
currentPlaybackSession?.let { currentPlaybackSession?.let {
it.updatedAt = mediaProgress.lastUpdate it.updatedAt = mediaProgress.lastUpdate
it.currentTime = mediaProgress.currentTime it.currentTime = mediaProgress.currentTime
MediaEventManager.syncEvent(mediaProgress, "Received from server get media progress request while playback session open")
DeviceManager.dbManager.saveLocalPlaybackSession(it) DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it) saveLocalProgress(it)
} }
@ -279,17 +283,18 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress() currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
} else { } else {
currentLocalMediaProgress = mediaProgress currentLocalMediaProgress = mediaProgress
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
} }
} else { } else {
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession) currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
} }
currentLocalMediaProgress?.let { currentLocalMediaProgress?.let {
if (it.progress.isNaN()) { if (it.progress.isNaN()) {
Log.e(tag, "Invalid progress on local media progress") Log.e(tag, "Invalid progress on local media progress")
} else { } else {
DeviceManager.dbManager.saveLocalMediaProgress(it) DeviceManager.dbManager.saveLocalMediaProgress(it)
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it) playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%") Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%")
} }
} }

View file

@ -4,7 +4,6 @@ import android.util.Log
import com.audiobookshelf.app.data.PlaybackSession import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.data.PlayerState import com.audiobookshelf.app.data.PlayerState
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaEventManager
import com.google.android.exoplayer2.PlaybackException import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
@ -17,8 +16,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
var lastPauseTime: Long = 0 //ms var lastPauseTime: Long = 0 //ms
} }
private var onSeekBack: Boolean = false private var lazyIsPlaying: Boolean = false
private var isSeeking: Boolean = false
override fun onPlayerError(error: PlaybackException) { override fun onPlayerError(error: PlaybackException) {
val errorMessage = error.message ?: "Unknown Error" val errorMessage = error.message ?: "Unknown Error"
@ -33,38 +31,37 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
) { ) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) { if (reason == Player.DISCONTINUITY_REASON_SEEK) {
// If playing set seeking flag // 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") Log.d(tag, "onPositionDiscontinuity: oldPosition=${oldPosition.positionMs}/${oldPosition.mediaItemIndex}, newPosition=${newPosition.positionMs}/${newPosition.mediaItemIndex}, isPlaying=${playerNotificationService.currentPlayer.isPlaying} reason=SEEK")
playerNotificationService.mediaProgressSyncer.seek() playerNotificationService.mediaProgressSyncer.seek()
lastPauseTime = 0 // When seeking while paused reset the auto-rewind
} else { } else {
Log.d(tag, "onPositionDiscontinuity: oldPosition=${oldPosition.positionMs}/${oldPosition.mediaItemIndex}, newPosition=${newPosition.positionMs}/${newPosition.mediaItemIndex}, isPlaying=${playerNotificationService.currentPlayer.isPlaying}, reason=$reason") 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) { override fun onIsPlayingChanged(isPlaying: Boolean) {
Log.d(tag, "onIsPlayingChanged to $isPlaying | ${playerNotificationService.getMediaPlayer()}") Log.d(tag, "onIsPlayingChanged to $isPlaying | ${playerNotificationService.getMediaPlayer()} | playbackState=${playerNotificationService.currentPlayer.playbackState}")
if (isSeeking) {
if (isPlaying) {
isSeeking = false
Log.d(tag, "onIsPlayingChanged isSeeking seek complete")
} else {
Log.d(tag, "onIsPlayingChanged isSeeking skipping pause")
}
return
}
val player = playerNotificationService.currentPlayer 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") Log.d(tag, "SeekBackTime: Player is playing")
if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) { 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") Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
seekBackTime = calcPauseSeekBackTime() var seekBackTime = calcPauseSeekBackTime()
if (seekBackTime > 0) { if (seekBackTime > 0) {
// Current chapter is used so that seek back does not go back to the previous chapter // Current chapter is used so that seek back does not go back to the previous chapter
val currentChapter = playerNotificationService.getCurrentBookChapter() val currentChapter = playerNotificationService.getCurrentBookChapter()
@ -76,8 +73,6 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
seekBackTime = currentTime - minSeekBackTime seekBackTime = currentTime - minSeekBackTime
} }
Log.d(tag, "SeekBackTime $seekBackTime") Log.d(tag, "SeekBackTime $seekBackTime")
onSeekBack = true
}
} }
// Check if playback session still exists or sync media progress if updated // Check if playback session still exists or sync media progress if updated
@ -92,12 +87,12 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
} }
} }
} else { } 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() lastPauseTime = System.currentTimeMillis()
} }
// Start/stop progress sync interval // Start/stop progress sync interval
if (player.isPlaying) { if (isPlaying) {
player.volume = 1F // Volume on sleep timer might have decreased this player.volume = 1F // Volume on sleep timer might have decreased this
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
playbackSession?.let { playerNotificationService.mediaProgressSyncer.play(it) } 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) DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
} }

View file

@ -823,6 +823,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
currentPlaybackSession = null currentPlaybackSession = null
mediaProgressSyncer.reset()
clientEventEmitter?.onPlaybackClosed() clientEventEmitter?.onPlaybackClosed()
PlayerListener.lastPauseTime = 0 PlayerListener.lastPauseTime = 0
isClosed = true isClosed = true

View file

@ -3,6 +3,7 @@ package com.audiobookshelf.app.data
import android.util.Log import android.util.Log
import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaEventManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 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 @PluginMethod
fun removeLocalMediaProgress(call:PluginCall) { fun removeLocalMediaProgress(call:PluginCall) {
val localMediaProgressId = call.getString("localMediaProgressId", "").toString() val localMediaProgressId = call.getString("localMediaProgressId", "").toString()
@ -256,6 +275,8 @@ class AbsDatabase : Plugin() {
Log.w(tag, "syncServerMediaProgressWithLocalMediaProgress Local media progress not found $localMediaProgressId") Log.w(tag, "syncServerMediaProgressWithLocalMediaProgress Local media progress not found $localMediaProgressId")
call.resolve() call.resolve()
} else { } else {
MediaEventManager.syncEvent(mediaProgress, "Received from webhook event")
localMediaProgress.updateFromServerMediaProgress(mediaProgress) localMediaProgress.updateFromServerMediaProgress(mediaProgress)
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress) DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress))) call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))

View file

@ -6,6 +6,7 @@ import android.net.NetworkCapabilities
import android.util.Log import android.util.Log
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaEventManager
import com.audiobookshelf.app.player.MediaProgressSyncData import com.audiobookshelf.app.player.MediaProgressSyncData
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.core.json.JsonReadFeature
@ -283,6 +284,8 @@ class ApiHandler(var ctx:Context) {
if (progressSyncResponsePayload.localProgressUpdates.isNotEmpty()) { if (progressSyncResponsePayload.localProgressUpdates.isNotEmpty()) {
// Update all local media progress // Update all local media progress
progressSyncResponsePayload.localProgressUpdates.forEach { localMediaProgress -> progressSyncResponsePayload.localProgressUpdates.forEach { localMediaProgress ->
MediaEventManager.syncEvent(localMediaProgress, "Received from server sync local API request")
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress) DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
} }
} }

View file

@ -184,6 +184,7 @@ export default {
async playLibraryItem(payload) { async playLibraryItem(payload) {
const libraryItemId = payload.libraryItemId const libraryItemId = payload.libraryItemId
const episodeId = payload.episodeId const episodeId = payload.episodeId
const startTime = payload.startTime
// When playing local library item and can also play this item from the server // 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 // 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.serverLibraryItemId = null
this.serverEpisodeId = null this.serverEpisodeId = null
@ -209,7 +220,9 @@ export default {
} }
console.log('Called playLibraryItem', libraryItemId) 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) => { .then((data) => {
if (data.error) { if (data.error) {
const errorMsg = data.error || 'Failed to play' const errorMsg = data.error || 'Failed to play'

View file

@ -208,11 +208,19 @@ export default {
console.log(`[default] userMediaProgressUpdate checking for local media progress ${prog.id}`) console.log(`[default] userMediaProgressUpdate checking for local media progress ${prog.id}`)
// Update local media progress if exists // 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 var newLocalMediaProgress = null
if (localProg && localProg.lastUpdate < prog.lastUpdate) { 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 // 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 = { const payload = {
localMediaProgressId: localProg.id, localMediaProgressId: localProg.id,
mediaProgress: prog mediaProgress: prog

View file

@ -10,16 +10,16 @@
<p class="my-2 text-gray-400 font-semibold">{{ name }}</p> <p class="my-2 text-gray-400 font-semibold">{{ name }}</p>
<div v-for="(evt, index) in events" :key="index" class="py-3 flex items-center"> <div v-for="(evt, index) in events" :key="index" class="py-3 flex items-center">
<p class="text-sm text-gray-400 w-12">{{ $formatDate(evt.timestamp, 'HH:mm') }}</p> <p class="text-sm text-gray-400 w-12">{{ $formatDate(evt.timestamp, 'HH:mm') }}</p>
<span class="material-icons px-2" :class="`text-${getEventColor(evt.name)}`">{{ getEventIcon(evt.name) }}</span> <span class="material-icons px-1" :class="`text-${getEventColor(evt.name)}`">{{ getEventIcon(evt.name) }}</span>
<p class="text-sm text-white">{{ evt.name }}</p> <p class="text-sm text-white px-1">{{ evt.name }}</p>
<span v-if="evt.serverSyncAttempted && evt.serverSyncSuccess" class="material-icons-outlined px-2 text-base text-success">cloud_done</span> <span v-if="evt.serverSyncAttempted && evt.serverSyncSuccess" class="material-icons-outlined px-1 text-base text-success">cloud_done</span>
<span v-if="evt.serverSyncAttempted && !evt.serverSyncSuccess" class="material-icons px-2 text-base text-error">error_outline</span> <span v-if="evt.serverSyncAttempted && !evt.serverSyncSuccess" class="material-icons px-1 text-base text-error">error_outline</span>
<p v-if="evt.num" class="text-sm text-gray-400 italic">+{{ evt.num }}</p> <p v-if="evt.num" class="text-sm text-gray-400 italic px-1">+{{ evt.num }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="text-base text-white">{{ $secondsToTimestampFull(evt.currentTime) }}</p> <p class="text-base text-white" @click="clickPlaybackTime(evt.currentTime)">{{ $secondsToTimestampFull(evt.currentTime) }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -43,7 +43,8 @@ export default {
}, },
data() { data() {
return { return {
onMediaItemHistoryUpdatedListener: null onMediaItemHistoryUpdatedListener: null,
startingPlayback: false
} }
}, },
computed: { computed: {
@ -55,6 +56,17 @@ export default {
if (!this.mediaItemHistory) return [] if (!this.mediaItemHistory) return []
return (this.mediaItemHistory.events || []).sort((a, b) => b.timestamp - a.timestamp) return (this.mediaItemHistory.events || []).sort((a, b) => b.timestamp - a.timestamp)
}, },
mediaItemIsLocal() {
return this.mediaItemHistory && this.mediaItemHistory.isLocal
},
mediaItemLibraryItemId() {
if (!this.mediaItemHistory) return null
return this.mediaItemHistory.libraryItemId
},
mediaItemEpisodeId() {
if (!this.mediaItemHistory) return null
return this.mediaItemHistory.episodeId
},
groupedMediaEvents() { groupedMediaEvents() {
const groups = {} const groups = {}
@ -63,11 +75,12 @@ export default {
let lastKey = null let lastKey = null
let numSaves = 0 let numSaves = 0
let index = 0 let numSyncs = 0
this.mediaEvents.forEach((evt) => { this.mediaEvents.forEach((evt) => {
const date = this.$formatDate(evt.timestamp, 'MMM dd, yyyy') const date = this.$formatDate(evt.timestamp, 'MMM dd, yyyy')
let include = true let include = true
let keyUpdated = false
let key = date let key = date
if (date === today) key = 'Today' if (date === today) key = 'Today'
@ -75,25 +88,41 @@ export default {
if (!groups[key]) groups[key] = [] if (!groups[key]) groups[key] = []
if (!lastKey) lastKey = key if (!lastKey || lastKey !== key) {
lastKey = key
keyUpdated = true
}
// Collapse saves // Collapse saves
if (evt.name === 'Save') { if (evt.name === 'Save') {
if (numSaves > 0 && lastKey === key) { if (numSaves > 0 && !keyUpdated) {
include = false include = false
groups[key][index - 1].num = numSaves const totalInGroup = groups[key].length
groups[key][totalInGroup - 1].num = numSaves
numSaves++ numSaves++
} else { } else {
lastKey = key
numSaves = 1 numSaves = 1
} }
} else { } else {
numSaves = 0 numSaves = 0
} }
// Collapse syncs
if (evt.name === 'Sync') {
if (numSyncs > 0 && !keyUpdated) {
include = false
const totalInGroup = groups[key].length
groups[key][totalInGroup - 1].num = numSyncs
numSyncs++
} else {
numSyncs = 1
}
} else {
numSyncs = 0
}
if (include) { if (include) {
groups[key].push(evt) groups[key].push(evt)
index++
} }
}) })
@ -101,6 +130,33 @@ export default {
} }
}, },
methods: { methods: {
async clickPlaybackTime(time) {
if (this.startingPlayback) return
this.startingPlayback = true
await this.$hapticsImpact()
console.log('Click playback time', time)
this.playAtTime(time)
setTimeout(() => {
this.startingPlayback = false
}, 1000)
},
playAtTime(startTime) {
if (this.mediaItemIsLocal) {
// Local only
this.$eventBus.$emit('play-item', { libraryItemId: this.mediaItemLibraryItemId, episodeId: this.mediaItemEpisodeId, startTime })
} else {
// Server may have local
const localProg = this.$store.getters['globals/getLocalMediaProgressByServerItemId'](this.mediaItemLibraryItemId, this.mediaItemEpisodeId)
if (localProg) {
// Has local copy so prefer
this.$eventBus.$emit('play-item', { libraryItemId: localProg.localLibraryItemId, episodeId: localProg.localEpisodeId, serverLibraryItemId: this.mediaItemLibraryItemId, serverEpisodeId: this.mediaItemEpisodeId, startTime })
} else {
// Only on server
this.$eventBus.$emit('play-item', { libraryItemId: this.mediaItemLibraryItemId, episodeId: this.mediaItemEpisodeId, startTime })
}
}
},
getEventIcon(name) { getEventIcon(name) {
switch (name) { switch (name) {
case 'Play': case 'Play':
@ -113,6 +169,8 @@ export default {
return 'sync' return 'sync'
case 'Seek': case 'Seek':
return 'commit' return 'commit'
case 'Sync':
return 'cloud_download'
default: default:
return 'info' return 'info'
} }
@ -129,6 +187,8 @@ export default {
return 'info' return 'info'
case 'Seek': case 'Seek':
return 'gray-200' return 'gray-200'
case 'Sync':
return 'accent'
default: default:
return 'info' return 'info'
} }

View file

@ -43,7 +43,7 @@ class AbsAudioPlayerWeb extends WebPlugin {
} }
// PluginMethod // PluginMethod
async prepareLibraryItem({ libraryItemId, episodeId, playWhenReady }) { async prepareLibraryItem({ libraryItemId, episodeId, playWhenReady, startTime }) {
console.log('[AbsAudioPlayer] Prepare library item', libraryItemId) console.log('[AbsAudioPlayer] Prepare library item', libraryItemId)
if (libraryItemId.startsWith('local_')) { if (libraryItemId.startsWith('local_')) {
@ -52,6 +52,7 @@ class AbsAudioPlayerWeb extends WebPlugin {
var route = !episodeId ? `/api/items/${libraryItemId}/play` : `/api/items/${libraryItemId}/play/${episodeId}` var route = !episodeId ? `/api/items/${libraryItemId}/play` : `/api/items/${libraryItemId}/play/${episodeId}`
var playbackSession = await $axios.$post(route, { mediaPlayer: 'html5-mobile', forceDirectPlay: true }) var playbackSession = await $axios.$post(route, { mediaPlayer: 'html5-mobile', forceDirectPlay: true })
if (playbackSession) { if (playbackSession) {
if (startTime !== undefined && startTime !== null) playbackSession.currentTime = startTime
this.setAudioPlayer(playbackSession, true) this.setAudioPlayer(playbackSession, true)
} }
} }

View file

@ -62,6 +62,10 @@ class DbService {
return AbsDatabase.getAllLocalMediaProgress().then((data) => data.value) return AbsDatabase.getAllLocalMediaProgress().then((data) => data.value)
} }
getLocalMediaProgressForServerItem(payload) {
return AbsDatabase.getLocalMediaProgressForServerItem(payload)
}
removeLocalMediaProgress(localMediaProgressId) { removeLocalMediaProgress(localMediaProgressId) {
return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId }) return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId })
} }

View file

@ -104,7 +104,7 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
} }
Vue.prototype.$secondsToTimestampFull = (seconds) => { Vue.prototype.$secondsToTimestampFull = (seconds) => {
let _seconds = seconds let _seconds = Math.round(seconds)
let _minutes = Math.floor(seconds / 60) let _minutes = Math.floor(seconds / 60)
_seconds -= _minutes * 60 _seconds -= _minutes * 60
let _hours = Math.floor(_minutes / 60) let _hours = Math.floor(_minutes / 60)