mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-30 14:49:47 +02:00
Android add media progress sync for server sessions
This commit is contained in:
parent
9ad351f0d7
commit
526fca98b9
10 changed files with 290 additions and 317 deletions
|
@ -19,130 +19,6 @@ data class DeviceData(
|
|||
var localLibraryItemIdPlaying:String?
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LocalLibraryItem(
|
||||
var id:String,
|
||||
var serverAddress:String?,
|
||||
var libraryItemId:String?,
|
||||
var folderId:String,
|
||||
var basePath:String,
|
||||
var absolutePath:String,
|
||||
var contentUrl:String,
|
||||
var isInvalid:Boolean,
|
||||
var mediaType:String,
|
||||
var media:MediaType,
|
||||
var localFiles:MutableList<LocalFile>,
|
||||
var coverContentUrl:String?,
|
||||
var coverAbsolutePath:String?,
|
||||
var isLocal:Boolean
|
||||
) {
|
||||
@JsonIgnore
|
||||
fun getDuration():Double {
|
||||
var total = 0.0
|
||||
var audioTracks = media.getAudioTracks()
|
||||
audioTracks.forEach{ total += it.duration }
|
||||
return total
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun updateFromScan(audioTracks:MutableList<AudioTrack>, _localFiles:MutableList<LocalFile>) {
|
||||
media.setAudioTracks(audioTracks)
|
||||
localFiles = _localFiles
|
||||
|
||||
if (coverContentUrl != null) {
|
||||
if (localFiles.find { it.contentUrl == coverContentUrl } == null) {
|
||||
// Cover was removed
|
||||
coverContentUrl = null
|
||||
coverAbsolutePath = null
|
||||
media.coverPath = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getPlaybackSession():PlaybackSession {
|
||||
var sessionId = "play-${UUID.randomUUID()}"
|
||||
|
||||
var mediaMetadata = media.metadata
|
||||
var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
|
||||
var authorName = "Unknown"
|
||||
if (mediaType == "book") {
|
||||
var bookMetadata = mediaMetadata as BookMetadata
|
||||
authorName = bookMetadata?.authorName ?: "Unknown"
|
||||
}
|
||||
return PlaybackSession(sessionId,null,null,null, mediaType, mediaMetadata, chapters, mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL, media.getAudioTracks() as MutableList<AudioTrack>,0.0,null,this,null,null)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun removeLocalFile(localFileId:String) {
|
||||
localFiles.removeIf { it.id == localFileId }
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LocalMediaItem(
|
||||
var id:String,
|
||||
var serverAddress:String?,
|
||||
var name: String,
|
||||
var mediaType:String,
|
||||
var folderId:String,
|
||||
var contentUrl:String,
|
||||
var simplePath: String,
|
||||
var basePath:String,
|
||||
var absolutePath:String,
|
||||
var audioTracks:MutableList<AudioTrack>,
|
||||
var localFiles:MutableList<LocalFile>,
|
||||
var coverContentUrl:String?,
|
||||
var coverAbsolutePath:String?
|
||||
) {
|
||||
|
||||
@JsonIgnore
|
||||
fun getDuration():Double {
|
||||
var total = 0.0
|
||||
audioTracks.forEach{ total += it.duration }
|
||||
return total
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getTotalSize():Long {
|
||||
var total = 0L
|
||||
localFiles.forEach { total += it.size }
|
||||
return total
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaMetadata():MediaTypeMetadata {
|
||||
return if (mediaType == "book") {
|
||||
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null,null)
|
||||
} else {
|
||||
PodcastMetadata(name,null,null, mutableListOf())
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getAudiobookChapters():List<BookChapter> {
|
||||
if (mediaType != "book" || audioTracks.isEmpty()) return mutableListOf()
|
||||
if (audioTracks.size == 1) { // Single track audiobook look for chapters from ffprobe
|
||||
return audioTracks[0].audioProbeResult?.getBookChapters() ?: mutableListOf()
|
||||
}
|
||||
// Multi-track make chapters from tracks
|
||||
return audioTracks.map { it.getBookChapter() }
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getLocalLibraryItem():LocalLibraryItem {
|
||||
var mediaMetadata = getMediaMetadata()
|
||||
if (mediaType == "book") {
|
||||
var chapters = getAudiobookChapters()
|
||||
var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
|
||||
return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true)
|
||||
} else {
|
||||
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false)
|
||||
return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LocalFile(
|
||||
var id:String,
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import java.util.*
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LocalLibraryItem(
|
||||
var id:String,
|
||||
var serverAddress:String?,
|
||||
var libraryItemId:String?,
|
||||
var folderId:String,
|
||||
var basePath:String,
|
||||
var absolutePath:String,
|
||||
var contentUrl:String,
|
||||
var isInvalid:Boolean,
|
||||
var mediaType:String,
|
||||
var media:MediaType,
|
||||
var localFiles:MutableList<LocalFile>,
|
||||
var coverContentUrl:String?,
|
||||
var coverAbsolutePath:String?,
|
||||
var isLocal:Boolean
|
||||
) {
|
||||
@JsonIgnore
|
||||
fun getDuration():Double {
|
||||
var total = 0.0
|
||||
var audioTracks = media.getAudioTracks()
|
||||
audioTracks.forEach{ total += it.duration }
|
||||
return total
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun updateFromScan(audioTracks:MutableList<AudioTrack>, _localFiles:MutableList<LocalFile>) {
|
||||
media.setAudioTracks(audioTracks)
|
||||
localFiles = _localFiles
|
||||
|
||||
if (coverContentUrl != null) {
|
||||
if (localFiles.find { it.contentUrl == coverContentUrl } == null) {
|
||||
// Cover was removed
|
||||
coverContentUrl = null
|
||||
coverAbsolutePath = null
|
||||
media.coverPath = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getPlaybackSession():PlaybackSession {
|
||||
var sessionId = "play-${UUID.randomUUID()}"
|
||||
|
||||
var mediaMetadata = media.metadata
|
||||
var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
|
||||
var authorName = "Unknown"
|
||||
if (mediaType == "book") {
|
||||
var bookMetadata = mediaMetadata as BookMetadata
|
||||
authorName = bookMetadata?.authorName ?: "Unknown"
|
||||
}
|
||||
return PlaybackSession(sessionId,null,null,null, mediaType, mediaMetadata, chapters, mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL, media.getAudioTracks() as MutableList<AudioTrack>,0.0,null,this,null,null)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun removeLocalFile(localFileId:String) {
|
||||
localFiles.removeIf { it.id == localFileId }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
/*
|
||||
Used as a helper class to generate LocalLibraryItem from scan results
|
||||
*/
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LocalMediaItem(
|
||||
var id:String,
|
||||
var serverAddress:String?,
|
||||
var name: String,
|
||||
var mediaType:String,
|
||||
var folderId:String,
|
||||
var contentUrl:String,
|
||||
var simplePath: String,
|
||||
var basePath:String,
|
||||
var absolutePath:String,
|
||||
var audioTracks:MutableList<AudioTrack>,
|
||||
var localFiles:MutableList<LocalFile>,
|
||||
var coverContentUrl:String?,
|
||||
var coverAbsolutePath:String?
|
||||
) {
|
||||
|
||||
@JsonIgnore
|
||||
fun getDuration():Double {
|
||||
var total = 0.0
|
||||
audioTracks.forEach{ total += it.duration }
|
||||
return total
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getTotalSize():Long {
|
||||
var total = 0L
|
||||
localFiles.forEach { total += it.size }
|
||||
return total
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaMetadata():MediaTypeMetadata {
|
||||
return if (mediaType == "book") {
|
||||
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null,null)
|
||||
} else {
|
||||
PodcastMetadata(name,null,null, mutableListOf())
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getAudiobookChapters():List<BookChapter> {
|
||||
if (mediaType != "book" || audioTracks.isEmpty()) return mutableListOf()
|
||||
if (audioTracks.size == 1) { // Single track audiobook look for chapters from ffprobe
|
||||
return audioTracks[0].audioProbeResult?.getBookChapters() ?: mutableListOf()
|
||||
}
|
||||
// Multi-track make chapters from tracks
|
||||
return audioTracks.map { it.getBookChapter() }
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getLocalLibraryItem():LocalLibraryItem {
|
||||
var mediaMetadata = getMediaMetadata()
|
||||
if (mediaType == "book") {
|
||||
var chapters = getAudiobookChapters()
|
||||
var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
|
||||
return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true)
|
||||
} else {
|
||||
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false)
|
||||
return LocalLibraryItem(id,serverAddress, null, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class MediaProgress(
|
||||
val id:String,
|
||||
val libraryItemId:String,
|
||||
val episodeId:String,
|
||||
val duration:Double,
|
||||
val progress:Double, // 0 to 1
|
||||
val currentTime:Int,
|
||||
val isFinished:Boolean,
|
||||
val lastUpdate:Long,
|
||||
val startedAt:Long,
|
||||
val finishedAt:Long,
|
||||
val isLocal:Boolean?
|
||||
)
|
|
@ -127,4 +127,9 @@ class PlaybackSession(
|
|||
}
|
||||
return mediaItems
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun clone():PlaybackSession {
|
||||
return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,audioTracks,currentTime,libraryItem,localLibraryItem,serverUrl,token)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
package com.audiobookshelf.app.player
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.getcapacitor.JSObject
|
||||
import okhttp3.*
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
/*
|
||||
* Normal progress sync is handled in webview, but when using android auto webview may not be open.
|
||||
* If webview is not open sync progress every 5s. Webview can be closed at any time so interval is always set.
|
||||
*/
|
||||
class AudiobookProgressSyncer constructor(playerNotificationService:PlayerNotificationService, client: OkHttpClient) {
|
||||
private val tag = "AudiobookProgressSync"
|
||||
private val playerNotificationService:PlayerNotificationService = playerNotificationService
|
||||
private val client:OkHttpClient = client
|
||||
|
||||
private var listeningTimerTask: TimerTask? = null
|
||||
var listeningTimerRunning:Boolean = false
|
||||
|
||||
private var webviewOpenOnStart:Boolean = false
|
||||
private var webviewClosedMidSession:Boolean = false
|
||||
private var listeningBookTitle:String? = ""
|
||||
private var listeningBookIsLocal:Boolean = false
|
||||
private var listeningBookId:String? = ""
|
||||
private var listeningStreamId:String? = ""
|
||||
|
||||
private var lastPlaybackTime:Long = 0
|
||||
private var lastUpdateTime:Long = 0
|
||||
|
||||
fun start() {
|
||||
if (listeningTimerRunning) {
|
||||
Log.d(tag, "start: Timer already running for $listeningBookTitle")
|
||||
if (playerNotificationService.getCurrentBookTitle() != listeningBookTitle) {
|
||||
Log.d(tag, "start: Changed audiobook stream - resetting timer")
|
||||
listeningTimerTask?.cancel()
|
||||
}
|
||||
}
|
||||
listeningTimerRunning = true
|
||||
|
||||
webviewOpenOnStart = playerNotificationService.getIsWebviewOpen()
|
||||
listeningBookTitle = playerNotificationService.getCurrentBookTitle()
|
||||
listeningBookIsLocal = false
|
||||
listeningBookId = "empty"
|
||||
listeningStreamId = "empty"
|
||||
|
||||
lastPlaybackTime = playerNotificationService.getCurrentTime()
|
||||
lastUpdateTime = System.currentTimeMillis() / 1000L
|
||||
|
||||
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
// Webview was closed while android auto is open - switch to native sync
|
||||
var isWebviewOpen = playerNotificationService.getIsWebviewOpen()
|
||||
if (!isWebviewOpen && webviewOpenOnStart) {
|
||||
Log.d(tag, "Listening Timer: webview closed Switching to native sync tracking")
|
||||
webviewOpenOnStart = false
|
||||
webviewClosedMidSession = true
|
||||
lastUpdateTime = System.currentTimeMillis() / 1000L
|
||||
} else if (isWebviewOpen && webviewClosedMidSession) {
|
||||
Log.d(tag, "Listening Timer: webview re-opened Switching back to webview sync tracking")
|
||||
webviewClosedMidSession = false
|
||||
webviewOpenOnStart = true
|
||||
lastUpdateTime = System.currentTimeMillis() / 1000L
|
||||
}
|
||||
if (!webviewOpenOnStart && playerNotificationService.currentPlayer.isPlaying) {
|
||||
sync()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (!listeningTimerRunning) return
|
||||
Log.d(tag, "stop: Stopping listening for $listeningBookTitle")
|
||||
|
||||
if (!webviewOpenOnStart) {
|
||||
sync()
|
||||
}
|
||||
reset()
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
listeningTimerTask?.cancel()
|
||||
listeningTimerTask = null
|
||||
listeningTimerRunning = false
|
||||
listeningBookTitle = ""
|
||||
listeningBookId = ""
|
||||
listeningBookIsLocal = false
|
||||
listeningStreamId = ""
|
||||
}
|
||||
|
||||
fun sync() {
|
||||
var currTime = System.currentTimeMillis() / 1000L
|
||||
var elapsed = currTime - lastUpdateTime
|
||||
lastUpdateTime = currTime
|
||||
|
||||
if (!listeningBookIsLocal) {
|
||||
Log.d(tag, "ListeningTimer: Sending sync data to server: elapsed $elapsed | $listeningStreamId | $listeningBookId")
|
||||
|
||||
// Send sync data only for streaming books
|
||||
var syncData: JSObject = JSObject()
|
||||
syncData.put("timeListened", elapsed)
|
||||
syncData.put("currentTime", playerNotificationService.getCurrentTime() / 1000)
|
||||
syncData.put("streamId", listeningStreamId)
|
||||
syncData.put("audiobookId", listeningBookId)
|
||||
sendStreamSyncData(syncData) {
|
||||
Log.d(tag, "Stream sync done")
|
||||
}
|
||||
} else if (listeningStreamId == "download") {
|
||||
// TODO: Save downloaded audiobook progress & send to server if connected
|
||||
Log.d(tag, "ListeningTimer: Is listening download")
|
||||
|
||||
// Send sync data only for local books
|
||||
var syncData: JSObject = JSObject()
|
||||
// var duration = playerNotificationService.getAudiobookDuration() / 1000
|
||||
var duration = 1000
|
||||
var currentTime = playerNotificationService.getCurrentTime() / 1000
|
||||
syncData.put("totalDuration", duration)
|
||||
syncData.put("currentTime", currentTime)
|
||||
syncData.put("progress", if (duration > 0) (currentTime / duration) else 0)
|
||||
syncData.put("isRead", false)
|
||||
syncData.put("lastUpdate", System.currentTimeMillis())
|
||||
syncData.put("audiobookId", listeningBookId)
|
||||
sendLocalSyncData(syncData) {
|
||||
Log.d(tag, "Local sync done")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendLocalSyncData(payload:JSObject, cb: (() -> Unit)) {
|
||||
var serverUrl = DeviceManager.serverAddress
|
||||
var token = DeviceManager.token
|
||||
|
||||
if (serverUrl == "" || token == "") {
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(tag, "Sync Local $serverUrl | $token")
|
||||
var url = "$serverUrl/api/syncLocal"
|
||||
sendServerRequest(url, token, payload, cb)
|
||||
}
|
||||
|
||||
fun sendStreamSyncData(payload:JSObject, cb: (() -> Unit)) {
|
||||
var serverUrl = DeviceManager.serverAddress
|
||||
var token = DeviceManager.token
|
||||
|
||||
if (serverUrl == "" || token == "") {
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(tag, "Sync Stream $serverUrl | $token")
|
||||
var url = "$serverUrl/api/syncStream"
|
||||
sendServerRequest(url, token, payload, cb)
|
||||
}
|
||||
|
||||
fun sendServerRequest(url:String, token:String, payload:JSObject, cb: () -> Unit) {
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val requestBody = payload.toString().toRequestBody(mediaType)
|
||||
val request = Request.Builder().post(requestBody)
|
||||
.url(url).addHeader("Authorization", "Bearer $token")
|
||||
.build()
|
||||
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
Log.d(tag, "FAILURE TO CONNECT")
|
||||
e.printStackTrace()
|
||||
cb()
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.use {
|
||||
if (!response.isSuccessful) throw IOException("Unexpected code $response")
|
||||
cb()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package com.audiobookshelf.app.player
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.data.MediaProgress
|
||||
import com.audiobookshelf.app.data.PlaybackSession
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
data class MediaProgressSyncData(
|
||||
var timeListened:Long, // seconds
|
||||
var duration:Double, // seconds
|
||||
var currentTime:Double // seconds
|
||||
)
|
||||
|
||||
class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, apiHandler: ApiHandler) {
|
||||
private val tag = "MediaProgressSync"
|
||||
private val playerNotificationService:PlayerNotificationService = playerNotificationService
|
||||
private val apiHandler = apiHandler
|
||||
|
||||
private var listeningTimerTask: TimerTask? = null
|
||||
var listeningTimerRunning:Boolean = false
|
||||
|
||||
private var lastSyncTime:Long = 0
|
||||
|
||||
var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing
|
||||
// var currentMediaProgress: MediaProgress? = null
|
||||
|
||||
val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
||||
val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
||||
val currentSessionId get() = currentPlaybackSession?.id ?: ""
|
||||
val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
|
||||
|
||||
fun start() {
|
||||
if (listeningTimerRunning) {
|
||||
Log.d(tag, "start: Timer already running for $currentDisplayTitle")
|
||||
if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
|
||||
Log.d(tag, "Playback session changed, reset timer")
|
||||
listeningTimerTask?.cancel()
|
||||
lastSyncTime = 0L
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
listeningTimerRunning = true
|
||||
lastSyncTime = System.currentTimeMillis()
|
||||
currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy()
|
||||
|
||||
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
if (playerNotificationService.currentPlayer.isPlaying) {
|
||||
var currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||
sync(currentTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (!listeningTimerRunning) return
|
||||
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
||||
|
||||
var currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||
sync(currentTime)
|
||||
reset()
|
||||
}
|
||||
|
||||
fun sync(currentTime:Double) {
|
||||
var diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
||||
if (diffSinceLastSync < 1000L) {
|
||||
return
|
||||
}
|
||||
var listeningTimeToAdd = diffSinceLastSync / 1000L
|
||||
lastSyncTime = System.currentTimeMillis()
|
||||
|
||||
var syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
|
||||
|
||||
if (currentIsLocal) {
|
||||
// TODO: Save local progress sync
|
||||
} else {
|
||||
apiHandler.sendProgressSync(currentSessionId,syncData) {
|
||||
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
listeningTimerTask?.cancel()
|
||||
listeningTimerTask = null
|
||||
listeningTimerRunning = false
|
||||
currentPlaybackSession = null
|
||||
lastSyncTime = 0L
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import androidx.media.utils.MediaConstants
|
|||
import com.audiobookshelf.app.Audiobook
|
||||
import com.audiobookshelf.app.AudiobookManager
|
||||
import com.audiobookshelf.app.data.PlaybackSession
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.getcapacitor.Bridge
|
||||
import com.getcapacitor.JSObject
|
||||
import com.google.android.exoplayer2.*
|
||||
|
@ -69,6 +70,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
private lateinit var mediaSession: MediaSessionCompat
|
||||
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
||||
private lateinit var audiobookManager: AudiobookManager
|
||||
lateinit var apiHandler: ApiHandler
|
||||
|
||||
lateinit var mPlayer: SimpleExoPlayer
|
||||
lateinit var currentPlayer:Player
|
||||
|
@ -76,7 +78,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
|
||||
lateinit var sleepTimerManager:SleepTimerManager
|
||||
lateinit var castManager:CastManager
|
||||
lateinit var audiobookProgressSyncer:AudiobookProgressSyncer
|
||||
lateinit var mediaProgressSyncer:MediaProgressSyncer
|
||||
|
||||
private var notificationId = 10;
|
||||
private var channelId = "audiobookshelf_channel"
|
||||
|
@ -195,7 +197,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
playerNotificationManager.setPlayer(null)
|
||||
mPlayer.release()
|
||||
mediaSession.release()
|
||||
audiobookProgressSyncer.reset()
|
||||
mediaProgressSyncer.reset()
|
||||
Log.d(tag, "onDestroy")
|
||||
isStarted = false
|
||||
|
||||
|
@ -236,14 +238,17 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
|
||||
var client: OkHttpClient = OkHttpClient()
|
||||
|
||||
// Initialize API
|
||||
apiHandler = ApiHandler(ctx)
|
||||
|
||||
// Initialize sleep timer
|
||||
sleepTimerManager = SleepTimerManager(this)
|
||||
|
||||
// Initialize Cast Manager
|
||||
castManager = CastManager(this)
|
||||
|
||||
// Initialize Audiobook Progress Syncer (Only used for android auto when webview is not open)
|
||||
audiobookProgressSyncer = AudiobookProgressSyncer(this, client)
|
||||
// Initialize Media Progress Syncer
|
||||
mediaProgressSyncer = MediaProgressSyncer(this, apiHandler)
|
||||
|
||||
// Initialize shake sensor
|
||||
Log.d(tag, "onCreate Register sensor listener ${mAccelerometer?.isWakeUpSensor}")
|
||||
|
@ -601,13 +606,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
} else lastPauseTime = System.currentTimeMillis()
|
||||
|
||||
// If app is only running in android auto then webview will not be open
|
||||
// so progress needs to be synced natively
|
||||
// Start/stop progress sync interval
|
||||
Log.d(tag, "Playing ${getCurrentBookTitle()} | ${currentPlayer.mediaMetadata.title} | ${currentPlayer.mediaMetadata.displayTitle}")
|
||||
if (player.isPlaying) {
|
||||
audiobookProgressSyncer.start()
|
||||
mediaProgressSyncer.start()
|
||||
} else {
|
||||
audiobookProgressSyncer.stop()
|
||||
mediaProgressSyncer.stop()
|
||||
}
|
||||
|
||||
clientEventEmitter?.onPlayingUpdate(player.isPlaying)
|
||||
|
@ -763,6 +767,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getCurrentTimeSeconds() : Double {
|
||||
return getCurrentTime() / 1000.0
|
||||
}
|
||||
|
||||
fun getBufferedTime() : Long {
|
||||
if (currentPlayer.mediaItemCount > 1) {
|
||||
var windowIndex = currentPlayer.currentWindowIndex
|
||||
|
@ -785,6 +793,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
return currentPlaybackSession?.displayTitle
|
||||
}
|
||||
|
||||
fun getCurrentPlaybackSessionCopy() :PlaybackSession? {
|
||||
return currentPlaybackSession?.clone()
|
||||
}
|
||||
|
||||
fun getCurrentPlaybackSessionId() :String? {
|
||||
return currentPlaybackSession?.id
|
||||
}
|
||||
|
||||
fun calcPauseSeekBackTime() : Long {
|
||||
if (lastPauseTime <= 0) return 0
|
||||
var time: Long = System.currentTimeMillis() - lastPauseTime
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.audiobookshelf.app.data.Library
|
|||
import com.audiobookshelf.app.data.LibraryItem
|
||||
import com.audiobookshelf.app.data.PlaybackSession
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.player.MediaProgressSyncData
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.getcapacitor.JSArray
|
||||
|
@ -119,4 +120,12 @@ class ApiHandler {
|
|||
cb(playbackSession)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: () -> Unit) {
|
||||
var payload = JSObject(jacksonObjectMapper().writeValueAsString(syncData))
|
||||
|
||||
postRequest("/api/session/$sessionId/sync", payload) {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export const getters = {
|
|||
if (!state.user || !state.user.mediaProgress) return null
|
||||
return state.user.mediaProgress.find(li => {
|
||||
if (episodeId && li.episodeId !== episodeId) return false
|
||||
return li.id == libraryItemId
|
||||
return li.libraryItemId == libraryItemId
|
||||
})
|
||||
},
|
||||
getUserBookmarksForItem: (state) => (libraryItemId) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue