mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-26 05:14:09 +02:00
Android clean up audio player, update android auto for new data model
This commit is contained in:
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package com.audiobookshelf.app.data
|
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.*
|
import com.fasterxml.jackson.annotation.*
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@@ -21,8 +25,36 @@ data class LibraryItem(
|
|||||||
var isInvalid:Boolean,
|
var isInvalid:Boolean,
|
||||||
var mediaType:String,
|
var mediaType:String,
|
||||||
var media:MediaType,
|
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
|
// This auto-detects whether it is a Book or Podcast
|
||||||
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
|
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
|
||||||
@@ -46,36 +78,36 @@ class Podcast(
|
|||||||
metadata:PodcastMetadata,
|
metadata:PodcastMetadata,
|
||||||
coverPath:String?,
|
coverPath:String?,
|
||||||
var tags:MutableList<String>,
|
var tags:MutableList<String>,
|
||||||
var episodes:MutableList<PodcastEpisode>,
|
var episodes:MutableList<PodcastEpisode>?,
|
||||||
var autoDownloadEpisodes:Boolean
|
var autoDownloadEpisodes:Boolean
|
||||||
) : MediaType(metadata, coverPath) {
|
) : MediaType(metadata, coverPath) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getAudioTracks():List<AudioTrack> {
|
override fun getAudioTracks():List<AudioTrack> {
|
||||||
var tracks = episodes.map { it.audioTrack }
|
var tracks = episodes?.map { it.audioTrack }
|
||||||
return tracks.filterNotNull()
|
return tracks?.filterNotNull() ?: mutableListOf()
|
||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
|
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
|
||||||
// Remove episodes no longer there in tracks
|
// Remove episodes no longer there in tracks
|
||||||
episodes = episodes.filter { ep ->
|
episodes = episodes?.filter { ep ->
|
||||||
audioTracks.find { it.localFileId == ep.audioTrack?.localFileId } != null
|
audioTracks.find { it.localFileId == ep.audioTrack?.localFileId } != null
|
||||||
} as MutableList<PodcastEpisode>
|
} as MutableList<PodcastEpisode>
|
||||||
// Add new episodes
|
// Add new episodes
|
||||||
audioTracks.forEach { at ->
|
audioTracks.forEach { at ->
|
||||||
if (episodes.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
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)
|
var newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at)
|
||||||
episodes.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun addAudioTrack(audioTrack:AudioTrack) {
|
override fun addAudioTrack(audioTrack:AudioTrack) {
|
||||||
var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes.size + 1,null,null,audioTrack.title,null,null,null,audioTrack)
|
var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack)
|
||||||
episodes.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun removeAudioTrack(localFileId:String) {
|
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,
|
metadata:BookMetadata,
|
||||||
coverPath:String?,
|
coverPath:String?,
|
||||||
var tags:List<String>,
|
var tags:List<String>,
|
||||||
var audioFiles:List<AudioFile>,
|
var audioFiles:List<AudioFile>?,
|
||||||
var chapters:List<BookChapter>,
|
var chapters:List<BookChapter>?,
|
||||||
var tracks:MutableList<AudioTrack>?,
|
var tracks:MutableList<AudioTrack>?,
|
||||||
var size:Long?,
|
var size:Long?,
|
||||||
var duration:Double?
|
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)
|
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
|
||||||
@JsonSubTypes(
|
@JsonSubTypes(
|
||||||
JsonSubTypes.Type(BookMetadata::class),
|
JsonSubTypes.Type(BookMetadata::class),
|
||||||
JsonSubTypes.Type(PodcastMetadata::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)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
class BookMetadata(
|
class BookMetadata(
|
||||||
title:String,
|
title:String,
|
||||||
var subtitle:String?,
|
var subtitle:String?,
|
||||||
var authors:MutableList<Author>,
|
var authors:MutableList<Author>?,
|
||||||
var narrators:MutableList<String>,
|
var narrators:MutableList<String>?,
|
||||||
var genres:MutableList<String>,
|
var genres:MutableList<String>,
|
||||||
var publishedYear:String?,
|
var publishedYear:String?,
|
||||||
var publishedDate:String?,
|
var publishedDate:String?,
|
||||||
@@ -164,7 +199,10 @@ class BookMetadata(
|
|||||||
var authorNameLF:String?,
|
var authorNameLF:String?,
|
||||||
var narratorName:String?,
|
var narratorName:String?,
|
||||||
var seriesName:String?
|
var seriesName:String?
|
||||||
) : MediaTypeMetadata(title)
|
) : MediaTypeMetadata(title) {
|
||||||
|
@JsonIgnore
|
||||||
|
override fun getAuthorDisplayName():String { return authorName ?: "Unknown" }
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
class PodcastMetadata(
|
class PodcastMetadata(
|
||||||
@@ -172,7 +210,10 @@ class PodcastMetadata(
|
|||||||
var author:String?,
|
var author:String?,
|
||||||
var feedUrl:String?,
|
var feedUrl:String?,
|
||||||
var genres:MutableList<String>
|
var genres:MutableList<String>
|
||||||
) : MediaTypeMetadata(title)
|
) : MediaTypeMetadata(title) {
|
||||||
|
@JsonIgnore
|
||||||
|
override fun getAuthorDisplayName():String { return author ?: "Unknown" }
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Author(
|
data class Author(
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ data class LocalLibraryItem(
|
|||||||
|
|
||||||
var episodeIdNullable = if (episodeId.isNullOrEmpty()) null else episodeId
|
var episodeIdNullable = if (episodeId.isNullOrEmpty()) null else episodeId
|
||||||
var dateNow = System.currentTimeMillis()
|
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
|
@JsonIgnore
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,14 +6,14 @@ import android.net.Uri
|
|||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.AnyRes
|
import androidx.annotation.AnyRes
|
||||||
import com.audiobookshelf.app.Audiobook
|
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
|
import com.audiobookshelf.app.data.LibraryItem
|
||||||
|
|
||||||
|
|
||||||
class BrowseTree(
|
class BrowseTree(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
audiobooksInProgress: List<Audiobook>,
|
itemsInProgress: List<LibraryItem>,
|
||||||
audiobookMetadata: List<MediaMetadataCompat>,
|
itemsMetadata: List<MediaMetadataCompat>,
|
||||||
downloadedMetadata: List<MediaMetadataCompat>
|
downloadedMetadata: List<MediaMetadataCompat>
|
||||||
) {
|
) {
|
||||||
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
||||||
@@ -35,7 +35,6 @@ class BrowseTree(
|
|||||||
init {
|
init {
|
||||||
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
|
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
|
||||||
|
|
||||||
|
|
||||||
val continueReadingMetadata = MediaMetadataCompat.Builder().apply {
|
val continueReadingMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Reading")
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Reading")
|
||||||
@@ -44,27 +43,20 @@ class BrowseTree(
|
|||||||
|
|
||||||
val allMetadata = MediaMetadataCompat.Builder().apply {
|
val allMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ALL_ROOT)
|
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()
|
var resource = getUriToDrawable(context, R.drawable.exo_icon_books).toString()
|
||||||
Log.d("BrowseTree", "RESOURCE $resource")
|
Log.d("BrowseTree", "RESOURCE $resource")
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, resource)
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, resource)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
|
||||||
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
|
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
// val localsMetadata = MediaMetadataCompat.Builder().apply {
|
if (itemsInProgress.isNotEmpty()) {
|
||||||
// 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()) {
|
|
||||||
rootList += continueReadingMetadata
|
rootList += continueReadingMetadata
|
||||||
}
|
}
|
||||||
rootList += allMetadata
|
rootList += allMetadata
|
||||||
@@ -72,13 +64,13 @@ class BrowseTree(
|
|||||||
// rootList += localsMetadata
|
// rootList += localsMetadata
|
||||||
mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
|
mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
|
||||||
|
|
||||||
audiobooksInProgress.forEach { audiobook ->
|
itemsInProgress.forEach { libraryItem ->
|
||||||
val children = mediaIdToChildren[CONTINUE_ROOT] ?: mutableListOf()
|
val children = mediaIdToChildren[CONTINUE_ROOT] ?: mutableListOf()
|
||||||
children += audiobook.toMediaMetadata()
|
children += libraryItem.getMediaMetadata()
|
||||||
mediaIdToChildren[CONTINUE_ROOT] = children
|
mediaIdToChildren[CONTINUE_ROOT] = children
|
||||||
}
|
}
|
||||||
|
|
||||||
audiobookMetadata.forEach {
|
itemsMetadata.forEach {
|
||||||
val allChildren = mediaIdToChildren[ALL_ROOT] ?: mutableListOf()
|
val allChildren = mediaIdToChildren[ALL_ROOT] ?: mutableListOf()
|
||||||
allChildren += it
|
allChildren += it
|
||||||
mediaIdToChildren[ALL_ROOT] = allChildren
|
mediaIdToChildren[ALL_ROOT] = allChildren
|
||||||
@@ -89,13 +81,6 @@ class BrowseTree(
|
|||||||
allChildren += it
|
allChildren += it
|
||||||
mediaIdToChildren[DOWNLOADS_ROOT] = allChildren
|
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]
|
operator fun get(mediaId: String) = mediaIdToChildren[mediaId]
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
|||||||
val castContext = CastContext.getSharedInstance(mainActivity)
|
val castContext = CastContext.getSharedInstance(mainActivity)
|
||||||
playerNotificationService.castPlayer = CastPlayer(castContext).apply {
|
playerNotificationService.castPlayer = CastPlayer(castContext).apply {
|
||||||
setSessionAvailabilityListener(CastSessionAvailabilityListener())
|
setSessionAvailabilityListener(CastSessionAvailabilityListener())
|
||||||
addListener(playerNotificationService.getPlayerListener())
|
addListener(PlayerListener(playerNotificationService))
|
||||||
}
|
}
|
||||||
Log.d(tag, "CAST Cast Player Applied")
|
Log.d(tag, "CAST Cast Player Applied")
|
||||||
switchToPlayer(true)
|
switchToPlayer(true)
|
||||||
@@ -313,8 +313,6 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
|||||||
"Exception thrown when attempting to obtain CastContext. " + e.message)
|
"Exception thrown when attempting to obtain CastContext. " + e.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// media.setSession(castSession)
|
// media.setSession(castSession)
|
||||||
// callback.onJoin(ChromecastUtilities.createSessionObject(castSession))
|
// callback.onJoin(ChromecastUtilities.createSessionObject(castSession))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+70
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-504
@@ -1,13 +1,11 @@
|
|||||||
package com.audiobookshelf.app.player
|
package com.audiobookshelf.app.player
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.hardware.Sensor
|
import android.hardware.Sensor
|
||||||
import android.hardware.SensorManager
|
import android.hardware.SensorManager
|
||||||
import android.net.Uri
|
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.support.v4.media.MediaBrowserCompat
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
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.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
import com.audiobookshelf.app.Audiobook
|
import com.audiobookshelf.app.data.LibraryItem
|
||||||
import com.audiobookshelf.app.AudiobookManager
|
|
||||||
import com.audiobookshelf.app.data.LocalMediaProgress
|
import com.audiobookshelf.app.data.LocalMediaProgress
|
||||||
import com.audiobookshelf.app.data.PlaybackSession
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.media.MediaManager
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import com.getcapacitor.Bridge
|
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||||
@@ -72,7 +68,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
private lateinit var playerNotificationManager: PlayerNotificationManager
|
private lateinit var playerNotificationManager: PlayerNotificationManager
|
||||||
private lateinit var mediaSession: MediaSessionCompat
|
private lateinit var mediaSession: MediaSessionCompat
|
||||||
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
||||||
private lateinit var audiobookManager: AudiobookManager
|
lateinit var mediaManager: MediaManager
|
||||||
lateinit var apiHandler: ApiHandler
|
lateinit var apiHandler: ApiHandler
|
||||||
|
|
||||||
lateinit var mPlayer: SimpleExoPlayer
|
lateinit var mPlayer: SimpleExoPlayer
|
||||||
@@ -89,15 +85,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
private var currentPlaybackSession:PlaybackSession? = null
|
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 isAndroidAuto = false
|
||||||
var webviewBridge:Bridge? = null
|
|
||||||
|
|
||||||
// The following are used for the shake detection
|
// The following are used for the shake detection
|
||||||
private var isShakeSensorRegistered:Boolean = false
|
private var isShakeSensorRegistered:Boolean = false
|
||||||
@@ -106,13 +94,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
private var mShakeDetector: ShakeDetector? = null
|
private var mShakeDetector: ShakeDetector? = null
|
||||||
private var shakeSensorUnregisterTask:TimerTask? = null
|
private var shakeSensorUnregisterTask:TimerTask? = null
|
||||||
|
|
||||||
fun setBridge(bridge: Bridge) {
|
|
||||||
webviewBridge = bridge
|
|
||||||
}
|
|
||||||
fun getIsWebviewOpen():Boolean {
|
|
||||||
return webviewBridge?.app?.isActive == true
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Service related stuff
|
Service related stuff
|
||||||
*/
|
*/
|
||||||
@@ -146,7 +127,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
override fun onStart(intent: Intent?, startId: Int) {
|
override fun onStart(intent: Intent?, startId: Int) {
|
||||||
Log.d(tag, "onStart $startId")
|
Log.d(tag, "onStart $startId")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@@ -160,41 +140,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return channelId
|
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
|
// detach player
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
playerNotificationManager.setPlayer(null)
|
playerNotificationManager.setPlayer(null)
|
||||||
@@ -233,7 +178,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
simpleExoPlayerBuilder.setSeekForwardIncrementMs(10000)
|
simpleExoPlayerBuilder.setSeekForwardIncrementMs(10000)
|
||||||
mPlayer = simpleExoPlayerBuilder.build()
|
mPlayer = simpleExoPlayerBuilder.build()
|
||||||
mPlayer.setHandleAudioBecomingNoisy(true)
|
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()
|
var audioAttributes:AudioAttributes = AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).setContentType(C.CONTENT_TYPE_SPEECH).build()
|
||||||
mPlayer.setAudioAttributes(audioAttributes, true)
|
mPlayer.setAudioAttributes(audioAttributes, true)
|
||||||
|
|
||||||
@@ -257,8 +202,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
Log.d(tag, "onCreate Register sensor listener ${mAccelerometer?.isWakeUpSensor}")
|
Log.d(tag, "onCreate Register sensor listener ${mAccelerometer?.isWakeUpSensor}")
|
||||||
initSensor()
|
initSensor()
|
||||||
|
|
||||||
// Initialize audiobook manager
|
// Initialize media manager
|
||||||
audiobookManager = AudiobookManager(ctx, client)
|
mediaManager = MediaManager(apiHandler)
|
||||||
|
|
||||||
channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
createNotificationChannel(channelId, channelName)
|
createNotificationChannel(channelId, channelName)
|
||||||
@@ -289,30 +234,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
channelId)
|
channelId)
|
||||||
|
|
||||||
builder.setMediaDescriptionAdapter(AbMediaDescriptionAdapter(mediaController, this))
|
builder.setMediaDescriptionAdapter(AbMediaDescriptionAdapter(mediaController, this))
|
||||||
|
builder.setNotificationListener(PlayerNotificationListener(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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
playerNotificationManager = builder.build()
|
playerNotificationManager = builder.build()
|
||||||
playerNotificationManager.setMediaSessionToken(mediaSession.sessionToken)
|
playerNotificationManager.setMediaSessionToken(mediaSession.sessionToken)
|
||||||
@@ -330,13 +252,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
transportControls = mediaController.transportControls
|
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)
|
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||||
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
||||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||||
@@ -350,39 +265,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
// .setMediaUri(currentPlaybackSession!!.getContentUri())
|
// .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(
|
mediaSessionConnector.setEnabledPlaybackActions(
|
||||||
PlaybackStateCompat.ACTION_PLAY_PAUSE
|
PlaybackStateCompat.ACTION_PLAY_PAUSE
|
||||||
or PlaybackStateCompat.ACTION_PLAY
|
or PlaybackStateCompat.ACTION_PLAY
|
||||||
@@ -393,234 +275,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
or PlaybackStateCompat.ACTION_STOP
|
or PlaybackStateCompat.ACTION_STOP
|
||||||
)
|
)
|
||||||
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
||||||
mediaSessionConnector.setPlaybackPreparer(myPlaybackPreparer)
|
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
|
||||||
mediaSessionConnector.setPlayer(mPlayer)
|
mediaSessionConnector.setPlayer(mPlayer)
|
||||||
|
|
||||||
//attach player to playerNotificationManager
|
//attach player to playerNotificationManager
|
||||||
playerNotificationManager.setPlayer(mPlayer)
|
playerNotificationManager.setPlayer(mPlayer)
|
||||||
|
mediaSession.setCallback(MediaSessionCallback(this))
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -668,9 +328,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
} else {
|
} else {
|
||||||
mPlayer.seekTo(playbackSession.currentTimeMs)
|
mPlayer.seekTo(playbackSession.currentTimeMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
} else if (castPlayer != null) {
|
} else if (castPlayer != null) {
|
||||||
//// var mediaQueue = currentAudiobookStreamData!!.getCastQueue()
|
//// var mediaQueue = currentAudiobookStreamData!!.getCastQueue()
|
||||||
// // TODO: Start position will need to be adjusted if using multi-track queue
|
// // 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.setPlaybackSpeed(1f) // TODO: Playback speed should come from settings
|
||||||
currentPlayer.prepare()
|
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) {
|
fun switchToPlayer(useCastPlayer: Boolean) {
|
||||||
currentPlayer = if (useCastPlayer) {
|
currentPlayer = if (useCastPlayer) {
|
||||||
@@ -784,10 +379,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTheLastPauseTime() : Long {
|
|
||||||
return lastPauseTime
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDuration() : Long {
|
fun getDuration() : Long {
|
||||||
return currentPlayer.duration
|
return currentPlayer.duration
|
||||||
}
|
}
|
||||||
@@ -804,19 +395,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return currentPlaybackSession?.id
|
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() {
|
fun play() {
|
||||||
if (currentPlayer.isPlaying) {
|
if (currentPlayer.isPlaying) {
|
||||||
Log.d(tag, "Already playing")
|
Log.d(tag, "Already playing")
|
||||||
@@ -859,8 +437,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
fun terminateStream() {
|
fun terminateStream() {
|
||||||
currentPlayer.clearMediaItems()
|
currentPlayer.clearMediaItems()
|
||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
lastPauseTime = 0
|
|
||||||
clientEventEmitter?.onPlaybackClosed()
|
clientEventEmitter?.onPlaybackClosed()
|
||||||
|
PlayerListener.lastPauseTime = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendClientMetadata(stateName: String) {
|
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
|
var flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
|
|
||||||
if (!audiobookManager.hasLoaded) {
|
result.detach()
|
||||||
result.detach()
|
mediaManager.loadLibraryItems { libraryItems ->
|
||||||
audiobookManager.load()
|
var itemMediaMetadata:List<MediaMetadataCompat> = libraryItems.map { it.getMediaMetadata() }
|
||||||
audiobookManager.loadAudiobooks() {
|
browseTree = BrowseTree(this, mutableListOf(), itemMediaMetadata, mutableListOf())
|
||||||
audiobookManager.isLoading = false
|
val children = browseTree[parentMediaId]?.map { item ->
|
||||||
|
MediaBrowserCompat.MediaItem(item.description, flag)
|
||||||
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>?)
|
|
||||||
}
|
}
|
||||||
return
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
} else if (audiobookManager.isLoading) {
|
|
||||||
Log.d(tag, "AUDIOBOOKS LOADING")
|
|
||||||
result.detach()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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:
|
// 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,
|
// build the MediaItem objects for the top level,
|
||||||
// and put them in the mediaItems list
|
// and put them in the mediaItems list
|
||||||
} else {
|
// } else {
|
||||||
// examine the passed parentMediaId to see which submenu we're at,
|
// examine the passed parentMediaId to see which submenu we're at,
|
||||||
// and put the children of that menu in the mediaItems list
|
// and put the children of that menu in the mediaItems list
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
result.detach()
|
||||||
|
mediaManager.loadLibraryItems { libraryItems ->
|
||||||
if (!audiobookManager.hasLoaded) {
|
var itemMediaMetadata:List<MediaMetadataCompat> = libraryItems.map { it.getMediaMetadata() }
|
||||||
result.detach()
|
browseTree = BrowseTree(this, mutableListOf(), itemMediaMetadata, mutableListOf())
|
||||||
audiobookManager.load()
|
val children = browseTree[ALL_ROOT]?.map { item ->
|
||||||
audiobookManager.loadAudiobooks() {
|
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
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>?)
|
|
||||||
}
|
}
|
||||||
return
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
} else if (audiobookManager.isLoading) {
|
|
||||||
Log.d(tag, "AUDIOBOOKS LOADING")
|
|
||||||
result.detach()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// SHAKE SENSOR
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
var foregroundServiceReady : () -> Unit = {
|
var foregroundServiceReady : () -> Unit = {
|
||||||
playerNotificationService = mainActivity.foregroundService
|
playerNotificationService = mainActivity.foregroundService
|
||||||
|
|
||||||
playerNotificationService.setBridge(bridge)
|
|
||||||
|
|
||||||
playerNotificationService.clientEventEmitter = (object : PlayerNotificationService.ClientEventEmitter {
|
playerNotificationService.clientEventEmitter = (object : PlayerNotificationService.ClientEventEmitter {
|
||||||
override fun onPlaybackSession(playbackSession: PlaybackSession) {
|
override fun onPlaybackSession(playbackSession: PlaybackSession) {
|
||||||
notifyListeners("onPlaybackSession", JSObject(jacksonObjectMapper().writeValueAsString(playbackSession)))
|
notifyListeners("onPlaybackSession", JSObject(jacksonObjectMapper().writeValueAsString(playbackSession)))
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class ApiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
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>()
|
val items = mutableListOf<LibraryItem>()
|
||||||
if (it.has("results")) {
|
if (it.has("results")) {
|
||||||
var array = it.getJSONArray("results")
|
var array = it.getJSONArray("results")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">AudioBookshelf</string>
|
<string name="app_name">audiobookshelf</string>
|
||||||
<string name="title_activity_main">AudioBookshelf</string>
|
<string name="title_activity_main">audiobookshelf</string>
|
||||||
<string name="package_name">com.audiobookshelf.app</string>
|
<string name="package_name">com.audiobookshelf.app</string>
|
||||||
<string name="custom_url_scheme">com.audiobookshelf.app</string>
|
<string name="custom_url_scheme">com.audiobookshelf.app</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user