mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-10 14:04:41 +02:00
Add chromecast support for android, update package versions
This commit is contained in:
parent
f7516889e4
commit
493d7aecc9
29 changed files with 1821 additions and 19074 deletions
|
@ -4,6 +4,22 @@ plugins {
|
|||
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 {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = ['-Xjvm-default=all']
|
||||
|
@ -42,14 +58,15 @@ dependencies {
|
|||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
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"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
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-jdk8:$kotlin_version"
|
||||
|
|
|
@ -5,18 +5,20 @@ import android.app.DownloadManager
|
|||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.*
|
||||
import android.os.StrictMode.VmPolicy
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import com.anggrayudi.storage.SimpleStorage
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.audiobookshelf.app.data.AbsDatabase
|
||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
||||
import com.audiobookshelf.app.plugins.AbsAudioPlayer
|
||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
||||
import com.audiobookshelf.app.plugins.AbsFileSystem
|
||||
import com.getcapacitor.BridgeActivity
|
||||
import io.paperdb.Paper
|
||||
|
||||
|
||||
class MainActivity : BridgeActivity() {
|
||||
private val tag = "MainActivity"
|
||||
|
||||
|
@ -51,6 +53,20 @@ class MainActivity : BridgeActivity() {
|
|||
}
|
||||
|
||||
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)
|
||||
|
||||
Log.d(tag, "onCreate")
|
||||
|
|
|
@ -20,19 +20,8 @@ class DbManager {
|
|||
var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
|
||||
Paper.book("localLibraryItems").allKeys.forEach {
|
||||
var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
|
||||
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 {
|
||||
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) {
|
||||
localLibraryItems.add(localLibraryItem)
|
||||
// }
|
||||
}
|
||||
}
|
||||
return localLibraryItems
|
||||
|
|
|
@ -73,7 +73,7 @@ data class LocalLibraryItem(
|
|||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -2,11 +2,13 @@ package com.audiobookshelf.app.data
|
|||
|
||||
import android.net.Uri
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.audiobookshelf.app.R
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.player.MediaProgressSyncData
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
import com.google.android.exoplayer2.MediaMetadata
|
||||
import com.google.android.gms.cast.MediaInfo
|
||||
|
@ -42,7 +44,8 @@ class PlaybackSession(
|
|||
var localLibraryItem:LocalLibraryItem?,
|
||||
var localEpisodeId:String?,
|
||||
var serverConnectionConfigId:String?,
|
||||
var serverAddress:String?
|
||||
var serverAddress:String?,
|
||||
var mediaPlayer:String?
|
||||
) {
|
||||
|
||||
@get:JsonIgnore
|
||||
|
@ -152,9 +155,14 @@ class PlaybackSession(
|
|||
@JsonIgnore
|
||||
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)
|
||||
castMetadata.addImage(WebImage(getCoverUri()))
|
||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle)
|
||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor)
|
||||
|
||||
coverPath?.let {
|
||||
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)
|
||||
return castMetadata
|
||||
}
|
||||
|
@ -164,21 +172,22 @@ class PlaybackSession(
|
|||
var castMetadata = getCastMediaMetadata(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)
|
||||
queueItem.setItemId(audioTrack.index)
|
||||
queueItem.setPlaybackDuration(audioTrack.duration)
|
||||
return queueItem.build()
|
||||
var mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply {
|
||||
setContentUrl(mediaUri.toString())
|
||||
setContentType(audioTrack.mimeType)
|
||||
setMetadata(castMetadata)
|
||||
setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||
}.build()
|
||||
|
||||
return MediaQueueItem.Builder(mediaInfo).apply {
|
||||
setPlaybackDuration(audioTrack.duration)
|
||||
}.build()
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
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
|
||||
|
|
|
@ -34,8 +34,8 @@ class MediaManager(var apiHandler: ApiHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
fun play(libraryItem:LibraryItem, cb: (PlaybackSession) -> Unit) {
|
||||
apiHandler.playLibraryItem(libraryItem.id,"",false) {
|
||||
fun play(libraryItem:LibraryItem, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
|
||||
apiHandler.playLibraryItem(libraryItem.id,"",false, mediaPlayer) {
|
||||
cb(it)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,8 @@ import kotlinx.coroutines.*
|
|||
|
||||
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 playerNotificationService:PlayerNotificationService = playerNotificationService
|
||||
|
||||
var currentIconUri: Uri? = null
|
||||
var currentBitmap: Bitmap? = null
|
||||
|
|
|
@ -2,31 +2,30 @@ package com.audiobookshelf.app.player
|
|||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.appcompat.R
|
||||
import androidx.mediarouter.app.MediaRouteChooserDialog
|
||||
import androidx.mediarouter.media.MediaRouteSelector
|
||||
import androidx.mediarouter.media.MediaRouter
|
||||
import com.getcapacitor.PluginCall
|
||||
import com.google.android.exoplayer2.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.gms.cast.*
|
||||
import com.google.android.gms.cast.framework.*
|
||||
import org.json.JSONObject
|
||||
|
||||
class CastManager constructor(playerNotificationService:PlayerNotificationService) {
|
||||
class CastManager constructor(val mainActivity:Activity) {
|
||||
private val tag = "CastManager"
|
||||
private val playerNotificationService:PlayerNotificationService = playerNotificationService
|
||||
|
||||
private var playerNotificationService:PlayerNotificationService? = null
|
||||
private var newConnectionListener: SessionListener? = null
|
||||
private var mainActivity:Activity? = null
|
||||
|
||||
private fun switchToPlayer(useCastPlayer:Boolean) {
|
||||
playerNotificationService.switchToPlayer(useCastPlayer)
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService?.switchToPlayer(useCastPlayer)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class CastSessionAvailabilityListener : SessionAvailabilityListener {
|
||||
|
@ -36,6 +35,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
|||
* remote Cast receiver rather than play audio locally.
|
||||
*/
|
||||
override fun onCastSessionAvailable() {
|
||||
Log.d(tag, "SessionAvailabilityListener: onCastSessionAvailable")
|
||||
switchToPlayer(true)
|
||||
}
|
||||
|
||||
|
@ -48,17 +48,13 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
|||
}
|
||||
}
|
||||
|
||||
fun requestSession(mainActivity: Activity, callback: RequestSessionCallback) {
|
||||
this.mainActivity = mainActivity
|
||||
|
||||
mainActivity.runOnUiThread(object : Runnable {
|
||||
override fun run() {
|
||||
Log.d(tag, "CAST RUNNING ON MAIN THREAD")
|
||||
fun requestSession(playerNotificationService: PlayerNotificationService, callback: RequestSessionCallback) {
|
||||
this.playerNotificationService = playerNotificationService
|
||||
|
||||
mainActivity.runOnUiThread {
|
||||
val session: CastSession? = getSession()
|
||||
if (session == null) {
|
||||
// show the "choose a connection" dialog
|
||||
|
||||
// Add the connection listener callback
|
||||
listenForConnection(callback)
|
||||
|
||||
|
@ -68,22 +64,23 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
|||
.build()
|
||||
builder.setCanceledOnTouchOutside(true)
|
||||
builder.setOnCancelListener {
|
||||
getSessionManager()!!.removeSessionManagerListener(newConnectionListener, CastSession::class.java)
|
||||
newConnectionListener?.let { ncl ->
|
||||
getSessionManager()?.removeSessionManagerListener(ncl, CastSession::class.java)
|
||||
}
|
||||
callback.onCancel()
|
||||
}
|
||||
builder.show()
|
||||
} else {
|
||||
// We are are already connected, so show the "connection options" Dialog
|
||||
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
|
||||
if (session.castDevice != null) {
|
||||
builder.setTitle(session.castDevice.friendlyName)
|
||||
session.castDevice?.let {
|
||||
builder.setTitle(it.friendlyName)
|
||||
}
|
||||
builder.setOnDismissListener { callback.onCancel() }
|
||||
builder.setPositiveButton("Stop Casting") { dialog, which -> endSession(true, null) }
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
abstract class RequestSessionCallback : ConnectionCallback {
|
||||
|
@ -103,12 +100,10 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
|||
fun endSession(stopCasting: Boolean, pluginCall: PluginCall?) {
|
||||
|
||||
getSessionManager()!!.addSessionManagerListener(object : SessionListener() {
|
||||
override fun onSessionEnded(castSession: CastSession?, error: Int) {
|
||||
override fun onSessionEnded(castSession: CastSession, error: Int) {
|
||||
getSessionManager()!!.removeSessionManagerListener(this, CastSession::class.java)
|
||||
Log.d(tag, "CAST END SESSION")
|
||||
// media.setSession(null)
|
||||
pluginCall?.resolve()
|
||||
// listener.onSessionEnd(ChromecastUtilities.createSessionObject(castSession, if (stopCasting) "stopped" else "disconnected"))
|
||||
}
|
||||
}, CastSession::class.java)
|
||||
getSessionManager()!!.endCurrentSession(stopCasting)
|
||||
|
@ -116,78 +111,47 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
|||
}
|
||||
|
||||
open class SessionListener : SessionManagerListener<CastSession> {
|
||||
override fun onSessionStarting(castSession: CastSession?) {}
|
||||
override fun onSessionStarted(castSession: CastSession?, sessionId: String) {}
|
||||
override fun onSessionStartFailed(castSession: CastSession?, error: Int) {}
|
||||
override fun onSessionEnding(castSession: CastSession?) {}
|
||||
override fun onSessionEnded(castSession: CastSession?, error: Int) {}
|
||||
override fun onSessionResuming(castSession: CastSession?, sessionId: String) {}
|
||||
override fun onSessionResumed(castSession: CastSession?, wasSuspended: Boolean) {}
|
||||
override fun onSessionResumeFailed(castSession: CastSession?, error: Int) {}
|
||||
override fun onSessionSuspended(castSession: CastSession?, reason: Int) {}
|
||||
}
|
||||
|
||||
private fun startRouteScan() {
|
||||
var connListener = object: ChromecastListener() {
|
||||
override fun onReceiverAvailableUpdate(available: Boolean) {
|
||||
Log.d(tag, "CAST RECEIVER UPDATE AVAILABLE $available")
|
||||
}
|
||||
|
||||
override fun onSessionRejoin(jsonSession: JSONObject?) {
|
||||
Log.d(tag, "CAST onSessionRejoin")
|
||||
}
|
||||
|
||||
override fun onMediaLoaded(jsonMedia: JSONObject?) {
|
||||
Log.d(tag, "CAST onMediaLoaded")
|
||||
}
|
||||
|
||||
override fun onMediaUpdate(jsonMedia: JSONObject?) {
|
||||
Log.d(tag, "CAST onMediaUpdate")
|
||||
}
|
||||
|
||||
override fun onSessionUpdate(jsonSession: JSONObject?) {
|
||||
Log.d(tag, "CAST onSessionUpdate")
|
||||
}
|
||||
|
||||
override fun onSessionEnd(jsonSession: JSONObject?) {
|
||||
Log.d(tag, "CAST onSessionEnd")
|
||||
}
|
||||
|
||||
override fun onMessageReceived(p0: CastDevice, p1: String, p2: String) {
|
||||
Log.d(tag, "CAST onMessageReceived")
|
||||
}
|
||||
override fun onSessionStarting(castSession: CastSession) {}
|
||||
override fun onSessionStarted(castSession: CastSession, sessionId: String) {}
|
||||
override fun onSessionStartFailed(castSession: CastSession, error: Int) {}
|
||||
override fun onSessionEnding(castSession: CastSession) {}
|
||||
override fun onSessionEnded(castSession: CastSession, error: Int) {}
|
||||
override fun onSessionResuming(castSession: CastSession, sessionId: String) {}
|
||||
override fun onSessionResumed(castSession: CastSession, wasSuspended: Boolean) {}
|
||||
override fun onSessionResumeFailed(castSession: CastSession, error: Int) {}
|
||||
override fun onSessionSuspended(castSession: CastSession, reason: Int) {}
|
||||
}
|
||||
|
||||
fun startRouteScan(connListener:ChromecastListener) {
|
||||
var callback = object : ScanCallback() {
|
||||
override fun onRouteUpdate(routes: List<MediaRouter.RouteInfo>?) {
|
||||
Log.d(tag, "CAST On ROUTE UPDATED ${routes?.size} | ${getContext().castState}")
|
||||
// if the routes have changed, we may have an available device
|
||||
// If there is at least one device available
|
||||
if (getContext().castState != CastState.NO_DEVICES_AVAILABLE) {
|
||||
|
||||
routes?.forEach { Log.d(tag, "CAST ROUTE ${it.description} | ${it.deviceType} | ${it.isBluetooth} | ${it.name}") }
|
||||
|
||||
// Stop the scan
|
||||
stopRouteScan(this, null);
|
||||
stopRouteScan(this, null)
|
||||
// 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
|
||||
var session = getSessionManager()?.currentCastSession;
|
||||
var session = getSessionManager()?.currentCastSession
|
||||
// If we do have a session
|
||||
if (session != null) {
|
||||
// Let the client know
|
||||
Log.d(tag, "LET SESSION KNOW ABOUT")
|
||||
// media.setSession(session);
|
||||
// connListener.onSessionRejoin(ChromecastUtilities.createSessionObject(session));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(tag, "No cast devices available")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback.setMediaRouter(getMediaRouter())
|
||||
|
||||
callback.onFilteredRouteUpdate();
|
||||
callback.onFilteredRouteUpdate()
|
||||
|
||||
getMediaRouter()!!.addCallback(MediaRouteSelector.Builder()
|
||||
getMediaRouter()?.addCallback(MediaRouteSelector.Builder()
|
||||
.addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))
|
||||
.build(),
|
||||
callback,
|
||||
|
@ -201,7 +165,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
|||
fun onSessionEnd(jsonSession: JSONObject?)
|
||||
}
|
||||
|
||||
internal abstract class ChromecastListener : CastStateListener, CastListener {
|
||||
abstract class ChromecastListener : CastStateListener, CastListener {
|
||||
abstract fun onReceiverAvailableUpdate(available: Boolean)
|
||||
abstract fun onSessionRejoin(jsonSession: JSONObject?)
|
||||
|
||||
|
@ -212,15 +176,19 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
|||
}
|
||||
|
||||
fun stopRouteScan(callback: ScanCallback?, completionCallback: Runnable?) {
|
||||
Log.d(tag, "stopRouteScan")
|
||||
if (callback == null) {
|
||||
completionCallback!!.run()
|
||||
completionCallback?.run()
|
||||
return
|
||||
}
|
||||
// ctx.runOnUiThread(Runnable {
|
||||
|
||||
// mainActivity.runOnUiThread {
|
||||
Log.d(tag, "Removing callback on media router")
|
||||
callback.stop()
|
||||
getMediaRouter()!!.removeCallback(callback)
|
||||
getMediaRouter()?.removeCallback(callback)
|
||||
completionCallback?.run()
|
||||
// })
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
abstract class ScanCallback : MediaRouter.Callback() {
|
||||
|
@ -271,7 +239,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
|||
}
|
||||
if (!route.isDefault
|
||||
&& !route.description.equals("Google Cast Multizone Member")
|
||||
&& route.playbackType === MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) {
|
||||
&& route.playbackType == MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) {
|
||||
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) {
|
||||
// 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() {
|
||||
override fun onSessionStarted(castSession: CastSession?, sessionId: String) {
|
||||
Log.d(tag, "CAST SESSION STARTED ${castSession?.castDevice?.friendlyName}")
|
||||
override fun onSessionStarted(castSession: CastSession, sessionId: String) {
|
||||
Log.d(tag, "CAST SESSION STARTED ${castSession.castDevice?.friendlyName}")
|
||||
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
|
||||
|
||||
try {
|
||||
val castContext = CastContext.getSharedInstance(mainActivity)
|
||||
|
||||
// Work in progress using the cast api
|
||||
var currentSession = playerNotificationService.getCurrentPlaybackSessionCopy()
|
||||
var firstTrack = currentSession?.audioTracks?.get(0)
|
||||
var uri = firstTrack?.let { currentSession?.getContentUri(it) } ?: Uri.EMPTY
|
||||
var url = uri.toString()
|
||||
var mimeType = firstTrack?.mimeType ?: ""
|
||||
var castMediaMetadata = firstTrack?.let { currentSession?.getCastMediaMetadata(it) }
|
||||
Log.d(tag, "CastManager set url $url")
|
||||
var duration = (currentSession?.getTotalDuration() ?: 0L * 1000L).toLong()
|
||||
|
||||
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())
|
||||
playerNotificationService?.let {
|
||||
if (it.castPlayer == null) {
|
||||
Log.d(tag, "Initializing castPlayer on session started - switch to cast player")
|
||||
it.castPlayer = CastPlayer(castContext).apply {
|
||||
addListener(PlayerListener(it))
|
||||
setSessionAvailabilityListener(CastSessionAvailabilityListener())
|
||||
}
|
||||
switchToPlayer(true)
|
||||
} else {
|
||||
Log.d(tag, "castPlayer is already initialized on session started")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not working using the exo player CastPlayer
|
||||
// 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) {
|
||||
override fun onSessionStartFailed(castSession: CastSession, errCode: Int) {
|
||||
if (callback.onSessionStartFailed(errCode)) {
|
||||
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSessionEnded(castSession: CastSession?, errCode: Int) {
|
||||
override fun onSessionEnded(castSession: CastSession, errCode: Int) {
|
||||
if (callback.onSessionEndedBeforeStart(errCode)) {
|
||||
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 {
|
||||
|
@ -383,7 +314,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
|||
}
|
||||
|
||||
private fun getMediaRouter(): MediaRouter? {
|
||||
return mainActivity?.let { MediaRouter.getInstance(it) }
|
||||
return MediaRouter.getInstance(mainActivity)
|
||||
}
|
||||
|
||||
private fun getSession(): CastSession? {
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||
override fun onPrepare() {
|
||||
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
||||
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}")
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,true)
|
||||
|
@ -48,7 +48,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||
Log.d(tag, "ON PLAY FROM SEARCH $query")
|
||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
||||
playerNotificationService.mediaManager.play(li) {
|
||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
||||
Log.d(tag, "About to prepare player with li ${li.title}")
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,true)
|
||||
|
@ -96,7 +96,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||
}
|
||||
|
||||
libraryItem?.let { li ->
|
||||
playerNotificationService.mediaManager.play(li) {
|
||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
||||
Log.d(tag, "About to prepare player with li ${li.title}")
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,true)
|
||||
|
|
|
@ -8,14 +8,13 @@ import android.os.ResultReceiver
|
|||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.data.LibraryItem
|
||||
import com.google.android.exoplayer2.ControlDispatcher
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
|
||||
class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificationService) : MediaSessionConnector.PlaybackPreparer {
|
||||
var tag = "MediaSessionPlaybackPreparer"
|
||||
|
||||
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
|
||||
override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
|
||||
Log.d(tag, "ON COMMAND $command")
|
||||
return false
|
||||
}
|
||||
|
@ -30,7 +29,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||
override fun onPrepare(playWhenReady: Boolean) {
|
||||
Log.d(tag, "ON PREPARE $playWhenReady")
|
||||
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
||||
playerNotificationService.mediaManager.play(li) {
|
||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,playWhenReady)
|
||||
}
|
||||
|
@ -43,7 +42,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||
|
||||
var libraryItem: LibraryItem? = playerNotificationService.mediaManager.getById(mediaId)
|
||||
libraryItem?.let { li ->
|
||||
playerNotificationService.mediaManager.play(li) {
|
||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
||||
Log.d(tag, "About to prepare player with li ${li.title}")
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,playWhenReady)
|
||||
|
@ -55,7 +54,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
||||
playerNotificationService.mediaManager.play(li) {
|
||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
||||
Log.d(tag, "About to prepare player with li ${li.title}")
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,playWhenReady)
|
||||
|
|
|
@ -21,17 +21,21 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||
}
|
||||
|
||||
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)) {
|
||||
Log.d(tag, "EVENT_POSITION_DISCONTINUITY")
|
||||
}
|
||||
|
||||
if (events.contains(Player.EVENT_IS_LOADING_CHANGED)) {
|
||||
Log.d(tag, "EVENT_IS_LOADING_CHANGED : " + playerNotificationService.mPlayer.isLoading.toString())
|
||||
Log.d(tag, "EVENT_IS_LOADING_CHANGED : " + playerNotificationService.currentPlayer.isLoading)
|
||||
}
|
||||
|
||||
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) {
|
||||
Log.d(tag, "STATE_READY : " + playerNotificationService.mPlayer.duration.toString())
|
||||
Log.d(tag, "STATE_READY : " + playerNotificationService.currentPlayer.duration)
|
||||
|
||||
if (lastPauseTime == 0L) {
|
||||
lastPauseTime = -1;
|
||||
|
@ -39,7 +43,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||
playerNotificationService.sendClientMetadata(PlayerState.READY)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
|
||||
|
@ -53,13 +57,13 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||
}
|
||||
|
||||
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)) {
|
||||
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED")
|
||||
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
|
||||
}
|
||||
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 (lastPauseTime > 0) {
|
||||
|
|
|
@ -22,19 +22,15 @@ import com.audiobookshelf.app.data.*
|
|||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.media.MediaManager
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.getcapacitor.JSObject
|
||||
import com.google.android.exoplayer2.*
|
||||
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.TimelineQueueNavigator
|
||||
import com.google.android.exoplayer2.source.MediaSource
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
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.upstream.*
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
|
@ -56,6 +52,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
fun onSleepTimerSet(sleepTimeRemaining: Int)
|
||||
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
|
||||
fun onPlaybackFailed(errorMessage:String)
|
||||
fun onMediaPlayerChanged(mediaPlayer:String)
|
||||
}
|
||||
|
||||
private val tag = "PlayerService"
|
||||
|
@ -68,6 +65,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
private lateinit var playerNotificationManager: PlayerNotificationManager
|
||||
private lateinit var mediaSession: MediaSessionCompat
|
||||
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
||||
|
||||
lateinit var mediaManager: MediaManager
|
||||
lateinit var apiHandler: ApiHandler
|
||||
|
||||
|
@ -76,7 +74,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
var castPlayer:CastPlayer? = null
|
||||
|
||||
lateinit var sleepTimerManager:SleepTimerManager
|
||||
lateinit var castManager:CastManager
|
||||
lateinit var mediaProgressSyncer:MediaProgressSyncer
|
||||
|
||||
private var notificationId = 10;
|
||||
|
@ -144,6 +141,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
override fun onDestroy() {
|
||||
playerNotificationManager.setPlayer(null)
|
||||
mPlayer.release()
|
||||
castPlayer?.release()
|
||||
mediaSession.release()
|
||||
mediaProgressSyncer.reset()
|
||||
Log.d(tag, "onDestroy")
|
||||
|
@ -190,9 +188,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
// Initialize sleep timer
|
||||
sleepTimerManager = SleepTimerManager(this)
|
||||
|
||||
// Initialize Cast Manager
|
||||
castManager = CastManager(this)
|
||||
|
||||
// Initialize Media Progress Syncer
|
||||
mediaProgressSyncer = MediaProgressSyncer(this, apiHandler)
|
||||
|
||||
|
@ -282,7 +277,23 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
User callable methods
|
||||
*/
|
||||
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
|
||||
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
|
||||
|
||||
clientEventEmitter?.onPlaybackSession(playbackSession)
|
||||
|
||||
|
@ -309,10 +320,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
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
|
||||
if (mediaItems.size > 1) {
|
||||
|
@ -331,17 +338,27 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
currentPlayer.playWhenReady = playWhenReady
|
||||
currentPlayer.setPlaybackSpeed(1f) // TODO: Playback speed should come from settings
|
||||
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) {
|
||||
// On error and was attempting to direct play - fallback to transcode
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
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 episodeId = playbackSession.episodeId
|
||||
apiHandler.playLibraryItem(libraryItemId, episodeId, true) {
|
||||
apiHandler.playLibraryItem(libraryItemId, episodeId, true, mediaPlayer) {
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
preparePlayer(it, true)
|
||||
}
|
||||
|
@ -354,17 +371,48 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
|
||||
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) {
|
||||
Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo)
|
||||
mediaSessionConnector.setPlayer(castPlayer)
|
||||
playerNotificationManager.setPlayer(castPlayer)
|
||||
castPlayer as CastPlayer
|
||||
} else {
|
||||
Log.d(tag, "switchToPlayer: Using ExoPlayer")
|
||||
mediaSessionConnector.setPlayer(mPlayer)
|
||||
playerNotificationManager.setPlayer(mPlayer)
|
||||
mPlayer
|
||||
}
|
||||
|
||||
clientEventEmitter?.onMediaPlayerChanged(getMediaPlayer())
|
||||
|
||||
if (currentPlaybackSession == null) {
|
||||
Log.d(tag, "switchToPlayer: No Current playback session")
|
||||
}
|
||||
|
||||
currentPlaybackSession?.let {
|
||||
Log.d(tag, "switchToPlayer: Preparing current playback session ${it.displayTitle}")
|
||||
if (wasPlaying) { // media is paused when switching players
|
||||
clientEventEmitter?.onPlayingUpdate(false)
|
||||
}
|
||||
preparePlayer(it, false)
|
||||
}
|
||||
}
|
||||
|
@ -441,6 +489,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
|
||||
fun seekPlayer(time: Long) {
|
||||
Log.d(tag, "seekPlayer mediaCount = ${currentPlayer.mediaItemCount} | $time")
|
||||
if (currentPlayer.mediaItemCount > 1) {
|
||||
currentPlaybackSession?.currentTime = time / 1000.0
|
||||
var newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
|
||||
|
@ -452,11 +501,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
|
||||
fun seekForward(amount: Long) {
|
||||
currentPlayer.seekTo(mPlayer.currentPosition + amount)
|
||||
currentPlayer.seekTo(currentPlayer.currentPosition + amount)
|
||||
}
|
||||
|
||||
fun seekBackward(amount: Long) {
|
||||
currentPlayer.seekTo(mPlayer.currentPosition - amount)
|
||||
currentPlayer.seekTo(currentPlayer.currentPosition - amount)
|
||||
}
|
||||
|
||||
fun setPlaybackSpeed(speed: Float) {
|
||||
|
@ -465,6 +514,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
|
||||
fun closePlayback() {
|
||||
currentPlayer.clearMediaItems()
|
||||
currentPlayer.stop()
|
||||
currentPlaybackSession = null
|
||||
clientEventEmitter?.onPlaybackClosed()
|
||||
PlayerListener.lastPauseTime = 0
|
||||
|
@ -475,6 +525,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
clientEventEmitter?.onMetadata(PlaybackMetadata(duration, getCurrentTimeSeconds(), playerState))
|
||||
}
|
||||
|
||||
fun getMediaPlayer():String {
|
||||
return if(currentPlayer == castPlayer) "cast-player" else "exo-player"
|
||||
}
|
||||
|
||||
//
|
||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||
//
|
||||
|
|
|
@ -15,6 +15,7 @@ import com.fasterxml.jackson.core.json.JsonReadFeature
|
|||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.getcapacitor.*
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import com.google.android.gms.cast.CastDevice
|
||||
import org.json.JSONObject
|
||||
|
||||
@CapacitorPlugin(name = "AbsAudioPlayer")
|
||||
|
@ -24,12 +25,18 @@ class AbsAudioPlayer : Plugin() {
|
|||
|
||||
lateinit var mainActivity: MainActivity
|
||||
lateinit var apiHandler:ApiHandler
|
||||
lateinit var castManager:CastManager
|
||||
|
||||
lateinit var playerNotificationService: PlayerNotificationService
|
||||
|
||||
private var isCastAvailable:Boolean = false
|
||||
|
||||
override fun load() {
|
||||
mainActivity = (activity as MainActivity)
|
||||
apiHandler = ApiHandler(mainActivity)
|
||||
|
||||
initCastManager()
|
||||
|
||||
var foregroundServiceReady : () -> Unit = {
|
||||
playerNotificationService = mainActivity.foregroundService
|
||||
|
||||
|
@ -72,17 +79,58 @@ class AbsAudioPlayer : Plugin() {
|
|||
override fun onPlaybackFailed(errorMessage: String) {
|
||||
emit("onPlaybackFailed", errorMessage)
|
||||
}
|
||||
|
||||
override fun onMediaPlayerChanged(mediaPlayer:String) {
|
||||
emit("onMediaPlayerChanged", mediaPlayer)
|
||||
}
|
||||
})
|
||||
}
|
||||
mainActivity.pluginCallback = foregroundServiceReady
|
||||
}
|
||||
|
||||
fun emit(evtName: String, value: Any) {
|
||||
var ret:JSObject = JSObject()
|
||||
var ret = JSObject()
|
||||
ret.put("value", value)
|
||||
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
|
||||
fun prepareLibraryItem(call: PluginCall) {
|
||||
// Need to make sure the player service has been started
|
||||
|
@ -122,7 +170,9 @@ class AbsAudioPlayer : Plugin() {
|
|||
return call.resolve(JSObject())
|
||||
}
|
||||
} 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() {
|
||||
Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}")
|
||||
|
@ -268,9 +318,10 @@ class AbsAudioPlayer : Plugin() {
|
|||
|
||||
@PluginMethod
|
||||
fun requestSession(call: PluginCall) {
|
||||
// Need to make sure the player service has been started
|
||||
Log.d(tag, "CAST REQUEST SESSION PLUGIN")
|
||||
call.resolve()
|
||||
playerNotificationService.castManager.requestSession(mainActivity, object : CastManager.RequestSessionCallback() {
|
||||
castManager.requestSession(playerNotificationService, object : CastManager.RequestSessionCallback() {
|
||||
override fun onError(errorCode: Int) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
payload.put("mediaPlayer", "exo-player")
|
||||
payload.put("mediaPlayer", mediaPlayer)
|
||||
|
||||
// Only if direct play fails do we force transcode
|
||||
if (!forceTranscode) payload.put("forceDirectPlay", true)
|
||||
|
|
|
@ -5,11 +5,12 @@ buildscript {
|
|||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
||||
classpath 'com.google.gms:google-services:4.3.5'
|
||||
// classpath 'com.android.tools.build:gradle:4.0.2'
|
||||
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"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
@ -22,7 +23,7 @@ apply from: "variables.gradle"
|
|||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,15 @@
|
|||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# 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.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
|
@ -22,3 +30,5 @@ org.gradle.jvmargs=-Xmx1536m
|
|||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
kapt.use.worker.api=false
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#Sun Apr 17 13:28:55 CDT 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
ext {
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 30
|
||||
compileSdkVersion = 31
|
||||
targetSdkVersion = 30
|
||||
androidxActivityVersion = '1.2.0'
|
||||
androidxAppCompatVersion = '1.2.0'
|
||||
androidxCoordinatorLayoutVersion = '1.1.0'
|
||||
androidxAppCompatVersion = '1.4.1'
|
||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||
androidxCoreVersion = '1.6.0'
|
||||
androidPlayCore = '1.9.0'
|
||||
androidxFragmentVersion = '1.3.0'
|
||||
junitVersion = '4.13.1'
|
||||
androidxJunitVersion = '1.1.2'
|
||||
androidxEspressoCoreVersion = '3.3.0'
|
||||
cordovaAndroidVersion = '7.0.0'
|
||||
cordovaAndroidVersion = '10.1.1'
|
||||
androidx_app_compat_version = '1.2.0'
|
||||
androidx_car_version = '1.0.0-alpha7'
|
||||
androidx_core_ktx_version = '1.6.0'
|
||||
androidx_media_version = '1.0.1'
|
||||
androidx_core_ktx_version = '1.7.0'
|
||||
androidx_media_version = '1.5.0'
|
||||
androidx_preference_version = '1.1.1'
|
||||
androidx_test_runner_version = '1.3.0'
|
||||
arch_lifecycle_version = '2.2.0'
|
||||
constraint_layout_version = '2.0.1'
|
||||
espresso_version = '3.3.0'
|
||||
exoplayer_version = '2.15.0'
|
||||
exoplayer_version = '2.17.0'
|
||||
fragment_version = '1.2.5'
|
||||
glide_version = '4.11.0'
|
||||
gms_strict_version_matcher_version = '1.0.3'
|
||||
gradle_version = '3.1.4'
|
||||
gson_version = '2.8.5'
|
||||
junit_version = '4.13'
|
||||
kotlin_version = '1.4.32'
|
||||
kotlin_version = '1.5.30'
|
||||
kotlin_coroutines_version = '1.1.0'
|
||||
multidex_version = '1.0.3'
|
||||
play_services_auth_version = '18.1.0'
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
|
||||
<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">
|
||||
<span class="material-icons" style="font-size: 1.75rem">search</span>
|
||||
</nuxt-link>
|
||||
|
@ -29,9 +33,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
onCastAvailableUpdateListener: null,
|
||||
isCastAvailable: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
socketConnected() {
|
||||
|
@ -55,9 +64,21 @@ export default {
|
|||
},
|
||||
username() {
|
||||
return this.user ? this.user.username : 'err'
|
||||
},
|
||||
isCasting() {
|
||||
return this.$store.state.isCasting
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
castClick() {
|
||||
if (this.$store.state.playerIsLocal) {
|
||||
this.$toast.warn('Cannot cast downloaded media item')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Cast Btn Click')
|
||||
AbsAudioPlayer.requestSession()
|
||||
},
|
||||
clickShowSideDrawer() {
|
||||
this.$store.commit('setShowSideDrawer', true)
|
||||
},
|
||||
|
@ -66,9 +87,20 @@ export default {
|
|||
},
|
||||
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>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
||||
</div>
|
||||
<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 class="top-4 right-4 absolute cursor-pointer">
|
||||
<ui-dropdown-menu ref="dropdownMenu" :items="menuItems" @action="clickMenuAction">
|
||||
|
@ -109,7 +109,7 @@ export default {
|
|||
return {
|
||||
playbackSession: null,
|
||||
showChapterModal: false,
|
||||
showCastBtn: false,
|
||||
showCastBtn: true,
|
||||
showFullscreen: false,
|
||||
totalDuration: 0,
|
||||
currentPlaybackRate: 1,
|
||||
|
@ -159,6 +159,12 @@ export default {
|
|||
}
|
||||
return this.showFullscreen ? 200 : 60
|
||||
},
|
||||
isCasting() {
|
||||
return this.mediaPlayer === 'cast-player'
|
||||
},
|
||||
mediaPlayer() {
|
||||
return this.playbackSession ? this.playbackSession.mediaPlayer : null
|
||||
},
|
||||
mediaType() {
|
||||
return this.playbackSession ? this.playbackSession.mediaType : null
|
||||
},
|
||||
|
@ -262,6 +268,11 @@ export default {
|
|||
},
|
||||
castClick() {
|
||||
console.log('Cast Btn Click')
|
||||
if (this.isLocalPlayMethod) {
|
||||
this.$toast.warn('Cannot cast downloaded media items')
|
||||
return
|
||||
}
|
||||
|
||||
AbsAudioPlayer.requestSession()
|
||||
},
|
||||
clickContainer() {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
<script>
|
||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
@ -28,6 +29,7 @@ export default {
|
|||
onLocalMediaProgressUpdateListener: null,
|
||||
onSleepTimerEndedListener: null,
|
||||
onSleepTimerSetListener: null,
|
||||
onMediaPlayerChangedListener: null,
|
||||
sleepInterval: null,
|
||||
currentEndOfChapterTime: 0
|
||||
}
|
||||
|
@ -169,6 +171,16 @@ export default {
|
|||
var libraryItemId = payload.libraryItemId
|
||||
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)
|
||||
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true })
|
||||
.then((data) => {
|
||||
|
@ -186,12 +198,17 @@ export default {
|
|||
onLocalMediaProgressUpdate(localMediaProgress) {
|
||||
console.log('Got local media progress update', localMediaProgress.progress, JSON.stringify(localMediaProgress))
|
||||
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
|
||||
},
|
||||
onMediaPlayerChanged(data) {
|
||||
var mediaPlayer = data.value
|
||||
this.$store.commit('setMediaPlayer', mediaPlayer)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.onLocalMediaProgressUpdateListener = AbsAudioPlayer.addListener('onLocalMediaProgressUpdate', this.onLocalMediaProgressUpdate)
|
||||
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
|
||||
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
|
||||
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
|
||||
|
||||
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
||||
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
||||
|
@ -206,6 +223,7 @@ export default {
|
|||
if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove()
|
||||
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
|
||||
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
|
||||
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
|
||||
|
||||
// if (this.$server.socket) {
|
||||
// this.$server.socket.off('stream_open', this.streamOpen)
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<ui-btn class="w-full" @click="newServerConfigClick">Add New Server</ui-btn>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-else 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>
|
||||
<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>
|
||||
</form>
|
||||
</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">
|
||||
<span class="material-icons mr-2 text-error" style="font-size: 1.1rem">warning</span>
|
||||
<p class="text-error">{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</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">
|
||||
|
@ -73,22 +73,6 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
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,
|
||||
showAuth: false,
|
||||
processing: false,
|
||||
|
@ -128,18 +112,23 @@ export default {
|
|||
}
|
||||
},
|
||||
async connectToServer(config) {
|
||||
console.log('[ServerConnectForm] connectToServer', config.address)
|
||||
this.processing = true
|
||||
this.serverConfig = {
|
||||
...config
|
||||
}
|
||||
this.showForm = true
|
||||
var success = await this.pingServerAddress(config.address)
|
||||
this.processing = false
|
||||
console.log(`[ServerConnectForm] pingServer result ${success}`)
|
||||
if (!success) {
|
||||
this.showForm = false
|
||||
this.showAuth = false
|
||||
console.log(`[ServerConnectForm] showForm ${this.showForm}`)
|
||||
return
|
||||
}
|
||||
|
||||
this.error = null
|
||||
this.processing = false
|
||||
var payload = await this.authenticateToken()
|
||||
|
||||
if (payload) {
|
||||
|
@ -179,8 +168,14 @@ export default {
|
|||
console.log('Edit server config', serverConfig)
|
||||
},
|
||||
newServerConfigClick() {
|
||||
this.serverConfig = {
|
||||
address: '',
|
||||
userId: '',
|
||||
username: ''
|
||||
}
|
||||
this.showForm = true
|
||||
this.showAuth = false
|
||||
this.error = null
|
||||
},
|
||||
editServerAddress() {
|
||||
this.error = null
|
||||
|
|
18823
package-lock.json
generated
18823
package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
package.json
18
package.json
|
@ -12,18 +12,18 @@
|
|||
"ionic:serve": "npm run start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^3.2.2",
|
||||
"@capacitor/app": "^1.0.7",
|
||||
"@capacitor/cli": "^3.1.2",
|
||||
"@capacitor/core": "^3.2.2",
|
||||
"@capacitor/dialog": "^1.0.3",
|
||||
"@capacitor/android": "^3.4.3",
|
||||
"@capacitor/app": "^1.1.1",
|
||||
"@capacitor/cli": "^3.4.3",
|
||||
"@capacitor/core": "^3.4.3",
|
||||
"@capacitor/dialog": "^1.0.7",
|
||||
"@capacitor/haptics": "^1.1.4",
|
||||
"@capacitor/ios": "^3.2.2",
|
||||
"@capacitor/network": "^1.0.3",
|
||||
"@capacitor/status-bar": "^1.0.6",
|
||||
"@capacitor/storage": "^1.1.0",
|
||||
"@capacitor/network": "^1.0.7",
|
||||
"@capacitor/status-bar": "^1.0.8",
|
||||
"@capacitor/storage": "^1.2.5",
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
"@robingenz/capacitor-app-update": "^1.0.0",
|
||||
"@robingenz/capacitor-app-update": "^1.3.1",
|
||||
"core-js": "^3.15.1",
|
||||
"date-fns": "^2.25.0",
|
||||
"epubjs": "^0.3.88",
|
||||
|
|
|
@ -5,6 +5,7 @@ export const state = () => ({
|
|||
playerEpisodeId: null,
|
||||
playerIsLocal: false,
|
||||
playerIsPlaying: false,
|
||||
isCasting: false,
|
||||
appUpdateInfo: null,
|
||||
socketConnected: false,
|
||||
networkConnected: false,
|
||||
|
@ -67,8 +68,14 @@ export const mutations = {
|
|||
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)
|
||||
},
|
||||
setMediaPlayer(state, mediaPlayer) {
|
||||
state.isCasting = mediaPlayer === 'cast-player'
|
||||
},
|
||||
setPlayerPlaying(state, val) {
|
||||
state.playerIsPlaying = val
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue