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'
|
id 'kotlin-kapt'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
kotlinDaemonJvmArgs = [
|
||||||
|
"-Dfile.encoding=UTF-8",
|
||||||
|
"--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
|
||||||
|
"--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
|
||||||
|
"--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
|
||||||
|
"--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
|
||||||
|
"--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
|
||||||
|
"--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
|
||||||
|
"--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
|
||||||
|
"--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
|
||||||
|
"--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
|
||||||
|
"--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
freeCompilerArgs = ['-Xjvm-default=all']
|
freeCompilerArgs = ['-Xjvm-default=all']
|
||||||
|
@ -42,14 +58,15 @@ dependencies {
|
||||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
implementation project(':capacitor-android')
|
implementation project(':capacitor-android')
|
||||||
|
|
||||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
|
|
||||||
testImplementation "junit:junit:$junitVersion"
|
testImplementation "junit:junit:$junitVersion"
|
||||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
implementation project(':capacitor-cordova-android-plugins')
|
implementation project(':capacitor-cordova-android-plugins')
|
||||||
|
|
||||||
implementation "androidx.core:core-ktx:1.6.0"
|
implementation "androidx.core:core-ktx:$androidx_core_ktx_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
|
|
@ -5,18 +5,20 @@ import android.app.DownloadManager
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.*
|
import android.os.*
|
||||||
|
import android.os.StrictMode.VmPolicy
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import com.anggrayudi.storage.SimpleStorage
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
import com.audiobookshelf.app.data.AbsDatabase
|
import com.audiobookshelf.app.data.AbsDatabase
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
|
||||||
import com.audiobookshelf.app.plugins.AbsAudioPlayer
|
import com.audiobookshelf.app.plugins.AbsAudioPlayer
|
||||||
|
import com.audiobookshelf.app.plugins.AbsDownloader
|
||||||
import com.audiobookshelf.app.plugins.AbsFileSystem
|
import com.audiobookshelf.app.plugins.AbsFileSystem
|
||||||
import com.getcapacitor.BridgeActivity
|
import com.getcapacitor.BridgeActivity
|
||||||
import io.paperdb.Paper
|
import io.paperdb.Paper
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : BridgeActivity() {
|
class MainActivity : BridgeActivity() {
|
||||||
private val tag = "MainActivity"
|
private val tag = "MainActivity"
|
||||||
|
|
||||||
|
@ -51,6 +53,20 @@ class MainActivity : BridgeActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
|
||||||
|
.detectDiskReads()
|
||||||
|
.detectDiskWrites().detectAll()
|
||||||
|
.detectNetwork() // or .detectAll() for all detectable problems
|
||||||
|
.penaltyLog()
|
||||||
|
.build())
|
||||||
|
StrictMode.setVmPolicy(VmPolicy.Builder()
|
||||||
|
.detectLeakedSqlLiteObjects()
|
||||||
|
.detectLeakedClosableObjects()
|
||||||
|
.penaltyLog()
|
||||||
|
// .penaltyDeath()
|
||||||
|
.build())
|
||||||
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
Log.d(tag, "onCreate")
|
Log.d(tag, "onCreate")
|
||||||
|
|
|
@ -20,19 +20,8 @@ class DbManager {
|
||||||
var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
|
var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
|
||||||
Paper.book("localLibraryItems").allKeys.forEach {
|
Paper.book("localLibraryItems").allKeys.forEach {
|
||||||
var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
|
var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
|
||||||
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem?.mediaType)) {
|
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) {
|
||||||
// TODO: Check to make sure all file paths exist
|
|
||||||
// if (localMediaItem.coverContentUrl != null) {
|
|
||||||
// var file = DocumentFile.fromSingleUri(ctx)
|
|
||||||
// if (!file.exists()) {
|
|
||||||
// Log.e(tag, "Local media item cover url does not exist ${localMediaItem.coverContentUrl}")
|
|
||||||
// removeLocalMediaItem(localMediaItem.id)
|
|
||||||
// } else {
|
|
||||||
// localMediaItems.add(localMediaItem)
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
localLibraryItems.add(localLibraryItem)
|
localLibraryItems.add(localLibraryItem)
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return localLibraryItems
|
return localLibraryItems
|
||||||
|
|
|
@ -73,7 +73,7 @@ data class LocalLibraryItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
var dateNow = System.currentTimeMillis()
|
var dateNow = System.currentTimeMillis()
|
||||||
return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, chapters ?: mutableListOf(), mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress)
|
return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, chapters ?: mutableListOf(), mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress, "exo-player")
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
|
|
@ -2,11 +2,13 @@ package com.audiobookshelf.app.data
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.player.MediaProgressSyncData
|
import com.audiobookshelf.app.player.MediaProgressSyncData
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
import com.google.android.exoplayer2.C
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.MediaMetadata
|
import com.google.android.exoplayer2.MediaMetadata
|
||||||
import com.google.android.gms.cast.MediaInfo
|
import com.google.android.gms.cast.MediaInfo
|
||||||
|
@ -42,7 +44,8 @@ class PlaybackSession(
|
||||||
var localLibraryItem:LocalLibraryItem?,
|
var localLibraryItem:LocalLibraryItem?,
|
||||||
var localEpisodeId:String?,
|
var localEpisodeId:String?,
|
||||||
var serverConnectionConfigId:String?,
|
var serverConnectionConfigId:String?,
|
||||||
var serverAddress:String?
|
var serverAddress:String?,
|
||||||
|
var mediaPlayer:String?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
|
@ -152,9 +155,14 @@ class PlaybackSession(
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata {
|
fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata {
|
||||||
var castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER)
|
var castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER)
|
||||||
castMetadata.addImage(WebImage(getCoverUri()))
|
|
||||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle)
|
coverPath?.let {
|
||||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor)
|
castMetadata.addImage(WebImage(Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle ?: "")
|
||||||
|
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor ?: "")
|
||||||
|
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_CHAPTER_TITLE, audioTrack.title)
|
||||||
castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index)
|
castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index)
|
||||||
return castMetadata
|
return castMetadata
|
||||||
}
|
}
|
||||||
|
@ -164,21 +172,22 @@ class PlaybackSession(
|
||||||
var castMetadata = getCastMediaMetadata(audioTrack)
|
var castMetadata = getCastMediaMetadata(audioTrack)
|
||||||
|
|
||||||
var mediaUri = getContentUri(audioTrack)
|
var mediaUri = getContentUri(audioTrack)
|
||||||
var mediaInfoBuilder = MediaInfo.Builder(mediaUri.toString())
|
|
||||||
mediaInfoBuilder.setContentUrl(mediaUri.toString())
|
|
||||||
mediaInfoBuilder.setMetadata(castMetadata)
|
|
||||||
mediaInfoBuilder.setContentType(audioTrack.mimeType)
|
|
||||||
var mediaInfo = mediaInfoBuilder.build()
|
|
||||||
|
|
||||||
var queueItem = MediaQueueItem.Builder(mediaInfo)
|
var mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply {
|
||||||
queueItem.setItemId(audioTrack.index)
|
setContentUrl(mediaUri.toString())
|
||||||
queueItem.setPlaybackDuration(audioTrack.duration)
|
setContentType(audioTrack.mimeType)
|
||||||
return queueItem.build()
|
setMetadata(castMetadata)
|
||||||
|
setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return MediaQueueItem.Builder(mediaInfo).apply {
|
||||||
|
setPlaybackDuration(audioTrack.duration)
|
||||||
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun clone():PlaybackSession {
|
fun clone():PlaybackSession {
|
||||||
return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,startedAt,updatedAt,timeListening,audioTracks,currentTime,libraryItem,localLibraryItem,localEpisodeId,serverConnectionConfigId,serverAddress)
|
return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,startedAt,updatedAt,timeListening,audioTracks,currentTime,libraryItem,localLibraryItem,localEpisodeId,serverConnectionConfigId,serverAddress, mediaPlayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
|
|
@ -34,8 +34,8 @@ class MediaManager(var apiHandler: ApiHandler) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun play(libraryItem:LibraryItem, cb: (PlaybackSession) -> Unit) {
|
fun play(libraryItem:LibraryItem, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
|
||||||
apiHandler.playLibraryItem(libraryItem.id,"",false) {
|
apiHandler.playLibraryItem(libraryItem.id,"",false, mediaPlayer) {
|
||||||
cb(it)
|
cb(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,8 @@ import kotlinx.coroutines.*
|
||||||
|
|
||||||
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
|
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
|
||||||
|
|
||||||
class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
|
class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
|
||||||
private val tag = "MediaDescriptionAdapter"
|
private val tag = "MediaDescriptionAdapter"
|
||||||
private val playerNotificationService:PlayerNotificationService = playerNotificationService
|
|
||||||
|
|
||||||
var currentIconUri: Uri? = null
|
var currentIconUri: Uri? = null
|
||||||
var currentBitmap: Bitmap? = null
|
var currentBitmap: Bitmap? = null
|
||||||
|
|
|
@ -2,31 +2,30 @@ package com.audiobookshelf.app.player
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.appcompat.R
|
import androidx.appcompat.R
|
||||||
import androidx.mediarouter.app.MediaRouteChooserDialog
|
import androidx.mediarouter.app.MediaRouteChooserDialog
|
||||||
import androidx.mediarouter.media.MediaRouteSelector
|
import androidx.mediarouter.media.MediaRouteSelector
|
||||||
import androidx.mediarouter.media.MediaRouter
|
import androidx.mediarouter.media.MediaRouter
|
||||||
import com.getcapacitor.PluginCall
|
import com.getcapacitor.PluginCall
|
||||||
import com.google.android.exoplayer2.MediaItem
|
|
||||||
import com.google.android.exoplayer2.ext.cast.CastPlayer
|
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItemConverter
|
|
||||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
|
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
|
||||||
import com.google.android.gms.cast.*
|
import com.google.android.gms.cast.*
|
||||||
import com.google.android.gms.cast.framework.*
|
import com.google.android.gms.cast.framework.*
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
class CastManager constructor(playerNotificationService:PlayerNotificationService) {
|
class CastManager constructor(val mainActivity:Activity) {
|
||||||
private val tag = "CastManager"
|
private val tag = "CastManager"
|
||||||
private val playerNotificationService:PlayerNotificationService = playerNotificationService
|
|
||||||
|
|
||||||
|
private var playerNotificationService:PlayerNotificationService? = null
|
||||||
private var newConnectionListener: SessionListener? = null
|
private var newConnectionListener: SessionListener? = null
|
||||||
private var mainActivity:Activity? = null
|
|
||||||
|
|
||||||
private fun switchToPlayer(useCastPlayer:Boolean) {
|
private fun switchToPlayer(useCastPlayer:Boolean) {
|
||||||
playerNotificationService.switchToPlayer(useCastPlayer)
|
Handler(Looper.getMainLooper()).post() {
|
||||||
|
playerNotificationService?.switchToPlayer(useCastPlayer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class CastSessionAvailabilityListener : SessionAvailabilityListener {
|
private inner class CastSessionAvailabilityListener : SessionAvailabilityListener {
|
||||||
|
@ -36,6 +35,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
||||||
* remote Cast receiver rather than play audio locally.
|
* remote Cast receiver rather than play audio locally.
|
||||||
*/
|
*/
|
||||||
override fun onCastSessionAvailable() {
|
override fun onCastSessionAvailable() {
|
||||||
|
Log.d(tag, "SessionAvailabilityListener: onCastSessionAvailable")
|
||||||
switchToPlayer(true)
|
switchToPlayer(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,42 +48,39 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestSession(mainActivity: Activity, callback: RequestSessionCallback) {
|
fun requestSession(playerNotificationService: PlayerNotificationService, callback: RequestSessionCallback) {
|
||||||
this.mainActivity = mainActivity
|
this.playerNotificationService = playerNotificationService
|
||||||
|
|
||||||
mainActivity.runOnUiThread(object : Runnable {
|
mainActivity.runOnUiThread {
|
||||||
override fun run() {
|
val session: CastSession? = getSession()
|
||||||
Log.d(tag, "CAST RUNNING ON MAIN THREAD")
|
if (session == null) {
|
||||||
|
// show the "choose a connection" dialog
|
||||||
|
// Add the connection listener callback
|
||||||
|
listenForConnection(callback)
|
||||||
|
|
||||||
val session: CastSession? = getSession()
|
val builder = MediaRouteChooserDialog(mainActivity, R.style.Theme_AppCompat_NoActionBar)
|
||||||
if (session == null) {
|
builder.routeSelector = MediaRouteSelector.Builder()
|
||||||
// show the "choose a connection" dialog
|
.addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))
|
||||||
|
.build()
|
||||||
// Add the connection listener callback
|
builder.setCanceledOnTouchOutside(true)
|
||||||
listenForConnection(callback)
|
builder.setOnCancelListener {
|
||||||
|
newConnectionListener?.let { ncl ->
|
||||||
val builder = MediaRouteChooserDialog(mainActivity, R.style.Theme_AppCompat_NoActionBar)
|
getSessionManager()?.removeSessionManagerListener(ncl, CastSession::class.java)
|
||||||
builder.routeSelector = MediaRouteSelector.Builder()
|
|
||||||
.addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))
|
|
||||||
.build()
|
|
||||||
builder.setCanceledOnTouchOutside(true)
|
|
||||||
builder.setOnCancelListener {
|
|
||||||
getSessionManager()!!.removeSessionManagerListener(newConnectionListener, CastSession::class.java)
|
|
||||||
callback.onCancel()
|
|
||||||
}
|
}
|
||||||
builder.show()
|
callback.onCancel()
|
||||||
} else {
|
|
||||||
// We are are already connected, so show the "connection options" Dialog
|
|
||||||
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
|
|
||||||
if (session.castDevice != null) {
|
|
||||||
builder.setTitle(session.castDevice.friendlyName)
|
|
||||||
}
|
|
||||||
builder.setOnDismissListener { callback.onCancel() }
|
|
||||||
builder.setPositiveButton("Stop Casting") { dialog, which -> endSession(true, null) }
|
|
||||||
builder.show()
|
|
||||||
}
|
}
|
||||||
|
builder.show()
|
||||||
|
} else {
|
||||||
|
// We are are already connected, so show the "connection options" Dialog
|
||||||
|
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
|
||||||
|
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 {
|
abstract class RequestSessionCallback : ConnectionCallback {
|
||||||
|
@ -103,12 +100,10 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
||||||
fun endSession(stopCasting: Boolean, pluginCall: PluginCall?) {
|
fun endSession(stopCasting: Boolean, pluginCall: PluginCall?) {
|
||||||
|
|
||||||
getSessionManager()!!.addSessionManagerListener(object : SessionListener() {
|
getSessionManager()!!.addSessionManagerListener(object : SessionListener() {
|
||||||
override fun onSessionEnded(castSession: CastSession?, error: Int) {
|
override fun onSessionEnded(castSession: CastSession, error: Int) {
|
||||||
getSessionManager()!!.removeSessionManagerListener(this, CastSession::class.java)
|
getSessionManager()!!.removeSessionManagerListener(this, CastSession::class.java)
|
||||||
Log.d(tag, "CAST END SESSION")
|
Log.d(tag, "CAST END SESSION")
|
||||||
// media.setSession(null)
|
|
||||||
pluginCall?.resolve()
|
pluginCall?.resolve()
|
||||||
// listener.onSessionEnd(ChromecastUtilities.createSessionObject(castSession, if (stopCasting) "stopped" else "disconnected"))
|
|
||||||
}
|
}
|
||||||
}, CastSession::class.java)
|
}, CastSession::class.java)
|
||||||
getSessionManager()!!.endCurrentSession(stopCasting)
|
getSessionManager()!!.endCurrentSession(stopCasting)
|
||||||
|
@ -116,78 +111,47 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
||||||
}
|
}
|
||||||
|
|
||||||
open class SessionListener : SessionManagerListener<CastSession> {
|
open class SessionListener : SessionManagerListener<CastSession> {
|
||||||
override fun onSessionStarting(castSession: CastSession?) {}
|
override fun onSessionStarting(castSession: CastSession) {}
|
||||||
override fun onSessionStarted(castSession: CastSession?, sessionId: String) {}
|
override fun onSessionStarted(castSession: CastSession, sessionId: String) {}
|
||||||
override fun onSessionStartFailed(castSession: CastSession?, error: Int) {}
|
override fun onSessionStartFailed(castSession: CastSession, error: Int) {}
|
||||||
override fun onSessionEnding(castSession: CastSession?) {}
|
override fun onSessionEnding(castSession: CastSession) {}
|
||||||
override fun onSessionEnded(castSession: CastSession?, error: Int) {}
|
override fun onSessionEnded(castSession: CastSession, error: Int) {}
|
||||||
override fun onSessionResuming(castSession: CastSession?, sessionId: String) {}
|
override fun onSessionResuming(castSession: CastSession, sessionId: String) {}
|
||||||
override fun onSessionResumed(castSession: CastSession?, wasSuspended: Boolean) {}
|
override fun onSessionResumed(castSession: CastSession, wasSuspended: Boolean) {}
|
||||||
override fun onSessionResumeFailed(castSession: CastSession?, error: Int) {}
|
override fun onSessionResumeFailed(castSession: CastSession, error: Int) {}
|
||||||
override fun onSessionSuspended(castSession: CastSession?, reason: Int) {}
|
override fun onSessionSuspended(castSession: CastSession, reason: Int) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRouteScan() {
|
fun startRouteScan(connListener:ChromecastListener) {
|
||||||
var connListener = object: ChromecastListener() {
|
|
||||||
override fun onReceiverAvailableUpdate(available: Boolean) {
|
|
||||||
Log.d(tag, "CAST RECEIVER UPDATE AVAILABLE $available")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSessionRejoin(jsonSession: JSONObject?) {
|
|
||||||
Log.d(tag, "CAST onSessionRejoin")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMediaLoaded(jsonMedia: JSONObject?) {
|
|
||||||
Log.d(tag, "CAST onMediaLoaded")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMediaUpdate(jsonMedia: JSONObject?) {
|
|
||||||
Log.d(tag, "CAST onMediaUpdate")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSessionUpdate(jsonSession: JSONObject?) {
|
|
||||||
Log.d(tag, "CAST onSessionUpdate")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSessionEnd(jsonSession: JSONObject?) {
|
|
||||||
Log.d(tag, "CAST onSessionEnd")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessageReceived(p0: CastDevice, p1: String, p2: String) {
|
|
||||||
Log.d(tag, "CAST onMessageReceived")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var callback = object : ScanCallback() {
|
var callback = object : ScanCallback() {
|
||||||
override fun onRouteUpdate(routes: List<MediaRouter.RouteInfo>?) {
|
override fun onRouteUpdate(routes: List<MediaRouter.RouteInfo>?) {
|
||||||
Log.d(tag, "CAST On ROUTE UPDATED ${routes?.size} | ${getContext().castState}")
|
Log.d(tag, "CAST On ROUTE UPDATED ${routes?.size} | ${getContext().castState}")
|
||||||
// if the routes have changed, we may have an available device
|
// if the routes have changed, we may have an available device
|
||||||
// If there is at least one device available
|
// If there is at least one device available
|
||||||
if (getContext().castState != CastState.NO_DEVICES_AVAILABLE) {
|
if (getContext().castState != CastState.NO_DEVICES_AVAILABLE) {
|
||||||
|
|
||||||
routes?.forEach { Log.d(tag, "CAST ROUTE ${it.description} | ${it.deviceType} | ${it.isBluetooth} | ${it.name}") }
|
routes?.forEach { Log.d(tag, "CAST ROUTE ${it.description} | ${it.deviceType} | ${it.isBluetooth} | ${it.name}") }
|
||||||
|
|
||||||
// Stop the scan
|
// Stop the scan
|
||||||
stopRouteScan(this, null);
|
stopRouteScan(this, null)
|
||||||
// Let the client know a receiver is available
|
// Let the client know a receiver is available
|
||||||
connListener.onReceiverAvailableUpdate(true);
|
connListener.onReceiverAvailableUpdate(true)
|
||||||
// Since we have a receiver we may also have an active session
|
// Since we have a receiver we may also have an active session
|
||||||
var session = getSessionManager()?.currentCastSession;
|
var session = getSessionManager()?.currentCastSession
|
||||||
// If we do have a session
|
// If we do have a session
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
// Let the client know
|
// Let the client know
|
||||||
Log.d(tag, "LET SESSION KNOW ABOUT")
|
|
||||||
// media.setSession(session);
|
|
||||||
// connListener.onSessionRejoin(ChromecastUtilities.createSessionObject(session));
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "No cast devices available")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callback.setMediaRouter(getMediaRouter())
|
callback.setMediaRouter(getMediaRouter())
|
||||||
|
|
||||||
callback.onFilteredRouteUpdate();
|
callback.onFilteredRouteUpdate()
|
||||||
|
|
||||||
getMediaRouter()!!.addCallback(MediaRouteSelector.Builder()
|
getMediaRouter()?.addCallback(MediaRouteSelector.Builder()
|
||||||
.addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))
|
.addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))
|
||||||
.build(),
|
.build(),
|
||||||
callback,
|
callback,
|
||||||
|
@ -201,7 +165,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
||||||
fun onSessionEnd(jsonSession: JSONObject?)
|
fun onSessionEnd(jsonSession: JSONObject?)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal abstract class ChromecastListener : CastStateListener, CastListener {
|
abstract class ChromecastListener : CastStateListener, CastListener {
|
||||||
abstract fun onReceiverAvailableUpdate(available: Boolean)
|
abstract fun onReceiverAvailableUpdate(available: Boolean)
|
||||||
abstract fun onSessionRejoin(jsonSession: JSONObject?)
|
abstract fun onSessionRejoin(jsonSession: JSONObject?)
|
||||||
|
|
||||||
|
@ -212,15 +176,19 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopRouteScan(callback: ScanCallback?, completionCallback: Runnable?) {
|
fun stopRouteScan(callback: ScanCallback?, completionCallback: Runnable?) {
|
||||||
|
Log.d(tag, "stopRouteScan")
|
||||||
if (callback == null) {
|
if (callback == null) {
|
||||||
completionCallback!!.run()
|
completionCallback?.run()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// ctx.runOnUiThread(Runnable {
|
|
||||||
callback.stop()
|
// mainActivity.runOnUiThread {
|
||||||
getMediaRouter()!!.removeCallback(callback)
|
Log.d(tag, "Removing callback on media router")
|
||||||
completionCallback?.run()
|
callback.stop()
|
||||||
// })
|
getMediaRouter()?.removeCallback(callback)
|
||||||
|
completionCallback?.run()
|
||||||
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class ScanCallback : MediaRouter.Callback() {
|
abstract class ScanCallback : MediaRouter.Callback() {
|
||||||
|
@ -271,7 +239,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
||||||
}
|
}
|
||||||
if (!route.isDefault
|
if (!route.isDefault
|
||||||
&& !route.description.equals("Google Cast Multizone Member")
|
&& !route.description.equals("Google Cast Multizone Member")
|
||||||
&& route.playbackType === MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) {
|
&& route.playbackType == MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) {
|
||||||
outRoutes.add(route)
|
outRoutes.add(route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -291,87 +259,50 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class CustomConverter : MediaItemConverter {
|
|
||||||
override fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem {
|
|
||||||
// The MediaQueueItem you build is expected to be in the tag.
|
|
||||||
var queueItem = (mediaItem.playbackProperties!!.tag as MediaQueueItem?)!!
|
|
||||||
Log.d(tag, "Test toMediaQueueItem ${queueItem.media!!.contentUrl} | ${queueItem.playbackDuration} | ${queueItem.itemId}")
|
|
||||||
return queueItem
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toMediaItem(mediaQueueItem: MediaQueueItem): MediaItem {
|
|
||||||
return MediaItem.Builder()
|
|
||||||
.setUri(mediaQueueItem.media!!.contentUrl)
|
|
||||||
.setTag(mediaQueueItem)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun listenForConnection(callback: ConnectionCallback) {
|
private fun listenForConnection(callback: ConnectionCallback) {
|
||||||
// We should only ever have one of these listeners active at a time, so remove previous
|
// We should only ever have one of these listeners active at a time, so remove previous
|
||||||
getSessionManager()?.removeSessionManagerListener(newConnectionListener, CastSession::class.java)
|
newConnectionListener?.let { ncl ->
|
||||||
|
getSessionManager()?.removeSessionManagerListener(ncl, CastSession::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
newConnectionListener = object : SessionListener() {
|
newConnectionListener = object : SessionListener() {
|
||||||
override fun onSessionStarted(castSession: CastSession?, sessionId: String) {
|
override fun onSessionStarted(castSession: CastSession, sessionId: String) {
|
||||||
Log.d(tag, "CAST SESSION STARTED ${castSession?.castDevice?.friendlyName}")
|
Log.d(tag, "CAST SESSION STARTED ${castSession.castDevice?.friendlyName}")
|
||||||
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
|
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
|
||||||
|
|
||||||
try {
|
val castContext = CastContext.getSharedInstance(mainActivity)
|
||||||
val castContext = CastContext.getSharedInstance(mainActivity)
|
|
||||||
|
|
||||||
// Work in progress using the cast api
|
playerNotificationService?.let {
|
||||||
var currentSession = playerNotificationService.getCurrentPlaybackSessionCopy()
|
if (it.castPlayer == null) {
|
||||||
var firstTrack = currentSession?.audioTracks?.get(0)
|
Log.d(tag, "Initializing castPlayer on session started - switch to cast player")
|
||||||
var uri = firstTrack?.let { currentSession?.getContentUri(it) } ?: Uri.EMPTY
|
it.castPlayer = CastPlayer(castContext).apply {
|
||||||
var url = uri.toString()
|
addListener(PlayerListener(it))
|
||||||
var mimeType = firstTrack?.mimeType ?: ""
|
setSessionAvailabilityListener(CastSessionAvailabilityListener())
|
||||||
var castMediaMetadata = firstTrack?.let { currentSession?.getCastMediaMetadata(it) }
|
}
|
||||||
Log.d(tag, "CastManager set url $url")
|
switchToPlayer(true)
|
||||||
var duration = (currentSession?.getTotalDuration() ?: 0L * 1000L).toLong()
|
} else {
|
||||||
|
Log.d(tag, "castPlayer is already initialized on session started")
|
||||||
if (castMediaMetadata != null) {
|
|
||||||
Log.d(tag, "CastManager duration $duration got cast media metadata $castMediaMetadata")
|
|
||||||
|
|
||||||
val mediaInfo = MediaInfo.Builder(url)
|
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
|
||||||
.setContentType(mimeType)
|
|
||||||
.setMetadata(castMediaMetadata)
|
|
||||||
.setStreamDuration(duration)
|
|
||||||
.build()
|
|
||||||
val remoteMediaClient = castSession?.remoteMediaClient
|
|
||||||
remoteMediaClient?.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not working using the exo player CastPlayer
|
|
||||||
// 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)) {
|
if (callback.onSessionStartFailed(errCode)) {
|
||||||
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
|
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSessionEnded(castSession: CastSession?, errCode: Int) {
|
override fun onSessionEnded(castSession: CastSession, errCode: Int) {
|
||||||
if (callback.onSessionEndedBeforeStart(errCode)) {
|
if (callback.onSessionEndedBeforeStart(errCode)) {
|
||||||
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
|
getSessionManager()?.removeSessionManagerListener(this, CastSession::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSessionManager()?.addSessionManagerListener(newConnectionListener, CastSession::class.java)
|
newConnectionListener?.let {
|
||||||
|
Log.d(tag, "Add session manager listener")
|
||||||
|
getSessionManager()?.addSessionManagerListener(it, CastSession::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getContext(): CastContext {
|
private fun getContext(): CastContext {
|
||||||
|
@ -383,7 +314,7 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMediaRouter(): MediaRouter? {
|
private fun getMediaRouter(): MediaRouter? {
|
||||||
return mainActivity?.let { MediaRouter.getInstance(it) }
|
return MediaRouter.getInstance(mainActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSession(): CastSession? {
|
private fun getSession(): CastSession? {
|
||||||
|
|
|
@ -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() {
|
override fun onPrepare() {
|
||||||
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
||||||
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li) {
|
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
||||||
Log.d(tag, "About to prepare player with li ${li.title}")
|
Log.d(tag, "About to prepare player with li ${li.title}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true)
|
playerNotificationService.preparePlayer(it,true)
|
||||||
|
@ -48,7 +48,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||||
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PLAY FROM SEARCH $query")
|
Log.d(tag, "ON PLAY FROM SEARCH $query")
|
||||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li) {
|
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
||||||
Log.d(tag, "About to prepare player with li ${li.title}")
|
Log.d(tag, "About to prepare player with li ${li.title}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true)
|
playerNotificationService.preparePlayer(it,true)
|
||||||
|
@ -96,7 +96,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryItem?.let { li ->
|
libraryItem?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li) {
|
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
||||||
Log.d(tag, "About to prepare player with li ${li.title}")
|
Log.d(tag, "About to prepare player with li ${li.title}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true)
|
playerNotificationService.preparePlayer(it,true)
|
||||||
|
|
|
@ -8,14 +8,13 @@ import android.os.ResultReceiver
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
import com.audiobookshelf.app.data.LibraryItem
|
||||||
import com.google.android.exoplayer2.ControlDispatcher
|
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
|
|
||||||
class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificationService) : MediaSessionConnector.PlaybackPreparer {
|
class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificationService) : MediaSessionConnector.PlaybackPreparer {
|
||||||
var tag = "MediaSessionPlaybackPreparer"
|
var tag = "MediaSessionPlaybackPreparer"
|
||||||
|
|
||||||
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
|
override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
|
||||||
Log.d(tag, "ON COMMAND $command")
|
Log.d(tag, "ON COMMAND $command")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -30,7 +29,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
||||||
override fun onPrepare(playWhenReady: Boolean) {
|
override fun onPrepare(playWhenReady: Boolean) {
|
||||||
Log.d(tag, "ON PREPARE $playWhenReady")
|
Log.d(tag, "ON PREPARE $playWhenReady")
|
||||||
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li) {
|
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady)
|
playerNotificationService.preparePlayer(it,playWhenReady)
|
||||||
}
|
}
|
||||||
|
@ -43,7 +42,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
||||||
|
|
||||||
var libraryItem: LibraryItem? = playerNotificationService.mediaManager.getById(mediaId)
|
var libraryItem: LibraryItem? = playerNotificationService.mediaManager.getById(mediaId)
|
||||||
libraryItem?.let { li ->
|
libraryItem?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li) {
|
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
||||||
Log.d(tag, "About to prepare player with li ${li.title}")
|
Log.d(tag, "About to prepare player with li ${li.title}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady)
|
playerNotificationService.preparePlayer(it,playWhenReady)
|
||||||
|
@ -55,7 +54,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
||||||
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
||||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li) {
|
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
||||||
Log.d(tag, "About to prepare player with li ${li.title}")
|
Log.d(tag, "About to prepare player with li ${li.title}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady)
|
playerNotificationService.preparePlayer(it,playWhenReady)
|
||||||
|
|
|
@ -21,17 +21,21 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
|
Log.d(tag, "onEvents ${player.deviceInfo} | ${playerNotificationService.getMediaPlayer()} | ${events.size()}")
|
||||||
|
|
||||||
if (events.contains(Player.EVENT_POSITION_DISCONTINUITY)) {
|
if (events.contains(Player.EVENT_POSITION_DISCONTINUITY)) {
|
||||||
Log.d(tag, "EVENT_POSITION_DISCONTINUITY")
|
Log.d(tag, "EVENT_POSITION_DISCONTINUITY")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (events.contains(Player.EVENT_IS_LOADING_CHANGED)) {
|
if (events.contains(Player.EVENT_IS_LOADING_CHANGED)) {
|
||||||
Log.d(tag, "EVENT_IS_LOADING_CHANGED : " + playerNotificationService.mPlayer.isLoading.toString())
|
Log.d(tag, "EVENT_IS_LOADING_CHANGED : " + playerNotificationService.currentPlayer.isLoading)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
|
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
|
||||||
|
Log.d(tag, "EVENT_PLAYBACK_STATE_CHANGED MediaPlayer = ${playerNotificationService.getMediaPlayer()}")
|
||||||
|
|
||||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_READY) {
|
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_READY) {
|
||||||
Log.d(tag, "STATE_READY : " + playerNotificationService.mPlayer.duration.toString())
|
Log.d(tag, "STATE_READY : " + playerNotificationService.currentPlayer.duration)
|
||||||
|
|
||||||
if (lastPauseTime == 0L) {
|
if (lastPauseTime == 0L) {
|
||||||
lastPauseTime = -1;
|
lastPauseTime = -1;
|
||||||
|
@ -39,7 +43,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||||
playerNotificationService.sendClientMetadata(PlayerState.READY)
|
playerNotificationService.sendClientMetadata(PlayerState.READY)
|
||||||
}
|
}
|
||||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_BUFFERING) {
|
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_BUFFERING) {
|
||||||
Log.d(tag, "STATE_BUFFERING : " + playerNotificationService.mPlayer.currentPosition.toString())
|
Log.d(tag, "STATE_BUFFERING : " + playerNotificationService.currentPlayer.currentPosition)
|
||||||
playerNotificationService.sendClientMetadata(PlayerState.BUFFERING)
|
playerNotificationService.sendClientMetadata(PlayerState.BUFFERING)
|
||||||
}
|
}
|
||||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
|
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
|
||||||
|
@ -53,13 +57,13 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||||
}
|
}
|
||||||
|
|
||||||
if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) {
|
if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) {
|
||||||
Log.d(tag, "EVENT_MEDIA_METADATA_CHANGED")
|
Log.d(tag, "EVENT_MEDIA_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
|
||||||
}
|
}
|
||||||
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
|
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
|
||||||
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED")
|
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
|
||||||
}
|
}
|
||||||
if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
|
if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
|
||||||
Log.d(tag, "EVENT IS PLAYING CHANGED")
|
Log.d(tag, "EVENT IS PLAYING CHANGED ${playerNotificationService.getMediaPlayer()}")
|
||||||
|
|
||||||
if (player.isPlaying) {
|
if (player.isPlaying) {
|
||||||
if (lastPauseTime > 0) {
|
if (lastPauseTime > 0) {
|
||||||
|
|
|
@ -22,19 +22,15 @@ import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.media.MediaManager
|
import com.audiobookshelf.app.media.MediaManager
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import com.getcapacitor.JSObject
|
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||||
import com.google.android.exoplayer2.ext.cast.CastPlayer
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
||||||
import com.google.android.exoplayer2.source.MediaSource
|
import com.google.android.exoplayer2.source.MediaSource
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||||
import com.google.android.exoplayer2.ui.PlayerControlView
|
|
||||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||||
import com.google.android.exoplayer2.upstream.*
|
import com.google.android.exoplayer2.upstream.*
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
|
@ -56,6 +52,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
fun onSleepTimerSet(sleepTimeRemaining: Int)
|
fun onSleepTimerSet(sleepTimeRemaining: Int)
|
||||||
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
|
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
|
||||||
fun onPlaybackFailed(errorMessage:String)
|
fun onPlaybackFailed(errorMessage:String)
|
||||||
|
fun onMediaPlayerChanged(mediaPlayer:String)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tag = "PlayerService"
|
private val tag = "PlayerService"
|
||||||
|
@ -68,6 +65,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
private lateinit var playerNotificationManager: PlayerNotificationManager
|
private lateinit var playerNotificationManager: PlayerNotificationManager
|
||||||
private lateinit var mediaSession: MediaSessionCompat
|
private lateinit var mediaSession: MediaSessionCompat
|
||||||
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
||||||
|
|
||||||
lateinit var mediaManager: MediaManager
|
lateinit var mediaManager: MediaManager
|
||||||
lateinit var apiHandler: ApiHandler
|
lateinit var apiHandler: ApiHandler
|
||||||
|
|
||||||
|
@ -76,7 +74,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
var castPlayer:CastPlayer? = null
|
var castPlayer:CastPlayer? = null
|
||||||
|
|
||||||
lateinit var sleepTimerManager:SleepTimerManager
|
lateinit var sleepTimerManager:SleepTimerManager
|
||||||
lateinit var castManager:CastManager
|
|
||||||
lateinit var mediaProgressSyncer:MediaProgressSyncer
|
lateinit var mediaProgressSyncer:MediaProgressSyncer
|
||||||
|
|
||||||
private var notificationId = 10;
|
private var notificationId = 10;
|
||||||
|
@ -144,6 +141,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
playerNotificationManager.setPlayer(null)
|
playerNotificationManager.setPlayer(null)
|
||||||
mPlayer.release()
|
mPlayer.release()
|
||||||
|
castPlayer?.release()
|
||||||
mediaSession.release()
|
mediaSession.release()
|
||||||
mediaProgressSyncer.reset()
|
mediaProgressSyncer.reset()
|
||||||
Log.d(tag, "onDestroy")
|
Log.d(tag, "onDestroy")
|
||||||
|
@ -190,9 +188,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
// Initialize sleep timer
|
// Initialize sleep timer
|
||||||
sleepTimerManager = SleepTimerManager(this)
|
sleepTimerManager = SleepTimerManager(this)
|
||||||
|
|
||||||
// Initialize Cast Manager
|
|
||||||
castManager = CastManager(this)
|
|
||||||
|
|
||||||
// Initialize Media Progress Syncer
|
// Initialize Media Progress Syncer
|
||||||
mediaProgressSyncer = MediaProgressSyncer(this, apiHandler)
|
mediaProgressSyncer = MediaProgressSyncer(this, apiHandler)
|
||||||
|
|
||||||
|
@ -282,7 +277,23 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
User callable methods
|
User callable methods
|
||||||
*/
|
*/
|
||||||
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean) {
|
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean) {
|
||||||
|
playbackSession.mediaPlayer = getMediaPlayer()
|
||||||
|
|
||||||
|
if (playbackSession.mediaPlayer == "cast-player" && playbackSession.isLocal) {
|
||||||
|
Log.w(tag, "Cannot cast local media item - switching player")
|
||||||
|
currentPlaybackSession = null
|
||||||
|
switchToPlayer(false)
|
||||||
|
playbackSession.mediaPlayer = getMediaPlayer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playbackSession.mediaPlayer == "cast-player") {
|
||||||
|
// If cast-player is the first player to be used
|
||||||
|
mediaSessionConnector.setPlayer(castPlayer)
|
||||||
|
playerNotificationManager.setPlayer(castPlayer)
|
||||||
|
}
|
||||||
|
|
||||||
currentPlaybackSession = playbackSession
|
currentPlaybackSession = playbackSession
|
||||||
|
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
|
||||||
|
|
||||||
clientEventEmitter?.onPlaybackSession(playbackSession)
|
clientEventEmitter?.onPlaybackSession(playbackSession)
|
||||||
|
|
||||||
|
@ -309,39 +320,45 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
mPlayer.setMediaSource(mediaSource)
|
mPlayer.setMediaSource(mediaSource)
|
||||||
|
|
||||||
|
|
||||||
|
// Add remaining media items if multi-track
|
||||||
|
if (mediaItems.size > 1) {
|
||||||
|
currentPlayer.addMediaItems(mediaItems.subList(1, mediaItems.size))
|
||||||
|
Log.d(tag, "currentPlayer total media items ${currentPlayer.mediaItemCount}")
|
||||||
|
|
||||||
|
var currentTrackIndex = playbackSession.getCurrentTrackIndex()
|
||||||
|
var currentTrackTime = playbackSession.getCurrentTrackTimeMs()
|
||||||
|
Log.d(tag, "currentPlayer current track index $currentTrackIndex & current track time $currentTrackTime")
|
||||||
|
currentPlayer.seekTo(currentTrackIndex, currentTrackTime)
|
||||||
|
} else {
|
||||||
|
currentPlayer.seekTo(playbackSession.currentTimeMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(tag, "Prepare complete for session ${currentPlaybackSession?.displayTitle} | ${currentPlayer.mediaItemCount}")
|
||||||
|
currentPlayer.playWhenReady = playWhenReady
|
||||||
|
currentPlayer.setPlaybackSpeed(1f) // TODO: Playback speed should come from settings
|
||||||
|
currentPlayer.prepare()
|
||||||
|
|
||||||
} else if (castPlayer != null) {
|
} 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) {
|
|
||||||
currentPlayer.addMediaItems(mediaItems.subList(1, mediaItems.size))
|
|
||||||
Log.d(tag, "currentPlayer total media items ${currentPlayer.mediaItemCount}")
|
|
||||||
|
|
||||||
var currentTrackIndex = playbackSession.getCurrentTrackIndex()
|
var currentTrackIndex = playbackSession.getCurrentTrackIndex()
|
||||||
var currentTrackTime = playbackSession.getCurrentTrackTimeMs()
|
var currentTrackTime = playbackSession.getCurrentTrackTimeMs()
|
||||||
Log.d(tag, "currentPlayer current track index $currentTrackIndex & current track time $currentTrackTime")
|
var mediaType = playbackSession.mediaType
|
||||||
currentPlayer.seekTo(currentTrackIndex, currentTrackTime)
|
Log.d(tag, "Loading cast player $currentTrackIndex $currentTrackTime $mediaType")
|
||||||
} else {
|
|
||||||
currentPlayer.seekTo(playbackSession.currentTimeMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(tag, "Prepare complete for session ${currentPlaybackSession?.displayTitle} | ${currentPlayer.mediaItemCount}")
|
castPlayer?.load(mediaItems, currentTrackIndex, currentTrackTime, playWhenReady, 1f, mediaType)
|
||||||
currentPlayer.playWhenReady = playWhenReady
|
}
|
||||||
currentPlayer.setPlaybackSpeed(1f) // TODO: Playback speed should come from settings
|
|
||||||
currentPlayer.prepare()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handlePlayerPlaybackError(errorMessage:String) {
|
fun handlePlayerPlaybackError(errorMessage:String) {
|
||||||
// On error and was attempting to direct play - fallback to transcode
|
// On error and was attempting to direct play - fallback to transcode
|
||||||
currentPlaybackSession?.let { playbackSession ->
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
if (playbackSession.isDirectPlay) {
|
if (playbackSession.isDirectPlay) {
|
||||||
Log.d(tag, "Fallback to transcode")
|
var mediaPlayer = getMediaPlayer()
|
||||||
|
Log.d(tag, "Fallback to transcode $mediaPlayer")
|
||||||
|
|
||||||
var libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
var libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
||||||
var episodeId = playbackSession.episodeId
|
var episodeId = playbackSession.episodeId
|
||||||
apiHandler.playLibraryItem(libraryItemId, episodeId, true) {
|
apiHandler.playLibraryItem(libraryItemId, episodeId, true, mediaPlayer) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
preparePlayer(it, true)
|
preparePlayer(it, true)
|
||||||
}
|
}
|
||||||
|
@ -354,17 +371,48 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun switchToPlayer(useCastPlayer: Boolean) {
|
fun switchToPlayer(useCastPlayer: Boolean) {
|
||||||
|
var wasPlaying = currentPlayer.isPlaying
|
||||||
|
if (useCastPlayer) {
|
||||||
|
if (currentPlayer == castPlayer) {
|
||||||
|
Log.d(tag, "switchToPlayer: Already using Cast Player " + castPlayer?.deviceInfo)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "switchToPlayer: Switching to cast player from exo player stop exo player")
|
||||||
|
mPlayer.stop()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (currentPlayer == mPlayer) {
|
||||||
|
Log.d(tag, "switchToPlayer: Already using Exo Player " + mPlayer.deviceInfo)
|
||||||
|
return
|
||||||
|
} else if (castPlayer != null) {
|
||||||
|
Log.d(tag, "switchToPlayer: Switching to exo player from cast player stop cast player")
|
||||||
|
castPlayer?.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentPlayer = if (useCastPlayer) {
|
currentPlayer = if (useCastPlayer) {
|
||||||
Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo)
|
Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo)
|
||||||
mediaSessionConnector.setPlayer(castPlayer)
|
mediaSessionConnector.setPlayer(castPlayer)
|
||||||
|
playerNotificationManager.setPlayer(castPlayer)
|
||||||
castPlayer as CastPlayer
|
castPlayer as CastPlayer
|
||||||
} else {
|
} else {
|
||||||
Log.d(tag, "switchToPlayer: Using ExoPlayer")
|
Log.d(tag, "switchToPlayer: Using ExoPlayer")
|
||||||
mediaSessionConnector.setPlayer(mPlayer)
|
mediaSessionConnector.setPlayer(mPlayer)
|
||||||
|
playerNotificationManager.setPlayer(mPlayer)
|
||||||
mPlayer
|
mPlayer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientEventEmitter?.onMediaPlayerChanged(getMediaPlayer())
|
||||||
|
|
||||||
|
if (currentPlaybackSession == null) {
|
||||||
|
Log.d(tag, "switchToPlayer: No Current playback session")
|
||||||
|
}
|
||||||
|
|
||||||
currentPlaybackSession?.let {
|
currentPlaybackSession?.let {
|
||||||
Log.d(tag, "switchToPlayer: Preparing current playback session ${it.displayTitle}")
|
Log.d(tag, "switchToPlayer: Preparing current playback session ${it.displayTitle}")
|
||||||
|
if (wasPlaying) { // media is paused when switching players
|
||||||
|
clientEventEmitter?.onPlayingUpdate(false)
|
||||||
|
}
|
||||||
preparePlayer(it, false)
|
preparePlayer(it, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -441,6 +489,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekPlayer(time: Long) {
|
fun seekPlayer(time: Long) {
|
||||||
|
Log.d(tag, "seekPlayer mediaCount = ${currentPlayer.mediaItemCount} | $time")
|
||||||
if (currentPlayer.mediaItemCount > 1) {
|
if (currentPlayer.mediaItemCount > 1) {
|
||||||
currentPlaybackSession?.currentTime = time / 1000.0
|
currentPlaybackSession?.currentTime = time / 1000.0
|
||||||
var newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
|
var newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
|
||||||
|
@ -452,11 +501,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekForward(amount: Long) {
|
fun seekForward(amount: Long) {
|
||||||
currentPlayer.seekTo(mPlayer.currentPosition + amount)
|
currentPlayer.seekTo(currentPlayer.currentPosition + amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekBackward(amount: Long) {
|
fun seekBackward(amount: Long) {
|
||||||
currentPlayer.seekTo(mPlayer.currentPosition - amount)
|
currentPlayer.seekTo(currentPlayer.currentPosition - amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPlaybackSpeed(speed: Float) {
|
fun setPlaybackSpeed(speed: Float) {
|
||||||
|
@ -465,6 +514,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
fun closePlayback() {
|
fun closePlayback() {
|
||||||
currentPlayer.clearMediaItems()
|
currentPlayer.clearMediaItems()
|
||||||
|
currentPlayer.stop()
|
||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
clientEventEmitter?.onPlaybackClosed()
|
clientEventEmitter?.onPlaybackClosed()
|
||||||
PlayerListener.lastPauseTime = 0
|
PlayerListener.lastPauseTime = 0
|
||||||
|
@ -475,6 +525,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
clientEventEmitter?.onMetadata(PlaybackMetadata(duration, getCurrentTimeSeconds(), playerState))
|
clientEventEmitter?.onMetadata(PlaybackMetadata(duration, getCurrentTimeSeconds(), playerState))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMediaPlayer():String {
|
||||||
|
return if(currentPlayer == castPlayer) "cast-player" else "exo-player"
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||||
//
|
//
|
||||||
|
|
|
@ -15,6 +15,7 @@ import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.getcapacitor.*
|
import com.getcapacitor.*
|
||||||
import com.getcapacitor.annotation.CapacitorPlugin
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
|
import com.google.android.gms.cast.CastDevice
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
@CapacitorPlugin(name = "AbsAudioPlayer")
|
@CapacitorPlugin(name = "AbsAudioPlayer")
|
||||||
|
@ -24,12 +25,18 @@ class AbsAudioPlayer : Plugin() {
|
||||||
|
|
||||||
lateinit var mainActivity: MainActivity
|
lateinit var mainActivity: MainActivity
|
||||||
lateinit var apiHandler:ApiHandler
|
lateinit var apiHandler:ApiHandler
|
||||||
|
lateinit var castManager:CastManager
|
||||||
|
|
||||||
lateinit var playerNotificationService: PlayerNotificationService
|
lateinit var playerNotificationService: PlayerNotificationService
|
||||||
|
|
||||||
|
private var isCastAvailable:Boolean = false
|
||||||
|
|
||||||
override fun load() {
|
override fun load() {
|
||||||
mainActivity = (activity as MainActivity)
|
mainActivity = (activity as MainActivity)
|
||||||
apiHandler = ApiHandler(mainActivity)
|
apiHandler = ApiHandler(mainActivity)
|
||||||
|
|
||||||
|
initCastManager()
|
||||||
|
|
||||||
var foregroundServiceReady : () -> Unit = {
|
var foregroundServiceReady : () -> Unit = {
|
||||||
playerNotificationService = mainActivity.foregroundService
|
playerNotificationService = mainActivity.foregroundService
|
||||||
|
|
||||||
|
@ -72,17 +79,58 @@ class AbsAudioPlayer : Plugin() {
|
||||||
override fun onPlaybackFailed(errorMessage: String) {
|
override fun onPlaybackFailed(errorMessage: String) {
|
||||||
emit("onPlaybackFailed", errorMessage)
|
emit("onPlaybackFailed", errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMediaPlayerChanged(mediaPlayer:String) {
|
||||||
|
emit("onMediaPlayerChanged", mediaPlayer)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
mainActivity.pluginCallback = foregroundServiceReady
|
mainActivity.pluginCallback = foregroundServiceReady
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emit(evtName: String, value: Any) {
|
fun emit(evtName: String, value: Any) {
|
||||||
var ret:JSObject = JSObject()
|
var ret = JSObject()
|
||||||
ret.put("value", value)
|
ret.put("value", value)
|
||||||
notifyListeners(evtName, ret)
|
notifyListeners(evtName, ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun initCastManager() {
|
||||||
|
var connListener = object: CastManager.ChromecastListener() {
|
||||||
|
override fun onReceiverAvailableUpdate(available: Boolean) {
|
||||||
|
Log.d(tag, "ChromecastListener: CAST Receiver Update Available $available")
|
||||||
|
isCastAvailable = available
|
||||||
|
emit("onCastAvailableUpdate", available)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionRejoin(jsonSession: JSONObject?) {
|
||||||
|
Log.d(tag, "ChromecastListener: CAST onSessionRejoin")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMediaLoaded(jsonMedia: JSONObject?) {
|
||||||
|
Log.d(tag, "ChromecastListener: CAST onMediaLoaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMediaUpdate(jsonMedia: JSONObject?) {
|
||||||
|
Log.d(tag, "ChromecastListener: CAST onMediaUpdate")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionUpdate(jsonSession: JSONObject?) {
|
||||||
|
Log.d(tag, "ChromecastListener: CAST onSessionUpdate")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionEnd(jsonSession: JSONObject?) {
|
||||||
|
Log.d(tag, "ChromecastListener: CAST onSessionEnd")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessageReceived(p0: CastDevice, p1: String, p2: String) {
|
||||||
|
Log.d(tag, "ChromecastListener: CAST onMessageReceived")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
castManager = CastManager(mainActivity)
|
||||||
|
castManager.startRouteScan(connListener)
|
||||||
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun prepareLibraryItem(call: PluginCall) {
|
fun prepareLibraryItem(call: PluginCall) {
|
||||||
// Need to make sure the player service has been started
|
// Need to make sure the player service has been started
|
||||||
|
@ -122,7 +170,9 @@ class AbsAudioPlayer : Plugin() {
|
||||||
return call.resolve(JSObject())
|
return call.resolve(JSObject())
|
||||||
}
|
}
|
||||||
} else { // Play library item from server
|
} else { // Play library item from server
|
||||||
apiHandler.playLibraryItem(libraryItemId, episodeId, false) {
|
var mediaPlayer = playerNotificationService.getMediaPlayer()
|
||||||
|
|
||||||
|
apiHandler.playLibraryItem(libraryItemId, episodeId, false, mediaPlayer) {
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}")
|
Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}")
|
||||||
|
@ -268,9 +318,10 @@ class AbsAudioPlayer : Plugin() {
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun requestSession(call: PluginCall) {
|
fun requestSession(call: PluginCall) {
|
||||||
|
// Need to make sure the player service has been started
|
||||||
Log.d(tag, "CAST REQUEST SESSION PLUGIN")
|
Log.d(tag, "CAST REQUEST SESSION PLUGIN")
|
||||||
call.resolve()
|
call.resolve()
|
||||||
playerNotificationService.castManager.requestSession(mainActivity, object : CastManager.RequestSessionCallback() {
|
castManager.requestSession(playerNotificationService, object : CastManager.RequestSessionCallback() {
|
||||||
override fun onError(errorCode: Int) {
|
override fun onError(errorCode: Int) {
|
||||||
Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode")
|
Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode")
|
||||||
}
|
}
|
||||||
|
@ -284,4 +335,11 @@ class AbsAudioPlayer : Plugin() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun getIsCastAvailable(call: PluginCall) {
|
||||||
|
var jsobj = JSObject()
|
||||||
|
jsobj.put("value", isCastAvailable)
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,9 +151,9 @@ class ApiHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playLibraryItem(libraryItemId:String, episodeId:String?, forceTranscode:Boolean, cb: (PlaybackSession) -> Unit) {
|
fun playLibraryItem(libraryItemId:String, episodeId:String?, forceTranscode:Boolean, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
|
||||||
var payload = JSObject()
|
var payload = JSObject()
|
||||||
payload.put("mediaPlayer", "exo-player")
|
payload.put("mediaPlayer", mediaPlayer)
|
||||||
|
|
||||||
// Only if direct play fails do we force transcode
|
// Only if direct play fails do we force transcode
|
||||||
if (!forceTranscode) payload.put("forceDirectPlay", true)
|
if (!forceTranscode) payload.put("forceDirectPlay", true)
|
||||||
|
|
|
@ -5,11 +5,12 @@ buildscript {
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
// classpath 'com.android.tools.build:gradle:4.0.2'
|
||||||
classpath 'com.google.gms:google-services:4.3.5'
|
classpath 'com.android.tools.build:gradle:7.3.0-alpha08'
|
||||||
|
classpath 'com.google.gms:google-services:4.3.10'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
@ -22,7 +23,7 @@ apply from: "variables.gradle"
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,15 @@
|
||||||
|
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
org.gradle.jvmargs=-Xmx1536m
|
#org.gradle.jvmargs=-Xmx1536m
|
||||||
|
org.gradle.jvmargs=-Dfile.encoding=UTF-8 \
|
||||||
|
--add-opens jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
|
||||||
|
--add-opens jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||||
|
--add-opens jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
|
||||||
|
--add-opens jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
|
||||||
|
--add-opens jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
|
||||||
|
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \
|
||||||
|
--add-opens jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
|
||||||
|
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
@ -22,3 +30,5 @@ org.gradle.jvmargs=-Xmx1536m
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# Automatically convert third-party libraries to use AndroidX
|
# Automatically convert third-party libraries to use AndroidX
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
|
||||||
|
kapt.use.worker.api=false
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
#Sun Apr 17 13:28:55 CDT 2022
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
ext {
|
ext {
|
||||||
minSdkVersion = 24
|
minSdkVersion = 24
|
||||||
compileSdkVersion = 30
|
compileSdkVersion = 31
|
||||||
targetSdkVersion = 30
|
targetSdkVersion = 30
|
||||||
androidxActivityVersion = '1.2.0'
|
androidxActivityVersion = '1.2.0'
|
||||||
androidxAppCompatVersion = '1.2.0'
|
androidxAppCompatVersion = '1.4.1'
|
||||||
androidxCoordinatorLayoutVersion = '1.1.0'
|
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||||
androidxCoreVersion = '1.6.0'
|
androidxCoreVersion = '1.6.0'
|
||||||
androidPlayCore = '1.9.0'
|
androidPlayCore = '1.9.0'
|
||||||
androidxFragmentVersion = '1.3.0'
|
androidxFragmentVersion = '1.3.0'
|
||||||
junitVersion = '4.13.1'
|
junitVersion = '4.13.1'
|
||||||
androidxJunitVersion = '1.1.2'
|
androidxJunitVersion = '1.1.2'
|
||||||
androidxEspressoCoreVersion = '3.3.0'
|
androidxEspressoCoreVersion = '3.3.0'
|
||||||
cordovaAndroidVersion = '7.0.0'
|
cordovaAndroidVersion = '10.1.1'
|
||||||
androidx_app_compat_version = '1.2.0'
|
androidx_app_compat_version = '1.2.0'
|
||||||
androidx_car_version = '1.0.0-alpha7'
|
androidx_car_version = '1.0.0-alpha7'
|
||||||
androidx_core_ktx_version = '1.6.0'
|
androidx_core_ktx_version = '1.7.0'
|
||||||
androidx_media_version = '1.0.1'
|
androidx_media_version = '1.5.0'
|
||||||
androidx_preference_version = '1.1.1'
|
androidx_preference_version = '1.1.1'
|
||||||
androidx_test_runner_version = '1.3.0'
|
androidx_test_runner_version = '1.3.0'
|
||||||
arch_lifecycle_version = '2.2.0'
|
arch_lifecycle_version = '2.2.0'
|
||||||
constraint_layout_version = '2.0.1'
|
constraint_layout_version = '2.0.1'
|
||||||
espresso_version = '3.3.0'
|
espresso_version = '3.3.0'
|
||||||
exoplayer_version = '2.15.0'
|
exoplayer_version = '2.17.0'
|
||||||
fragment_version = '1.2.5'
|
fragment_version = '1.2.5'
|
||||||
glide_version = '4.11.0'
|
glide_version = '4.11.0'
|
||||||
gms_strict_version_matcher_version = '1.0.3'
|
gms_strict_version_matcher_version = '1.0.3'
|
||||||
gradle_version = '3.1.4'
|
gradle_version = '3.1.4'
|
||||||
gson_version = '2.8.5'
|
gson_version = '2.8.5'
|
||||||
junit_version = '4.13'
|
junit_version = '4.13'
|
||||||
kotlin_version = '1.4.32'
|
kotlin_version = '1.5.30'
|
||||||
kotlin_coroutines_version = '1.1.0'
|
kotlin_coroutines_version = '1.1.0'
|
||||||
multidex_version = '1.0.3'
|
multidex_version = '1.0.3'
|
||||||
play_services_auth_version = '18.1.0'
|
play_services_auth_version = '18.1.0'
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
|
|
||||||
<widgets-download-progress-indicator />
|
<widgets-download-progress-indicator />
|
||||||
|
|
||||||
|
<div v-show="isCastAvailable" class="mx-2 cursor-pointer">
|
||||||
|
<span class="material-icons" :class="isCasting ? 'text-success' : ''" style="font-size: 1.75rem" @click="castClick">cast</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-if="user" class="h-7 mx-2" to="/search">
|
<nuxt-link v-if="user" class="h-7 mx-2" to="/search">
|
||||||
<span class="material-icons" style="font-size: 1.75rem">search</span>
|
<span class="material-icons" style="font-size: 1.75rem">search</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
@ -29,9 +33,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
onCastAvailableUpdateListener: null,
|
||||||
|
isCastAvailable: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
socketConnected() {
|
socketConnected() {
|
||||||
|
@ -55,9 +64,21 @@ export default {
|
||||||
},
|
},
|
||||||
username() {
|
username() {
|
||||||
return this.user ? this.user.username : 'err'
|
return this.user ? this.user.username : 'err'
|
||||||
|
},
|
||||||
|
isCasting() {
|
||||||
|
return this.$store.state.isCasting
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
castClick() {
|
||||||
|
if (this.$store.state.playerIsLocal) {
|
||||||
|
this.$toast.warn('Cannot cast downloaded media item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Cast Btn Click')
|
||||||
|
AbsAudioPlayer.requestSession()
|
||||||
|
},
|
||||||
clickShowSideDrawer() {
|
clickShowSideDrawer() {
|
||||||
this.$store.commit('setShowSideDrawer', true)
|
this.$store.commit('setShowSideDrawer', true)
|
||||||
},
|
},
|
||||||
|
@ -66,9 +87,20 @@ export default {
|
||||||
},
|
},
|
||||||
back() {
|
back() {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
|
},
|
||||||
|
onCastAvailableUpdate(data) {
|
||||||
|
this.isCastAvailable = data && data.value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {
|
||||||
|
AbsAudioPlayer.getIsCastAvailable().then((data) => {
|
||||||
|
this.isCastAvailable = data && data.value
|
||||||
|
})
|
||||||
|
this.onCastAvailableUpdateListener = AbsAudioPlayer.addListener('onCastAvailableUpdate', this.onCastAvailableUpdate)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.onCastAvailableUpdateListener) this.onCastAvailableUpdateListener.remove()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showCastBtn" class="top-3.5 right-20 absolute cursor-pointer">
|
<div v-show="showCastBtn" class="top-3.5 right-20 absolute cursor-pointer">
|
||||||
<span class="material-icons text-3xl" @click="castClick">cast</span>
|
<span class="material-icons text-3xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-4 right-4 absolute cursor-pointer">
|
<div class="top-4 right-4 absolute cursor-pointer">
|
||||||
<ui-dropdown-menu ref="dropdownMenu" :items="menuItems" @action="clickMenuAction">
|
<ui-dropdown-menu ref="dropdownMenu" :items="menuItems" @action="clickMenuAction">
|
||||||
|
@ -109,7 +109,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
playbackSession: null,
|
playbackSession: null,
|
||||||
showChapterModal: false,
|
showChapterModal: false,
|
||||||
showCastBtn: false,
|
showCastBtn: true,
|
||||||
showFullscreen: false,
|
showFullscreen: false,
|
||||||
totalDuration: 0,
|
totalDuration: 0,
|
||||||
currentPlaybackRate: 1,
|
currentPlaybackRate: 1,
|
||||||
|
@ -159,6 +159,12 @@ export default {
|
||||||
}
|
}
|
||||||
return this.showFullscreen ? 200 : 60
|
return this.showFullscreen ? 200 : 60
|
||||||
},
|
},
|
||||||
|
isCasting() {
|
||||||
|
return this.mediaPlayer === 'cast-player'
|
||||||
|
},
|
||||||
|
mediaPlayer() {
|
||||||
|
return this.playbackSession ? this.playbackSession.mediaPlayer : null
|
||||||
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.playbackSession ? this.playbackSession.mediaType : null
|
return this.playbackSession ? this.playbackSession.mediaType : null
|
||||||
},
|
},
|
||||||
|
@ -262,6 +268,11 @@ export default {
|
||||||
},
|
},
|
||||||
castClick() {
|
castClick() {
|
||||||
console.log('Cast Btn Click')
|
console.log('Cast Btn Click')
|
||||||
|
if (this.isLocalPlayMethod) {
|
||||||
|
this.$toast.warn('Cannot cast downloaded media items')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
AbsAudioPlayer.requestSession()
|
AbsAudioPlayer.requestSession()
|
||||||
},
|
},
|
||||||
clickContainer() {
|
clickContainer() {
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
||||||
|
import { Dialog } from '@capacitor/dialog'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
@ -28,6 +29,7 @@ export default {
|
||||||
onLocalMediaProgressUpdateListener: null,
|
onLocalMediaProgressUpdateListener: null,
|
||||||
onSleepTimerEndedListener: null,
|
onSleepTimerEndedListener: null,
|
||||||
onSleepTimerSetListener: null,
|
onSleepTimerSetListener: null,
|
||||||
|
onMediaPlayerChangedListener: null,
|
||||||
sleepInterval: null,
|
sleepInterval: null,
|
||||||
currentEndOfChapterTime: 0
|
currentEndOfChapterTime: 0
|
||||||
}
|
}
|
||||||
|
@ -169,6 +171,16 @@ export default {
|
||||||
var libraryItemId = payload.libraryItemId
|
var libraryItemId = payload.libraryItemId
|
||||||
var episodeId = payload.episodeId
|
var episodeId = payload.episodeId
|
||||||
|
|
||||||
|
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
|
||||||
|
const { value } = await Dialog.confirm({
|
||||||
|
title: 'Warning',
|
||||||
|
message: `Cannot cast downloaded media items. Confirm to close cast and play on your device.`
|
||||||
|
})
|
||||||
|
if (!value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Called playLibraryItem', libraryItemId)
|
console.log('Called playLibraryItem', libraryItemId)
|
||||||
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true })
|
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
@ -186,12 +198,17 @@ export default {
|
||||||
onLocalMediaProgressUpdate(localMediaProgress) {
|
onLocalMediaProgressUpdate(localMediaProgress) {
|
||||||
console.log('Got local media progress update', localMediaProgress.progress, JSON.stringify(localMediaProgress))
|
console.log('Got local media progress update', localMediaProgress.progress, JSON.stringify(localMediaProgress))
|
||||||
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
|
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
|
||||||
|
},
|
||||||
|
onMediaPlayerChanged(data) {
|
||||||
|
var mediaPlayer = data.value
|
||||||
|
this.$store.commit('setMediaPlayer', mediaPlayer)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.onLocalMediaProgressUpdateListener = AbsAudioPlayer.addListener('onLocalMediaProgressUpdate', this.onLocalMediaProgressUpdate)
|
this.onLocalMediaProgressUpdateListener = AbsAudioPlayer.addListener('onLocalMediaProgressUpdate', this.onLocalMediaProgressUpdate)
|
||||||
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
|
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
|
||||||
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
|
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
|
||||||
|
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
|
||||||
|
|
||||||
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
||||||
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
||||||
|
@ -206,6 +223,7 @@ export default {
|
||||||
if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove()
|
if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove()
|
||||||
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
|
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
|
||||||
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
|
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
|
||||||
|
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
|
||||||
|
|
||||||
// if (this.$server.socket) {
|
// if (this.$server.socket) {
|
||||||
// this.$server.socket.off('stream_open', this.streamOpen)
|
// this.$server.socket.off('stream_open', this.streamOpen)
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<ui-btn class="w-full" @click="newServerConfigClick">Add New Server</ui-btn>
|
<ui-btn class="w-full" @click="newServerConfigClick">Add New Server</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<div v-else class="w-full">
|
||||||
<form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full">
|
<form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full">
|
||||||
<h2 class="text-lg leading-7 mb-2">Server address</h2>
|
<h2 class="text-lg leading-7 mb-2">Server address</h2>
|
||||||
<ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full sm:w-72 h-10" />
|
<ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full sm:w-72 h-10" />
|
||||||
|
@ -44,12 +44,12 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-show="error" class="w-full rounded-lg bg-red-600 bg-opacity-10 border border-error border-opacity-50 py-3 px-2 flex items-center mt-4">
|
<div v-show="error" class="w-full rounded-lg bg-red-600 bg-opacity-10 border border-error border-opacity-50 py-3 px-2 flex items-center mt-4">
|
||||||
<span class="material-icons mr-2 text-error" style="font-size: 1.1rem">warning</span>
|
<span class="material-icons mr-2 text-error" style="font-size: 1.1rem">warning</span>
|
||||||
<p class="text-error">{{ error }}</p>
|
<p class="text-error">{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black bg-opacity-75 flex items-center justify-center z-30 transition-opacity duration-500">
|
<div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black bg-opacity-75 flex items-center justify-center z-30 transition-opacity duration-500">
|
||||||
|
@ -73,22 +73,6 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
deviceData: null,
|
deviceData: null,
|
||||||
// serverConnectionConfigs: [
|
|
||||||
// {
|
|
||||||
// id: 'test1',
|
|
||||||
// name: 'http://192.168.0.1:3333 (root)',
|
|
||||||
// address: 'http://192.168.0.1:3333',
|
|
||||||
// username: 'root',
|
|
||||||
// token: 'asdf'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'test2',
|
|
||||||
// name: 'https://someserver.com (user)',
|
|
||||||
// address: 'https://someserver.com',
|
|
||||||
// username: 'user',
|
|
||||||
// token: 'asdf'
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
showAuth: false,
|
showAuth: false,
|
||||||
processing: false,
|
processing: false,
|
||||||
|
@ -128,18 +112,23 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async connectToServer(config) {
|
async connectToServer(config) {
|
||||||
|
console.log('[ServerConnectForm] connectToServer', config.address)
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.serverConfig = {
|
this.serverConfig = {
|
||||||
...config
|
...config
|
||||||
}
|
}
|
||||||
this.showForm = true
|
this.showForm = true
|
||||||
var success = await this.pingServerAddress(config.address)
|
var success = await this.pingServerAddress(config.address)
|
||||||
|
this.processing = false
|
||||||
|
console.log(`[ServerConnectForm] pingServer result ${success}`)
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
this.showForm = false
|
||||||
|
this.showAuth = false
|
||||||
|
console.log(`[ServerConnectForm] showForm ${this.showForm}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.error = null
|
this.error = null
|
||||||
this.processing = false
|
|
||||||
var payload = await this.authenticateToken()
|
var payload = await this.authenticateToken()
|
||||||
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
|
@ -179,8 +168,14 @@ export default {
|
||||||
console.log('Edit server config', serverConfig)
|
console.log('Edit server config', serverConfig)
|
||||||
},
|
},
|
||||||
newServerConfigClick() {
|
newServerConfigClick() {
|
||||||
|
this.serverConfig = {
|
||||||
|
address: '',
|
||||||
|
userId: '',
|
||||||
|
username: ''
|
||||||
|
}
|
||||||
this.showForm = true
|
this.showForm = true
|
||||||
this.showAuth = false
|
this.showAuth = false
|
||||||
|
this.error = null
|
||||||
},
|
},
|
||||||
editServerAddress() {
|
editServerAddress() {
|
||||||
this.error = null
|
this.error = null
|
||||||
|
|
18823
package-lock.json
generated
18823
package-lock.json
generated
File diff suppressed because it is too large
Load diff
20
package.json
20
package.json
|
@ -12,18 +12,18 @@
|
||||||
"ionic:serve": "npm run start"
|
"ionic:serve": "npm run start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^3.2.2",
|
"@capacitor/android": "^3.4.3",
|
||||||
"@capacitor/app": "^1.0.7",
|
"@capacitor/app": "^1.1.1",
|
||||||
"@capacitor/cli": "^3.1.2",
|
"@capacitor/cli": "^3.4.3",
|
||||||
"@capacitor/core": "^3.2.2",
|
"@capacitor/core": "^3.4.3",
|
||||||
"@capacitor/dialog": "^1.0.3",
|
"@capacitor/dialog": "^1.0.7",
|
||||||
"@capacitor/haptics": "^1.1.4",
|
"@capacitor/haptics": "^1.1.4",
|
||||||
"@capacitor/ios": "^3.2.2",
|
"@capacitor/ios": "^3.2.2",
|
||||||
"@capacitor/network": "^1.0.3",
|
"@capacitor/network": "^1.0.7",
|
||||||
"@capacitor/status-bar": "^1.0.6",
|
"@capacitor/status-bar": "^1.0.8",
|
||||||
"@capacitor/storage": "^1.1.0",
|
"@capacitor/storage": "^1.2.5",
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@robingenz/capacitor-app-update": "^1.0.0",
|
"@robingenz/capacitor-app-update": "^1.3.1",
|
||||||
"core-js": "^3.15.1",
|
"core-js": "^3.15.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
"epubjs": "^0.3.88",
|
"epubjs": "^0.3.88",
|
||||||
|
@ -39,4 +39,4 @@
|
||||||
"@nuxtjs/tailwindcss": "^4.2.0",
|
"@nuxtjs/tailwindcss": "^4.2.0",
|
||||||
"postcss": "^8.3.5"
|
"postcss": "^8.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ export const state = () => ({
|
||||||
playerEpisodeId: null,
|
playerEpisodeId: null,
|
||||||
playerIsLocal: false,
|
playerIsLocal: false,
|
||||||
playerIsPlaying: false,
|
playerIsPlaying: false,
|
||||||
|
isCasting: false,
|
||||||
appUpdateInfo: null,
|
appUpdateInfo: null,
|
||||||
socketConnected: false,
|
socketConnected: false,
|
||||||
networkConnected: false,
|
networkConnected: false,
|
||||||
|
@ -67,8 +68,14 @@ export const mutations = {
|
||||||
state.playerEpisodeId = playbackSession ? playbackSession.episodeId || null : null
|
state.playerEpisodeId = playbackSession ? playbackSession.episodeId || null : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mediaPlayer = playbackSession ? playbackSession.mediaPlayer : null
|
||||||
|
state.isCasting = mediaPlayer === "cast-player"
|
||||||
|
|
||||||
console.log('setPlayerItem', state.playerLibraryItemId, state.playerEpisodeId, state.playerIsLocal)
|
console.log('setPlayerItem', state.playerLibraryItemId, state.playerEpisodeId, state.playerIsLocal)
|
||||||
},
|
},
|
||||||
|
setMediaPlayer(state, mediaPlayer) {
|
||||||
|
state.isCasting = mediaPlayer === 'cast-player'
|
||||||
|
},
|
||||||
setPlayerPlaying(state, val) {
|
setPlayerPlaying(state, val) {
|
||||||
state.playerIsPlaying = val
|
state.playerIsPlaying = val
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue