Add chromecast support for android, update package versions

This commit is contained in:
advplyr 2022-04-17 16:59:49 -05:00
parent f7516889e4
commit 493d7aecc9
29 changed files with 1821 additions and 19074 deletions

View file

@ -4,6 +4,22 @@ plugins {
id 'kotlin-kapt' id 'kotlin-kapt'
} }
kotlin {
kotlinDaemonJvmArgs = [
"-Dfile.encoding=UTF-8",
"--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
]
}
android { android {
kotlinOptions { kotlinOptions {
freeCompilerArgs = ['-Xjvm-default=all'] freeCompilerArgs = ['-Xjvm-default=all']
@ -42,14 +58,15 @@ dependencies {
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation project(':capacitor-android') implementation project(':capacitor-android')
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins') implementation project(':capacitor-cordova-android-plugins')
implementation "androidx.core:core-ktx:1.6.0" implementation "androidx.core:core-ktx:$androidx_core_ktx_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

View file

@ -5,18 +5,20 @@ import android.app.DownloadManager
import android.content.* import android.content.*
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.* import android.os.*
import android.os.StrictMode.VmPolicy
import android.util.Log import android.util.Log
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import com.anggrayudi.storage.SimpleStorage import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.SimpleStorageHelper import com.anggrayudi.storage.SimpleStorageHelper
import com.audiobookshelf.app.data.AbsDatabase import com.audiobookshelf.app.data.AbsDatabase
import com.audiobookshelf.app.player.PlayerNotificationService import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.plugins.AbsDownloader
import com.audiobookshelf.app.plugins.AbsAudioPlayer import com.audiobookshelf.app.plugins.AbsAudioPlayer
import com.audiobookshelf.app.plugins.AbsDownloader
import com.audiobookshelf.app.plugins.AbsFileSystem import com.audiobookshelf.app.plugins.AbsFileSystem
import com.getcapacitor.BridgeActivity import com.getcapacitor.BridgeActivity
import io.paperdb.Paper import io.paperdb.Paper
class MainActivity : BridgeActivity() { class MainActivity : BridgeActivity() {
private val tag = "MainActivity" private val tag = "MainActivity"
@ -51,6 +53,20 @@ class MainActivity : BridgeActivity() {
} }
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites().detectAll()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build())
StrictMode.setVmPolicy(VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
// .penaltyDeath()
.build())
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d(tag, "onCreate") Log.d(tag, "onCreate")

View file

@ -20,19 +20,8 @@ class DbManager {
var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf() var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
Paper.book("localLibraryItems").allKeys.forEach { Paper.book("localLibraryItems").allKeys.forEach {
var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it) var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem?.mediaType)) { if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) {
// TODO: Check to make sure all file paths exist
// if (localMediaItem.coverContentUrl != null) {
// var file = DocumentFile.fromSingleUri(ctx)
// if (!file.exists()) {
// Log.e(tag, "Local media item cover url does not exist ${localMediaItem.coverContentUrl}")
// removeLocalMediaItem(localMediaItem.id)
// } else {
// localMediaItems.add(localMediaItem)
// }
// } else {
localLibraryItems.add(localLibraryItem) localLibraryItems.add(localLibraryItem)
// }
} }
} }
return localLibraryItems return localLibraryItems

View file

@ -73,7 +73,7 @@ data class LocalLibraryItem(
} }
var dateNow = System.currentTimeMillis() var dateNow = System.currentTimeMillis()
return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, chapters ?: mutableListOf(), mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress) return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, chapters ?: mutableListOf(), mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress, "exo-player")
} }
@JsonIgnore @JsonIgnore

View file

@ -2,11 +2,13 @@ package com.audiobookshelf.app.data
import android.net.Uri import android.net.Uri
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import androidx.core.app.NotificationCompat
import com.audiobookshelf.app.R import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.MediaProgressSyncData import com.audiobookshelf.app.player.MediaProgressSyncData
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.MediaMetadata import com.google.android.exoplayer2.MediaMetadata
import com.google.android.gms.cast.MediaInfo import com.google.android.gms.cast.MediaInfo
@ -42,7 +44,8 @@ class PlaybackSession(
var localLibraryItem:LocalLibraryItem?, var localLibraryItem:LocalLibraryItem?,
var localEpisodeId:String?, var localEpisodeId:String?,
var serverConnectionConfigId:String?, var serverConnectionConfigId:String?,
var serverAddress:String? var serverAddress:String?,
var mediaPlayer:String?
) { ) {
@get:JsonIgnore @get:JsonIgnore
@ -152,9 +155,14 @@ class PlaybackSession(
@JsonIgnore @JsonIgnore
fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata { fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata {
var castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER) var castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER)
castMetadata.addImage(WebImage(getCoverUri()))
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle) coverPath?.let {
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor) castMetadata.addImage(WebImage(Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")))
}
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle ?: "")
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor ?: "")
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_CHAPTER_TITLE, audioTrack.title)
castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index) castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index)
return castMetadata return castMetadata
} }
@ -164,21 +172,22 @@ class PlaybackSession(
var castMetadata = getCastMediaMetadata(audioTrack) var castMetadata = getCastMediaMetadata(audioTrack)
var mediaUri = getContentUri(audioTrack) var mediaUri = getContentUri(audioTrack)
var mediaInfoBuilder = MediaInfo.Builder(mediaUri.toString())
mediaInfoBuilder.setContentUrl(mediaUri.toString())
mediaInfoBuilder.setMetadata(castMetadata)
mediaInfoBuilder.setContentType(audioTrack.mimeType)
var mediaInfo = mediaInfoBuilder.build()
var queueItem = MediaQueueItem.Builder(mediaInfo) var mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply {
queueItem.setItemId(audioTrack.index) setContentUrl(mediaUri.toString())
queueItem.setPlaybackDuration(audioTrack.duration) setContentType(audioTrack.mimeType)
return queueItem.build() setMetadata(castMetadata)
setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
}.build()
return MediaQueueItem.Builder(mediaInfo).apply {
setPlaybackDuration(audioTrack.duration)
}.build()
} }
@JsonIgnore @JsonIgnore
fun clone():PlaybackSession { fun clone():PlaybackSession {
return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,startedAt,updatedAt,timeListening,audioTracks,currentTime,libraryItem,localLibraryItem,localEpisodeId,serverConnectionConfigId,serverAddress) return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,startedAt,updatedAt,timeListening,audioTracks,currentTime,libraryItem,localLibraryItem,localEpisodeId,serverConnectionConfigId,serverAddress, mediaPlayer)
} }
@JsonIgnore @JsonIgnore

View file

@ -34,8 +34,8 @@ class MediaManager(var apiHandler: ApiHandler) {
} }
} }
fun play(libraryItem:LibraryItem, cb: (PlaybackSession) -> Unit) { fun play(libraryItem:LibraryItem, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
apiHandler.playLibraryItem(libraryItem.id,"",false) { apiHandler.playLibraryItem(libraryItem.id,"",false, mediaPlayer) {
cb(it) cb(it)
} }
} }

View file

@ -15,9 +15,8 @@ import kotlinx.coroutines.*
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter { class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
private val tag = "MediaDescriptionAdapter" private val tag = "MediaDescriptionAdapter"
private val playerNotificationService:PlayerNotificationService = playerNotificationService
var currentIconUri: Uri? = null var currentIconUri: Uri? = null
var currentBitmap: Bitmap? = null var currentBitmap: Bitmap? = null

View file

@ -2,31 +2,30 @@ package com.audiobookshelf.app.player
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import androidx.appcompat.R import androidx.appcompat.R
import androidx.mediarouter.app.MediaRouteChooserDialog import androidx.mediarouter.app.MediaRouteChooserDialog
import androidx.mediarouter.media.MediaRouteSelector import androidx.mediarouter.media.MediaRouteSelector
import androidx.mediarouter.media.MediaRouter import androidx.mediarouter.media.MediaRouter
import com.getcapacitor.PluginCall import com.getcapacitor.PluginCall
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.ext.cast.CastPlayer
import com.google.android.exoplayer2.ext.cast.MediaItemConverter
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
import com.google.android.gms.cast.* import com.google.android.gms.cast.*
import com.google.android.gms.cast.framework.* import com.google.android.gms.cast.framework.*
import org.json.JSONObject import org.json.JSONObject
class CastManager constructor(playerNotificationService:PlayerNotificationService) { class CastManager constructor(val mainActivity:Activity) {
private val tag = "CastManager" private val tag = "CastManager"
private val playerNotificationService:PlayerNotificationService = playerNotificationService
private var playerNotificationService:PlayerNotificationService? = null
private var newConnectionListener: SessionListener? = null private var newConnectionListener: SessionListener? = null
private var mainActivity:Activity? = null
private fun switchToPlayer(useCastPlayer:Boolean) { private fun switchToPlayer(useCastPlayer:Boolean) {
playerNotificationService.switchToPlayer(useCastPlayer) Handler(Looper.getMainLooper()).post() {
playerNotificationService?.switchToPlayer(useCastPlayer)
}
} }
private inner class CastSessionAvailabilityListener : SessionAvailabilityListener { private inner class CastSessionAvailabilityListener : SessionAvailabilityListener {
@ -36,6 +35,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
* remote Cast receiver rather than play audio locally. * remote Cast receiver rather than play audio locally.
*/ */
override fun onCastSessionAvailable() { override fun onCastSessionAvailable() {
Log.d(tag, "SessionAvailabilityListener: onCastSessionAvailable")
switchToPlayer(true) switchToPlayer(true)
} }
@ -48,17 +48,13 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
} }
} }
fun requestSession(mainActivity: Activity, callback: RequestSessionCallback) { fun requestSession(playerNotificationService: PlayerNotificationService, callback: RequestSessionCallback) {
this.mainActivity = mainActivity this.playerNotificationService = playerNotificationService
mainActivity.runOnUiThread(object : Runnable {
override fun run() {
Log.d(tag, "CAST RUNNING ON MAIN THREAD")
mainActivity.runOnUiThread {
val session: CastSession? = getSession() val session: CastSession? = getSession()
if (session == null) { if (session == null) {
// show the "choose a connection" dialog // show the "choose a connection" dialog
// Add the connection listener callback // Add the connection listener callback
listenForConnection(callback) listenForConnection(callback)
@ -68,22 +64,23 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
.build() .build()
builder.setCanceledOnTouchOutside(true) builder.setCanceledOnTouchOutside(true)
builder.setOnCancelListener { builder.setOnCancelListener {
getSessionManager()!!.removeSessionManagerListener(newConnectionListener, CastSession::class.java) newConnectionListener?.let { ncl ->
getSessionManager()?.removeSessionManagerListener(ncl, CastSession::class.java)
}
callback.onCancel() callback.onCancel()
} }
builder.show() builder.show()
} else { } else {
// We are are already connected, so show the "connection options" Dialog // We are are already connected, so show the "connection options" Dialog
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity) val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
if (session.castDevice != null) { session.castDevice?.let {
builder.setTitle(session.castDevice.friendlyName) builder.setTitle(it.friendlyName)
} }
builder.setOnDismissListener { callback.onCancel() } builder.setOnDismissListener { callback.onCancel() }
builder.setPositiveButton("Stop Casting") { dialog, which -> endSession(true, null) } builder.setPositiveButton("Stop Casting") { dialog, which -> endSession(true, null) }
builder.show() builder.show()
} }
} }
})
} }
abstract class RequestSessionCallback : ConnectionCallback { abstract class RequestSessionCallback : ConnectionCallback {
@ -103,12 +100,10 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
fun endSession(stopCasting: Boolean, pluginCall: PluginCall?) { fun endSession(stopCasting: Boolean, pluginCall: PluginCall?) {
getSessionManager()!!.addSessionManagerListener(object : SessionListener() { getSessionManager()!!.addSessionManagerListener(object : SessionListener() {
override fun onSessionEnded(castSession: CastSession?, error: Int) { override fun onSessionEnded(castSession: CastSession, error: Int) {
getSessionManager()!!.removeSessionManagerListener(this, CastSession::class.java) getSessionManager()!!.removeSessionManagerListener(this, CastSession::class.java)
Log.d(tag, "CAST END SESSION") Log.d(tag, "CAST END SESSION")
// media.setSession(null)
pluginCall?.resolve() pluginCall?.resolve()
// listener.onSessionEnd(ChromecastUtilities.createSessionObject(castSession, if (stopCasting) "stopped" else "disconnected"))
} }
}, CastSession::class.java) }, CastSession::class.java)
getSessionManager()!!.endCurrentSession(stopCasting) getSessionManager()!!.endCurrentSession(stopCasting)
@ -116,78 +111,47 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
} }
open class SessionListener : SessionManagerListener<CastSession> { open class SessionListener : SessionManagerListener<CastSession> {
override fun onSessionStarting(castSession: CastSession?) {} override fun onSessionStarting(castSession: CastSession) {}
override fun onSessionStarted(castSession: CastSession?, sessionId: String) {} override fun onSessionStarted(castSession: CastSession, sessionId: String) {}
override fun onSessionStartFailed(castSession: CastSession?, error: Int) {} override fun onSessionStartFailed(castSession: CastSession, error: Int) {}
override fun onSessionEnding(castSession: CastSession?) {} override fun onSessionEnding(castSession: CastSession) {}
override fun onSessionEnded(castSession: CastSession?, error: Int) {} override fun onSessionEnded(castSession: CastSession, error: Int) {}
override fun onSessionResuming(castSession: CastSession?, sessionId: String) {} override fun onSessionResuming(castSession: CastSession, sessionId: String) {}
override fun onSessionResumed(castSession: CastSession?, wasSuspended: Boolean) {} override fun onSessionResumed(castSession: CastSession, wasSuspended: Boolean) {}
override fun onSessionResumeFailed(castSession: CastSession?, error: Int) {} override fun onSessionResumeFailed(castSession: CastSession, error: Int) {}
override fun onSessionSuspended(castSession: CastSession?, reason: 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")
}
} }
fun startRouteScan(connListener:ChromecastListener) {
var callback = object : ScanCallback() { var callback = object : ScanCallback() {
override fun onRouteUpdate(routes: List<MediaRouter.RouteInfo>?) { override fun onRouteUpdate(routes: List<MediaRouter.RouteInfo>?) {
Log.d(tag, "CAST On ROUTE UPDATED ${routes?.size} | ${getContext().castState}") Log.d(tag, "CAST On ROUTE UPDATED ${routes?.size} | ${getContext().castState}")
// if the routes have changed, we may have an available device // if the routes have changed, we may have an available device
// If there is at least one device available // If there is at least one device available
if (getContext().castState != CastState.NO_DEVICES_AVAILABLE) { if (getContext().castState != CastState.NO_DEVICES_AVAILABLE) {
routes?.forEach { Log.d(tag, "CAST ROUTE ${it.description} | ${it.deviceType} | ${it.isBluetooth} | ${it.name}") } routes?.forEach { Log.d(tag, "CAST ROUTE ${it.description} | ${it.deviceType} | ${it.isBluetooth} | ${it.name}") }
// Stop the scan // Stop the scan
stopRouteScan(this, null); stopRouteScan(this, null)
// Let the client know a receiver is available // Let the client know a receiver is available
connListener.onReceiverAvailableUpdate(true); connListener.onReceiverAvailableUpdate(true)
// Since we have a receiver we may also have an active session // Since we have a receiver we may also have an active session
var session = getSessionManager()?.currentCastSession; var session = getSessionManager()?.currentCastSession
// If we do have a session // If we do have a session
if (session != null) { if (session != null) {
// Let the client know // Let the client know
Log.d(tag, "LET SESSION KNOW ABOUT") }
// media.setSession(session); } else {
// connListener.onSessionRejoin(ChromecastUtilities.createSessionObject(session)); Log.d(tag, "No cast devices available")
}
} }
} }
} }
callback.setMediaRouter(getMediaRouter()) callback.setMediaRouter(getMediaRouter())
callback.onFilteredRouteUpdate(); callback.onFilteredRouteUpdate()
getMediaRouter()!!.addCallback(MediaRouteSelector.Builder() getMediaRouter()?.addCallback(MediaRouteSelector.Builder()
.addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)) .addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))
.build(), .build(),
callback, callback,
@ -201,7 +165,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
fun onSessionEnd(jsonSession: JSONObject?) fun onSessionEnd(jsonSession: JSONObject?)
} }
internal abstract class ChromecastListener : CastStateListener, CastListener { abstract class ChromecastListener : CastStateListener, CastListener {
abstract fun onReceiverAvailableUpdate(available: Boolean) abstract fun onReceiverAvailableUpdate(available: Boolean)
abstract fun onSessionRejoin(jsonSession: JSONObject?) abstract fun onSessionRejoin(jsonSession: JSONObject?)
@ -212,15 +176,19 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
} }
fun stopRouteScan(callback: ScanCallback?, completionCallback: Runnable?) { fun stopRouteScan(callback: ScanCallback?, completionCallback: Runnable?) {
Log.d(tag, "stopRouteScan")
if (callback == null) { if (callback == null) {
completionCallback!!.run() completionCallback?.run()
return return
} }
// ctx.runOnUiThread(Runnable {
// mainActivity.runOnUiThread {
Log.d(tag, "Removing callback on media router")
callback.stop() callback.stop()
getMediaRouter()!!.removeCallback(callback) getMediaRouter()?.removeCallback(callback)
completionCallback?.run() completionCallback?.run()
// }) // }
} }
abstract class ScanCallback : MediaRouter.Callback() { abstract class ScanCallback : MediaRouter.Callback() {
@ -271,7 +239,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
} }
if (!route.isDefault if (!route.isDefault
&& !route.description.equals("Google Cast Multizone Member") && !route.description.equals("Google Cast Multizone Member")
&& route.playbackType === MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) { && route.playbackType == MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) {
outRoutes.add(route) outRoutes.add(route)
} }
} }
@ -291,87 +259,50 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
} }
} }
inner class CustomConverter : MediaItemConverter {
override fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem {
// The MediaQueueItem you build is expected to be in the tag.
var queueItem = (mediaItem.playbackProperties!!.tag as MediaQueueItem?)!!
Log.d(tag, "Test toMediaQueueItem ${queueItem.media!!.contentUrl} | ${queueItem.playbackDuration} | ${queueItem.itemId}")
return queueItem
}
override fun toMediaItem(mediaQueueItem: MediaQueueItem): MediaItem {
return MediaItem.Builder()
.setUri(mediaQueueItem.media!!.contentUrl)
.setTag(mediaQueueItem)
.build()
}
}
private fun listenForConnection(callback: ConnectionCallback) { private fun listenForConnection(callback: ConnectionCallback) {
// We should only ever have one of these listeners active at a time, so remove previous // We should only ever have one of these listeners active at a time, so remove previous
getSessionManager()?.removeSessionManagerListener(newConnectionListener, CastSession::class.java) newConnectionListener?.let { ncl ->
getSessionManager()?.removeSessionManagerListener(ncl, CastSession::class.java)
}
newConnectionListener = object : SessionListener() { newConnectionListener = object : SessionListener() {
override fun onSessionStarted(castSession: CastSession?, sessionId: String) { override fun onSessionStarted(castSession: CastSession, sessionId: String) {
Log.d(tag, "CAST SESSION STARTED ${castSession?.castDevice?.friendlyName}") Log.d(tag, "CAST SESSION STARTED ${castSession.castDevice?.friendlyName}")
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java) getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
try {
val castContext = CastContext.getSharedInstance(mainActivity) val castContext = CastContext.getSharedInstance(mainActivity)
// Work in progress using the cast api playerNotificationService?.let {
var currentSession = playerNotificationService.getCurrentPlaybackSessionCopy() if (it.castPlayer == null) {
var firstTrack = currentSession?.audioTracks?.get(0) Log.d(tag, "Initializing castPlayer on session started - switch to cast player")
var uri = firstTrack?.let { currentSession?.getContentUri(it) } ?: Uri.EMPTY it.castPlayer = CastPlayer(castContext).apply {
var url = uri.toString() addListener(PlayerListener(it))
var mimeType = firstTrack?.mimeType ?: "" setSessionAvailabilityListener(CastSessionAvailabilityListener())
var castMediaMetadata = firstTrack?.let { currentSession?.getCastMediaMetadata(it) } }
Log.d(tag, "CastManager set url $url") switchToPlayer(true)
var duration = (currentSession?.getTotalDuration() ?: 0L * 1000L).toLong() } else {
Log.d(tag, "castPlayer is already initialized on session started")
if (castMediaMetadata != null) { }
Log.d(tag, "CastManager duration $duration got cast media metadata $castMediaMetadata") }
val mediaInfo = MediaInfo.Builder(url)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(mimeType)
.setMetadata(castMediaMetadata)
.setStreamDuration(duration)
.build()
val remoteMediaClient = castSession?.remoteMediaClient
remoteMediaClient?.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
} }
// Not working using the exo player CastPlayer override fun onSessionStartFailed(castSession: CastSession, errCode: Int) {
// playerNotificationService.castPlayer = CastPlayer(castContext, CustomConverter()).apply {
// setSessionAvailabilityListener(CastSessionAvailabilityListener())
// addListener(PlayerListener(playerNotificationService))
// }
// 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)) { if (callback.onSessionStartFailed(errCode)) {
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java) getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
} }
} }
override fun onSessionEnded(castSession: CastSession?, errCode: Int) { override fun onSessionEnded(castSession: CastSession, errCode: Int) {
if (callback.onSessionEndedBeforeStart(errCode)) { if (callback.onSessionEndedBeforeStart(errCode)) {
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java) getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
} }
} }
} }
getSessionManager()?.addSessionManagerListener(newConnectionListener, CastSession::class.java) newConnectionListener?.let {
Log.d(tag, "Add session manager listener")
getSessionManager()?.addSessionManagerListener(it, CastSession::class.java)
}
} }
private fun getContext(): CastContext { private fun getContext(): CastContext {
@ -383,7 +314,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
} }
private fun getMediaRouter(): MediaRouter? { private fun getMediaRouter(): MediaRouter? {
return mainActivity?.let { MediaRouter.getInstance(it) } return MediaRouter.getInstance(mainActivity)
} }
private fun getSession(): CastSession? { private fun getSession(): CastSession? {

View file

@ -0,0 +1,983 @@
package com.audiobookshelf.app.player
import android.annotation.SuppressLint
import android.os.Looper
import android.util.Log
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.TextureView
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.C.TrackType
import com.google.android.exoplayer2.MediaMetadata
import com.google.android.exoplayer2.Player.*
import com.google.android.exoplayer2.TracksInfo.TrackGroupInfo
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
import com.google.android.exoplayer2.source.TrackGroup
import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.text.Cue
import com.google.android.exoplayer2.trackselection.TrackSelection
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters
import com.google.android.exoplayer2.util.*
import com.google.android.exoplayer2.util.Util.castNonNull
import com.google.android.exoplayer2.video.VideoSize
import com.google.android.gms.cast.*
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.SessionManagerListener
import com.google.android.gms.cast.framework.media.RemoteMediaClient
import com.google.android.gms.common.api.PendingResult
import com.google.android.gms.common.api.ResultCallback
import org.json.JSONObject
class CastPlayer(var castContext: CastContext) : BasePlayer() {
val tag = "CastPlayer"
private val RENDERER_COUNT = 3
private val RENDERER_INDEX_VIDEO = 0
private val RENDERER_INDEX_AUDIO = 1
private val RENDERER_INDEX_TEXT = 2
private val PROGRESS_REPORT_PERIOD_MS: Long = 1000
private val EMPTY_TRACK_SELECTION_ARRAY = TrackSelectionArray()
private val EMPTY_TRACK_ID_ARRAY = LongArray(0)
private val seekBackIncrementMs: Long = 5000L
private val seekForwardIncrementMs: Long = 5000L
private var myPlayWhenReady:Boolean = false
private var playbackParameters:PlaybackParameters = PlaybackParameters.DEFAULT
private var myAvailableCommands: Commands
private var myCurrentTracksInfo: TracksInfo
private var myCurrentTrackGroups: TrackGroupArray
private var myCurrentTrackSelections: TrackSelectionArray
var currentMediaItems:List<MediaItem> = mutableListOf()
var remoteMediaClient:RemoteMediaClient? = null
var sessionAvailabilityListener:SessionAvailabilityListener? = null
var myCurrentTimeline: CastTimeline
val timelineTracker: CastTimelineTracker
val period: Timeline.Period
var listeners: ListenerSet<Player.Listener>
/* package */
val PERMANENT_AVAILABLE_COMMANDS = Commands.Builder()
.addAll(
COMMAND_PLAY_PAUSE,
COMMAND_PREPARE,
COMMAND_STOP,
COMMAND_SEEK_TO_DEFAULT_POSITION,
COMMAND_SEEK_TO_MEDIA_ITEM,
COMMAND_SET_REPEAT_MODE,
COMMAND_SET_SPEED_AND_PITCH,
COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACK_INFOS)
.build()
var currentPlaybackState = Player.STATE_IDLE
var statusListener:StatusListener
val deviceInfo get() = castContext.sessionManager.currentCastSession?.castDevice.toString()
var lastReportedPositionMs = 0L
private var currentMediaItemIndex = 0
private var pendingMediaItemRemovalPosition:PositionInfo? = null
private var pendingSeekCount = 0
private var pendingSeekWindowIndex = 0
private var pendingSeekPositionMs = 0L
init {
Log.d(tag, "Init CastPlayer")
myCurrentTrackGroups = TrackGroupArray.EMPTY
myCurrentTrackSelections = EMPTY_TRACK_SELECTION_ARRAY
myCurrentTracksInfo = TracksInfo.EMPTY
statusListener = StatusListener()
timelineTracker = CastTimelineTracker()
myCurrentTimeline = CastTimeline.EMPTY_CAST_TIMELINE
period = Timeline.Period()
myAvailableCommands = Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build()
listeners = ListenerSet(
Looper.getMainLooper(),
Clock.DEFAULT
) { listener: Player.Listener, flags: FlagSet? -> listener.onEvents(this, Player.Events(flags ?: FlagSet.Builder().build())) }
val sessionManager = castContext.sessionManager
sessionManager.addSessionManagerListener(statusListener, CastSession::class.java)
val session = sessionManager.currentCastSession
setRemoteMediaClient(session?.remoteMediaClient)
}
fun load(mediaItems:List<MediaItem>, startIndex:Int, startTime:Long, playWhenReady:Boolean, playbackRate:Float, mediaType:String) {
Log.d(tag, "Load called")
if (remoteMediaClient == null) {
Log.d(tag, "Remote Media Client not set")
return
}
currentMediaItems = mediaItems
var mediaQueueItems = mediaItems.map { toMediaQueueItem(it) }
var queueData = MediaQueueData.Builder().apply {
setItems(mediaQueueItems)
setQueueType(if (mediaType == "book") MediaQueueData.MEDIA_QUEUE_TYPE_AUDIO_BOOK else MediaQueueData.MEDIA_QUEUE_TYPE_PODCAST_SERIES)
setRepeatMode(MediaStatus.REPEAT_MODE_REPEAT_OFF)
setStartIndex(startIndex)
setStartTime(startTime)
}.build()
var loadRequestData = MediaLoadRequestData.Builder().apply {
setPlaybackRate(playbackRate.toDouble())
setQueueData(queueData)
setAutoplay(playWhenReady)
setCurrentTime(startTime)
}.build()
remoteMediaClient?.load(loadRequestData)?.setResultCallback {
Log.d(tag, "Loaded cast player result ${it.status} | ${it.mediaError} | ${it.customData}")
}
Log.d(tag, "Loaded cast player request data $loadRequestData")
}
private fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem {
// The MediaQueueItem you build is expected to be in the tag.
return (mediaItem.playbackProperties!!.tag as MediaQueueItem?)!!
}
@JvmName("setRemoteMediaClient1")
private fun setRemoteMediaClient(remoteMediaClient: RemoteMediaClient?) {
if (this.remoteMediaClient === remoteMediaClient) {
// Do nothing.
return
}
if (this.remoteMediaClient != null) {
this.remoteMediaClient?.unregisterCallback(statusListener)
this.remoteMediaClient?.removeProgressListener(statusListener)
}
this.remoteMediaClient = remoteMediaClient
if (remoteMediaClient != null) {
sessionAvailabilityListener?.let {
it.onCastSessionAvailable()
}
remoteMediaClient.registerCallback(statusListener)
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS)
updateInternalStateAndNotifyIfChanged()
} else {
updateTimelineAndNotifyIfChanged()
sessionAvailabilityListener?.let {
it.onCastSessionUnavailable()
}
}
}
private fun setPlayerStateAndNotifyIfChanged(
playWhenReady: Boolean,
@PlayWhenReadyChangeReason playWhenReadyChangeReason: Int,
@Player.State playbackState: Int) {
val wasPlaying = this.currentPlaybackState == STATE_READY && this.playWhenReady
val playWhenReadyChanged = this.playWhenReady != playWhenReady
val playbackStateChanged = this.currentPlaybackState != playbackState
Log.d(tag, "setPlayerStateAndNotifyIfChanged newPlayWhenReady:$playWhenReady | playbackStateChanged:$playbackStateChanged | playWhenReadyChanged:$playWhenReadyChanged")
if (playWhenReadyChanged || playbackStateChanged) {
this.currentPlaybackState = playbackState
this.myPlayWhenReady = playWhenReady
if (playbackStateChanged) {
Log.d(tag, "CastPlayer About to emit onPlaybackStateChanged")
listeners.queueEvent(EVENT_PLAYBACK_STATE_CHANGED) {
it.onPlaybackStateChanged(playbackState)
}
}
if (playWhenReadyChanged) {
listeners.queueEvent(EVENT_PLAY_WHEN_READY_CHANGED) {
it.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason)
}
}
val isPlaying = playbackState == STATE_READY && playWhenReady
if (wasPlaying != isPlaying) {
Log.d(tag, "CastPlayer About to emit onIsPlayingChanged $isPlaying")
listeners.queueEvent(EVENT_IS_PLAYING_CHANGED) {
it.onIsPlayingChanged(isPlaying)
}
} else {
Log.d(tag, "Is playing and was playing are equal $wasPlaying")
}
} else {
Log.d(tag, "setPlayerStateAndNotifyIfChanged No changes")
}
listeners.flushEvents()
}
private fun updatePlayerStateAndNotifyIfChanged() {
var newPlayWhenReadyValue = remoteMediaClient?.isPaused != true
@PlayWhenReadyChangeReason val playWhenReadyChangeReason = if (newPlayWhenReadyValue != playWhenReady) PLAY_WHEN_READY_CHANGE_REASON_REMOTE else PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST
// We do not mask the playback state, so try setting it regardless of the playWhenReady masking.
setPlayerStateAndNotifyIfChanged(
newPlayWhenReadyValue, playWhenReadyChangeReason, fetchPlaybackState(remoteMediaClient))
}
private fun fetchPlaybackState(remoteMediaClient: RemoteMediaClient?): Int {
return when (remoteMediaClient?.playerState) {
MediaStatus.PLAYER_STATE_BUFFERING -> STATE_BUFFERING
MediaStatus.PLAYER_STATE_PLAYING, MediaStatus.PLAYER_STATE_PAUSED -> STATE_READY
MediaStatus.PLAYER_STATE_IDLE, MediaStatus.PLAYER_STATE_UNKNOWN -> STATE_IDLE
else -> STATE_IDLE
}
}
private fun updatePlaybackRateAndNotifyIfChanged() {
val mediaStatus = remoteMediaClient!!.mediaStatus
val speed = mediaStatus?.playbackRate?.toFloat() ?: PlaybackParameters.DEFAULT.speed
if (speed > 0.0f) {
// Set the speed if not paused.
setPlaybackParametersAndNotifyIfChanged(PlaybackParameters(speed))
}
}
private fun setPlaybackParametersAndNotifyIfChanged(playbackParameters: PlaybackParameters) {
if (this.playbackParameters == playbackParameters) {
return
}
this.playbackParameters = playbackParameters
listeners.queueEvent(
EVENT_PLAYBACK_PARAMETERS_CHANGED
) { listener -> listener.onPlaybackParametersChanged(playbackParameters) }
listeners.flushEvents()
updateAvailableCommandsAndNotifyIfChanged()
}
private fun updateTimelineAndNotifyIfChanged(): Boolean {
val oldTimeline: Timeline = this.myCurrentTimeline
val oldWindowIndex = currentMediaItemIndex
var playingPeriodChanged = false
if (updateTimeline()) {
val timeline: Timeline = this.myCurrentTimeline
// Call onTimelineChanged.
listeners.queueEvent(
EVENT_TIMELINE_CHANGED
) { listener: Player.Listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_SOURCE_UPDATE) }
// Call onPositionDiscontinuity if required.
val currentTimeline = currentTimeline
var playingPeriodRemoved = false
if (!oldTimeline.isEmpty) {
val oldPeriodUid = castNonNull(oldTimeline.getPeriod(oldWindowIndex, period, /* setIds= */true).uid)
playingPeriodRemoved = currentTimeline.getIndexOfPeriod(oldPeriodUid) == C.INDEX_UNSET
}
if (playingPeriodRemoved) {
var oldPosition: PositionInfo = getCurrentPositionInfo()
if (pendingMediaItemRemovalPosition != null) {
pendingMediaItemRemovalPosition?.let {
oldPosition = it
}
pendingMediaItemRemovalPosition = null
} else {
// If the media item has been removed by another client, we don't know the removal
// position. We use the current position as a fallback.
oldTimeline.getPeriod(oldWindowIndex, period, /* setIds= */true)
oldTimeline.getWindow(period.windowIndex, window)
oldPosition = PositionInfo(
window.uid,
period.windowIndex,
window.mediaItem,
period.uid,
period.windowIndex,
currentPosition,
contentPosition,
C.INDEX_UNSET,/* adGroupIndex= */
C.INDEX_UNSET) /* adIndexInAdGroup= */
}
val newPosition: PositionInfo = getCurrentPositionInfo()
listeners.queueEvent(
EVENT_POSITION_DISCONTINUITY
) { listener: Player.Listener ->
listener.onPositionDiscontinuity(
oldPosition, newPosition, DISCONTINUITY_REASON_REMOVE)
}
}
// Call onMediaItemTransition if required.
playingPeriodChanged = currentTimeline.isEmpty != oldTimeline.isEmpty || playingPeriodRemoved
if (playingPeriodChanged) {
listeners.queueEvent(
EVENT_MEDIA_ITEM_TRANSITION
) { listener: Player.Listener ->
listener.onMediaItemTransition(
currentMediaItem, MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)
}
}
updateAvailableCommandsAndNotifyIfChanged()
}
return playingPeriodChanged
}
private fun updateInternalStateAndNotifyIfChanged() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return
}
val oldWindowIndex = this.currentMediaItemIndex
val oldPeriodUid = if (!currentTimeline.isEmpty) currentTimeline.getPeriod(oldWindowIndex, period, /* setIds= */true).uid else null
updatePlayerStateAndNotifyIfChanged()
// updateRepeatModeAndNotifyIfChanged( /* resultCallback= */null)
updatePlaybackRateAndNotifyIfChanged()
val playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged()
val currentTimeline = currentTimeline
currentMediaItemIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline)
val currentPeriodUid = if (!currentTimeline.isEmpty) currentTimeline.getPeriod(currentMediaItemIndex, period, /* setIds= */true).uid else null
if (!playingPeriodChangedByTimelineChange
&& !Util.areEqual(oldPeriodUid, currentPeriodUid)
&& pendingSeekCount == 0) {
// Report discontinuity and media item auto transition.
currentTimeline.getPeriod(oldWindowIndex, period, /* setIds= */true)
currentTimeline.getWindow(oldWindowIndex, window)
val windowDurationMs = window.durationMs
val oldPosition = PositionInfo(
window.uid,
period.windowIndex,
window.mediaItem,
period.uid,
period.windowIndex, /* positionMs= */
windowDurationMs, /* contentPositionMs= */
windowDurationMs, /* adGroupIndex= */
C.INDEX_UNSET, /* adIndexInAdGroup= */
C.INDEX_UNSET)
currentTimeline.getPeriod(currentMediaItemIndex, period, /* setIds= */true)
currentTimeline.getWindow(currentMediaItemIndex, window)
val newPosition = PositionInfo(
window.uid,
period.windowIndex,
window.mediaItem,
period.uid,
period.windowIndex, /* positionMs= */
window.defaultPositionMs, /* contentPositionMs= */
window.defaultPositionMs, /* adGroupIndex= */
C.INDEX_UNSET, /* adIndexInAdGroup= */
C.INDEX_UNSET)
listeners.queueEvent(
EVENT_POSITION_DISCONTINUITY
) { listener: Listener ->
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AUTO_TRANSITION)
listener.onPositionDiscontinuity(
oldPosition, newPosition, DISCONTINUITY_REASON_AUTO_TRANSITION)
}
listeners.queueEvent(
EVENT_MEDIA_ITEM_TRANSITION
) { listener: Listener ->
listener.onMediaItemTransition(
currentMediaItem, MEDIA_ITEM_TRANSITION_REASON_AUTO)
}
}
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
listeners.queueEvent(
EVENT_TRACKS_CHANGED
) { listener: Listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelections) }
listeners.queueEvent(
EVENT_TRACKS_CHANGED) { listener: Listener -> listener.onTracksInfoChanged(currentTracksInfo) }
}
updateAvailableCommandsAndNotifyIfChanged()
listeners.flushEvents()
}
/** Updates the internal tracks and selection and returns whether they have changed. */
@SuppressLint("WrongConstant")
private fun updateTracksAndSelectionsAndNotifyIfChanged(): Boolean {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return false
}
val mediaStatus = getMediaStatus()
val mediaInfo = mediaStatus?.mediaInfo
val castMediaTracks = mediaInfo?.mediaTracks
if (castMediaTracks == null || castMediaTracks.isEmpty()) {
val hasChanged = !myCurrentTrackGroups.isEmpty
myCurrentTrackGroups = TrackGroupArray.EMPTY
myCurrentTrackSelections = EMPTY_TRACK_SELECTION_ARRAY
myCurrentTracksInfo = TracksInfo.EMPTY
return hasChanged
}
var activeTrackIds = mediaStatus.activeTrackIds
if (activeTrackIds == null) {
activeTrackIds = EMPTY_TRACK_ID_ARRAY
}
val trackGroups = arrayOfNulls<TrackGroup>(castMediaTracks.size)
val trackSelections = arrayOfNulls<TrackSelection>(RENDERER_COUNT)
val trackGroupInfos = arrayOfNulls<TrackGroupInfo>(castMediaTracks.size)
for (i in castMediaTracks.indices) {
val mediaTrack = castMediaTracks[i]
trackGroups[i] = TrackGroup( /* id= */i.toString(), mediaTrackToFormat(mediaTrack))
val id = mediaTrack.id
val trackType = MimeTypes.getTrackType(mediaTrack.contentType)
val rendererIndex: Int = getRendererIndexForTrackType(trackType)
val supported = rendererIndex != C.INDEX_UNSET
val selected = isTrackActive(id, activeTrackIds) && supported && trackSelections[rendererIndex] == null
if (selected) {
trackSelections[rendererIndex] = trackGroups[i]?.let { CastTrackSelection(it) }
}
val trackSupport = intArrayOf(if (supported) C.FORMAT_HANDLED else C.FORMAT_UNSUPPORTED_TYPE)
val trackSelected = booleanArrayOf(selected)
trackGroupInfos[i] = TrackGroupInfo(trackGroups[i]!!, trackSupport, trackType, trackSelected)
}
val tg = trackGroups.filterNotNull().toTypedArray()
var ts = trackSelections.filterNotNull().toTypedArray()
var tgi = trackGroupInfos.filterNotNull().toMutableList()
val newTrackGroups = TrackGroupArray(*tg)
val newTrackSelections = TrackSelectionArray(*ts)
val newTracksInfo = TracksInfo(tgi)
if (newTrackGroups != currentTrackGroups
|| newTrackSelections != currentTrackSelections
|| newTracksInfo != currentTracksInfo) {
myCurrentTrackSelections = newTrackSelections
myCurrentTrackGroups = newTrackGroups
myCurrentTracksInfo = newTracksInfo
return true
}
return false
}
/**
* Creates a [Format] instance containing all information contained in the given [ ] object.
*
* @param mediaTrack The [MediaTrack].
* @return The equivalent [Format].
*/
fun mediaTrackToFormat(mediaTrack: MediaTrack): Format {
return Format.Builder()
.setId(mediaTrack.contentId)
.setContainerMimeType(mediaTrack.contentType)
.setLanguage(mediaTrack.language)
.build()
}
private fun getRendererIndexForTrackType(trackType: @TrackType Int): Int {
return if (trackType == C.TRACK_TYPE_VIDEO) RENDERER_INDEX_VIDEO else if (trackType == C.TRACK_TYPE_AUDIO) RENDERER_INDEX_AUDIO else if (trackType == C.TRACK_TYPE_TEXT) RENDERER_INDEX_TEXT else C.INDEX_UNSET
}
private fun isTrackActive(id: Long, activeTrackIds: LongArray): Boolean {
for (activeTrackId in activeTrackIds) {
if (activeTrackId == id) {
return true
}
}
return false
}
private fun getCurrentPositionInfo(): PositionInfo {
val currentTimeline = currentTimeline
var newPeriodUid: Any? = null
var newWindowUid: Any? = null
var newMediaItem: MediaItem? = null
if (!currentTimeline.isEmpty) {
newPeriodUid = currentTimeline.getPeriod(currentPeriodIndex, period, /* setIds= */true).uid
newWindowUid = currentTimeline.getWindow(period.windowIndex, window).uid
newMediaItem = window.mediaItem
}
return PositionInfo(
newWindowUid,
currentMediaItemIndex,
newMediaItem,
newPeriodUid,
currentPeriodIndex,
currentPosition,
contentPosition,
C.INDEX_UNSET,/* adGroupIndex= */
C.INDEX_UNSET) /* adIndexInAdGroup= */
}
private fun updateTimeline(): Boolean {
val oldTimeline = this.myCurrentTimeline
val status: MediaStatus? = getMediaStatus()
remoteMediaClient?.let {
this.myCurrentTimeline = if (status != null) timelineTracker.getCastTimeline(it) else CastTimeline.EMPTY_CAST_TIMELINE
}
val timelineChanged = oldTimeline != this.myCurrentTimeline
if (timelineChanged) {
currentMediaItemIndex = fetchCurrentWindowIndex(remoteMediaClient, this.myCurrentTimeline)
Log.d(tag, "timelineChanged $currentMediaItemIndex")
}
return timelineChanged
}
private fun fetchCurrentWindowIndex(remoteMediaClient: RemoteMediaClient?, timeline: Timeline): Int {
if (remoteMediaClient == null) {
return 0
}
var currentWindowIndex = C.INDEX_UNSET
val currentItem = remoteMediaClient.currentItem
if (currentItem != null) {
currentWindowIndex = timeline.getIndexOfPeriod(currentItem.itemId)
}
if (currentWindowIndex == C.INDEX_UNSET) {
// The timeline is empty. Fall back to index 0.
currentWindowIndex = 0
}
return currentWindowIndex
}
private fun getMediaStatus():MediaStatus? {
return remoteMediaClient?.mediaStatus
}
@JvmName("setSessionAvailabilityListener1")
fun setSessionAvailabilityListener(listener: SessionAvailabilityListener) {
sessionAvailabilityListener = listener
}
override fun getApplicationLooper(): Looper {
return Looper.getMainLooper()
}
override fun addListener(listener: Player.Listener) {
listeners.add(listener)
Log.d(tag, "addListener player listener $listener")
}
override fun removeListener(listener: Player.Listener) {
listeners.remove(listener)
}
override fun setMediaItems(mediaItems: MutableList<MediaItem>, resetPosition: Boolean) {
}
override fun setMediaItems(mediaItems: MutableList<MediaItem>, startWindowIndex: Int, startPositionMs: Long) {
}
override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) {
}
override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) {
}
override fun removeMediaItems(fromIndex: Int, toIndex: Int) {
Log.d(tag, "removeMediaItems called not configured yet $fromIndex to $toIndex")
}
override fun getAvailableCommands(): Player.Commands {
return myAvailableCommands
}
override fun prepare() {
}
override fun getPlaybackState(): Int {
return currentPlaybackState
}
override fun getPlaybackSuppressionReason(): Int {
return Player.PLAYBACK_SUPPRESSION_REASON_NONE
}
override fun getPlayerError(): PlaybackException? {
return null
}
override fun setPlayWhenReady(playWhenReady: Boolean) {
setPlayerStateAndNotifyIfChanged(
playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, playbackState)
listeners.flushEvents()
val pendingResult: PendingResult<RemoteMediaClient.MediaChannelResult> = if (playWhenReady) remoteMediaClient!!.play() else remoteMediaClient!!.pause()
var resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
updatePlayerStateAndNotifyIfChanged()
}
pendingResult.setResultCallback(resultCb)
}
override fun getPlayWhenReady(): Boolean {
return myPlayWhenReady
}
override fun setRepeatMode(repeatMode: Int) {
}
override fun getRepeatMode(): Int {
return Player.REPEAT_MODE_OFF
}
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {
}
override fun getShuffleModeEnabled(): Boolean {
return false
}
override fun isLoading(): Boolean {
return false
}
override fun seekTo(mediaItemIndex: Int, positionMs: Long) {
Log.d(tag, "seekTo $mediaItemIndex position $positionMs")
var resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
val statusCode: Int = it.status.statusCode
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
Log.e(tag, "Seek failed. Error code $statusCode")
}
if (--pendingSeekCount == 0) {
currentMediaItemIndex = pendingSeekWindowIndex
Log.d(tag, "seekTo CB $currentMediaItemIndex")
pendingSeekWindowIndex = C.INDEX_UNSET
pendingSeekPositionMs = C.TIME_UNSET
listeners.sendEvent( /* eventFlag= */C.INDEX_UNSET) { obj: Player.Listener -> obj.onSeekProcessed() }
// Playback state change will send metadata to client and stop seek loading
listeners.sendEvent(EVENT_PLAYBACK_STATE_CHANGED) { obj: Player.Listener -> obj.onPlaybackStateChanged(currentPlaybackState) }
}
}
val mediaStatus = getMediaStatus()
// We assume the default position is 0. There is no support for seeking to the default position
// in RemoteMediaClient.
var positionMs = if (positionMs != C.TIME_UNSET) positionMs else 0
if (mediaStatus != null) {
if (currentMediaItemIndex != mediaItemIndex) {
Log.d(tag, "seekTo: Changing media item index from $currentMediaItemIndex to $mediaItemIndex")
remoteMediaClient?.queueJumpToItem(myCurrentTimeline.getPeriod(mediaItemIndex, period).uid as Int, positionMs, JSONObject())?.setResultCallback(resultCb)
} else {
Log.d(tag, "seekTo: Same media index seek to position $positionMs")
var mediaSeekOptions = MediaSeekOptions.Builder().setPosition(positionMs).build()
remoteMediaClient?.seek(mediaSeekOptions)?.setResultCallback(resultCb)
}
val oldPosition = getCurrentPositionInfo()
pendingSeekCount++
pendingSeekWindowIndex = mediaItemIndex
pendingSeekPositionMs = positionMs
val newPosition = getCurrentPositionInfo()
listeners.queueEvent(
EVENT_POSITION_DISCONTINUITY
) { listener: Player.Listener ->
listener.onPositionDiscontinuity(oldPosition, newPosition, DISCONTINUITY_REASON_SEEK)
}
if (oldPosition.windowIndex != newPosition.windowIndex) {
val mediaItem = currentTimeline.getWindow(mediaItemIndex, window).mediaItem
listeners.queueEvent(
EVENT_MEDIA_ITEM_TRANSITION
) { listener: Player.Listener -> listener.onMediaItemTransition(mediaItem, MEDIA_ITEM_TRANSITION_REASON_SEEK) }
}
updateAvailableCommandsAndNotifyIfChanged()
} else if (pendingSeekCount == 0) {
Log.w(tag, "seekTo Media Status is null")
listeners.queueEvent( /* eventFlag= */C.INDEX_UNSET) { obj: Player.Listener -> obj.onSeekProcessed() }
}
listeners.flushEvents()
}
private fun updateAvailableCommandsAndNotifyIfChanged() {
val previousAvailableCommands = availableCommands
myAvailableCommands = Util.getAvailableCommands(this,PERMANENT_AVAILABLE_COMMANDS)
if (availableCommands != previousAvailableCommands) {
listeners.queueEvent(
EVENT_AVAILABLE_COMMANDS_CHANGED
) { listener: Listener -> listener.onAvailableCommandsChanged(availableCommands) }
}
}
override fun getSeekBackIncrement(): Long {
return seekBackIncrementMs
}
override fun getSeekForwardIncrement(): Long {
return seekForwardIncrementMs
}
override fun getMaxSeekToPreviousPosition(): Long {
return C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS
}
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
val actualPlaybackParameters = PlaybackParameters(playbackParameters.speed)
setPlaybackParametersAndNotifyIfChanged(actualPlaybackParameters)
listeners.flushEvents()
val pendingResult: PendingResult<RemoteMediaClient.MediaChannelResult>? = remoteMediaClient?.setPlaybackRate(actualPlaybackParameters.speed.toDouble(), /* customData= */null)
var resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
updatePlaybackRateAndNotifyIfChanged()
listeners.flushEvents()
}
pendingResult?.setResultCallback(resultCb)
}
override fun getPlaybackParameters(): PlaybackParameters {
return playbackParameters
}
override fun stop() {
Log.d(tag, "stop CastPlayer")
currentPlaybackState = Player.STATE_IDLE
remoteMediaClient?.stop()
}
override fun stop(reset: Boolean) {
stop()
}
override fun release() {
val sessionManager = castContext.sessionManager
sessionManager.removeSessionManagerListener(statusListener, CastSession::class.java)
sessionManager.endCurrentSession(false)
}
override fun getCurrentTrackGroups(): TrackGroupArray {
return this.myCurrentTrackGroups
}
override fun getCurrentTrackSelections(): TrackSelectionArray {
return this.myCurrentTrackSelections
}
override fun getCurrentTracksInfo(): TracksInfo {
return this.myCurrentTracksInfo
}
override fun getTrackSelectionParameters(): TrackSelectionParameters {
return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT
}
override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {}
override fun getMediaMetadata(): MediaMetadata {
return MediaMetadata.EMPTY
}
override fun getPlaylistMetadata(): MediaMetadata {
return MediaMetadata.EMPTY
}
override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) {
}
override fun getCurrentTimeline(): Timeline {
return this.myCurrentTimeline
}
override fun getCurrentPeriodIndex(): Int {
return getCurrentMediaItemIndex()
}
override fun getCurrentMediaItemIndex(): Int {
return if (pendingSeekWindowIndex != C.INDEX_UNSET) pendingSeekWindowIndex else currentMediaItemIndex
}
override fun getDuration(): Long {
return contentDuration
}
override fun getCurrentPosition(): Long {
return remoteMediaClient?.approximateStreamPosition ?: 0L
}
override fun getBufferedPosition(): Long {
return currentPosition
}
override fun getTotalBufferedDuration(): Long {
return 0L
}
override fun isPlayingAd(): Boolean {
return false
}
override fun getCurrentAdGroupIndex(): Int {
return 0
}
override fun getCurrentAdIndexInAdGroup(): Int {
return 0
}
override fun getContentPosition(): Long {
return currentPosition
}
override fun getContentBufferedPosition(): Long {
return currentPosition
}
override fun getAudioAttributes(): AudioAttributes {
return AudioAttributes.DEFAULT
}
override fun setVolume(audioVolume: Float) {
}
override fun getVolume(): Float {
return 0F
}
override fun clearVideoSurface() {
}
override fun clearVideoSurface(surface: Surface?) {
}
override fun setVideoSurface(surface: Surface?) {
}
override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
}
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
}
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
}
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
}
override fun setVideoTextureView(textureView: TextureView?) {
}
override fun clearVideoTextureView(textureView: TextureView?) {
}
override fun getVideoSize(): VideoSize {
return VideoSize.UNKNOWN
}
override fun getCurrentCues(): MutableList<Cue> {
return mutableListOf()
}
override fun getDeviceInfo(): DeviceInfo {
return DeviceInfo.UNKNOWN
}
override fun getDeviceVolume(): Int {
return 0
}
override fun isDeviceMuted(): Boolean {
return false
}
override fun setDeviceVolume(volume: Int) {
}
override fun increaseDeviceVolume() {
}
override fun decreaseDeviceVolume() {
}
override fun setDeviceMuted(muted: Boolean) {
}
inner class StatusListener() : RemoteMediaClient.Callback(), SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
val TAG = "StatusListener"
// RemoteMediaClient.ProgressListener implementation.
override fun onProgressUpdated(progressMs: Long, unusedDurationMs: Long) {
lastReportedPositionMs = progressMs
}
// RemoteMediaClient.Callback implementation.
override fun onStatusUpdated() {
Log.d(TAG, "StatusListener status queue items " + remoteMediaClient?.mediaStatus?.queueItemCount)
if (remoteMediaClient?.playerState == MediaStatus.PLAYER_STATE_IDLE) {
Log.d(TAG, "StatusListener CastPlayer STATE_IDLE")
} else if (remoteMediaClient?.playerState == MediaStatus.PLAYER_STATE_BUFFERING) {
Log.d(TAG, "StatusListener CastPlayer BUFFERING")
} else if (remoteMediaClient?.playerState == MediaStatus.PLAYER_STATE_LOADING) {
Log.d(TAG, "StatusListener CastPlayer PLAYER_STATE_LOADING")
} else if (remoteMediaClient?.playerState == MediaStatus.PLAYER_STATE_PAUSED) {
Log.d(TAG, "StatusListener CastPlayer PLAYER_STATE_PAUSED")
} else if (remoteMediaClient?.playerState == MediaStatus.PLAYER_STATE_PLAYING) {
Log.d(TAG, "StatusListener CastPlayer PLAYER_STATE_PLAYING")
} else if (remoteMediaClient?.playerState == MediaStatus.PLAYER_STATE_UNKNOWN) {
Log.d(TAG, "StatusListener CastPlayer PLAYER_STATE_UNKNOWN")
}
updateInternalStateAndNotifyIfChanged()
}
override fun onMetadataUpdated() {}
override fun onQueueStatusUpdated() {
Log.d(TAG, "onQueueStatusUpdated")
updateTimelineAndNotifyIfChanged()
listeners.flushEvents()
}
override fun onPreloadStatusUpdated() {}
override fun onSendingRemoteMediaRequest() {}
override fun onAdBreakStatusUpdated() {}
// SessionManagerListener implementation.
override fun onSessionStarted(castSession: CastSession, s: String) {
Log.d(TAG, "StatusListener onSessionStarted")
setRemoteMediaClient(castSession.remoteMediaClient)
}
override fun onSessionResumed(castSession: CastSession, b: Boolean) {
setRemoteMediaClient(castSession.remoteMediaClient)
}
override fun onSessionEnded(castSession: CastSession, i: Int) {
Log.d(TAG, "StatusListener onSessionEnded")
setRemoteMediaClient(null)
}
override fun onSessionSuspended(castSession: CastSession, i: Int) {
setRemoteMediaClient(null)
}
override fun onSessionResumeFailed(castSession: CastSession, statusCode: Int) {
Log.e(
TAG,
"Session resume failed. Error code "
+ statusCode)
}
override fun onSessionStarting(castSession: CastSession) {
Log.d(TAG, "StatusListener onSessionStarting")
// Do nothing.
}
override fun onSessionStartFailed(castSession: CastSession, statusCode: Int) {
Log.e(
TAG,
("Session start failed. Error code "
+ statusCode))
}
override fun onSessionEnding(castSession: CastSession) {
// Do nothing.
}
override fun onSessionResuming(castSession: CastSession, s: String) {
// Do nothing.
}
}
}

View file

@ -0,0 +1,162 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.audiobookshelf.app.player
import android.net.Uri
import android.util.Log
import android.util.SparseArray
import android.util.SparseIntArray
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Timeline
import java.util.*
/** A [Timeline] for Cast media queues. */ /* package */
class CastTimeline(itemIds: IntArray, itemIdToData: SparseArray<ItemData>) : Timeline() {
/** Holds [Timeline] related data for a Cast media item. */
class ItemData(
/** The duration of the item in microseconds, or [C.TIME_UNSET] if unknown. */
val durationUs: Long,
/**
* The default start position of the item in microseconds, or [C.TIME_UNSET] if unknown.
*/
var defaultPositionUs: Long, isLive: Boolean) {
/** Whether the item is live content, or `false` if unknown. */
val isLive: Boolean = isLive
/**
* Returns a copy of this instance with the given values.
*
* @param durationUs The duration in microseconds, or [C.TIME_UNSET] if unknown.
* @param defaultPositionUs The default start position in microseconds, or [C.TIME_UNSET]
* if unknown.
* @param isLive Whether the item is live, or `false` if unknown.
*/
fun copyWithNewValues(durationUs: Long, defaultPositionUs: Long, isLive: Boolean): ItemData {
return if (durationUs == this.durationUs && defaultPositionUs == this.defaultPositionUs && isLive == this.isLive) {
this
} else ItemData(durationUs, defaultPositionUs, isLive)
}
companion object {
/** Holds no media information. */
val EMPTY = ItemData( /* durationUs= */
C.TIME_UNSET, /* defaultPositionUs= */
C.TIME_UNSET, /* isLive= */
false)
}
}
private val idsToIndex: SparseIntArray
private val ids: IntArray
private val durationsUs: LongArray
private val defaultPositionsUs: LongArray
private val isLive: BooleanArray
// Timeline implementation.
override fun getWindowCount(): Int {
return ids.size
}
override fun getWindow(windowIndex: Int, window: Window, defaultPositionProjectionUs: Long): Window {
val durationUs = durationsUs[windowIndex]
val isDynamic = durationUs == C.TIME_UNSET
val mediaItem = MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build()
return window.set( /* uid= */
ids[windowIndex], /* mediaItem= */
mediaItem, /* manifest= */
null, /* presentationStartTimeMs= */
C.TIME_UNSET, /* windowStartTimeMs= */
C.TIME_UNSET, /* elapsedRealtimeEpochOffsetMs= */
C.TIME_UNSET, /* isSeekable= */
!isDynamic,
isDynamic,
if (isLive[windowIndex]) mediaItem.liveConfiguration else null,
defaultPositionsUs[windowIndex],
durationUs, /* firstPeriodIndex= */
windowIndex, /* lastPeriodIndex= */
windowIndex, /* positionInFirstPeriodUs= */
0)
}
override fun getPeriodCount(): Int {
return ids.size
}
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
val id = ids[periodIndex]
return period.set(id, id, periodIndex, durationsUs[periodIndex], 0)
}
override fun getIndexOfPeriod(uid: Any): Int {
return if (uid is Int) idsToIndex[uid, C.INDEX_UNSET] else C.INDEX_UNSET
}
override fun getUidOfPeriod(periodIndex: Int): Int {
return ids[periodIndex]
}
// equals and hashCode implementations.
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
} else if (other !is CastTimeline) {
return false
}
val that = other
return (Arrays.equals(ids, that.ids)
&& Arrays.equals(durationsUs, that.durationsUs)
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs)
&& Arrays.equals(isLive, that.isLive))
}
override fun hashCode(): Int {
var result = Arrays.hashCode(ids)
result = 31 * result + Arrays.hashCode(durationsUs)
result = 31 * result + Arrays.hashCode(defaultPositionsUs)
result = 31 * result + Arrays.hashCode(isLive)
return result
}
companion object {
/** [Timeline] for a cast queue that has no items. */
val EMPTY_CAST_TIMELINE = CastTimeline(IntArray(0), SparseArray())
}
/**
* Creates a Cast timeline from the given data.
*
* @param itemIds The ids of the items in the timeline.
* @param itemIdToData Maps item ids to [ItemData].
*/
init {
val itemCount = itemIds.size
idsToIndex = SparseIntArray(itemCount)
ids = itemIds.copyOf(itemCount)
durationsUs = LongArray(itemCount)
defaultPositionsUs = LongArray(itemCount)
isLive = BooleanArray(itemCount)
for (i in ids.indices) {
val id = ids[i]
idsToIndex.put(id, i)
val data = itemIdToData[id, ItemData.EMPTY]
durationsUs[i] = data.durationUs
defaultPositionsUs[i] = if (data.defaultPositionUs == C.TIME_UNSET) 0 else data.defaultPositionUs
isLive[i] = data.isLive
}
}
}

View file

@ -0,0 +1,109 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.audiobookshelf.app.player
import android.util.SparseArray
import com.google.android.exoplayer2.C
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.framework.media.RemoteMediaClient
/**
* Creates [CastTimelines][CastTimeline] from cast receiver app status updates.
*
*
* This class keeps track of the duration reported by the current item to fill any missing
* durations in the media queue items [See internal: b/65152553].
*/
/* package */
class CastTimelineTracker {
private val itemIdToData: SparseArray<CastTimeline.ItemData>
/**
* Returns a [CastTimeline] that represents the state of the given `remoteMediaClient`.
*
*
* Returned timelines may contain values obtained from `remoteMediaClient` in previous
* invocations of this method.
*
* @param remoteMediaClient The Cast media client.
* @return A [CastTimeline] that represents the given `remoteMediaClient` status.
*/
fun getCastTimeline(remoteMediaClient: RemoteMediaClient): CastTimeline {
val itemIds = remoteMediaClient.mediaQueue.itemIds
if (itemIds.size > 0) {
// Only remove unused items when there is something in the queue to avoid removing all entries
// if the remote media client clears the queue temporarily. See [Internal ref: b/128825216].
removeUnusedItemDataEntries(itemIds)
}
// TODO: Reset state when the app instance changes [Internal ref: b/129672468].
val mediaStatus = remoteMediaClient.mediaStatus
?: return CastTimeline.EMPTY_CAST_TIMELINE
val currentItemId = mediaStatus.currentItemId
updateItemData(
currentItemId, mediaStatus.mediaInfo, /* defaultPositionUs= */C.TIME_UNSET)
for (item in mediaStatus.queueItems) {
val defaultPositionUs = (item.startTime * C.MICROS_PER_SECOND).toLong()
updateItemData(item.itemId, item.media, defaultPositionUs)
}
return CastTimeline(itemIds, itemIdToData)
}
private fun updateItemData(itemId: Int, mediaInfo: MediaInfo?, defaultPositionUs: Long) {
var defaultPositionUs = defaultPositionUs
val previousData = itemIdToData[itemId, CastTimeline.ItemData.EMPTY]
var durationUs = getStreamDurationUs(mediaInfo)
if (durationUs == C.TIME_UNSET) {
durationUs = previousData.durationUs
}
val isLive = if (mediaInfo == null) previousData.isLive else mediaInfo.streamType == MediaInfo.STREAM_TYPE_LIVE
if (defaultPositionUs == C.TIME_UNSET) {
defaultPositionUs = previousData.defaultPositionUs
}
itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive))
}
private fun getStreamDurationUs(mediaInfo: MediaInfo?): Long {
if (mediaInfo == null) {
return C.TIME_UNSET
}
val durationMs = mediaInfo.streamDuration
return if (durationMs != MediaInfo.UNKNOWN_DURATION) msToUs(durationMs) else C.TIME_UNSET
}
fun msToUs(timeMs: Long): Long {
return if (timeMs == C.TIME_UNSET || timeMs == C.TIME_END_OF_SOURCE) timeMs else timeMs * 1000
}
private fun removeUnusedItemDataEntries(itemIds: IntArray) {
val scratchItemIds = HashSet<Int>( /* initialCapacity= */itemIds.size * 2)
for (id in itemIds) {
scratchItemIds.add(id)
}
var index = 0
while (index < itemIdToData.size()) {
if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
itemIdToData.removeAt(index)
} else {
index++
}
}
}
init {
itemIdToData = SparseArray()
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.audiobookshelf.app.player
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.source.TrackGroup
import com.google.android.exoplayer2.trackselection.TrackSelection
import com.google.android.exoplayer2.util.Assertions
/**
* [TrackSelection] that only selects the first track of the provided [TrackGroup].
*
*
* This relies on [CastPlayer] track groups only having one track.
*/
/* package */
internal class CastTrackSelection
/** @param trackGroup The [TrackGroup] from which the first track will only be selected.
*/(private val trackGroup: TrackGroup) : TrackSelection {
override fun getType(): Int {
return TrackSelection.TYPE_UNSET
}
override fun getTrackGroup(): TrackGroup {
return trackGroup
}
override fun length(): Int {
return 1
}
override fun getFormat(index: Int): Format {
Assertions.checkArgument(index == 0)
return trackGroup.getFormat(0)
}
override fun getIndexInTrackGroup(index: Int): Int {
return if (index == 0) 0 else C.INDEX_UNSET
}
override fun indexOf(format: Format): Int {
return if (format === trackGroup.getFormat(0)) 0 else C.INDEX_UNSET
}
override fun indexOf(indexInTrackGroup: Int): Int {
return if (indexInTrackGroup == 0) 0 else C.INDEX_UNSET
}
// Object overrides.
override fun hashCode(): Int {
return System.identityHashCode(trackGroup)
}
// Track groups are compared by identity not value, as distinct groups may have the same value.
override fun equals(obj: Any?): Boolean {
if (this === obj) {
return true
}
if (obj == null || javaClass != obj.javaClass) {
return false
}
val other = obj as CastTrackSelection
return trackGroup === other.trackGroup
}
}

View file

@ -26,7 +26,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
override fun onPrepare() { override fun onPrepare() {
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT") Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
playerNotificationService.mediaManager.getFirstItem()?.let { li -> playerNotificationService.mediaManager.getFirstItem()?.let { li ->
playerNotificationService.mediaManager.play(li) { playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with li ${li.title}") Log.d(tag, "About to prepare player with li ${li.title}")
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true) playerNotificationService.preparePlayer(it,true)
@ -48,7 +48,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
override fun onPlayFromSearch(query: String?, extras: Bundle?) { override fun onPlayFromSearch(query: String?, extras: Bundle?) {
Log.d(tag, "ON PLAY FROM SEARCH $query") Log.d(tag, "ON PLAY FROM SEARCH $query")
playerNotificationService.mediaManager.getFromSearch(query)?.let { li -> playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
playerNotificationService.mediaManager.play(li) { playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with li ${li.title}") Log.d(tag, "About to prepare player with li ${li.title}")
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true) playerNotificationService.preparePlayer(it,true)
@ -96,7 +96,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
} }
libraryItem?.let { li -> libraryItem?.let { li ->
playerNotificationService.mediaManager.play(li) { playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with li ${li.title}") Log.d(tag, "About to prepare player with li ${li.title}")
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true) playerNotificationService.preparePlayer(it,true)

View file

@ -8,14 +8,13 @@ import android.os.ResultReceiver
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log import android.util.Log
import com.audiobookshelf.app.data.LibraryItem import com.audiobookshelf.app.data.LibraryItem
import com.google.android.exoplayer2.ControlDispatcher
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificationService) : MediaSessionConnector.PlaybackPreparer { class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificationService) : MediaSessionConnector.PlaybackPreparer {
var tag = "MediaSessionPlaybackPreparer" var tag = "MediaSessionPlaybackPreparer"
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean { override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
Log.d(tag, "ON COMMAND $command") Log.d(tag, "ON COMMAND $command")
return false return false
} }
@ -30,7 +29,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
override fun onPrepare(playWhenReady: Boolean) { override fun onPrepare(playWhenReady: Boolean) {
Log.d(tag, "ON PREPARE $playWhenReady") Log.d(tag, "ON PREPARE $playWhenReady")
playerNotificationService.mediaManager.getFirstItem()?.let { li -> playerNotificationService.mediaManager.getFirstItem()?.let { li ->
playerNotificationService.mediaManager.play(li) { playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady) playerNotificationService.preparePlayer(it,playWhenReady)
} }
@ -43,7 +42,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
var libraryItem: LibraryItem? = playerNotificationService.mediaManager.getById(mediaId) var libraryItem: LibraryItem? = playerNotificationService.mediaManager.getById(mediaId)
libraryItem?.let { li -> libraryItem?.let { li ->
playerNotificationService.mediaManager.play(li) { playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with li ${li.title}") Log.d(tag, "About to prepare player with li ${li.title}")
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady) playerNotificationService.preparePlayer(it,playWhenReady)
@ -55,7 +54,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) { override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM SEARCH $query") Log.d(tag, "ON PREPARE FROM SEARCH $query")
playerNotificationService.mediaManager.getFromSearch(query)?.let { li -> playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
playerNotificationService.mediaManager.play(li) { playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with li ${li.title}") Log.d(tag, "About to prepare player with li ${li.title}")
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady) playerNotificationService.preparePlayer(it,playWhenReady)

View file

@ -21,17 +21,21 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
} }
override fun onEvents(player: Player, events: Player.Events) { override fun onEvents(player: Player, events: Player.Events) {
Log.d(tag, "onEvents ${player.deviceInfo} | ${playerNotificationService.getMediaPlayer()} | ${events.size()}")
if (events.contains(Player.EVENT_POSITION_DISCONTINUITY)) { if (events.contains(Player.EVENT_POSITION_DISCONTINUITY)) {
Log.d(tag, "EVENT_POSITION_DISCONTINUITY") Log.d(tag, "EVENT_POSITION_DISCONTINUITY")
} }
if (events.contains(Player.EVENT_IS_LOADING_CHANGED)) { if (events.contains(Player.EVENT_IS_LOADING_CHANGED)) {
Log.d(tag, "EVENT_IS_LOADING_CHANGED : " + playerNotificationService.mPlayer.isLoading.toString()) Log.d(tag, "EVENT_IS_LOADING_CHANGED : " + playerNotificationService.currentPlayer.isLoading)
} }
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) { if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
Log.d(tag, "EVENT_PLAYBACK_STATE_CHANGED MediaPlayer = ${playerNotificationService.getMediaPlayer()}")
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_READY) { if (playerNotificationService.currentPlayer.playbackState == Player.STATE_READY) {
Log.d(tag, "STATE_READY : " + playerNotificationService.mPlayer.duration.toString()) Log.d(tag, "STATE_READY : " + playerNotificationService.currentPlayer.duration)
if (lastPauseTime == 0L) { if (lastPauseTime == 0L) {
lastPauseTime = -1; lastPauseTime = -1;
@ -39,7 +43,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
playerNotificationService.sendClientMetadata(PlayerState.READY) playerNotificationService.sendClientMetadata(PlayerState.READY)
} }
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_BUFFERING) { if (playerNotificationService.currentPlayer.playbackState == Player.STATE_BUFFERING) {
Log.d(tag, "STATE_BUFFERING : " + playerNotificationService.mPlayer.currentPosition.toString()) Log.d(tag, "STATE_BUFFERING : " + playerNotificationService.currentPlayer.currentPosition)
playerNotificationService.sendClientMetadata(PlayerState.BUFFERING) playerNotificationService.sendClientMetadata(PlayerState.BUFFERING)
} }
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) { if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
@ -53,13 +57,13 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
} }
if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) { if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) {
Log.d(tag, "EVENT_MEDIA_METADATA_CHANGED") Log.d(tag, "EVENT_MEDIA_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
} }
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) { if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED") Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
} }
if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) { if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
Log.d(tag, "EVENT IS PLAYING CHANGED") Log.d(tag, "EVENT IS PLAYING CHANGED ${playerNotificationService.getMediaPlayer()}")
if (player.isPlaying) { if (player.isPlaying) {
if (lastPauseTime > 0) { if (lastPauseTime > 0) {

View file

@ -22,19 +22,15 @@ import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaManager import com.audiobookshelf.app.media.MediaManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
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
import com.google.android.exoplayer2.ext.cast.CastPlayer
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.ui.PlayerControlView
import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.upstream.* import com.google.android.exoplayer2.upstream.*
import okhttp3.OkHttpClient
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
@ -56,6 +52,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun onSleepTimerSet(sleepTimeRemaining: Int) fun onSleepTimerSet(sleepTimeRemaining: Int)
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
fun onPlaybackFailed(errorMessage:String) fun onPlaybackFailed(errorMessage:String)
fun onMediaPlayerChanged(mediaPlayer:String)
} }
private val tag = "PlayerService" private val tag = "PlayerService"
@ -68,6 +65,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
lateinit var mediaManager: MediaManager lateinit var mediaManager: MediaManager
lateinit var apiHandler: ApiHandler lateinit var apiHandler: ApiHandler
@ -76,7 +74,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
var castPlayer:CastPlayer? = null var castPlayer:CastPlayer? = null
lateinit var sleepTimerManager:SleepTimerManager lateinit var sleepTimerManager:SleepTimerManager
lateinit var castManager:CastManager
lateinit var mediaProgressSyncer:MediaProgressSyncer lateinit var mediaProgressSyncer:MediaProgressSyncer
private var notificationId = 10; private var notificationId = 10;
@ -144,6 +141,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
override fun onDestroy() { override fun onDestroy() {
playerNotificationManager.setPlayer(null) playerNotificationManager.setPlayer(null)
mPlayer.release() mPlayer.release()
castPlayer?.release()
mediaSession.release() mediaSession.release()
mediaProgressSyncer.reset() mediaProgressSyncer.reset()
Log.d(tag, "onDestroy") Log.d(tag, "onDestroy")
@ -190,9 +188,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// Initialize sleep timer // Initialize sleep timer
sleepTimerManager = SleepTimerManager(this) sleepTimerManager = SleepTimerManager(this)
// Initialize Cast Manager
castManager = CastManager(this)
// Initialize Media Progress Syncer // Initialize Media Progress Syncer
mediaProgressSyncer = MediaProgressSyncer(this, apiHandler) mediaProgressSyncer = MediaProgressSyncer(this, apiHandler)
@ -282,7 +277,23 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
User callable methods User callable methods
*/ */
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean) { fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean) {
playbackSession.mediaPlayer = getMediaPlayer()
if (playbackSession.mediaPlayer == "cast-player" && playbackSession.isLocal) {
Log.w(tag, "Cannot cast local media item - switching player")
currentPlaybackSession = null
switchToPlayer(false)
playbackSession.mediaPlayer = getMediaPlayer()
}
if (playbackSession.mediaPlayer == "cast-player") {
// If cast-player is the first player to be used
mediaSessionConnector.setPlayer(castPlayer)
playerNotificationManager.setPlayer(castPlayer)
}
currentPlaybackSession = playbackSession currentPlaybackSession = playbackSession
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
clientEventEmitter?.onPlaybackSession(playbackSession) clientEventEmitter?.onPlaybackSession(playbackSession)
@ -309,10 +320,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
mPlayer.setMediaSource(mediaSource) mPlayer.setMediaSource(mediaSource)
} else if (castPlayer != null) {
castPlayer?.addMediaItem(mediaItems[0]) // TODO: Media items never actually get added, not sure what is going on....
Log.d(tag, "Cast Player ADDED MEDIA ITEM ${castPlayer?.currentMediaItem} | ${castPlayer?.duration} | ${castPlayer?.mediaItemCount}")
}
// Add remaining media items if multi-track // Add remaining media items if multi-track
if (mediaItems.size > 1) { if (mediaItems.size > 1) {
@ -331,17 +338,27 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
currentPlayer.playWhenReady = playWhenReady currentPlayer.playWhenReady = playWhenReady
currentPlayer.setPlaybackSpeed(1f) // TODO: Playback speed should come from settings currentPlayer.setPlaybackSpeed(1f) // TODO: Playback speed should come from settings
currentPlayer.prepare() currentPlayer.prepare()
} else if (castPlayer != null) {
var currentTrackIndex = playbackSession.getCurrentTrackIndex()
var currentTrackTime = playbackSession.getCurrentTrackTimeMs()
var mediaType = playbackSession.mediaType
Log.d(tag, "Loading cast player $currentTrackIndex $currentTrackTime $mediaType")
castPlayer?.load(mediaItems, currentTrackIndex, currentTrackTime, playWhenReady, 1f, mediaType)
}
} }
fun handlePlayerPlaybackError(errorMessage:String) { fun handlePlayerPlaybackError(errorMessage:String) {
// On error and was attempting to direct play - fallback to transcode // On error and was attempting to direct play - fallback to transcode
currentPlaybackSession?.let { playbackSession -> currentPlaybackSession?.let { playbackSession ->
if (playbackSession.isDirectPlay) { if (playbackSession.isDirectPlay) {
Log.d(tag, "Fallback to transcode") var mediaPlayer = getMediaPlayer()
Log.d(tag, "Fallback to transcode $mediaPlayer")
var libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play var libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
var episodeId = playbackSession.episodeId var episodeId = playbackSession.episodeId
apiHandler.playLibraryItem(libraryItemId, episodeId, true) { apiHandler.playLibraryItem(libraryItemId, episodeId, true, mediaPlayer) {
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
preparePlayer(it, true) preparePlayer(it, true)
} }
@ -354,17 +371,48 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
fun switchToPlayer(useCastPlayer: Boolean) { fun switchToPlayer(useCastPlayer: Boolean) {
var wasPlaying = currentPlayer.isPlaying
if (useCastPlayer) {
if (currentPlayer == castPlayer) {
Log.d(tag, "switchToPlayer: Already using Cast Player " + castPlayer?.deviceInfo)
return
} else {
Log.d(tag, "switchToPlayer: Switching to cast player from exo player stop exo player")
mPlayer.stop()
}
} else {
if (currentPlayer == mPlayer) {
Log.d(tag, "switchToPlayer: Already using Exo Player " + mPlayer.deviceInfo)
return
} else if (castPlayer != null) {
Log.d(tag, "switchToPlayer: Switching to exo player from cast player stop cast player")
castPlayer?.stop()
}
}
currentPlayer = if (useCastPlayer) { currentPlayer = if (useCastPlayer) {
Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo) Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo)
mediaSessionConnector.setPlayer(castPlayer) mediaSessionConnector.setPlayer(castPlayer)
playerNotificationManager.setPlayer(castPlayer)
castPlayer as CastPlayer castPlayer as CastPlayer
} else { } else {
Log.d(tag, "switchToPlayer: Using ExoPlayer") Log.d(tag, "switchToPlayer: Using ExoPlayer")
mediaSessionConnector.setPlayer(mPlayer) mediaSessionConnector.setPlayer(mPlayer)
playerNotificationManager.setPlayer(mPlayer)
mPlayer mPlayer
} }
clientEventEmitter?.onMediaPlayerChanged(getMediaPlayer())
if (currentPlaybackSession == null) {
Log.d(tag, "switchToPlayer: No Current playback session")
}
currentPlaybackSession?.let { currentPlaybackSession?.let {
Log.d(tag, "switchToPlayer: Preparing current playback session ${it.displayTitle}") Log.d(tag, "switchToPlayer: Preparing current playback session ${it.displayTitle}")
if (wasPlaying) { // media is paused when switching players
clientEventEmitter?.onPlayingUpdate(false)
}
preparePlayer(it, false) preparePlayer(it, false)
} }
} }
@ -441,6 +489,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
fun seekPlayer(time: Long) { fun seekPlayer(time: Long) {
Log.d(tag, "seekPlayer mediaCount = ${currentPlayer.mediaItemCount} | $time")
if (currentPlayer.mediaItemCount > 1) { if (currentPlayer.mediaItemCount > 1) {
currentPlaybackSession?.currentTime = time / 1000.0 currentPlaybackSession?.currentTime = time / 1000.0
var newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0 var newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
@ -452,11 +501,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
fun seekForward(amount: Long) { fun seekForward(amount: Long) {
currentPlayer.seekTo(mPlayer.currentPosition + amount) currentPlayer.seekTo(currentPlayer.currentPosition + amount)
} }
fun seekBackward(amount: Long) { fun seekBackward(amount: Long) {
currentPlayer.seekTo(mPlayer.currentPosition - amount) currentPlayer.seekTo(currentPlayer.currentPosition - amount)
} }
fun setPlaybackSpeed(speed: Float) { fun setPlaybackSpeed(speed: Float) {
@ -465,6 +514,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun closePlayback() { fun closePlayback() {
currentPlayer.clearMediaItems() currentPlayer.clearMediaItems()
currentPlayer.stop()
currentPlaybackSession = null currentPlaybackSession = null
clientEventEmitter?.onPlaybackClosed() clientEventEmitter?.onPlaybackClosed()
PlayerListener.lastPauseTime = 0 PlayerListener.lastPauseTime = 0
@ -475,6 +525,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
clientEventEmitter?.onMetadata(PlaybackMetadata(duration, getCurrentTimeSeconds(), playerState)) clientEventEmitter?.onMetadata(PlaybackMetadata(duration, getCurrentTimeSeconds(), playerState))
} }
fun getMediaPlayer():String {
return if(currentPlayer == castPlayer) "cast-player" else "exo-player"
}
// //
// MEDIA BROWSER STUFF (ANDROID AUTO) // MEDIA BROWSER STUFF (ANDROID AUTO)
// //

View file

@ -15,6 +15,7 @@ import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.* import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin import com.getcapacitor.annotation.CapacitorPlugin
import com.google.android.gms.cast.CastDevice
import org.json.JSONObject import org.json.JSONObject
@CapacitorPlugin(name = "AbsAudioPlayer") @CapacitorPlugin(name = "AbsAudioPlayer")
@ -24,12 +25,18 @@ class AbsAudioPlayer : Plugin() {
lateinit var mainActivity: MainActivity lateinit var mainActivity: MainActivity
lateinit var apiHandler:ApiHandler lateinit var apiHandler:ApiHandler
lateinit var castManager:CastManager
lateinit var playerNotificationService: PlayerNotificationService lateinit var playerNotificationService: PlayerNotificationService
private var isCastAvailable:Boolean = false
override fun load() { override fun load() {
mainActivity = (activity as MainActivity) mainActivity = (activity as MainActivity)
apiHandler = ApiHandler(mainActivity) apiHandler = ApiHandler(mainActivity)
initCastManager()
var foregroundServiceReady : () -> Unit = { var foregroundServiceReady : () -> Unit = {
playerNotificationService = mainActivity.foregroundService playerNotificationService = mainActivity.foregroundService
@ -72,17 +79,58 @@ class AbsAudioPlayer : Plugin() {
override fun onPlaybackFailed(errorMessage: String) { override fun onPlaybackFailed(errorMessage: String) {
emit("onPlaybackFailed", errorMessage) emit("onPlaybackFailed", errorMessage)
} }
override fun onMediaPlayerChanged(mediaPlayer:String) {
emit("onMediaPlayerChanged", mediaPlayer)
}
}) })
} }
mainActivity.pluginCallback = foregroundServiceReady mainActivity.pluginCallback = foregroundServiceReady
} }
fun emit(evtName: String, value: Any) { fun emit(evtName: String, value: Any) {
var ret:JSObject = JSObject() var ret = JSObject()
ret.put("value", value) ret.put("value", value)
notifyListeners(evtName, ret) notifyListeners(evtName, ret)
} }
fun initCastManager() {
var connListener = object: CastManager.ChromecastListener() {
override fun onReceiverAvailableUpdate(available: Boolean) {
Log.d(tag, "ChromecastListener: CAST Receiver Update Available $available")
isCastAvailable = available
emit("onCastAvailableUpdate", available)
}
override fun onSessionRejoin(jsonSession: JSONObject?) {
Log.d(tag, "ChromecastListener: CAST onSessionRejoin")
}
override fun onMediaLoaded(jsonMedia: JSONObject?) {
Log.d(tag, "ChromecastListener: CAST onMediaLoaded")
}
override fun onMediaUpdate(jsonMedia: JSONObject?) {
Log.d(tag, "ChromecastListener: CAST onMediaUpdate")
}
override fun onSessionUpdate(jsonSession: JSONObject?) {
Log.d(tag, "ChromecastListener: CAST onSessionUpdate")
}
override fun onSessionEnd(jsonSession: JSONObject?) {
Log.d(tag, "ChromecastListener: CAST onSessionEnd")
}
override fun onMessageReceived(p0: CastDevice, p1: String, p2: String) {
Log.d(tag, "ChromecastListener: CAST onMessageReceived")
}
}
castManager = CastManager(mainActivity)
castManager.startRouteScan(connListener)
}
@PluginMethod @PluginMethod
fun prepareLibraryItem(call: PluginCall) { fun prepareLibraryItem(call: PluginCall) {
// Need to make sure the player service has been started // Need to make sure the player service has been started
@ -122,7 +170,9 @@ class AbsAudioPlayer : Plugin() {
return call.resolve(JSObject()) return call.resolve(JSObject())
} }
} else { // Play library item from server } else { // Play library item from server
apiHandler.playLibraryItem(libraryItemId, episodeId, false) { var mediaPlayer = playerNotificationService.getMediaPlayer()
apiHandler.playLibraryItem(libraryItemId, episodeId, false, mediaPlayer) {
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}") Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}")
@ -268,9 +318,10 @@ class AbsAudioPlayer : Plugin() {
@PluginMethod @PluginMethod
fun requestSession(call: PluginCall) { fun requestSession(call: PluginCall) {
// Need to make sure the player service has been started
Log.d(tag, "CAST REQUEST SESSION PLUGIN") Log.d(tag, "CAST REQUEST SESSION PLUGIN")
call.resolve() call.resolve()
playerNotificationService.castManager.requestSession(mainActivity, object : CastManager.RequestSessionCallback() { castManager.requestSession(playerNotificationService, object : CastManager.RequestSessionCallback() {
override fun onError(errorCode: Int) { override fun onError(errorCode: Int) {
Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode") Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode")
} }
@ -284,4 +335,11 @@ class AbsAudioPlayer : Plugin() {
} }
}) })
} }
@PluginMethod
fun getIsCastAvailable(call: PluginCall) {
var jsobj = JSObject()
jsobj.put("value", isCastAvailable)
call.resolve(jsobj)
}
} }

View file

@ -151,9 +151,9 @@ class ApiHandler {
} }
} }
fun playLibraryItem(libraryItemId:String, episodeId:String?, forceTranscode:Boolean, cb: (PlaybackSession) -> Unit) { fun playLibraryItem(libraryItemId:String, episodeId:String?, forceTranscode:Boolean, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
var payload = JSObject() var payload = JSObject()
payload.put("mediaPlayer", "exo-player") payload.put("mediaPlayer", mediaPlayer)
// Only if direct play fails do we force transcode // Only if direct play fails do we force transcode
if (!forceTranscode) payload.put("forceDirectPlay", true) if (!forceTranscode) payload.put("forceDirectPlay", true)

View file

@ -5,11 +5,12 @@ buildscript {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.0.1' // classpath 'com.android.tools.build:gradle:4.0.2'
classpath 'com.google.gms:google-services:4.3.5' classpath 'com.android.tools.build:gradle:7.3.0-alpha08'
classpath 'com.google.gms:google-services:4.3.10'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
@ -22,7 +23,7 @@ apply from: "variables.gradle"
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
} }

View file

@ -9,7 +9,15 @@
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m #org.gradle.jvmargs=-Xmx1536m
org.gradle.jvmargs=-Dfile.encoding=UTF-8 \
--add-opens jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
--add-opens jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-opens jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
--add-opens jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
--add-opens jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \
--add-opens jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
@ -22,3 +30,5 @@ org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true android.enableJetifier=true
kapt.use.worker.api=false

View file

@ -1,5 +1,6 @@
#Sun Apr 17 13:28:55 CDT 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View file

@ -1,34 +1,34 @@
ext { ext {
minSdkVersion = 24 minSdkVersion = 24
compileSdkVersion = 30 compileSdkVersion = 31
targetSdkVersion = 30 targetSdkVersion = 30
androidxActivityVersion = '1.2.0' androidxActivityVersion = '1.2.0'
androidxAppCompatVersion = '1.2.0' androidxAppCompatVersion = '1.4.1'
androidxCoordinatorLayoutVersion = '1.1.0' androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.6.0' androidxCoreVersion = '1.6.0'
androidPlayCore = '1.9.0' androidPlayCore = '1.9.0'
androidxFragmentVersion = '1.3.0' androidxFragmentVersion = '1.3.0'
junitVersion = '4.13.1' junitVersion = '4.13.1'
androidxJunitVersion = '1.1.2' androidxJunitVersion = '1.1.2'
androidxEspressoCoreVersion = '3.3.0' androidxEspressoCoreVersion = '3.3.0'
cordovaAndroidVersion = '7.0.0' cordovaAndroidVersion = '10.1.1'
androidx_app_compat_version = '1.2.0' androidx_app_compat_version = '1.2.0'
androidx_car_version = '1.0.0-alpha7' androidx_car_version = '1.0.0-alpha7'
androidx_core_ktx_version = '1.6.0' androidx_core_ktx_version = '1.7.0'
androidx_media_version = '1.0.1' androidx_media_version = '1.5.0'
androidx_preference_version = '1.1.1' androidx_preference_version = '1.1.1'
androidx_test_runner_version = '1.3.0' androidx_test_runner_version = '1.3.0'
arch_lifecycle_version = '2.2.0' arch_lifecycle_version = '2.2.0'
constraint_layout_version = '2.0.1' constraint_layout_version = '2.0.1'
espresso_version = '3.3.0' espresso_version = '3.3.0'
exoplayer_version = '2.15.0' exoplayer_version = '2.17.0'
fragment_version = '1.2.5' fragment_version = '1.2.5'
glide_version = '4.11.0' glide_version = '4.11.0'
gms_strict_version_matcher_version = '1.0.3' gms_strict_version_matcher_version = '1.0.3'
gradle_version = '3.1.4' gradle_version = '3.1.4'
gson_version = '2.8.5' gson_version = '2.8.5'
junit_version = '4.13' junit_version = '4.13'
kotlin_version = '1.4.32' kotlin_version = '1.5.30'
kotlin_coroutines_version = '1.1.0' kotlin_coroutines_version = '1.1.0'
multidex_version = '1.0.3' multidex_version = '1.0.3'
play_services_auth_version = '18.1.0' play_services_auth_version = '18.1.0'

View file

@ -17,6 +17,10 @@
<widgets-download-progress-indicator /> <widgets-download-progress-indicator />
<div v-show="isCastAvailable" class="mx-2 cursor-pointer">
<span class="material-icons" :class="isCasting ? 'text-success' : ''" style="font-size: 1.75rem" @click="castClick">cast</span>
</div>
<nuxt-link v-if="user" class="h-7 mx-2" to="/search"> <nuxt-link v-if="user" class="h-7 mx-2" to="/search">
<span class="material-icons" style="font-size: 1.75rem">search</span> <span class="material-icons" style="font-size: 1.75rem">search</span>
</nuxt-link> </nuxt-link>
@ -29,9 +33,14 @@
</template> </template>
<script> <script>
import { AbsAudioPlayer } from '@/plugins/capacitor'
export default { export default {
data() { data() {
return {} return {
onCastAvailableUpdateListener: null,
isCastAvailable: false
}
}, },
computed: { computed: {
socketConnected() { socketConnected() {
@ -55,9 +64,21 @@ export default {
}, },
username() { username() {
return this.user ? this.user.username : 'err' return this.user ? this.user.username : 'err'
},
isCasting() {
return this.$store.state.isCasting
} }
}, },
methods: { methods: {
castClick() {
if (this.$store.state.playerIsLocal) {
this.$toast.warn('Cannot cast downloaded media item')
return
}
console.log('Cast Btn Click')
AbsAudioPlayer.requestSession()
},
clickShowSideDrawer() { clickShowSideDrawer() {
this.$store.commit('setShowSideDrawer', true) this.$store.commit('setShowSideDrawer', true)
}, },
@ -66,9 +87,20 @@ export default {
}, },
back() { back() {
window.history.back() window.history.back()
},
onCastAvailableUpdate(data) {
this.isCastAvailable = data && data.value
} }
}, },
mounted() {} mounted() {
AbsAudioPlayer.getIsCastAvailable().then((data) => {
this.isCastAvailable = data && data.value
})
this.onCastAvailableUpdateListener = AbsAudioPlayer.addListener('onCastAvailableUpdate', this.onCastAvailableUpdate)
},
beforeDestroy() {
if (this.onCastAvailableUpdateListener) this.onCastAvailableUpdateListener.remove()
}
} }
</script> </script>

View file

@ -5,7 +5,7 @@
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span> <span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
</div> </div>
<div v-show="showCastBtn" class="top-3.5 right-20 absolute cursor-pointer"> <div v-show="showCastBtn" class="top-3.5 right-20 absolute cursor-pointer">
<span class="material-icons text-3xl" @click="castClick">cast</span> <span class="material-icons text-3xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
</div> </div>
<div class="top-4 right-4 absolute cursor-pointer"> <div class="top-4 right-4 absolute cursor-pointer">
<ui-dropdown-menu ref="dropdownMenu" :items="menuItems" @action="clickMenuAction"> <ui-dropdown-menu ref="dropdownMenu" :items="menuItems" @action="clickMenuAction">
@ -109,7 +109,7 @@ export default {
return { return {
playbackSession: null, playbackSession: null,
showChapterModal: false, showChapterModal: false,
showCastBtn: false, showCastBtn: true,
showFullscreen: false, showFullscreen: false,
totalDuration: 0, totalDuration: 0,
currentPlaybackRate: 1, currentPlaybackRate: 1,
@ -159,6 +159,12 @@ export default {
} }
return this.showFullscreen ? 200 : 60 return this.showFullscreen ? 200 : 60
}, },
isCasting() {
return this.mediaPlayer === 'cast-player'
},
mediaPlayer() {
return this.playbackSession ? this.playbackSession.mediaPlayer : null
},
mediaType() { mediaType() {
return this.playbackSession ? this.playbackSession.mediaType : null return this.playbackSession ? this.playbackSession.mediaType : null
}, },
@ -262,6 +268,11 @@ export default {
}, },
castClick() { castClick() {
console.log('Cast Btn Click') console.log('Cast Btn Click')
if (this.isLocalPlayMethod) {
this.$toast.warn('Cannot cast downloaded media items')
return
}
AbsAudioPlayer.requestSession() AbsAudioPlayer.requestSession()
}, },
clickContainer() { clickContainer() {

View file

@ -10,6 +10,7 @@
<script> <script>
import { AbsAudioPlayer } from '@/plugins/capacitor' import { AbsAudioPlayer } from '@/plugins/capacitor'
import { Dialog } from '@capacitor/dialog'
export default { export default {
data() { data() {
@ -28,6 +29,7 @@ export default {
onLocalMediaProgressUpdateListener: null, onLocalMediaProgressUpdateListener: null,
onSleepTimerEndedListener: null, onSleepTimerEndedListener: null,
onSleepTimerSetListener: null, onSleepTimerSetListener: null,
onMediaPlayerChangedListener: null,
sleepInterval: null, sleepInterval: null,
currentEndOfChapterTime: 0 currentEndOfChapterTime: 0
} }
@ -169,6 +171,16 @@ export default {
var libraryItemId = payload.libraryItemId var libraryItemId = payload.libraryItemId
var episodeId = payload.episodeId var episodeId = payload.episodeId
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
const { value } = await Dialog.confirm({
title: 'Warning',
message: `Cannot cast downloaded media items. Confirm to close cast and play on your device.`
})
if (!value) {
return
}
}
console.log('Called playLibraryItem', libraryItemId) console.log('Called playLibraryItem', libraryItemId)
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true }) AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true })
.then((data) => { .then((data) => {
@ -186,12 +198,17 @@ export default {
onLocalMediaProgressUpdate(localMediaProgress) { onLocalMediaProgressUpdate(localMediaProgress) {
console.log('Got local media progress update', localMediaProgress.progress, JSON.stringify(localMediaProgress)) console.log('Got local media progress update', localMediaProgress.progress, JSON.stringify(localMediaProgress))
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress) this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
},
onMediaPlayerChanged(data) {
var mediaPlayer = data.value
this.$store.commit('setMediaPlayer', mediaPlayer)
} }
}, },
mounted() { mounted() {
this.onLocalMediaProgressUpdateListener = AbsAudioPlayer.addListener('onLocalMediaProgressUpdate', this.onLocalMediaProgressUpdate) this.onLocalMediaProgressUpdateListener = AbsAudioPlayer.addListener('onLocalMediaProgressUpdate', this.onLocalMediaProgressUpdate)
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded) this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet) this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate') this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`) console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
@ -206,6 +223,7 @@ export default {
if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove() if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove()
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove() if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove() if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
// if (this.$server.socket) { // if (this.$server.socket) {
// this.$server.socket.off('stream_open', this.streamOpen) // this.$server.socket.off('stream_open', this.streamOpen)

View file

@ -14,7 +14,7 @@
<ui-btn class="w-full" @click="newServerConfigClick">Add New Server</ui-btn> <ui-btn class="w-full" @click="newServerConfigClick">Add New Server</ui-btn>
</div> </div>
</template> </template>
<template v-else> <div v-else class="w-full">
<form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full"> <form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full">
<h2 class="text-lg leading-7 mb-2">Server address</h2> <h2 class="text-lg leading-7 mb-2">Server address</h2>
<ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full sm:w-72 h-10" /> <ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full sm:w-72 h-10" />
@ -44,12 +44,12 @@
</div> </div>
</form> </form>
</template> </template>
</div>
<div v-show="error" class="w-full rounded-lg bg-red-600 bg-opacity-10 border border-error border-opacity-50 py-3 px-2 flex items-center mt-4"> <div v-show="error" class="w-full rounded-lg bg-red-600 bg-opacity-10 border border-error border-opacity-50 py-3 px-2 flex items-center mt-4">
<span class="material-icons mr-2 text-error" style="font-size: 1.1rem">warning</span> <span class="material-icons mr-2 text-error" style="font-size: 1.1rem">warning</span>
<p class="text-error">{{ error }}</p> <p class="text-error">{{ error }}</p>
</div> </div>
</template>
</div> </div>
<div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black bg-opacity-75 flex items-center justify-center z-30 transition-opacity duration-500"> <div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black bg-opacity-75 flex items-center justify-center z-30 transition-opacity duration-500">
@ -73,22 +73,6 @@ export default {
data() { data() {
return { return {
deviceData: null, deviceData: null,
// serverConnectionConfigs: [
// {
// id: 'test1',
// name: 'http://192.168.0.1:3333 (root)',
// address: 'http://192.168.0.1:3333',
// username: 'root',
// token: 'asdf'
// },
// {
// id: 'test2',
// name: 'https://someserver.com (user)',
// address: 'https://someserver.com',
// username: 'user',
// token: 'asdf'
// }
// ],
loggedIn: false, loggedIn: false,
showAuth: false, showAuth: false,
processing: false, processing: false,
@ -128,18 +112,23 @@ export default {
} }
}, },
async connectToServer(config) { async connectToServer(config) {
console.log('[ServerConnectForm] connectToServer', config.address)
this.processing = true this.processing = true
this.serverConfig = { this.serverConfig = {
...config ...config
} }
this.showForm = true this.showForm = true
var success = await this.pingServerAddress(config.address) var success = await this.pingServerAddress(config.address)
this.processing = false
console.log(`[ServerConnectForm] pingServer result ${success}`)
if (!success) { if (!success) {
this.showForm = false
this.showAuth = false
console.log(`[ServerConnectForm] showForm ${this.showForm}`)
return return
} }
this.error = null this.error = null
this.processing = false
var payload = await this.authenticateToken() var payload = await this.authenticateToken()
if (payload) { if (payload) {
@ -179,8 +168,14 @@ export default {
console.log('Edit server config', serverConfig) console.log('Edit server config', serverConfig)
}, },
newServerConfigClick() { newServerConfigClick() {
this.serverConfig = {
address: '',
userId: '',
username: ''
}
this.showForm = true this.showForm = true
this.showAuth = false this.showAuth = false
this.error = null
}, },
editServerAddress() { editServerAddress() {
this.error = null this.error = null

18823
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,18 +12,18 @@
"ionic:serve": "npm run start" "ionic:serve": "npm run start"
}, },
"dependencies": { "dependencies": {
"@capacitor/android": "^3.2.2", "@capacitor/android": "^3.4.3",
"@capacitor/app": "^1.0.7", "@capacitor/app": "^1.1.1",
"@capacitor/cli": "^3.1.2", "@capacitor/cli": "^3.4.3",
"@capacitor/core": "^3.2.2", "@capacitor/core": "^3.4.3",
"@capacitor/dialog": "^1.0.3", "@capacitor/dialog": "^1.0.7",
"@capacitor/haptics": "^1.1.4", "@capacitor/haptics": "^1.1.4",
"@capacitor/ios": "^3.2.2", "@capacitor/ios": "^3.2.2",
"@capacitor/network": "^1.0.3", "@capacitor/network": "^1.0.7",
"@capacitor/status-bar": "^1.0.6", "@capacitor/status-bar": "^1.0.8",
"@capacitor/storage": "^1.1.0", "@capacitor/storage": "^1.2.5",
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",
"@robingenz/capacitor-app-update": "^1.0.0", "@robingenz/capacitor-app-update": "^1.3.1",
"core-js": "^3.15.1", "core-js": "^3.15.1",
"date-fns": "^2.25.0", "date-fns": "^2.25.0",
"epubjs": "^0.3.88", "epubjs": "^0.3.88",

View file

@ -5,6 +5,7 @@ export const state = () => ({
playerEpisodeId: null, playerEpisodeId: null,
playerIsLocal: false, playerIsLocal: false,
playerIsPlaying: false, playerIsPlaying: false,
isCasting: false,
appUpdateInfo: null, appUpdateInfo: null,
socketConnected: false, socketConnected: false,
networkConnected: false, networkConnected: false,
@ -67,8 +68,14 @@ export const mutations = {
state.playerEpisodeId = playbackSession ? playbackSession.episodeId || null : null state.playerEpisodeId = playbackSession ? playbackSession.episodeId || null : null
} }
var mediaPlayer = playbackSession ? playbackSession.mediaPlayer : null
state.isCasting = mediaPlayer === "cast-player"
console.log('setPlayerItem', state.playerLibraryItemId, state.playerEpisodeId, state.playerIsLocal) console.log('setPlayerItem', state.playerLibraryItemId, state.playerEpisodeId, state.playerIsLocal)
}, },
setMediaPlayer(state, mediaPlayer) {
state.isCasting = mediaPlayer === 'cast-player'
},
setPlayerPlaying(state, val) { setPlayerPlaying(state, val) {
state.playerIsPlaying = val state.playerIsPlaying = val
}, },