Android clean up audio player, update android auto for new data model

This commit is contained in:
advplyr 2022-04-10 14:22:57 -05:00
parent abf140bd21
commit e5c8d5d4d4
16 changed files with 568 additions and 920 deletions

View file

@ -1,95 +0,0 @@
package com.audiobookshelf.app
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import com.getcapacitor.JSObject
class Audiobook {
var id:String
var ino:String
var libraryId:String
var folderId:String
var book:Book
var duration:Float
var size:Long
var numTracks:Int
var isMissing:Boolean
var isInvalid:Boolean
var path:String
var isDownloaded:Boolean = false
var downloadFolderUrl:String = ""
var folderUrl:String = ""
var contentUrl:String = ""
var filename:String = ""
var localCoverUrl:String = ""
var localCover:String = ""
var serverUrl:String = ""
var token:String = ""
constructor(jsobj: JSObject, serverUrl:String, token:String) {
this.serverUrl = serverUrl
this.token = token
id = jsobj.getString("id", "").toString()
ino = jsobj.getString("ino", "").toString()
libraryId = jsobj.getString("libraryId", "").toString()
folderId = jsobj.getString("folderId", "").toString()
var bookJsObj = jsobj.getJSObject("book")
book = bookJsObj?.let { Book(it) }!!
duration = jsobj.getDouble("duration").toFloat()
size = jsobj.getLong("size")
numTracks = jsobj.getInteger("numTracks")!!
isMissing = jsobj.getBoolean("isMissing")
isInvalid = jsobj.getBoolean("isInvalid")
path = jsobj.getString("path", "").toString()
isDownloaded = jsobj.getBoolean("isDownloaded")
if (isDownloaded) {
downloadFolderUrl = jsobj.getString("downloadFolderUrl", "").toString()
folderUrl = jsobj.getString("folderUrl", "").toString()
contentUrl = jsobj.getString("contentUrl", "").toString()
filename = jsobj.getString("filename", "").toString()
localCover = jsobj.getString("localCover", "").toString()
localCoverUrl = jsobj.getString("localCoverUrl", "").toString()
}
}
fun getCover():Uri {
if (isDownloaded) {
// return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
return Uri.parse(localCoverUrl)
}
if (book.cover == "" || serverUrl == "" || token == "") return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
return Uri.parse("$serverUrl${book.cover}?token=$token&ts=${book.lastUpdate}")
}
fun toMediaMetadata():MediaMetadataCompat {
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, book.title)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, book.title)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, book.authorFL)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCover().toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCover().toString())
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCover().toString())
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, book.authorFL)
// val extras = Bundle()
// if (isDownloaded) {
// extras.putLong(
// MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
// MediaDescriptionCompat.STATUS_DOWNLOADED)
// }
// extras.putInt(
// MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
// MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED)
// putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, RESOURCE_ROOT_URI +
// context.resources.getResourceEntryName(R.drawable.notification_bg_low_normal))
}.build()
}
}

View file

@ -1,230 +0,0 @@
package com.audiobookshelf.app
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import com.audiobookshelf.app.device.DeviceManager
import com.getcapacitor.JSObject
import okhttp3.*
import org.json.JSONArray
import java.io.IOException
class AudiobookManager {
var tag = "AudiobookManager"
var hasLoaded = false
var isLoading = false
var ctx: Context
private var client:OkHttpClient
var audiobooks:MutableList<Audiobook> = mutableListOf()
var audiobooksInProgress:MutableList<Audiobook> = mutableListOf()
constructor(_ctx:Context, _client:OkHttpClient) {
ctx = _ctx
client = _client
}
fun loadCategories(cb: (() -> Unit)) {
var url = "${DeviceManager.serverAddress}/api/libraries/main/categories"
val request = Request.Builder()
.url(url).addHeader("Authorization", "Bearer ${DeviceManager.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")
var bodyString = response.body!!.string()
var results = JSONArray(bodyString)
// var results = resJson.getJSONArray("results")
var totalShelves = results.length() - 1
Log.d(tag, "Got categories $totalShelves")
for (i in 0..totalShelves) {
var shelfobj = results.get(i)
var jsobj = JSObject(shelfobj.toString())
var shelfId = jsobj.getString("id", "")
Log.d(tag, "Category shelf id $shelfId")
if (shelfId == "continue-reading") {
var entities = jsobj.getJSONArray("entities")
var totalEntities = entities.length() - 1
Log.d(tag, "Shelf total entities $totalEntities")
for (y in 0..totalEntities) {
var abobj = entities.get(y)
Log.d(tag, "Shelf category ab id $y = ${abobj.toString()}")
var abjsobj = JSObject(abobj.toString())
abjsobj.put("isDownloaded", false)
var audiobook = Audiobook(abjsobj, DeviceManager.serverAddress, DeviceManager.token)
if (audiobook.isMissing || audiobook.isInvalid || audiobook.numTracks <= 0) {
Log.d(tag, "Not an audiobook or invalid/missing")
} else {
var audiobookExists = audiobooksInProgress.find { it.id == audiobook.id }
if (audiobookExists == null) {
audiobooksInProgress.add(audiobook)
}
}
}
}
}
Log.d(tag, "${audiobooksInProgress.size} Audiobooks In Progress Loaded")
cb()
}
}
})
}
fun loadAudiobooks(cb: (() -> Unit)) {
if (DeviceManager.serverAddress == "" || DeviceManager.token == "") {
Log.d(tag, "Load Audiobooks: No Server or Token set")
cb()
return
} else if (!DeviceManager.serverAddress.startsWith("http")) {
Log.e(tag, "Load Audiobooks: Invalid server url ${DeviceManager.serverAddress}")
cb()
return
}
// First load currently reading
loadCategories() {
// Then load all
var url = "${DeviceManager.serverAddress}/api/libraries/main/books/all?sort=book.title"
val request = Request.Builder()
.url(url).addHeader("Authorization", "Bearer ${DeviceManager.token}")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.d(tag, "Load Audiobooks: FAILURE TO CONNECT")
e.printStackTrace()
cb()
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response")
var bodyString = response.body!!.string()
var resJson = JSObject(bodyString)
var results = resJson.getJSONArray("results")
var totalBooks = results.length() - 1
for (i in 0..totalBooks) {
var abobj = results.get(i)
var jsobj = JSObject(abobj.toString())
jsobj.put("isDownloaded", false)
var audiobook = Audiobook(jsobj, DeviceManager.serverAddress, DeviceManager.token)
if (audiobook.isMissing || audiobook.isInvalid) {
Log.d(tag, "Audiobook ${audiobook.book.title} is missing or invalid")
} else if (audiobook.numTracks <= 0) {
Log.d(tag, "Audiobook ${audiobook.book.title} has audio tracks")
} else {
var audiobookExists = audiobooks.find { it.id == audiobook.id }
if (audiobookExists == null) {
audiobooks.add(audiobook)
} else {
Log.d(tag, "Audiobook already there from downloaded")
}
}
}
Log.d(tag, "${audiobooks.size} Audiobooks Loaded")
cb()
}
}
})
}
}
fun load() {
isLoading = true
hasLoaded = true
}
private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int {
val lhsLength = lhs.length + 1
val rhsLength = rhs.length + 1
var cost = Array(lhsLength) { it }
var newCost = Array(lhsLength) { 0 }
for (i in 1..rhsLength-1) {
newCost[0] = i
for (j in 1..lhsLength-1) {
val match = if(lhs[j - 1] == rhs[i - 1]) 0 else 1
val costReplace = cost[j - 1] + match
val costInsert = cost[j] + 1
val costDelete = newCost[j - 1] + 1
newCost[j] = Math.min(Math.min(costInsert, costDelete), costReplace)
}
val swap = cost
cost = newCost
newCost = swap
}
return cost[lhsLength - 1]
}
fun searchForAudiobook(query:String):Audiobook? {
var closestDistance = 99
var closestMatch:Audiobook? = null
audiobooks.forEach {
var dist = levenshtein(it.book.title, query)
Log.d(tag, "LEVENSHTEIN $dist")
if (dist < closestDistance) {
closestDistance = dist
closestMatch = it
}
}
if (closestMatch != null) {
Log.d(tag, "Closest Search is ${closestMatch?.book?.title} with distance $closestDistance")
if (closestDistance < 2) {
return closestMatch
}
return null
}
return null
}
fun getFirstAudiobook():Audiobook? {
return null
}
// Used for media browser loadChildren, fallback to using the samples if no audiobooks are there
fun getAudiobooksMediaMetadata() : List<MediaMetadataCompat> {
var mediaMetadata:MutableList<MediaMetadataCompat> = mutableListOf()
if (audiobooks.isEmpty()) {
} else {
audiobooks.forEach { mediaMetadata.add(it.toMediaMetadata()) }
}
return mediaMetadata
}
// Used for media browser loadChildren, fallback to using the samples if no audiobooks are there
fun getDownloadedAudiobooksMediaMetadata() : List<MediaMetadataCompat> {
var mediaMetadata:MutableList<MediaMetadataCompat> = mutableListOf()
if (audiobooks.isEmpty()) {
} else {
audiobooks.forEach { if (it.isDownloaded) { mediaMetadata.add(it.toMediaMetadata()) } }
}
return mediaMetadata
}
}

View file

@ -1,39 +0,0 @@
package com.audiobookshelf.app
import com.getcapacitor.JSObject
class Book {
var title:String
var subtitle:String
var author:String
var authorFL:String
var narrator:String
var series:String
var volumeNumber:String
var publisher:String
var description:String
var publishYear:String
var language:String
var cover:String
var coverFullPath:String
var genres:String
var lastUpdate:Long
constructor(jsobj: JSObject) {
title = jsobj.getString("title", "").toString()
subtitle = jsobj.getString("subtitle", "").toString()
author = jsobj.getString("author", "").toString()
authorFL = jsobj.getString("authorFL", "").toString()
narrator = jsobj.getString("narrator", "").toString()
series = jsobj.getString("series", "").toString()
volumeNumber = jsobj.getString("volumeNumber", "").toString()
publisher = jsobj.getString("publisher", "").toString()
description = jsobj.getString("description", "").toString()
publishYear = jsobj.getString("publishYear", "").toString()
language = jsobj.getString("language", "").toString()
cover = jsobj.getString("cover", "").toString()
coverFullPath = jsobj.getString("coverFullPath", "").toString()
genres = jsobj.getString("genres", "").toString()
lastUpdate = jsobj.getLong("lastUpdate")
}
}

View file

@ -1,5 +1,9 @@
package com.audiobookshelf.app.data
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.annotation.*
@JsonIgnoreProperties(ignoreUnknown = true)
@ -21,8 +25,36 @@ data class LibraryItem(
var isInvalid:Boolean,
var mediaType:String,
var media:MediaType,
var libraryFiles:MutableList<LibraryFile>
)
var libraryFiles:MutableList<LibraryFile>?
) {
@get:JsonIgnore
val title get() = media.metadata.title
@get:JsonIgnore
val authorName get() = media.metadata.getAuthorDisplayName()
@JsonIgnore
fun getCoverUri():Uri {
if (media.coverPath == null) {
return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
}
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
}
@JsonIgnore
fun getMediaMetadata(): MediaMetadataCompat {
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
}.build()
}
}
// This auto-detects whether it is a Book or Podcast
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
@ -46,36 +78,36 @@ class Podcast(
metadata:PodcastMetadata,
coverPath:String?,
var tags:MutableList<String>,
var episodes:MutableList<PodcastEpisode>,
var episodes:MutableList<PodcastEpisode>?,
var autoDownloadEpisodes:Boolean
) : MediaType(metadata, coverPath) {
@JsonIgnore
override fun getAudioTracks():List<AudioTrack> {
var tracks = episodes.map { it.audioTrack }
return tracks.filterNotNull()
var tracks = episodes?.map { it.audioTrack }
return tracks?.filterNotNull() ?: mutableListOf()
}
@JsonIgnore
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
// Remove episodes no longer there in tracks
episodes = episodes.filter { ep ->
episodes = episodes?.filter { ep ->
audioTracks.find { it.localFileId == ep.audioTrack?.localFileId } != null
} as MutableList<PodcastEpisode>
// Add new episodes
audioTracks.forEach { at ->
if (episodes.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
var newEpisode = PodcastEpisode("local_" + at.localFileId,episodes.size + 1,null,null,at.title,null,null,null,at)
episodes.add(newEpisode)
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
var newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at)
episodes?.add(newEpisode)
}
}
}
@JsonIgnore
override fun addAudioTrack(audioTrack:AudioTrack) {
var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes.size + 1,null,null,audioTrack.title,null,null,null,audioTrack)
episodes.add(newEpisode)
var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack)
episodes?.add(newEpisode)
}
@JsonIgnore
override fun removeAudioTrack(localFileId:String) {
episodes.removeIf { it.audioTrack?.localFileId == localFileId }
episodes?.removeIf { it.audioTrack?.localFileId == localFileId }
}
}
@ -84,8 +116,8 @@ class Book(
metadata:BookMetadata,
coverPath:String?,
var tags:List<String>,
var audioFiles:List<AudioFile>,
var chapters:List<BookChapter>,
var audioFiles:List<AudioFile>?,
var chapters:List<BookChapter>?,
var tracks:MutableList<AudioTrack>?,
var size:Long?,
var duration:Double?
@ -136,20 +168,23 @@ class Book(
}
}
// This auto-detects whether it is a Book or Podcast
// This auto-detects whether it is a BookMetadata or PodcastMetadata
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes(
JsonSubTypes.Type(BookMetadata::class),
JsonSubTypes.Type(PodcastMetadata::class)
)
open class MediaTypeMetadata(var title:String) {}
open class MediaTypeMetadata(var title:String) {
@JsonIgnore
open fun getAuthorDisplayName():String { return "Unknown" }
}
@JsonIgnoreProperties(ignoreUnknown = true)
class BookMetadata(
title:String,
var subtitle:String?,
var authors:MutableList<Author>,
var narrators:MutableList<String>,
var authors:MutableList<Author>?,
var narrators:MutableList<String>?,
var genres:MutableList<String>,
var publishedYear:String?,
var publishedDate:String?,
@ -164,7 +199,10 @@ class BookMetadata(
var authorNameLF:String?,
var narratorName:String?,
var seriesName:String?
) : MediaTypeMetadata(title)
) : MediaTypeMetadata(title) {
@JsonIgnore
override fun getAuthorDisplayName():String { return authorName ?: "Unknown" }
}
@JsonIgnoreProperties(ignoreUnknown = true)
class PodcastMetadata(
@ -172,7 +210,10 @@ class PodcastMetadata(
var author:String?,
var feedUrl:String?,
var genres:MutableList<String>
) : MediaTypeMetadata(title)
) : MediaTypeMetadata(title) {
@JsonIgnore
override fun getAuthorDisplayName():String { return author ?: "Unknown" }
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class Author(

View file

@ -68,7 +68,7 @@ data class LocalLibraryItem(
var episodeIdNullable = if (episodeId.isNullOrEmpty()) null else episodeId
var dateNow = System.currentTimeMillis()
return PlaybackSession(sessionId,serverUserId,libraryItemId,episodeIdNullable, mediaType, mediaMetadata, chapters, mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, media.getAudioTracks() as MutableList<AudioTrack>,currentTime,null,this,serverConnectionConfigId, serverAddress)
return PlaybackSession(sessionId,serverUserId,libraryItemId,episodeIdNullable, mediaType, mediaMetadata, chapters ?: mutableListOf(), mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, media.getAudioTracks() as MutableList<AudioTrack>,currentTime,null,this,serverConnectionConfigId, serverAddress)
}
@JsonIgnore

View file

@ -0,0 +1,70 @@
package com.audiobookshelf.app.media
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.server.ApiHandler
import java.util.*
class MediaManager(var apiHandler: ApiHandler) {
var serverLibraryItems = listOf<LibraryItem>()
fun loadLibraryItems(cb: (List<LibraryItem>) -> Unit) {
if (serverLibraryItems.isNotEmpty()) {
cb(serverLibraryItems)
} else {
apiHandler.getLibraryItems("main") { libraryItems ->
serverLibraryItems = libraryItems
cb(libraryItems)
}
}
}
fun getFirstItem() : LibraryItem? {
return if (serverLibraryItems.isNotEmpty()) serverLibraryItems[0] else null
}
fun getById(id:String) : LibraryItem? {
return serverLibraryItems.find { it.id == id }
}
fun getFromSearch(query:String?) : LibraryItem? {
if (query.isNullOrEmpty()) return getFirstItem()
return serverLibraryItems.find {
it.title.lowercase(Locale.getDefault()).contains(query.lowercase(Locale.getDefault()))
}
}
fun play(libraryItem:LibraryItem, cb: (PlaybackSession) -> Unit) {
apiHandler.playLibraryItem(libraryItem.id,"",false) {
cb(it)
}
}
private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int {
val lhsLength = lhs.length + 1
val rhsLength = rhs.length + 1
var cost = Array(lhsLength) { it }
var newCost = Array(lhsLength) { 0 }
for (i in 1..rhsLength-1) {
newCost[0] = i
for (j in 1..lhsLength-1) {
val match = if(lhs[j - 1] == rhs[i - 1]) 0 else 1
val costReplace = cost[j - 1] + match
val costInsert = cost[j] + 1
val costDelete = newCost[j - 1] + 1
newCost[j] = Math.min(Math.min(costInsert, costDelete), costReplace)
}
val swap = cost
cost = newCost
newCost = swap
}
return cost[lhsLength - 1]
}
}

View file

@ -6,14 +6,14 @@ import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import androidx.annotation.AnyRes
import com.audiobookshelf.app.Audiobook
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.LibraryItem
class BrowseTree(
val context: Context,
audiobooksInProgress: List<Audiobook>,
audiobookMetadata: List<MediaMetadataCompat>,
itemsInProgress: List<LibraryItem>,
itemsMetadata: List<MediaMetadataCompat>,
downloadedMetadata: List<MediaMetadataCompat>
) {
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
@ -35,7 +35,6 @@ class BrowseTree(
init {
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
val continueReadingMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Reading")
@ -44,27 +43,20 @@ class BrowseTree(
val allMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ALL_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Audiobooks")
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Library Items")
var resource = getUriToDrawable(context, R.drawable.exo_icon_books).toString()
Log.d("BrowseTree", "RESOURCE $resource")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, resource)
}.build()
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
}.build()
// val localsMetadata = MediaMetadataCompat.Builder().apply {
// putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LOCAL_ROOT)
// putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Samples")
// putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
// }.build()
if (audiobooksInProgress.isNotEmpty()) {
if (itemsInProgress.isNotEmpty()) {
rootList += continueReadingMetadata
}
rootList += allMetadata
@ -72,13 +64,13 @@ class BrowseTree(
// rootList += localsMetadata
mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
audiobooksInProgress.forEach { audiobook ->
itemsInProgress.forEach { libraryItem ->
val children = mediaIdToChildren[CONTINUE_ROOT] ?: mutableListOf()
children += audiobook.toMediaMetadata()
children += libraryItem.getMediaMetadata()
mediaIdToChildren[CONTINUE_ROOT] = children
}
audiobookMetadata.forEach {
itemsMetadata.forEach {
val allChildren = mediaIdToChildren[ALL_ROOT] ?: mutableListOf()
allChildren += it
mediaIdToChildren[ALL_ROOT] = allChildren
@ -89,13 +81,6 @@ class BrowseTree(
allChildren += it
mediaIdToChildren[DOWNLOADS_ROOT] = allChildren
}
// localAudio.forEach { local ->
// val localChildren = mediaIdToChildren[LOCAL_ROOT] ?: mutableListOf()
// localChildren += local.toMediaMetadata()
// mediaIdToChildren[LOCAL_ROOT] = localChildren
// }
// Log.d("BrowseTree", "Set LOCAL AUDIO ${localAudio.size}")
}
operator fun get(mediaId: String) = mediaIdToChildren[mediaId]

View file

@ -304,7 +304,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
val castContext = CastContext.getSharedInstance(mainActivity)
playerNotificationService.castPlayer = CastPlayer(castContext).apply {
setSessionAvailabilityListener(CastSessionAvailabilityListener())
addListener(playerNotificationService.getPlayerListener())
addListener(PlayerListener(playerNotificationService))
}
Log.d(tag, "CAST Cast Player Applied")
switchToPlayer(true)
@ -313,8 +313,6 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
"Exception thrown when attempting to obtain CastContext. " + e.message)
return
}
// media.setSession(castSession)
// callback.onJoin(ChromecastUtilities.createSessionObject(castSession))
}

View file

@ -0,0 +1,194 @@
package com.audiobookshelf.app.player
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import android.view.KeyEvent
import com.audiobookshelf.app.data.LibraryItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.*
import kotlin.concurrent.schedule
class MediaSessionCallback(var playerNotificationService:PlayerNotificationService) : MediaSessionCompat.Callback() {
var tag = "MediaSessionCallback"
private var mediaButtonClickCount: Int = 0
var mediaButtonClickTimeout: Long = 1000 //ms
var seekAmount: Long = 20000 //ms
override fun onPrepare() {
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
playerNotificationService.mediaManager.play(li) {
Log.d(tag, "About to prepare player with li ${li.title}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true)
}
}
}
}
override fun onPlay() {
Log.d(tag, "ON PLAY MEDIA SESSION COMPAT")
playerNotificationService.play()
}
override fun onPrepareFromSearch(query: String?, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM SEARCH $query")
super.onPrepareFromSearch(query, extras)
}
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
Log.d(tag, "ON PLAY FROM SEARCH $query")
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
playerNotificationService.mediaManager.play(li) {
Log.d(tag, "About to prepare player with li ${li.title}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true)
}
}
}
}
override fun onPause() {
Log.d(tag, "ON PAUSE MEDIA SESSION COMPAT")
playerNotificationService.pause()
}
override fun onStop() {
playerNotificationService.pause()
}
override fun onSkipToPrevious() {
playerNotificationService.seekBackward(seekAmount)
}
override fun onSkipToNext() {
playerNotificationService.seekForward(seekAmount)
}
override fun onFastForward() {
playerNotificationService.seekForward(seekAmount)
}
override fun onRewind() {
playerNotificationService.seekForward(seekAmount)
}
override fun onSeekTo(pos: Long) {
playerNotificationService.seekPlayer(pos)
}
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
var libraryItem: LibraryItem? = null
if (mediaId.isNullOrEmpty()) {
libraryItem = playerNotificationService.mediaManager.getFirstItem()
} else {
libraryItem = playerNotificationService.mediaManager.getById(mediaId)
}
libraryItem?.let { li ->
playerNotificationService.mediaManager.play(li) {
Log.d(tag, "About to prepare player with li ${li.title}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true)
}
}
}
}
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
return handleCallMediaButton(mediaButtonEvent)
}
fun handleCallMediaButton(intent: Intent): Boolean {
if(Intent.ACTION_MEDIA_BUTTON == intent.getAction()) {
var keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
if (keyEvent?.getAction() == KeyEvent.ACTION_UP) {
when (keyEvent?.getKeyCode()) {
KeyEvent.KEYCODE_HEADSETHOOK -> {
if (0 == mediaButtonClickCount) {
if (playerNotificationService.mPlayer.isPlaying)
playerNotificationService.pause()
else
playerNotificationService.play()
}
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_PLAY -> {
if (0 == mediaButtonClickCount) {
playerNotificationService.play()
playerNotificationService.sleepTimerManager.checkShouldExtendSleepTimer()
}
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
if (0 == mediaButtonClickCount) playerNotificationService.pause()
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
playerNotificationService.seekForward(seekAmount)
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
playerNotificationService.seekBackward(seekAmount)
}
KeyEvent.KEYCODE_MEDIA_STOP -> {
playerNotificationService.terminateStream()
}
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
if (playerNotificationService.mPlayer.isPlaying) {
if (0 == mediaButtonClickCount) playerNotificationService.pause()
handleMediaButtonClickCount()
} else {
if (0 == mediaButtonClickCount) {
playerNotificationService.play()
playerNotificationService.sleepTimerManager.checkShouldExtendSleepTimer()
}
handleMediaButtonClickCount()
}
}
else -> {
Log.d(tag, "KeyCode:${keyEvent?.getKeyCode()}")
return false
}
}
}
}
return true
}
fun handleMediaButtonClickCount() {
mediaButtonClickCount++
if (1 == mediaButtonClickCount) {
Timer().schedule(mediaButtonClickTimeout) {
mediaBtnHandler.sendEmptyMessage(mediaButtonClickCount)
mediaButtonClickCount = 0
}
}
}
private val mediaBtnHandler : Handler = @SuppressLint("HandlerLeak")
object : Handler(){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
if (2 == msg.what) {
playerNotificationService.seekBackward(seekAmount)
playerNotificationService.play()
}
else if (msg.what >= 3) {
playerNotificationService.seekForward(seekAmount)
playerNotificationService.play()
}
}
}
}

View file

@ -0,0 +1,70 @@
package com.audiobookshelf.app.player
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.ResultReceiver
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import com.audiobookshelf.app.data.LibraryItem
import com.google.android.exoplayer2.ControlDispatcher
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificationService) : MediaSessionConnector.PlaybackPreparer {
var tag = "MediaSessionPlaybackPreparer"
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
Log.d(tag, "ON COMMAND $command")
return false
}
override fun getSupportedPrepareActions(): Long {
return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
}
override fun onPrepare(playWhenReady: Boolean) {
Log.d(tag, "ON PREPARE $playWhenReady")
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
playerNotificationService.mediaManager.play(li) {
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady)
}
}
}
}
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
var libraryItem: LibraryItem? = playerNotificationService.mediaManager.getById(mediaId)
libraryItem?.let { li ->
playerNotificationService.mediaManager.play(li) {
Log.d(tag, "About to prepare player with li ${li.title}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady)
}
}
}
}
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM SEARCH $query")
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
playerNotificationService.mediaManager.play(li) {
Log.d(tag, "About to prepare player with li ${li.title}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady)
}
}
}
}
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM URI $uri")
}
}

View file

@ -0,0 +1,102 @@
package com.audiobookshelf.app.player
import android.util.Log
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
class PlayerListener(var playerNotificationService:PlayerNotificationService) : Player.Listener {
var tag = "PlayerListener"
companion object {
var lastPauseTime: Long = 0 //ms
}
private var onSeekBack: Boolean = false
override fun onPlayerError(error: PlaybackException) {
error.message?.let { Log.e(tag, it) }
error.localizedMessage?.let { Log.e(tag, it) }
}
override fun onEvents(player: Player, events: Player.Events) {
if (events.contains(Player.EVENT_POSITION_DISCONTINUITY)) {
Log.d(tag, "EVENT_POSITION_DISCONTINUITY")
}
if (events.contains(Player.EVENT_IS_LOADING_CHANGED)) {
Log.d(tag, "EVENT_IS_LOADING_CHANGED : " + playerNotificationService.mPlayer.isLoading.toString())
}
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_READY) {
Log.d(tag, "STATE_READY : " + playerNotificationService.mPlayer.duration.toString())
if (lastPauseTime == 0L) {
playerNotificationService.sendClientMetadata("ready_no_sync")
lastPauseTime = -1;
} else playerNotificationService.sendClientMetadata("ready")
}
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_BUFFERING) {
Log.d(tag, "STATE_BUFFERING : " + playerNotificationService.mPlayer.currentPosition.toString())
if (lastPauseTime == 0L) playerNotificationService.sendClientMetadata("buffering_no_sync")
else playerNotificationService.sendClientMetadata("buffering")
}
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
Log.d(tag, "STATE_ENDED")
playerNotificationService.sendClientMetadata("ended")
}
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_IDLE) {
Log.d(tag, "STATE_IDLE")
playerNotificationService.sendClientMetadata("idle")
}
}
if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) {
Log.d(tag, "EVENT_MEDIA_METADATA_CHANGED")
}
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED")
}
if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
Log.d(tag, "EVENT IS PLAYING CHANGED")
if (player.isPlaying) {
if (lastPauseTime > 0) {
if (onSeekBack) onSeekBack = false
else {
var backTime = calcPauseSeekBackTime()
if (backTime > 0) {
if (backTime >= playerNotificationService.mPlayer.currentPosition) backTime = playerNotificationService.mPlayer.currentPosition - 500
Log.d(tag, "SeekBackTime $backTime")
onSeekBack = true
playerNotificationService.seekBackward(backTime)
}
}
}
} else lastPauseTime = System.currentTimeMillis()
// Start/stop progress sync interval
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
if (player.isPlaying) {
playerNotificationService.mediaProgressSyncer.start()
} else {
playerNotificationService.mediaProgressSyncer.stop()
}
playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying)
}
}
fun calcPauseSeekBackTime() : Long {
if (lastPauseTime <= 0) return 0
var time: Long = System.currentTimeMillis() - lastPauseTime
var seekback: Long = 0
if (time < 60000) seekback = 0
else if (time < 120000) seekback = 10000
else if (time < 300000) seekback = 15000
else if (time < 1800000) seekback = 20000
else if (time < 3600000) seekback = 25000
else seekback = 29500
return seekback
}
}

View file

@ -0,0 +1,31 @@
package com.audiobookshelf.app.player
import android.app.Notification
import android.util.Log
import com.google.android.exoplayer2.ui.PlayerNotificationManager
class PlayerNotificationListener(var playerNotificationService:PlayerNotificationService) : PlayerNotificationManager.NotificationListener {
var tag = "PlayerNotificationListener"
override fun onNotificationPosted(
notificationId: Int,
notification: Notification,
onGoing: Boolean) {
// Start foreground service
Log.d(tag, "Notification Posted $notificationId - Start Foreground | $notification")
playerNotificationService.startForeground(notificationId, notification)
}
override fun onNotificationCancelled(
notificationId: Int,
dismissedByUser: Boolean
) {
if (dismissedByUser) {
Log.d(tag, "onNotificationCancelled dismissed by user")
playerNotificationService.stopSelf()
} else {
Log.d(tag, "onNotificationCancelled not dismissed by user")
}
}
}

View file

@ -1,13 +1,11 @@
package com.audiobookshelf.app.player
import android.annotation.SuppressLint
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.hardware.Sensor
import android.hardware.SensorManager
import android.net.Uri
import android.os.*
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
@ -16,18 +14,16 @@ import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import android.view.KeyEvent
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.Audiobook
import com.audiobookshelf.app.AudiobookManager
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.LocalMediaProgress
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaManager
import com.audiobookshelf.app.server.ApiHandler
import com.getcapacitor.Bridge
import com.getcapacitor.JSObject
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes
@ -72,7 +68,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private lateinit var playerNotificationManager: PlayerNotificationManager
private lateinit var mediaSession: MediaSessionCompat
private lateinit var transportControls:MediaControllerCompat.TransportControls
private lateinit var audiobookManager: AudiobookManager
lateinit var mediaManager: MediaManager
lateinit var apiHandler: ApiHandler
lateinit var mPlayer: SimpleExoPlayer
@ -89,15 +85,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private var currentPlaybackSession:PlaybackSession? = null
private var mediaButtonClickCount: Int = 0
var mediaButtonClickTimeout: Long = 1000 //ms
var seekAmount: Long = 20000 //ms
private var lastPauseTime: Long = 0 //ms
private var onSeekBack: Boolean = false
var isAndroidAuto = false
var webviewBridge:Bridge? = null
// The following are used for the shake detection
private var isShakeSensorRegistered:Boolean = false
@ -106,13 +94,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private var mShakeDetector: ShakeDetector? = null
private var shakeSensorUnregisterTask:TimerTask? = null
fun setBridge(bridge: Bridge) {
webviewBridge = bridge
}
fun getIsWebviewOpen():Boolean {
return webviewBridge?.app?.isActive == true
}
/*
Service related stuff
*/
@ -146,7 +127,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
override fun onStart(intent: Intent?, startId: Int) {
Log.d(tag, "onStart $startId")
}
@RequiresApi(Build.VERSION_CODES.O)
@ -160,41 +140,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return channelId
}
private fun playAudiobookFromMediaBrowser(audiobook: Audiobook, playWhenReady: Boolean) {
}
private fun playFirstAudiobook(playWhenReady: Boolean) {
}
private fun openFromMediaId(mediaId: String, playWhenReady: Boolean) {
var audiobook = audiobookManager.audiobooks.find { it.id == mediaId }
if (audiobook == null) {
Log.e(tag, "Audiobook NOT FOUND")
return
}
playAudiobookFromMediaBrowser(audiobook, playWhenReady)
}
private fun openFromSearch(query: String?, playWhenReady: Boolean) {
if (query?.isNullOrEmpty() == true) {
Log.d(tag, "Empty search query play first audiobook")
playFirstAudiobook(playWhenReady)
return
}
var audiobook = audiobookManager.searchForAudiobook(query)
if (audiobook == null) {
Log.e(tag, "No Audiobook found for search $query")
pause()
return
}
playAudiobookFromMediaBrowser(audiobook, playWhenReady)
}
// detach player
override fun onDestroy() {
playerNotificationManager.setPlayer(null)
@ -233,7 +178,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
simpleExoPlayerBuilder.setSeekForwardIncrementMs(10000)
mPlayer = simpleExoPlayerBuilder.build()
mPlayer.setHandleAudioBecomingNoisy(true)
mPlayer.addListener(getPlayerListener())
mPlayer.addListener(PlayerListener(this))
var audioAttributes:AudioAttributes = AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).setContentType(C.CONTENT_TYPE_SPEECH).build()
mPlayer.setAudioAttributes(audioAttributes, true)
@ -257,8 +202,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
Log.d(tag, "onCreate Register sensor listener ${mAccelerometer?.isWakeUpSensor}")
initSensor()
// Initialize audiobook manager
audiobookManager = AudiobookManager(ctx, client)
// Initialize media manager
mediaManager = MediaManager(apiHandler)
channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(channelId, channelName)
@ -289,30 +234,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
channelId)
builder.setMediaDescriptionAdapter(AbMediaDescriptionAdapter(mediaController, this))
builder.setNotificationListener(object : PlayerNotificationManager.NotificationListener {
override fun onNotificationPosted(
notificationId: Int,
notification: Notification,
onGoing: Boolean) {
// Start foreground service
Log.d(tag, "Notification Posted $notificationId - Start Foreground | $notification")
startForeground(notificationId, notification)
}
override fun onNotificationCancelled(
notificationId: Int,
dismissedByUser: Boolean
) {
if (dismissedByUser) {
Log.d(tag, "onNotificationCancelled dismissed by user")
stopSelf()
} else {
Log.d(tag, "onNotificationCancelled not dismissed by user")
}
}
})
builder.setNotificationListener(PlayerNotificationListener(this))
playerNotificationManager = builder.build()
playerNotificationManager.setMediaSessionToken(mediaSession.sessionToken)
@ -330,13 +252,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
transportControls = mediaController.transportControls
// Color is set based on the art - cannot override
// playerNotificationManager.setColor(Color.RED)
// playerNotificationManager.setColorized(true)
// Icon needs to be black and white
// playerNotificationManager.setSmallIcon(R.drawable.icon_32)
mediaSessionConnector = MediaSessionConnector(mediaSession)
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
@ -350,39 +265,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// .setMediaUri(currentPlaybackSession!!.getContentUri())
}
val myPlaybackPreparer:MediaSessionConnector.PlaybackPreparer = object :MediaSessionConnector.PlaybackPreparer {
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
Log.d(tag, "ON COMMAND $command")
return false
}
override fun getSupportedPrepareActions(): Long {
return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
}
override fun onPrepare(playWhenReady: Boolean) {
Log.d(tag, "ON PREPARE $playWhenReady")
playFirstAudiobook(playWhenReady)
}
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
openFromMediaId(mediaId, playWhenReady)
}
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM SEARCH $query")
openFromSearch(query, playWhenReady)
}
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM URI $uri")
}
}
mediaSessionConnector.setEnabledPlaybackActions(
PlaybackStateCompat.ACTION_PLAY_PAUSE
or PlaybackStateCompat.ACTION_PLAY
@ -393,234 +275,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
or PlaybackStateCompat.ACTION_STOP
)
mediaSessionConnector.setQueueNavigator(queueNavigator)
mediaSessionConnector.setPlaybackPreparer(myPlaybackPreparer)
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
mediaSessionConnector.setPlayer(mPlayer)
//attach player to playerNotificationManager
playerNotificationManager.setPlayer(mPlayer)
mediaSession.setCallback(object : MediaSessionCompat.Callback() {
override fun onPrepare() {
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
playFirstAudiobook(true)
}
override fun onPlay() {
Log.d(tag, "ON PLAY MEDIA SESSION COMPAT")
play()
}
override fun onPrepareFromSearch(query: String?, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM SEARCH $query")
super.onPrepareFromSearch(query, extras)
}
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
Log.d(tag, "ON PLAY FROM SEARCH $query")
openFromSearch(query, true)
}
override fun onPause() {
Log.d(tag, "ON PAUSE MEDIA SESSION COMPAT")
pause()
}
override fun onStop() {
pause()
}
override fun onSkipToPrevious() {
seekBackward(seekAmount)
}
override fun onSkipToNext() {
seekForward(seekAmount)
}
override fun onFastForward() {
seekForward(seekAmount)
}
override fun onRewind() {
seekForward(seekAmount)
}
override fun onSeekTo(pos: Long) {
seekPlayer(pos)
}
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
if (mediaId.isNullOrEmpty()) {
playFirstAudiobook(true)
return
}
openFromMediaId(mediaId, true)
}
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
return handleCallMediaButton(mediaButtonEvent)
}
})
}
fun handleCallMediaButton(intent: Intent): Boolean {
if(Intent.ACTION_MEDIA_BUTTON == intent.getAction()) {
var keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
if (keyEvent?.getAction() == KeyEvent.ACTION_UP) {
when (keyEvent?.getKeyCode()) {
KeyEvent.KEYCODE_HEADSETHOOK -> {
if (0 == mediaButtonClickCount) {
if (mPlayer.isPlaying)
pause()
else
play()
}
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_PLAY -> {
if (0 == mediaButtonClickCount) {
play()
sleepTimerManager.checkShouldExtendSleepTimer()
}
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
if (0 == mediaButtonClickCount) pause()
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
seekForward(seekAmount)
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
seekBackward(seekAmount)
}
KeyEvent.KEYCODE_MEDIA_STOP -> {
terminateStream()
}
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
if (mPlayer.isPlaying) {
if (0 == mediaButtonClickCount) pause()
handleMediaButtonClickCount()
} else {
if (0 == mediaButtonClickCount) {
play()
sleepTimerManager.checkShouldExtendSleepTimer()
}
handleMediaButtonClickCount()
}
}
else -> {
Log.d(tag, "KeyCode:${keyEvent?.getKeyCode()}")
return false
}
}
}
}
return true
}
fun handleMediaButtonClickCount() {
mediaButtonClickCount++
if (1 == mediaButtonClickCount) {
Timer().schedule(mediaButtonClickTimeout) {
mediaBtnHandler.sendEmptyMessage(mediaButtonClickCount)
mediaButtonClickCount = 0
}
}
}
private val mediaBtnHandler : Handler = @SuppressLint("HandlerLeak")
object : Handler(){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
if (2 == msg.what) {
seekBackward(seekAmount)
play()
}
else if (msg.what >= 3) {
seekForward(seekAmount)
play()
}
}
}
fun getPlayerListener(): Player.Listener {
return object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
error.message?.let { Log.e(tag, it) }
error.localizedMessage?.let { Log.e(tag, it) }
}
override fun onEvents(player: Player, events: Player.Events) {
if (events.contains(Player.EVENT_POSITION_DISCONTINUITY)) {
Log.d(tag, "EVENT_POSITION_DISCONTINUITY")
}
if (events.contains(Player.EVENT_IS_LOADING_CHANGED)) {
Log.d(tag, "EVENT_IS_LOADING_CHANGED : " + mPlayer.isLoading.toString())
}
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
if (currentPlayer.playbackState == Player.STATE_READY) {
Log.d(tag, "STATE_READY : " + mPlayer.duration.toString())
if (lastPauseTime == 0L) {
sendClientMetadata("ready_no_sync")
lastPauseTime = -1;
} else sendClientMetadata("ready")
}
if (currentPlayer.playbackState == Player.STATE_BUFFERING) {
Log.d(tag, "STATE_BUFFERING : " + mPlayer.currentPosition.toString())
if (lastPauseTime == 0L) sendClientMetadata("buffering_no_sync")
else sendClientMetadata("buffering")
}
if (currentPlayer.playbackState == Player.STATE_ENDED) {
Log.d(tag, "STATE_ENDED")
sendClientMetadata("ended")
}
if (currentPlayer.playbackState == Player.STATE_IDLE) {
Log.d(tag, "STATE_IDLE")
sendClientMetadata("idle")
}
}
if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) {
Log.d(tag, "EVENT_MEDIA_METADATA_CHANGED")
}
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED")
}
if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
Log.d(tag, "EVENT IS PLAYING CHANGED")
if (player.isPlaying) {
if (lastPauseTime > 0) {
if (onSeekBack) onSeekBack = false
else {
var backTime = calcPauseSeekBackTime()
if (backTime > 0) {
if (backTime >= mPlayer.currentPosition) backTime = mPlayer.currentPosition - 500
Log.d(tag, "SeekBackTime $backTime")
onSeekBack = true
seekBackward(backTime)
}
}
}
} else lastPauseTime = System.currentTimeMillis()
// Start/stop progress sync interval
Log.d(tag, "Playing ${getCurrentBookTitle()} | ${currentPlayer.mediaMetadata.title} | ${currentPlayer.mediaMetadata.displayTitle}")
if (player.isPlaying) {
mediaProgressSyncer.start()
} else {
mediaProgressSyncer.stop()
}
clientEventEmitter?.onPlayingUpdate(player.isPlaying)
}
}
}
mediaSession.setCallback(MediaSessionCallback(this))
}
/*
@ -668,9 +328,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} else {
mPlayer.seekTo(playbackSession.currentTimeMs)
}
} else if (castPlayer != null) {
//// var mediaQueue = currentAudiobookStreamData!!.getCastQueue()
// // TODO: Start position will need to be adjusted if using multi-track queue
@ -681,68 +338,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
currentPlayer.setPlaybackSpeed(1f) // TODO: Playback speed should come from settings
currentPlayer.prepare()
}
//
// fun initPlayer(audiobookStreamData: AudiobookStreamData) {
// currentAudiobookStreamData = audiobookStreamData
//
// Log.d(tag, "Init Player Audiobook ${currentAudiobookStreamData!!.playlistUrl} | ${currentAudiobookStreamData!!.title} | ${currentAudiobookStreamData!!.author}")
//
// if (mPlayer.isPlaying) {
// Log.d(tag, "Init Player audiobook already playing")
// }
//
// // Issue with onenote plus crashing when using local cover art. https://github.com/advplyr/audiobookshelf-app/issues/35
// // Same issue with sony xperia https://github.com/advplyr/audiobookshelf-app/issues/94
// if (currentAudiobookStreamData?.coverUri != null && currentAudiobookStreamData?.isLocal == true) {
// var deviceName = Build.DEVICE
// var deviceMan = Build.MANUFACTURER
// var deviceModel = Build.MODEL
// Log.d(tag, "Checking device $deviceName | Model $deviceModel | Manufacturer $deviceMan")
// if (deviceMan.lowercase(Locale.getDefault()).contains("oneplus") || deviceName.lowercase(Locale.getDefault()).contains("oneplus")) {
// Log.d(tag, "Detected OnePlus device - removing local cover")
// currentAudiobookStreamData?.clearCover()
// } else if (deviceName.lowercase(Locale.getDefault()).contains("xperia") || deviceModel.lowercase(Locale.getDefault()).contains("xperia")) {
// Log.d(tag, "Detected Sony Xperia device - removing local cover")
// currentAudiobookStreamData?.clearCover()
// }
// }
//
// var metadata = currentAudiobookStreamData!!.getMediaMetadataCompat()
// mediaSession.setMetadata(metadata)
//
// var mediaUri:Uri = currentAudiobookStreamData!!.getMediaUri()
// var mimeType:String = currentAudiobookStreamData!!.getMimeType()
//
// var mediaMetadata = currentAudiobookStreamData!!.getMediaMetadata()
// var mediaItem = MediaItem.Builder().setUri(mediaUri).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
//
// if (mPlayer == currentPlayer) {
// var mediaSource:MediaSource
//
// if (currentAudiobookStreamData!!.isLocal) {
// Log.d(tag, "Playing Local File")
// var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
// mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
// } else {
// Log.d(tag, "Playing HLS File")
// var dataSourceFactory = DefaultHttpDataSource.Factory()
// dataSourceFactory.setUserAgent(channelId)
// dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${currentAudiobookStreamData!!.token}"))
// mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
// }
// mPlayer.setMediaSource(mediaSource, currentAudiobookStreamData!!.startTime)
// } else if (castPlayer != null) {
// var mediaQueue = currentAudiobookStreamData!!.getCastQueue()
// // TODO: Start position will need to be adjusted if using multi-track queue
// castPlayer?.setMediaItems(mediaQueue, 0, 0)
// }
//
// currentPlayer.prepare()
// currentPlayer.playWhenReady = currentAudiobookStreamData!!.playWhenReady
// currentPlayer.setPlaybackSpeed(audiobookStreamData.playbackSpeed)
//
// lastPauseTime = 0
// }
fun switchToPlayer(useCastPlayer: Boolean) {
currentPlayer = if (useCastPlayer) {
@ -784,10 +379,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
}
fun getTheLastPauseTime() : Long {
return lastPauseTime
}
fun getDuration() : Long {
return currentPlayer.duration
}
@ -804,19 +395,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return currentPlaybackSession?.id
}
fun calcPauseSeekBackTime() : Long {
if (lastPauseTime <= 0) return 0
var time: Long = System.currentTimeMillis() - lastPauseTime
var seekback: Long = 0
if (time < 60000) seekback = 0
else if (time < 120000) seekback = 10000
else if (time < 300000) seekback = 15000
else if (time < 1800000) seekback = 20000
else if (time < 3600000) seekback = 25000
else seekback = 29500
return seekback
}
fun play() {
if (currentPlayer.isPlaying) {
Log.d(tag, "Already playing")
@ -859,8 +437,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun terminateStream() {
currentPlayer.clearMediaItems()
currentPlaybackSession = null
lastPauseTime = 0
clientEventEmitter?.onPlaybackClosed()
PlayerListener.lastPauseTime = 0
}
fun sendClientMetadata(stateName: String) {
@ -926,93 +504,38 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
var flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
if (!audiobookManager.hasLoaded) {
result.detach()
audiobookManager.load()
audiobookManager.loadAudiobooks() {
audiobookManager.isLoading = false
Log.d(tag, "LOADED AUDIOBOOKS")
var audiobooks:List<MediaMetadataCompat> = audiobookManager.getAudiobooksMediaMetadata()
var downloadedBooks:List<MediaMetadataCompat> = audiobookManager.getDownloadedAudiobooksMediaMetadata()
browseTree = BrowseTree(this, audiobookManager.audiobooksInProgress, audiobooks, downloadedBooks)
val children = browseTree[parentMediaId]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, flag)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
result.detach()
mediaManager.loadLibraryItems { libraryItems ->
var itemMediaMetadata:List<MediaMetadataCompat> = libraryItems.map { it.getMediaMetadata() }
browseTree = BrowseTree(this, mutableListOf(), itemMediaMetadata, mutableListOf())
val children = browseTree[parentMediaId]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, flag)
}
return
} else if (audiobookManager.isLoading) {
Log.d(tag, "AUDIOBOOKS LOADING")
result.detach()
return
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
val children = browseTree[parentMediaId]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, flag)
}
if (children != null) {
Log.d(tag, "BROWSE TREE $parentMediaId CHILDREN ${children.size}")
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
// TODO: For using sub menus. Check if this is the root menu:
if (AUTO_MEDIA_ROOT == parentMediaId) {
// if (AUTO_MEDIA_ROOT == parentMediaId) {
// build the MediaItem objects for the top level,
// and put them in the mediaItems list
} else {
// } else {
// examine the passed parentMediaId to see which submenu we're at,
// and put the children of that menu in the mediaItems list
}
// }
}
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
if (!audiobookManager.hasLoaded) {
result.detach()
audiobookManager.load()
audiobookManager.loadAudiobooks() {
audiobookManager.isLoading = false
Log.d(tag, "LOADED AUDIOBOOKS")
var audiobooks:List<MediaMetadataCompat> = audiobookManager.getAudiobooksMediaMetadata()
var downloadedBooks:List<MediaMetadataCompat> = audiobookManager.getDownloadedAudiobooksMediaMetadata()
browseTree = BrowseTree(this, audiobookManager.audiobooksInProgress, audiobooks, downloadedBooks)
val children = browseTree[ALL_ROOT]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
if (children != null) {
Log.d(tag, "BROWSE TREE CHILDREN ${children.size}")
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
result.detach()
mediaManager.loadLibraryItems { libraryItems ->
var itemMediaMetadata:List<MediaMetadataCompat> = libraryItems.map { it.getMediaMetadata() }
browseTree = BrowseTree(this, mutableListOf(), itemMediaMetadata, mutableListOf())
val children = browseTree[ALL_ROOT]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
return
} else if (audiobookManager.isLoading) {
Log.d(tag, "AUDIOBOOKS LOADING")
result.detach()
return
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
if (audiobookManager.audiobooks.size == 0) {
Log.d(tag, "AudiobookManager: Sending no items")
result.sendResult(mediaItems)
return
}
val children = browseTree[ALL_ROOT]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
if (children != null) {
Log.d(tag, "NO CHILDREN ON SEARCH ${children.size}")
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
//
// SHAKE SENSOR
//

View file

@ -32,8 +32,6 @@ class AbsAudioPlayer : Plugin() {
var foregroundServiceReady : () -> Unit = {
playerNotificationService = mainActivity.foregroundService
playerNotificationService.setBridge(bridge)
playerNotificationService.clientEventEmitter = (object : PlayerNotificationService.ClientEventEmitter {
override fun onPlaybackSession(playbackSession: PlaybackSession) {
notifyListeners("onPlaybackSession", JSObject(jacksonObjectMapper().writeValueAsString(playbackSession)))

View file

@ -127,7 +127,7 @@ class ApiHandler {
}
fun getLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
getRequest("/api/libraries/$libraryId/items") {
getRequest("/api/libraries/$libraryId/items?limit=100&minified=1") {
val items = mutableListOf<LibraryItem>()
if (it.has("results")) {
var array = it.getJSONArray("results")

View file

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">AudioBookshelf</string>
<string name="title_activity_main">AudioBookshelf</string>
<string name="app_name">audiobookshelf</string>
<string name="title_activity_main">audiobookshelf</string>
<string name="package_name">com.audiobookshelf.app</string>
<string name="custom_url_scheme">com.audiobookshelf.app</string>
</resources>