mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-26 21:34:05 +02:00
MediaItemHistory and history page
This commit is contained in:
@@ -242,4 +242,12 @@ class DbManager {
|
||||
fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? {
|
||||
return Paper.book("localPlaybackSession").read(playbackSessionId)
|
||||
}
|
||||
|
||||
|
||||
fun saveMediaItemHistory(mediaItemHistory:MediaItemHistory) {
|
||||
Paper.book("mediaItemHistory").write(mediaItemHistory.id,mediaItemHistory)
|
||||
}
|
||||
fun getMediaItemHistory(id:String):MediaItemHistory? {
|
||||
return Paper.book("mediaItemHistory").read(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.audiobookshelf.app.data
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class MediaItemEvent(
|
||||
var name:String, // e.g. Play/Pause/Stop/Seek/Save
|
||||
var type:String, // Playback/Info
|
||||
var description:String?,
|
||||
var currentTime:Number?, // Seconds
|
||||
var serverSyncAttempted:Boolean?,
|
||||
var serverSyncSuccess:Boolean?,
|
||||
var serverSyncMessage:String?,
|
||||
var timestamp: Long
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.audiobookshelf.app.data
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class MediaItemHistory(
|
||||
var id: String, // media id
|
||||
var mediaDisplayTitle: String,
|
||||
var libraryItemId: String,
|
||||
var episodeId: String?,
|
||||
var isLocal:Boolean,
|
||||
var serverConnectionConfigId:String?,
|
||||
var serverAddress:String?,
|
||||
var serverUserId:String?,
|
||||
var createdAt: Long,
|
||||
var events:MutableList<MediaItemEvent>,
|
||||
)
|
||||
@@ -65,11 +65,13 @@ class PlaybackSession(
|
||||
@get:JsonIgnore
|
||||
val localLibraryItemId get() = localLibraryItem?.id ?: ""
|
||||
@get:JsonIgnore
|
||||
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||
val localMediaProgressId get() = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||
@get:JsonIgnore
|
||||
val progress get() = currentTime / getTotalDuration()
|
||||
@get:JsonIgnore
|
||||
val isLocalLibraryItemOnly get() = localLibraryItemId != "" && libraryItemId == null
|
||||
@get:JsonIgnore
|
||||
val mediaItemId get() = if (isLocalLibraryItemOnly) localMediaProgressId else if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId"
|
||||
|
||||
@JsonIgnore
|
||||
fun getCurrentTrackIndex():Int {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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.device.DeviceManager
|
||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||
import com.audiobookshelf.app.player.SyncResult
|
||||
|
||||
object MediaEventManager {
|
||||
const val tag = "MediaEventManager"
|
||||
|
||||
var clientEventEmitter: PlayerNotificationService.ClientEventEmitter? = null
|
||||
|
||||
fun playEvent(playbackSession: PlaybackSession) {
|
||||
Log.i(tag, "Play Event for media \"${playbackSession.displayTitle}\"")
|
||||
addPlaybackEvent("Play", playbackSession, null)
|
||||
}
|
||||
|
||||
fun pauseEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||
Log.i(tag, "Pause Event for media \"${playbackSession.displayTitle}\"")
|
||||
addPlaybackEvent("Pause", playbackSession, syncResult)
|
||||
}
|
||||
|
||||
fun stopEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||
Log.i(tag, "Stop Event for media \"${playbackSession.displayTitle}\"")
|
||||
addPlaybackEvent("Stop", playbackSession, syncResult)
|
||||
}
|
||||
|
||||
fun saveEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||
Log.i(tag, "Save Event for media \"${playbackSession.displayTitle}\"")
|
||||
addPlaybackEvent("Save", playbackSession, syncResult)
|
||||
}
|
||||
|
||||
fun finishedEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||
Log.i(tag, "Finished Event for media \"${playbackSession.displayTitle}\"")
|
||||
addPlaybackEvent("Finished", playbackSession, syncResult)
|
||||
}
|
||||
|
||||
fun seekEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||
Log.i(tag, "Seek Event for media \"${playbackSession.displayTitle}\", currentTime=${playbackSession.currentTime}")
|
||||
addPlaybackEvent("Seek", playbackSession, syncResult)
|
||||
}
|
||||
|
||||
private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult:SyncResult?) {
|
||||
val mediaItemHistory = getMediaItemHistoryForSession(playbackSession) ?: createMediaItemHistoryForSession(playbackSession)
|
||||
|
||||
val mediaItemEvent = MediaItemEvent(
|
||||
name = eventName,
|
||||
type = "Playback",
|
||||
description = "",
|
||||
currentTime = playbackSession.currentTime,
|
||||
serverSyncAttempted = syncResult?.serverSyncAttempted ?: false,
|
||||
serverSyncSuccess = syncResult?.serverSyncSuccess,
|
||||
serverSyncMessage = syncResult?.serverSyncMessage,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
mediaItemHistory.events.add(mediaItemEvent)
|
||||
DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory)
|
||||
|
||||
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
|
||||
}
|
||||
|
||||
private fun getMediaItemHistoryForSession(playbackSession: PlaybackSession) : MediaItemHistory? {
|
||||
return DeviceManager.dbManager.getMediaItemHistory(playbackSession.mediaItemId)
|
||||
}
|
||||
|
||||
private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession):MediaItemHistory {
|
||||
Log.i(tag, "Creating new media item history for media \"${playbackSession.displayTitle}\"")
|
||||
val isLocalOnly = playbackSession.isLocalLibraryItemOnly
|
||||
val libraryItemId = if (isLocalOnly) playbackSession.localLibraryItemId else playbackSession.libraryItemId ?: ""
|
||||
val episodeId:String? = if (isLocalOnly && playbackSession.localEpisodeId != null) playbackSession.localEpisodeId else playbackSession.episodeId
|
||||
return MediaItemHistory(
|
||||
id = playbackSession.mediaItemId,
|
||||
mediaDisplayTitle = playbackSession.displayTitle ?: "Unset",
|
||||
libraryItemId,
|
||||
episodeId,
|
||||
isLocalOnly,
|
||||
playbackSession.
|
||||
serverConnectionConfigId,
|
||||
playbackSession.serverAddress,
|
||||
playbackSession.userId,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
events = mutableListOf())
|
||||
}
|
||||
}
|
||||
+82
-25
@@ -7,6 +7,7 @@ import com.audiobookshelf.app.data.LocalMediaProgress
|
||||
import com.audiobookshelf.app.data.MediaProgress
|
||||
import com.audiobookshelf.app.data.PlaybackSession
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.media.MediaEventManager
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
@@ -17,6 +18,12 @@ data class MediaProgressSyncData(
|
||||
var currentTime:Double // seconds
|
||||
)
|
||||
|
||||
data class SyncResult(
|
||||
var serverSyncAttempted:Boolean,
|
||||
var serverSyncSuccess:Boolean?,
|
||||
var serverSyncMessage:String?
|
||||
)
|
||||
|
||||
class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) {
|
||||
private val tag = "MediaProgressSync"
|
||||
private val METERED_CONNECTION_SYNC_INTERVAL = 60000
|
||||
@@ -30,15 +37,15 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing
|
||||
var currentLocalMediaProgress: LocalMediaProgress? = null
|
||||
|
||||
val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
||||
val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
||||
private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
||||
private val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
||||
val currentSessionId get() = currentPlaybackSession?.id ?: ""
|
||||
val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
|
||||
private val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
|
||||
|
||||
fun start() {
|
||||
fun start(playbackSession:PlaybackSession) {
|
||||
if (listeningTimerRunning) {
|
||||
Log.d(tag, "start: Timer already running for $currentDisplayTitle")
|
||||
if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
|
||||
if (playbackSession.id != currentSessionId) {
|
||||
Log.d(tag, "Playback session changed, reset timer")
|
||||
currentLocalMediaProgress = null
|
||||
listeningTimerTask?.cancel()
|
||||
@@ -48,25 +55,29 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
|
||||
} else if (playbackSession.id != currentSessionId) {
|
||||
currentLocalMediaProgress = null
|
||||
}
|
||||
|
||||
listeningTimerRunning = true
|
||||
lastSyncTime = System.currentTimeMillis()
|
||||
Log.d(tag, "start: init last sync time $lastSyncTime")
|
||||
currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy()
|
||||
currentPlaybackSession = playbackSession.clone()
|
||||
|
||||
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
|
||||
listeningTimerTask = Timer("ListeningTimer", false).schedule(15000L, 15000L) {
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
if (playerNotificationService.currentPlayer.isPlaying) {
|
||||
// Only sync with server on unmetered connection every 5s OR sync with server if last sync time is >= 60s
|
||||
// Only sync with server on unmetered connection every 15s OR sync with server if last sync time is >= 60s
|
||||
val shouldSyncServer = PlayerNotificationService.isUnmeteredNetwork || System.currentTimeMillis() - lastSyncTime >= METERED_CONNECTION_SYNC_INTERVAL
|
||||
|
||||
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||
if (currentTime > 0) {
|
||||
sync(shouldSyncServer, currentTime) {
|
||||
sync(shouldSyncServer, currentTime) { syncResult ->
|
||||
Log.d(tag, "Sync complete")
|
||||
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
MediaEventManager.saveEvent(playbackSession, syncResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,8 +85,16 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
}
|
||||
}
|
||||
|
||||
fun play(playbackSession:PlaybackSession) {
|
||||
Log.d(tag, "play ${playbackSession.displayTitle}")
|
||||
MediaEventManager.playEvent(playbackSession)
|
||||
|
||||
start(playbackSession)
|
||||
}
|
||||
|
||||
fun stop(shouldSync:Boolean? = true, cb: () -> Unit) {
|
||||
if (!listeningTimerRunning) return
|
||||
|
||||
listeningTimerTask?.cancel()
|
||||
listeningTimerTask = null
|
||||
listeningTimerRunning = false
|
||||
@@ -83,11 +102,19 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
|
||||
val currentTime = if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0
|
||||
if (currentTime > 0) { // Current time should always be > 0 on stop
|
||||
sync(true, currentTime) {
|
||||
sync(true, currentTime) { syncResult ->
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
MediaEventManager.stopEvent(playbackSession, syncResult)
|
||||
}
|
||||
|
||||
reset()
|
||||
cb()
|
||||
}
|
||||
} else {
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
MediaEventManager.stopEvent(playbackSession, null)
|
||||
}
|
||||
|
||||
reset()
|
||||
cb()
|
||||
}
|
||||
@@ -95,6 +122,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
|
||||
fun pause(cb: () -> Unit) {
|
||||
if (!listeningTimerRunning) return
|
||||
|
||||
listeningTimerTask?.cancel()
|
||||
listeningTimerTask = null
|
||||
listeningTimerRunning = false
|
||||
@@ -103,33 +131,61 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
|
||||
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||
if (currentTime > 0) { // Current time should always be > 0 on pause
|
||||
sync(true, currentTime) {
|
||||
sync(true, currentTime) { syncResult ->
|
||||
lastSyncTime = 0L
|
||||
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime")
|
||||
failedSyncs = 0
|
||||
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
MediaEventManager.pauseEvent(playbackSession, syncResult)
|
||||
}
|
||||
|
||||
cb()
|
||||
}
|
||||
} else {
|
||||
lastSyncTime = 0L
|
||||
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime (current time < 0)")
|
||||
failedSyncs = 0
|
||||
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
MediaEventManager.pauseEvent(playbackSession, null)
|
||||
}
|
||||
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
fun finished(cb: () -> Unit) {
|
||||
if (!listeningTimerRunning) return
|
||||
|
||||
listeningTimerTask?.cancel()
|
||||
listeningTimerTask = null
|
||||
listeningTimerRunning = false
|
||||
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
||||
Log.d(tag, "finished: Stopping listening for $currentDisplayTitle")
|
||||
|
||||
sync(true, currentPlaybackSession?.duration ?: 0.0) {
|
||||
sync(true, currentPlaybackSession?.duration ?: 0.0) { syncResult ->
|
||||
reset()
|
||||
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
MediaEventManager.finishedEvent(playbackSession, syncResult)
|
||||
}
|
||||
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
fun seek() {
|
||||
currentPlaybackSession?.currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||
Log.d(tag, "seek: $currentDisplayTitle, currentTime=${currentPlaybackSession?.currentTime}")
|
||||
|
||||
if (currentPlaybackSession == null) {
|
||||
Log.e(tag, "seek: Playback session not set")
|
||||
return
|
||||
}
|
||||
|
||||
MediaEventManager.seekEvent(currentPlaybackSession!!, null)
|
||||
}
|
||||
|
||||
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
||||
currentPlaybackSession?.let {
|
||||
it.updatedAt = mediaProgress.lastUpdate
|
||||
@@ -139,15 +195,15 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
}
|
||||
}
|
||||
|
||||
fun sync(shouldSyncServer:Boolean, currentTime:Double, cb: () -> Unit) {
|
||||
fun sync(shouldSyncServer:Boolean, currentTime:Double, cb: (SyncResult?) -> Unit) {
|
||||
if (lastSyncTime <= 0) {
|
||||
Log.e(tag, "Last sync time is not set $lastSyncTime")
|
||||
return
|
||||
return cb(null)
|
||||
}
|
||||
|
||||
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
||||
if (diffSinceLastSync < 1000L) {
|
||||
return cb()
|
||||
return cb(null)
|
||||
}
|
||||
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
||||
|
||||
@@ -157,7 +213,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
|
||||
if (currentPlaybackSession?.progress?.isNaN() == true) {
|
||||
Log.e(tag, "Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}")
|
||||
return cb()
|
||||
return cb(null)
|
||||
}
|
||||
|
||||
if (currentIsLocal) {
|
||||
@@ -170,11 +226,12 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
// Local library item is linked to a server library item
|
||||
// Send sync to server also if connected to this server and local item belongs to this server
|
||||
if (shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
||||
apiHandler.sendLocalProgressSync(it) { syncSuccess ->
|
||||
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
|
||||
Log.d(
|
||||
tag,
|
||||
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
|
||||
)
|
||||
|
||||
if (syncSuccess) {
|
||||
failedSyncs = 0
|
||||
playerNotificationService.alertSyncSuccess()
|
||||
@@ -187,15 +244,15 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
||||
}
|
||||
|
||||
cb()
|
||||
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||
}
|
||||
} else {
|
||||
cb()
|
||||
cb(SyncResult(false, null, null))
|
||||
}
|
||||
}
|
||||
} else if (shouldSyncServer) {
|
||||
apiHandler.sendProgressSync(currentSessionId, syncData) {
|
||||
if (it) {
|
||||
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
|
||||
if (syncSuccess) {
|
||||
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
||||
failedSyncs = 0
|
||||
playerNotificationService.alertSyncSuccess()
|
||||
@@ -208,10 +265,10 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
}
|
||||
Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
||||
}
|
||||
cb()
|
||||
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||
}
|
||||
} else {
|
||||
cb()
|
||||
cb(SyncResult(false, null, null))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.audiobookshelf.app.player
|
||||
|
||||
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
|
||||
|
||||
@@ -16,6 +18,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||
}
|
||||
|
||||
private var onSeekBack: Boolean = false
|
||||
private var isSeeking: Boolean = false
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
val errorMessage = error.message ?: "Unknown Error"
|
||||
@@ -23,6 +26,92 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||
playerNotificationService.handlePlayerPlaybackError(errorMessage) // If was direct playing session, fallback to transcode
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
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()
|
||||
} 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
|
||||
}
|
||||
|
||||
val player = playerNotificationService.currentPlayer
|
||||
|
||||
if (player.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
|
||||
|
||||
val currentTime = playerNotificationService.getCurrentTime()
|
||||
val newTime = currentTime - seekBackTime
|
||||
if (newTime < minSeekBackTime) {
|
||||
seekBackTime = currentTime - minSeekBackTime
|
||||
}
|
||||
Log.d(tag, "SeekBackTime $seekBackTime")
|
||||
onSeekBack = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if playback session still exists or sync media progress if updated
|
||||
val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
|
||||
if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
|
||||
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
|
||||
if (!shouldCarryOn) return
|
||||
}
|
||||
|
||||
if (seekBackTime > 0L) {
|
||||
playerNotificationService.seekBackward(seekBackTime)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(tag, "SeekBackTime: Player not playing set last pause time")
|
||||
lastPauseTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
// Start/stop progress sync interval
|
||||
if (player.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) }
|
||||
} else {
|
||||
playerNotificationService.mediaProgressSyncer.pause {
|
||||
Log.d(tag, "Media Progress Syncer paused and synced")
|
||||
}
|
||||
}
|
||||
|
||||
playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying)
|
||||
|
||||
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
|
||||
}
|
||||
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
Log.d(tag, "onEvents ${playerNotificationService.getMediaPlayer()} | ${events.size()}")
|
||||
|
||||
@@ -67,62 +156,6 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
|
||||
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
|
||||
}
|
||||
if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
|
||||
Log.d(tag, "EVENT IS PLAYING CHANGED ${playerNotificationService.getMediaPlayer()}")
|
||||
|
||||
if (player.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
|
||||
|
||||
val currentTime = playerNotificationService.getCurrentTime()
|
||||
val newTime = currentTime - seekBackTime
|
||||
if (newTime < minSeekBackTime) {
|
||||
seekBackTime = currentTime - minSeekBackTime
|
||||
}
|
||||
Log.d(tag, "SeekBackTime $seekBackTime")
|
||||
onSeekBack = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if playback session still exists or sync media progress if updated
|
||||
val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
|
||||
if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
|
||||
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
|
||||
if (!shouldCarryOn) return
|
||||
}
|
||||
|
||||
if (seekBackTime > 0L) {
|
||||
playerNotificationService.seekBackward(seekBackTime)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(tag, "SeekBackTime: Player not playing set last pause time")
|
||||
lastPauseTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
// Start/stop progress sync interval
|
||||
if (player.isPlaying) {
|
||||
player.volume = 1F // Volume on sleep timer might have decreased this
|
||||
playerNotificationService.mediaProgressSyncer.start()
|
||||
} else {
|
||||
playerNotificationService.mediaProgressSyncer.pause {
|
||||
Log.d(tag, "Media Progress Syncer paused and synced")
|
||||
}
|
||||
}
|
||||
|
||||
playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying)
|
||||
|
||||
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calcPauseSeekBackTime() : Long {
|
||||
|
||||
+6
-4
@@ -74,6 +74,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
fun onProgressSyncFailing()
|
||||
fun onProgressSyncSuccess()
|
||||
fun onNetworkMeteredChanged(isUnmetered:Boolean)
|
||||
fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory)
|
||||
}
|
||||
|
||||
private val tag = "PlayerService"
|
||||
@@ -101,7 +102,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
private var channelId = "audiobookshelf_channel"
|
||||
private var channelName = "Audiobookshelf Channel"
|
||||
|
||||
private var currentPlaybackSession:PlaybackSession? = null
|
||||
var currentPlaybackSession:PlaybackSession? = null
|
||||
private var initialPlaybackRate:Float? = null
|
||||
|
||||
private var isAndroidAuto = false
|
||||
@@ -682,7 +683,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
seekPlayer(playbackSession.currentTimeMs)
|
||||
// Should already be playing
|
||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||
mediaProgressSyncer.start()
|
||||
currentPlaybackSession?.let { mediaProgressSyncer.play(it) }
|
||||
clientEventEmitter?.onPlayingUpdate(true)
|
||||
}
|
||||
} else {
|
||||
@@ -690,9 +691,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
if (seekBackTime > 0L) {
|
||||
seekBackward(seekBackTime)
|
||||
}
|
||||
|
||||
// Should already be playing
|
||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||
mediaProgressSyncer.start()
|
||||
currentPlaybackSession?.let { mediaProgressSyncer.play(it) }
|
||||
clientEventEmitter?.onPlayingUpdate(true)
|
||||
}
|
||||
}
|
||||
@@ -722,7 +724,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
|
||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||
mediaProgressSyncer.start()
|
||||
mediaProgressSyncer.play(it)
|
||||
clientEventEmitter?.onPlayingUpdate(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import android.util.Log
|
||||
import com.audiobookshelf.app.MainActivity
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.media.MediaEventManager
|
||||
import com.audiobookshelf.app.player.CastManager
|
||||
import com.audiobookshelf.app.player.MediaProgressSyncer
|
||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||
@@ -88,7 +90,13 @@ class AbsAudioPlayer : Plugin() {
|
||||
override fun onNetworkMeteredChanged(isUnmetered:Boolean) {
|
||||
emit("onNetworkMeteredChanged", isUnmetered)
|
||||
}
|
||||
|
||||
override fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) {
|
||||
notifyListeners("onMediaItemHistoryUpdated", JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
|
||||
}
|
||||
})
|
||||
|
||||
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
|
||||
}
|
||||
mainActivity.pluginCallback = foregroundServiceReady
|
||||
}
|
||||
@@ -267,6 +275,7 @@ class AbsAudioPlayer : Plugin() {
|
||||
@PluginMethod
|
||||
fun seek(call: PluginCall) {
|
||||
val time:Int = call.getInt("value", 0) ?: 0 // Value in seconds
|
||||
Log.d(tag, "seek action to $time")
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
playerNotificationService.seekPlayer(time * 1000L) // convert to ms
|
||||
call.resolve()
|
||||
|
||||
@@ -406,4 +406,19 @@ class AbsDatabase : Plugin() {
|
||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(DeviceManager.deviceData)))
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun getMediaItemHistory(call:PluginCall) { // Returns device data
|
||||
Log.d(tag, "getMediaItemHistory ${call.data}")
|
||||
val mediaId = call.getString("mediaId") ?: ""
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val mediaItemHistory = DeviceManager.dbManager.getMediaItemHistory(mediaId)
|
||||
if (mediaItemHistory == null) {
|
||||
call.resolve()
|
||||
} else {
|
||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,26 +226,26 @@ class ApiHandler(var ctx:Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean) -> Unit) {
|
||||
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean, String?) -> Unit) {
|
||||
val payload = JSObject(jacksonMapper.writeValueAsString(syncData))
|
||||
|
||||
postRequest("/api/session/$sessionId/sync", payload, null) {
|
||||
if (!it.getString("error").isNullOrEmpty()) {
|
||||
cb(false)
|
||||
cb(false, it.getString("error"))
|
||||
} else {
|
||||
cb(true)
|
||||
cb(true, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean) -> Unit) {
|
||||
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean, String?) -> Unit) {
|
||||
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
||||
|
||||
postRequest("/api/session/local", payload, null) {
|
||||
if (!it.getString("error").isNullOrEmpty()) {
|
||||
cb(false)
|
||||
cb(false, it.getString("error"))
|
||||
} else {
|
||||
cb(true)
|
||||
cb(true, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user