mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-09 05:18:40 +02:00
Rebuilding audio player and handling playback sessions in android. Moving all logic natively
This commit is contained in:
@@ -5,6 +5,7 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.audiobookshelf.app.data.PlaybackSession
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.capacitorjs.plugins.app.AppPlugin
|
||||
@@ -30,7 +31,15 @@ class MyNativeAudio : Plugin() {
|
||||
|
||||
playerNotificationService.setBridge(bridge)
|
||||
|
||||
playerNotificationService.setCustomObjectListener(object : PlayerNotificationService.MyCustomObjectListener {
|
||||
playerNotificationService.clientEventEmitter = (object : PlayerNotificationService.ClientEventEmitter {
|
||||
override fun onPlaybackSession(playbackSession: PlaybackSession) {
|
||||
notifyListeners("onPlaybackSession", JSObject(jacksonObjectMapper().writeValueAsString(playbackSession)))
|
||||
}
|
||||
|
||||
override fun onPlaybackClosed() {
|
||||
emit("onPlaybackClosed", true)
|
||||
}
|
||||
|
||||
override fun onPlayingUpdate(isPlaying: Boolean) {
|
||||
emit("onPlayingUpdate", isPlaying)
|
||||
}
|
||||
@@ -67,10 +76,9 @@ class MyNativeAudio : Plugin() {
|
||||
@PluginMethod
|
||||
fun prepareLibraryItem(call: PluginCall) {
|
||||
var libraryItemId = call.getString("libraryItemId", "").toString()
|
||||
var mediaEntityId = call.getString("mediaEntityId", "").toString()
|
||||
var playWhenReady = call.getBoolean("playWhenReady") == true
|
||||
|
||||
apiHandler.playLibraryItem(libraryItemId) {
|
||||
apiHandler.playLibraryItem(libraryItemId, false) {
|
||||
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
Log.d(tag, "Preparing Player TEST ${jacksonObjectMapper().writeValueAsString(it)}")
|
||||
|
||||
@@ -54,7 +54,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
var isStarted = false
|
||||
}
|
||||
|
||||
interface MyCustomObjectListener {
|
||||
interface ClientEventEmitter {
|
||||
fun onPlaybackSession(playbackSession:PlaybackSession)
|
||||
fun onPlaybackClosed()
|
||||
fun onPlayingUpdate(isPlaying: Boolean)
|
||||
fun onMetadata(metadata: JSObject)
|
||||
fun onPrepare(audiobookId: String, playWhenReady: Boolean)
|
||||
@@ -65,7 +67,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
private val tag = "PlayerService"
|
||||
private val binder = LocalBinder()
|
||||
|
||||
var listener:MyCustomObjectListener? = null
|
||||
var clientEventEmitter:ClientEventEmitter? = null
|
||||
|
||||
private lateinit var ctx:Context
|
||||
private lateinit var mediaSessionConnector: MediaSessionConnector
|
||||
@@ -106,9 +108,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
private var mShakeDetector: ShakeDetector? = null
|
||||
private var shakeSensorUnregisterTask:TimerTask? = null
|
||||
|
||||
fun setCustomObjectListener(mylistener: MyCustomObjectListener) {
|
||||
listener = mylistener
|
||||
}
|
||||
fun setBridge(bridge: Bridge) {
|
||||
webviewBridge = bridge
|
||||
}
|
||||
@@ -380,8 +379,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||
var builder = MediaDescriptionCompat.Builder()
|
||||
.setMediaId(currentPlaybackSession!!.id)
|
||||
.setTitle(currentPlaybackSession!!.getTitle())
|
||||
.setSubtitle(currentPlaybackSession!!.getAuthor())
|
||||
.setTitle(currentPlaybackSession!!.displayTitle)
|
||||
.setSubtitle(currentPlaybackSession!!.displayAuthor)
|
||||
.setMediaUri(currentPlaybackSession!!.getContentUri())
|
||||
.setIconUri(currentPlaybackSession!!.getCoverUri())
|
||||
return builder.build()
|
||||
@@ -656,7 +655,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
audiobookProgressSyncer.stop()
|
||||
}
|
||||
|
||||
listener?.onPlayingUpdate(player.isPlaying)
|
||||
clientEventEmitter?.onPlayingUpdate(player.isPlaying)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -668,6 +667,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
*/
|
||||
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean) {
|
||||
currentPlaybackSession = playbackSession
|
||||
|
||||
clientEventEmitter?.onPlaybackSession(playbackSession)
|
||||
|
||||
var metadata = playbackSession.getMediaMetadataCompat()
|
||||
mediaSession.setMetadata(metadata)
|
||||
var mediaMetadata = playbackSession.getExoMediaMetadata()
|
||||
@@ -679,7 +681,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
if (mPlayer == currentPlayer) {
|
||||
var mediaSource:MediaSource
|
||||
|
||||
if (currentPlaybackSession?.isLocal == true) {
|
||||
if (!playbackSession.isHLS) {
|
||||
Log.d(tag, "Playing Local File")
|
||||
var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
|
||||
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
||||
@@ -881,11 +883,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
|
||||
fun terminateStream() {
|
||||
if (currentPlayer.playbackState == Player.STATE_READY) {
|
||||
currentPlayer.clearMediaItems()
|
||||
}
|
||||
currentAudiobookStreamData?.id = ""
|
||||
// if (currentPlayer.playbackState == Player.STATE_READY) {
|
||||
// currentPlayer.clearMediaItems()
|
||||
// }
|
||||
currentPlayer.clearMediaItems()
|
||||
currentPlaybackSession = null
|
||||
lastPauseTime = 0
|
||||
clientEventEmitter?.onPlaybackClosed()
|
||||
}
|
||||
|
||||
fun sendClientMetadata(stateName: String) {
|
||||
@@ -895,7 +899,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
metadata.put("duration", duration)
|
||||
metadata.put("currentTime", mPlayer.currentPosition)
|
||||
metadata.put("stateName", stateName)
|
||||
listener?.onMetadata(metadata)
|
||||
clientEventEmitter?.onMetadata(metadata)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
||||
}
|
||||
}
|
||||
|
||||
playerNotificationService.listener?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
|
||||
sleepTimerRunning = true
|
||||
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
|
||||
@@ -99,14 +99,14 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
||||
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
|
||||
|
||||
if (sleepTimeSecondsRemaining > 0) {
|
||||
playerNotificationService.listener?.onSleepTimerSet(sleepTimeSecondsRemaining)
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining)
|
||||
}
|
||||
|
||||
if (sleepTimeSecondsRemaining <= 0) {
|
||||
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
|
||||
pause()
|
||||
|
||||
playerNotificationService.listener?.onSleepTimerEnded(getCurrentTime())
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
|
||||
clearSleepTimer()
|
||||
sleepTimerFinishedAt = System.currentTimeMillis()
|
||||
} else if (sleepTimeSecondsRemaining <= 30) {
|
||||
@@ -136,7 +136,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
||||
fun cancelSleepTimer() {
|
||||
Log.d(tag, "Canceling Sleep Timer")
|
||||
clearSleepTimer()
|
||||
playerNotificationService.listener?.onSleepTimerSet(0)
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0)
|
||||
}
|
||||
|
||||
private fun extendSleepTime() {
|
||||
@@ -150,7 +150,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
||||
if (sleepTimerEndTime > getDuration()) sleepTimerEndTime = getDuration()
|
||||
}
|
||||
|
||||
playerNotificationService.listener?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
}
|
||||
|
||||
fun checkShouldExtendSleepTimer() {
|
||||
@@ -197,7 +197,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
||||
}
|
||||
|
||||
setVolume(1F)
|
||||
playerNotificationService.listener?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
}
|
||||
|
||||
fun decreaseSleepTime(time: Long) {
|
||||
@@ -219,6 +219,6 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
||||
}
|
||||
|
||||
setVolume(1F)
|
||||
playerNotificationService.listener?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ data class Book(
|
||||
var metadata:BookMetadata,
|
||||
var coverPath:String?,
|
||||
var tags:MutableList<String>,
|
||||
var audioFiles:MutableList<AudioFile>
|
||||
var audioFiles:MutableList<AudioFile>,
|
||||
var chapters:MutableList<BookChapter>
|
||||
) : MediaType()
|
||||
|
||||
// This auto-detects whether it is a Book or Podcast
|
||||
@@ -154,3 +155,11 @@ data class AudioTrack(
|
||||
var localFileId:String?,
|
||||
var audioProbeResult:AudioProbeResult?
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class BookChapter(
|
||||
var id:Int,
|
||||
var start:Double,
|
||||
var end:Double,
|
||||
var title:String?
|
||||
)
|
||||
|
||||
@@ -52,11 +52,9 @@ class DbManager : Plugin() {
|
||||
}
|
||||
|
||||
fun saveLocalMediaItems(localMediaItems:List<LocalMediaItem>) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
localMediaItems.map {
|
||||
Paper.book("localMediaItems").write(it.id, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLocalFolder(localFolder:LocalFolder) {
|
||||
|
||||
@@ -29,7 +29,7 @@ data class LocalMediaItem(
|
||||
var absolutePath:String,
|
||||
var audioTracks:MutableList<AudioTrack>,
|
||||
var localFiles:MutableList<LocalFile>,
|
||||
var coverPath:String?
|
||||
var coverContentUrl:String?
|
||||
) {
|
||||
|
||||
@JsonIgnore
|
||||
@@ -53,7 +53,7 @@ data class LocalMediaItem(
|
||||
var sessionId = "play-${UUID.randomUUID()}"
|
||||
|
||||
var mediaMetadata = getMediaMetadata()
|
||||
return PlaybackSession(sessionId,null,null,null,null,mediaType,mediaMetadata,null,getDuration(),PLAYMETHOD_LOCAL,audioTracks,0.0,null,this,null,null)
|
||||
return PlaybackSession(sessionId,null,null,null, mediaType, mediaMetadata, mutableListOf(), name, "author name here",null,getDuration(),PLAYMETHOD_LOCAL,audioTracks,0.0,null,this,null,null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,11 @@ class PlaybackSession(
|
||||
var userId:String?,
|
||||
var libraryItemId:String?,
|
||||
var episodeId:String?,
|
||||
var mediaEntityId:String?,
|
||||
var mediaType:String,
|
||||
var mediaMetadata:MediaTypeMetadata,
|
||||
var chapters:MutableList<BookChapter>,
|
||||
var displayTitle: String?,
|
||||
var displayAuthor: String?,
|
||||
var coverPath:String?,
|
||||
var duration:Double,
|
||||
var playMethod:Int,
|
||||
@@ -37,23 +39,9 @@ class PlaybackSession(
|
||||
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
|
||||
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
||||
|
||||
@JsonIgnore
|
||||
fun getTitle():String {
|
||||
if (mediaMetadata == null) return "Unset"
|
||||
var metadata = mediaMetadata as BookMetadata
|
||||
return metadata.title
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getAuthor():String {
|
||||
if (mediaMetadata == null) return "Unset"
|
||||
var metadata = mediaMetadata as BookMetadata
|
||||
return metadata.authorName ?: "Unset"
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getCoverUri(): Uri {
|
||||
if (localMediaItem?.coverPath != null) return Uri.parse(localMediaItem?.coverPath) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||
if (localMediaItem?.coverContentUrl != null) return Uri.parse(localMediaItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||
|
||||
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||
return Uri.parse("$serverUrl/api/items/$libraryItemId/cover?token=$token")
|
||||
@@ -75,11 +63,11 @@ class PlaybackSession(
|
||||
@JsonIgnore
|
||||
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
||||
var metadataBuilder = MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, this.getTitle())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, getTitle())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, this.getAuthor())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, this.getAuthor())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, this.getAuthor())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "series")
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||
return metadataBuilder.build()
|
||||
@@ -87,13 +75,12 @@ class PlaybackSession(
|
||||
|
||||
@JsonIgnore
|
||||
fun getExoMediaMetadata(): MediaMetadata {
|
||||
var authorName = this.getAuthor()
|
||||
var metadataBuilder = MediaMetadata.Builder()
|
||||
.setTitle(this.getTitle())
|
||||
.setDisplayTitle(this.getTitle())
|
||||
.setArtist(authorName)
|
||||
.setAlbumArtist(authorName)
|
||||
.setSubtitle(authorName)
|
||||
.setTitle(displayTitle)
|
||||
.setDisplayTitle(displayTitle)
|
||||
.setArtist(displayAuthor)
|
||||
.setAlbumArtist(displayAuthor)
|
||||
.setSubtitle(displayAuthor)
|
||||
|
||||
var contentUri = this.getContentUri()
|
||||
metadataBuilder.setMediaUri(contentUri)
|
||||
|
||||
@@ -11,6 +11,9 @@ import com.arthenica.ffmpegkit.Level
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FolderScanner(var ctx: Context) {
|
||||
private val tag = "FolderScanner"
|
||||
@@ -69,7 +72,7 @@ class FolderScanner(var ctx: Context) {
|
||||
var localFiles = mutableListOf<LocalFile>()
|
||||
var index = 1
|
||||
var startOffset = 0.0
|
||||
var coverPath:String? = null
|
||||
var coverContentUrl:String? = null
|
||||
|
||||
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||
|
||||
@@ -146,14 +149,14 @@ class FolderScanner(var ctx: Context) {
|
||||
if (existingLocalFile == null) {
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
if (existingMediaItem != null && existingMediaItem.coverPath == null) {
|
||||
if (existingMediaItem != null && existingMediaItem.coverContentUrl == null) {
|
||||
// Existing media item did not have a cover - cover found on scan
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
|
||||
// First image file use as cover path
|
||||
if (coverPath == null) {
|
||||
coverPath = localFile.contentUrl
|
||||
if (coverContentUrl == null) {
|
||||
coverContentUrl = localFile.contentUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +173,7 @@ class FolderScanner(var ctx: Context) {
|
||||
else mediaItemsAdded++
|
||||
|
||||
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
|
||||
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverPath)
|
||||
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl)
|
||||
mediaItems.add(localMediaItem)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,11 +109,14 @@ class ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fun playLibraryItem(libraryItemId:String, cb: (PlaybackSession) -> Unit) {
|
||||
fun playLibraryItem(libraryItemId:String, forceTranscode:Boolean, cb: (PlaybackSession) -> Unit) {
|
||||
val mapper = jacksonObjectMapper()
|
||||
var payload = JSObject()
|
||||
payload.put("mediaPlayer", "exo-player")
|
||||
payload.put("forceDirectPlay", true)
|
||||
|
||||
// Only if direct play fails do we force transcode
|
||||
if (!forceTranscode) payload.put("forceDirectPlay", true)
|
||||
else payload.put("forceTranscode", true)
|
||||
|
||||
postRequest("/api/items/$libraryItemId/play", payload) {
|
||||
it.put("serverUrl", serverUrl)
|
||||
|
||||
Reference in New Issue
Block a user