mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-12 23:14:48 +02:00
Local media sync events
This commit is contained in:
parent
297eca6a86
commit
8f6dd72df2
15 changed files with 234 additions and 80 deletions
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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}%")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,51 +31,48 @@ 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
|
Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
|
||||||
if (onSeekBack) onSeekBack = false
|
var seekBackTime = calcPauseSeekBackTime()
|
||||||
else {
|
if (seekBackTime > 0) {
|
||||||
Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
|
// Current chapter is used so that seek back does not go back to the previous chapter
|
||||||
seekBackTime = calcPauseSeekBackTime()
|
val currentChapter = playerNotificationService.getCurrentBookChapter()
|
||||||
if (seekBackTime > 0) {
|
val minSeekBackTime = currentChapter?.startMs ?: 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 currentTime = playerNotificationService.getCurrentTime()
|
||||||
val newTime = currentTime - seekBackTime
|
val newTime = currentTime - seekBackTime
|
||||||
if (newTime < minSeekBackTime) {
|
if (newTime < minSeekBackTime) {
|
||||||
seekBackTime = currentTime - minSeekBackTime
|
seekBackTime = currentTime - minSeekBackTime
|
||||||
}
|
|
||||||
Log.d(tag, "SeekBackTime $seekBackTime")
|
|
||||||
onSeekBack = true
|
|
||||||
}
|
}
|
||||||
|
Log.d(tag, "SeekBackTime $seekBackTime")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue