Change:Cast support use audio tracks instead of HLS #11, Clean up playernotificationmanager

This commit is contained in:
advplyr 2021-12-18 18:26:50 -06:00
parent b9de407c8a
commit f11efd93c7
8 changed files with 868 additions and 729 deletions

View file

@ -81,12 +81,3 @@ dependencies {
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.warn("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View file

@ -0,0 +1,82 @@
package com.audiobookshelf.app
import android.app.PendingIntent
import android.graphics.Bitmap
import android.net.Uri
import android.support.v4.media.session.MediaControllerCompat
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import kotlinx.coroutines.*
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
private val tag = "MediaDescriptionAdapter"
private val playerNotificationService:PlayerNotificationService = playerNotificationService
var currentIconUri: Uri? = null
var currentBitmap: Bitmap? = null
private val glideOptions = RequestOptions()
.fallback(R.drawable.icon)
.diskCacheStrategy(DiskCacheStrategy.DATA)
private val serviceJob = SupervisorJob()
private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)
override fun createCurrentContentIntent(player: Player): PendingIntent? =
controller.sessionActivity
override fun getCurrentContentText(player: Player) = controller.metadata.description.subtitle.toString()
override fun getCurrentContentTitle(player: Player) = controller.metadata.description.title.toString()
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
val albumArtUri = controller.metadata.description.iconUri
return if (currentIconUri != albumArtUri || currentBitmap == null) {
// Cache the bitmap for the current audiobook so that successive calls to
// `getCurrentLargeIcon` don't cause the bitmap to be recreated.
currentIconUri = albumArtUri
Log.d(tag, "ART $currentIconUri")
serviceScope.launch {
currentBitmap = albumArtUri?.let {
resolveUriAsBitmap(it)
}
currentBitmap?.let { callback.onBitmap(it) }
}
null
} else {
currentBitmap
}
}
private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? {
return withContext(Dispatchers.IO) {
// Block on downloading artwork.
try {
Glide.with(playerNotificationService).applyDefaultRequestOptions(glideOptions)
.asBitmap()
.load(uri)
.placeholder(R.drawable.icon)
.error(R.drawable.icon)
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
.get()
} catch (e: Exception) {
e.printStackTrace()
Glide.with(playerNotificationService).applyDefaultRequestOptions(glideOptions)
.asBitmap()
.load(Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon))
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
.get()
}
}
}
}

View file

@ -1,7 +1,13 @@
package com.audiobookshelf.app
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import com.getcapacitor.JSObject
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.MediaMetadata
import com.google.android.exoplayer2.util.MimeTypes
import java.lang.Exception
class AudiobookStreamData {
var id:String = "audiobook"
@ -15,6 +21,7 @@ class AudiobookStreamData {
var startTime:Long = 0
var playbackSpeed:Float = 1f
var duration:Long = 0
var tracks:MutableList<String> = mutableListOf()
var isLocal:Boolean = false
var contentUrl:String = ""
@ -65,5 +72,98 @@ class AudiobookStreamData {
if (contentUrl != "") {
contentUri = Uri.parse(contentUrl)
}
// Tracks for cast
try {
var tracksTest = jsondata.getJSONArray("tracks")
Log.d("AudiobookStreamData", "Load tracks from json array ${tracksTest.length()}")
for (i in 0 until tracksTest.length()) {
var track = tracksTest.get(i)
Log.d("AudiobookStreamData", "Extracting track $track")
tracks.add(track as String)
}
} catch(e:Exception) {
Log.d("AudiobookStreamData", "No tracks found $e")
}
}
fun getMediaMetadataCompat():MediaMetadataCompat {
var metadataBuilder = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, author)
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, author)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, author)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, series)
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
if (cover != "") {
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, cover)
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, cover)
}
return metadataBuilder.build()
}
fun getMediaMetadata():MediaMetadata {
var metadataBuilder = MediaMetadata.Builder()
.setTitle(title)
.setDisplayTitle(title)
.setArtist(author)
.setAlbumArtist(author)
.setSubtitle(author)
if (coverUri != Uri.EMPTY) {
metadataBuilder.setArtworkUri(coverUri)
}
if (playlistUri != Uri.EMPTY) {
metadataBuilder.setMediaUri(playlistUri)
}
if (contentUri != Uri.EMPTY) {
metadataBuilder.setMediaUri(contentUri)
}
return metadataBuilder.build()
}
fun getMimeType():String {
return if (isLocal) {
MimeTypes.BASE_TYPE_AUDIO
} else {
MimeTypes.APPLICATION_M3U8
}
}
fun getMediaUri():Uri {
return if (isLocal) {
contentUri
} else {
Uri.parse("$playlistUrl?token=$token")
}
}
fun getCastQueue():ArrayList<MediaItem> {
var mediaQueue: java.util.ArrayList<MediaItem> = java.util.ArrayList<MediaItem>()
for (i in 0 until tracks.size) {
var track = tracks[i]
var metadataBuilder = MediaMetadata.Builder()
.setTitle(title)
.setDisplayTitle(title)
.setArtist(author)
.setAlbumArtist(author)
.setSubtitle(author)
.setTrackNumber(i + 1)
if (coverUri != Uri.EMPTY) {
metadataBuilder.setArtworkUri(coverUri)
}
var mimeType = MimeTypes.BASE_TYPE_AUDIO
var mediaMetadata = metadataBuilder.build()
var mediaItem = MediaItem.Builder().setUri(Uri.parse(track)).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
mediaQueue.add(mediaItem)
}
return mediaQueue
}
}

View file

@ -0,0 +1,379 @@
package com.audiobookshelf.app
import android.app.Activity
import android.app.AlertDialog
import android.os.Bundle
import android.util.Log
import androidx.appcompat.R
import androidx.mediarouter.app.MediaRouteChooserDialog
import androidx.mediarouter.media.MediaRouteSelector
import androidx.mediarouter.media.MediaRouter
import com.getcapacitor.PluginCall
import com.google.android.exoplayer2.ext.cast.CastPlayer
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
import com.google.android.gms.cast.Cast
import com.google.android.gms.cast.CastDevice
import com.google.android.gms.cast.CastMediaControlIntent
import com.google.android.gms.cast.framework.*
import org.json.JSONObject
import java.util.ArrayList
class CastManager constructor(playerNotificationService:PlayerNotificationService) {
private val tag = "SleepTimerManager"
private val playerNotificationService:PlayerNotificationService = playerNotificationService
private var newConnectionListener:SessionListener? = null
private var mainActivity:Activity? = null
private fun switchToPlayer(useCastPlayer:Boolean) {
playerNotificationService.switchToPlayer(useCastPlayer)
}
private inner class CastSessionAvailabilityListener : SessionAvailabilityListener {
/**
* Called when a Cast session has started and the user wishes to control playback on a
* remote Cast receiver rather than play audio locally.
*/
override fun onCastSessionAvailable() {
switchToPlayer(true)
}
/**
* Called when a Cast session has ended and the user wishes to control playback locally.
*/
override fun onCastSessionUnavailable() {
Log.d(tag, "onCastSessionUnavailable")
switchToPlayer(false)
}
}
fun requestSession(mainActivity: Activity, callback: RequestSessionCallback) {
this.mainActivity = mainActivity
mainActivity.runOnUiThread(object : Runnable {
override fun run() {
Log.d(tag, "CAST RUNNING ON MAIN THREAD")
val session: CastSession? = getSession()
if (session == null) {
// show the "choose a connection" dialog
// Add the connection listener callback
listenForConnection(callback)
val builder = MediaRouteChooserDialog(mainActivity, R.style.Theme_AppCompat_NoActionBar)
builder.routeSelector = MediaRouteSelector.Builder()
.addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))
.build()
builder.setCanceledOnTouchOutside(true)
builder.setOnCancelListener {
getSessionManager()!!.removeSessionManagerListener(newConnectionListener, CastSession::class.java)
callback.onCancel()
}
builder.show()
} else {
// We are are already connected, so show the "connection options" Dialog
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
if (session.castDevice != null) {
builder.setTitle(session.castDevice.friendlyName)
}
builder.setOnDismissListener { callback.onCancel() }
builder.setPositiveButton("Stop Casting") { dialog, which -> endSession(true, null) }
builder.show()
}
}
})
}
abstract class RequestSessionCallback : ConnectionCallback {
abstract fun onError(errorCode: Int)
abstract fun onCancel()
override fun onSessionEndedBeforeStart(errorCode: Int): Boolean {
onSessionStartFailed(errorCode)
return true
}
override fun onSessionStartFailed(errorCode: Int): Boolean {
onError(errorCode)
return true
}
}
fun endSession(stopCasting: Boolean, pluginCall: PluginCall?) {
getSessionManager()!!.addSessionManagerListener(object : SessionListener() {
override fun onSessionEnded(castSession: CastSession?, error: Int) {
getSessionManager()!!.removeSessionManagerListener(this, CastSession::class.java)
Log.d(tag, "CAST END SESSION")
// media.setSession(null)
pluginCall?.resolve()
// listener.onSessionEnd(ChromecastUtilities.createSessionObject(castSession, if (stopCasting) "stopped" else "disconnected"))
}
}, CastSession::class.java)
getSessionManager()!!.endCurrentSession(stopCasting)
}
open class SessionListener : SessionManagerListener<CastSession> {
override fun onSessionStarting(castSession: CastSession?) {}
override fun onSessionStarted(castSession: CastSession?, sessionId: String) {}
override fun onSessionStartFailed(castSession: CastSession?, error: Int) {}
override fun onSessionEnding(castSession: CastSession?) {}
override fun onSessionEnded(castSession: CastSession?, error: Int) {}
override fun onSessionResuming(castSession: CastSession?, sessionId: String) {}
override fun onSessionResumed(castSession: CastSession?, wasSuspended: Boolean) {}
override fun onSessionResumeFailed(castSession: CastSession?, error: Int) {}
override fun onSessionSuspended(castSession: CastSession?, reason: Int) {}
}
private fun startRouteScan() {
var connListener = object: ChromecastListener() {
override fun onReceiverAvailableUpdate(available: Boolean) {
Log.d(tag, "CAST RECEIVER UPDATE AVAILABLE $available")
}
override fun onSessionRejoin(jsonSession: JSONObject?) {
Log.d(tag, "CAST onSessionRejoin")
}
override fun onMediaLoaded(jsonMedia: JSONObject?) {
Log.d(tag, "CAST onMediaLoaded")
}
override fun onMediaUpdate(jsonMedia: JSONObject?) {
Log.d(tag, "CAST onMediaUpdate")
}
override fun onSessionUpdate(jsonSession: JSONObject?) {
Log.d(tag, "CAST onSessionUpdate")
}
override fun onSessionEnd(jsonSession: JSONObject?) {
Log.d(tag, "CAST onSessionEnd")
}
override fun onMessageReceived(p0: CastDevice, p1: String, p2: String) {
Log.d(tag, "CAST onMessageReceived")
}
}
var callback = object : ScanCallback() {
override fun onRouteUpdate(routes: List<MediaRouter.RouteInfo>?) {
Log.d(tag, "CAST On ROUTE UPDATED ${routes?.size} | ${getContext().castState}")
// if the routes have changed, we may have an available device
// If there is at least one device available
if (getContext().castState != CastState.NO_DEVICES_AVAILABLE) {
routes?.forEach { Log.d(tag, "CAST ROUTE ${it.description} | ${it.deviceType} | ${it.isBluetooth} | ${it.name}") }
// Stop the scan
stopRouteScan(this, null);
// Let the client know a receiver is available
connListener.onReceiverAvailableUpdate(true);
// Since we have a receiver we may also have an active session
var session = getSessionManager()?.currentCastSession;
// If we do have a session
if (session != null) {
// Let the client know
Log.d(tag, "LET SESSION KNOW ABOUT")
// media.setSession(session);
// connListener.onSessionRejoin(ChromecastUtilities.createSessionObject(session));
}
}
}
}
callback.setMediaRouter(getMediaRouter())
callback.onFilteredRouteUpdate();
getMediaRouter()!!.addCallback(MediaRouteSelector.Builder()
.addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))
.build(),
callback,
MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN)
}
internal interface CastListener : Cast.MessageReceivedCallback {
fun onMediaLoaded(jsonMedia: JSONObject?)
fun onMediaUpdate(jsonMedia: JSONObject?)
fun onSessionUpdate(jsonSession: JSONObject?)
fun onSessionEnd(jsonSession: JSONObject?)
}
internal abstract class ChromecastListener : CastStateListener, CastListener {
abstract fun onReceiverAvailableUpdate(available: Boolean)
abstract fun onSessionRejoin(jsonSession: JSONObject?)
/** CastStateListener functions. */
override fun onCastStateChanged(state: Int) {
onReceiverAvailableUpdate(state != CastState.NO_DEVICES_AVAILABLE)
}
}
fun stopRouteScan(callback: ScanCallback?, completionCallback: Runnable?) {
if (callback == null) {
completionCallback!!.run()
return
}
// ctx.runOnUiThread(Runnable {
callback.stop()
getMediaRouter()!!.removeCallback(callback)
completionCallback?.run()
// })
}
abstract class ScanCallback : MediaRouter.Callback() {
/**
* Called whenever a route is updated.
* @param routes the currently available routes
*/
abstract fun onRouteUpdate(routes: List<MediaRouter.RouteInfo>?)
/** records whether we have been stopped or not. */
private var stopped = false
/** Global mediaRouter object. */
private var mediaRouter: MediaRouter? = null
/**
* Sets the mediaRouter object.
* @param router mediaRouter object
*/
fun setMediaRouter(router: MediaRouter?) {
mediaRouter = router
}
/**
* Call this method when you wish to stop scanning.
* It is important that it is called, otherwise battery
* life will drain more quickly.
*/
fun stop() {
stopped = true
}
fun onFilteredRouteUpdate() {
if (stopped || mediaRouter == null) {
return
}
val outRoutes: MutableList<MediaRouter.RouteInfo> = ArrayList()
// Filter the routes
for (route in mediaRouter!!.routes) {
// We don't want default routes, or duplicate active routes
// or multizone duplicates https://github.com/jellyfin/cordova-plugin-chromecast/issues/32
val extras: Bundle? = route.extras
if (extras != null) {
CastDevice.getFromBundle(extras)
if (extras.getString("com.google.android.gms.cast.EXTRA_SESSION_ID") != null) {
continue
}
}
if (!route.isDefault
&& !route.description.equals("Google Cast Multizone Member")
&& route.playbackType === MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) {
outRoutes.add(route)
}
}
onRouteUpdate(outRoutes)
}
override fun onRouteAdded(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
onFilteredRouteUpdate()
}
override fun onRouteChanged(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
onFilteredRouteUpdate()
}
override fun onRouteRemoved(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
onFilteredRouteUpdate()
}
}
private fun listenForConnection(callback: ConnectionCallback) {
// We should only ever have one of these listeners active at a time, so remove previous
getSessionManager()?.removeSessionManagerListener(newConnectionListener, CastSession::class.java)
newConnectionListener = object : SessionListener() {
override fun onSessionStarted(castSession: CastSession?, sessionId: String) {
Log.d(tag, "CAST SESSION STARTED ${castSession?.castDevice?.friendlyName}")
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
try {
val castContext = CastContext.getSharedInstance(mainActivity)
playerNotificationService.castPlayer = CastPlayer(castContext).apply {
setSessionAvailabilityListener(CastSessionAvailabilityListener())
addListener(playerNotificationService.getPlayerListener())
}
Log.d(tag, "CAST Cast Player Applied")
switchToPlayer(true)
} catch (e: Exception) {
Log.i(tag, "Cast is not available on this device. " +
"Exception thrown when attempting to obtain CastContext. " + e.message)
return
}
// media.setSession(castSession)
// callback.onJoin(ChromecastUtilities.createSessionObject(castSession))
}
override fun onSessionStartFailed(castSession: CastSession?, errCode: Int) {
if (callback.onSessionStartFailed(errCode)) {
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
}
}
override fun onSessionEnded(castSession: CastSession?, errCode: Int) {
if (callback.onSessionEndedBeforeStart(errCode)) {
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
}
}
}
getSessionManager()?.addSessionManagerListener(newConnectionListener, CastSession::class.java)
}
private fun getContext(): CastContext {
return CastContext.getSharedInstance(mainActivity)
}
private fun getSessionManager(): SessionManager? {
return getContext().sessionManager
}
private fun getMediaRouter(): MediaRouter? {
return mainActivity?.let { MediaRouter.getInstance(it) }
}
private fun getSession(): CastSession? {
return getSessionManager()?.currentCastSession
}
internal interface ConnectionCallback {
/**
* Successfully joined a session on a route.
* @param jsonSession the session we joined
*/
fun onJoin(jsonSession: JSONObject?)
/**
* Called if we received an error.
* @param errorCode You can find the error meaning here:
* https://developers.google.com/android/reference/com/google/android/gms/cast/CastStatusCodes
* @return true if we are done listening for join, false, if we to keep listening
*/
fun onSessionStartFailed(errorCode: Int): Boolean
/**
* Called when we detect a session ended event before session started.
* See issues:
* https://github.com/jellyfin/cordova-plugin-chromecast/issues/49
* https://github.com/jellyfin/cordova-plugin-chromecast/issues/48
* @param errorCode error to output
* @return true if we are done listening for join, false, if we to keep listening
*/
fun onSessionEndedBeforeStart(errorCode: Int): Boolean
}
}

View file

@ -60,8 +60,6 @@ class MyNativeAudio : Plugin() {
Intent(mainActivity, PlayerNotificationService::class.java).also { intent ->
ContextCompat.startForegroundService(mainActivity, intent)
}
} else {
Log.w(tag, "Service already started --")
}
var jsobj = JSObject()
@ -179,7 +177,7 @@ class MyNativeAudio : Plugin() {
var isChapterTime:Boolean = call.getBoolean("isChapterTime", false) == true
Handler(Looper.getMainLooper()).post() {
var success:Boolean = playerNotificationService.setSleepTimer(time, isChapterTime)
var success:Boolean = playerNotificationService.sleepTimerManager.setSleepTimer(time, isChapterTime)
val ret = JSObject()
ret.put("success", success)
call.resolve(ret)
@ -188,7 +186,7 @@ class MyNativeAudio : Plugin() {
@PluginMethod
fun getSleepTimerTime(call: PluginCall) {
var time = playerNotificationService.getSleepTimerTime()
var time = playerNotificationService.sleepTimerManager.getSleepTimerTime()
val ret = JSObject()
ret.put("value", time)
call.resolve(ret)
@ -199,7 +197,7 @@ class MyNativeAudio : Plugin() {
var time:Long = call.getString("time", "300000")!!.toLong()
Handler(Looper.getMainLooper()).post() {
playerNotificationService.increaseSleepTime(time)
playerNotificationService.sleepTimerManager.increaseSleepTime(time)
val ret = JSObject()
ret.put("success", true)
call.resolve()
@ -211,7 +209,7 @@ class MyNativeAudio : Plugin() {
var time:Long = call.getString("time", "300000")!!.toLong()
Handler(Looper.getMainLooper()).post() {
playerNotificationService.decreaseSleepTime(time)
playerNotificationService.sleepTimerManager.decreaseSleepTime(time)
val ret = JSObject()
ret.put("success", true)
call.resolve()
@ -220,7 +218,7 @@ class MyNativeAudio : Plugin() {
@PluginMethod
fun cancelSleepTimer(call: PluginCall) {
playerNotificationService.cancelSleepTimer()
playerNotificationService.sleepTimerManager.cancelSleepTimer()
call.resolve()
}
@ -228,7 +226,7 @@ class MyNativeAudio : Plugin() {
fun requestSession(call:PluginCall) {
Log.d(tag, "CAST REQUEST SESSION PLUGIN")
playerNotificationService.requestSession(mainActivity, object : PlayerNotificationService.RequestSessionCallback() {
playerNotificationService.castManager.requestSession(mainActivity, object : CastManager.RequestSessionCallback() {
override fun onError(errorCode: Int) {
Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode")
}

View file

@ -0,0 +1,190 @@
package com.audiobookshelf.app
import android.hardware.SensorManager
import android.os.Handler
import android.os.Looper
import android.util.Log
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
const val SLEEP_EXTENSION_TIME = 900000L // 15m
class SleepTimerManager constructor(playerNotificationService:PlayerNotificationService) {
private val tag = "SleepTimerManager"
private val playerNotificationService:PlayerNotificationService = playerNotificationService
private var sleepTimerTask:TimerTask? = null
private var sleepTimerRunning:Boolean = false
private var sleepTimerEndTime:Long = 0L
private var sleepTimerExtensionTime:Long = 0L
private var sleepTimerFinishedAt:Long = 0L
private fun getCurrentTime():Long {
return playerNotificationService.getCurrentTime()
}
private fun getDuration():Long {
return playerNotificationService.getDuration()
}
private fun getIsPlaying():Boolean {
return playerNotificationService.currentPlayer.isPlaying
}
private fun setVolume(volume:Float) {
playerNotificationService.currentPlayer.volume = volume
}
private fun pause() {
playerNotificationService.currentPlayer.pause()
}
private fun play() {
playerNotificationService.currentPlayer.play()
}
private fun getSleepTimerTimeRemainingSeconds():Int {
if (sleepTimerEndTime <= 0) return 0
var sleepTimeRemaining = sleepTimerEndTime - getCurrentTime()
return ((sleepTimeRemaining / 1000).toDouble()).roundToInt()
}
fun getIsSleepTimerRunning():Boolean {
return sleepTimerRunning
}
fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean {
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
sleepTimerTask?.cancel()
sleepTimerRunning = false
sleepTimerFinishedAt = 0L
// Register shake sensor
playerNotificationService.registerSensor()
var currentTime = getCurrentTime()
if (isChapterTime) {
if (currentTime > time) {
Log.d(tag, "Invalid sleep timer - current time is already passed chapter time $time")
return false
}
sleepTimerEndTime = time
sleepTimerExtensionTime = SLEEP_EXTENSION_TIME
} else {
sleepTimerEndTime = currentTime + time
sleepTimerExtensionTime = time
}
if (sleepTimerEndTime > getDuration()) {
sleepTimerEndTime = getDuration()
}
playerNotificationService.listener?.onSleepTimerSet(sleepTimerEndTime)
sleepTimerRunning = true
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
Handler(Looper.getMainLooper()).post() {
if (getIsPlaying()) {
var sleepTimeSecondsRemaining = getSleepTimerTimeRemainingSeconds()
Log.d(tag, "Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
if (sleepTimeSecondsRemaining <= 0) {
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
pause()
playerNotificationService.listener?.onSleepTimerEnded(getCurrentTime())
clearSleepTimer()
sleepTimerFinishedAt = System.currentTimeMillis()
} else if (sleepTimeSecondsRemaining <= 30) {
// Start fading out audio
var volume = sleepTimeSecondsRemaining / 30F
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
setVolume(volume)
}
}
}
}
return true
}
fun clearSleepTimer() {
sleepTimerTask?.cancel()
sleepTimerTask = null
sleepTimerEndTime = 0
sleepTimerRunning = false
playerNotificationService.unregisterSensor()
}
fun getSleepTimerTime():Long? {
return sleepTimerEndTime
}
fun cancelSleepTimer() {
Log.d(tag, "Canceling Sleep Timer")
clearSleepTimer()
playerNotificationService.listener?.onSleepTimerSet(0)
}
private fun extendSleepTime() {
if (!sleepTimerRunning) return
setVolume(1F)
sleepTimerEndTime += sleepTimerExtensionTime
if (sleepTimerEndTime > getDuration()) sleepTimerEndTime = getDuration()
playerNotificationService.listener?.onSleepTimerSet(sleepTimerEndTime)
}
fun checkShouldExtendSleepTimer() {
if (!sleepTimerRunning) {
if (sleepTimerFinishedAt <= 0L) return
var finishedAtDistance = System.currentTimeMillis() - sleepTimerFinishedAt
if (finishedAtDistance > SLEEP_TIMER_WAKE_UP_EXPIRATION) // 2 minutes
{
Log.d(tag, "Sleep timer finished over 2 mins ago, clearing it")
sleepTimerFinishedAt = 0L
return
}
var newSleepTime = if (sleepTimerExtensionTime >= 0) sleepTimerExtensionTime else SLEEP_EXTENSION_TIME
setSleepTimer(newSleepTime, false)
play()
return
}
// Only extend if within 30 seconds of finishing
var sleepTimeRemaining = getSleepTimerTimeRemainingSeconds()
if (sleepTimeRemaining <= 30) extendSleepTime()
}
fun handleShake() {
Log.d(tag, "HANDLE SHAKE HERE")
if (sleepTimerRunning || sleepTimerFinishedAt > 0L) checkShouldExtendSleepTimer()
}
fun increaseSleepTime(time: Long) {
Log.d(tag, "Increase Sleep time $time")
if (!sleepTimerRunning) return
var newSleepEndTime = sleepTimerEndTime + time
sleepTimerEndTime = if (newSleepEndTime >= getDuration()) {
getDuration()
} else {
newSleepEndTime
}
setVolume(1F)
playerNotificationService.listener?.onSleepTimerSet(sleepTimerEndTime)
}
fun decreaseSleepTime(time: Long) {
Log.d(tag, "Decrease Sleep time $time")
if (!sleepTimerRunning) return
var newSleepEndTime = sleepTimerEndTime - time
sleepTimerEndTime = if (newSleepEndTime <= 1000) {
// End sleep timer in 1 second
getCurrentTime() + 1000
} else {
newSleepEndTime
}
setVolume(1F)
playerNotificationService.listener?.onSleepTimerSet(sleepTimerEndTime)
}
}

View file

@ -138,6 +138,18 @@ export default {
var coverSrc = this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook)
return coverSrc
},
tracksForCast() {
if (!this.audiobook || !this.audiobook.tracks) {
return []
}
var abpath = this.audiobook.path
var tracks = this.audiobook.tracks.map((t) => {
var trelpath = t.path.replace(abpath, '')
if (trelpath.startsWith('/')) trelpath = trelpath.substr(1)
return `${this.$store.state.serverUrl}/s/book/${this.audiobook.id}/${trelpath}?token=${this.userToken}`
})
return tracks
},
sleepTimeRemaining() {
if (!this.sleepTimerEndTime) return 0
return Math.max(0, this.sleepTimerEndTime / 1000 - this.currentTime)
@ -390,8 +402,10 @@ export default {
series: this.seriesTxt,
playlistUrl: this.$server.url + playlistUrl,
token: this.userToken,
audiobookId: this.audiobookId
audiobookId: this.audiobookId,
tracks: this.tracksForCast
}
this.$refs.audioPlayer.set(audiobookStreamData, stream, !this.stream)
this.stream = stream