diff --git a/android/app/src/main/java/com/audiobookshelf/app/CastOptionsProvider.kt b/android/app/src/main/java/com/audiobookshelf/app/CastOptionsProvider.kt index 27b34caf..b48d17de 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/CastOptionsProvider.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/CastOptionsProvider.kt @@ -11,6 +11,7 @@ import com.google.android.gms.cast.framework.media.CastMediaOptions class CastOptionsProvider : OptionsProvider { override fun getCastOptions(context: Context): CastOptions { Log.d("CastOptionsProvider", "getCastOptions") + var appId = "FD1F76C5" return CastOptions.Builder() .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID).setCastMediaOptions( CastMediaOptions.Builder() diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt index 749f8b3e..7a04d72c 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt @@ -9,6 +9,9 @@ import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaMetadata +import com.google.android.gms.cast.MediaInfo +import com.google.android.gms.cast.MediaQueueItem +import com.google.android.gms.common.images.WebImage // TODO: enum or something in kotlin? val PLAYMETHOD_DIRECTPLAY = 0 @@ -135,12 +138,41 @@ class PlaybackSession( var mediaMetadata = this.getExoMediaMetadata(audioTrack) var mediaUri = this.getContentUri(audioTrack) var mimeType = audioTrack.mimeType - var mediaItem = MediaItem.Builder().setUri(mediaUri).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build() + + var queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager + var mediaItem = MediaItem.Builder().setUri(mediaUri).setTag(queueItem).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build() mediaItems.add(mediaItem) } return mediaItems } + @JsonIgnore + fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata { + var castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER) + castMetadata.addImage(WebImage(getCoverUri())) + castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle) + castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor) + castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index) + return castMetadata + } + + @JsonIgnore + fun getQueueItem(audioTrack:AudioTrack):MediaQueueItem { + var castMetadata = getCastMediaMetadata(audioTrack) + + var mediaUri = getContentUri(audioTrack) + var mediaInfoBuilder = MediaInfo.Builder(mediaUri.toString()) + mediaInfoBuilder.setContentUrl(mediaUri.toString()) + mediaInfoBuilder.setMetadata(castMetadata) + mediaInfoBuilder.setContentType(audioTrack.mimeType) + var mediaInfo = mediaInfoBuilder.build() + + var queueItem = MediaQueueItem.Builder(mediaInfo) + queueItem.setItemId(audioTrack.index) + queueItem.setPlaybackDuration(audioTrack.duration) + return queueItem.build() + } + @JsonIgnore fun clone():PlaybackSession { return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,startedAt,updatedAt,timeListening,audioTracks,currentTime,libraryItem,localLibraryItem,serverConnectionConfigId,serverAddress) diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/CastManager.kt b/android/app/src/main/java/com/audiobookshelf/app/player/CastManager.kt index e5e53315..30b349a0 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/CastManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/CastManager.kt @@ -9,17 +9,19 @@ import androidx.mediarouter.app.MediaRouteChooserDialog import androidx.mediarouter.media.MediaRouteSelector import androidx.mediarouter.media.MediaRouter import com.getcapacitor.PluginCall +import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.ext.cast.CastPlayer +import com.google.android.exoplayer2.ext.cast.MediaItemConverter import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener import com.google.android.gms.cast.Cast import com.google.android.gms.cast.CastDevice import com.google.android.gms.cast.CastMediaControlIntent +import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.framework.* import org.json.JSONObject -import java.util.ArrayList class CastManager constructor(playerNotificationService:PlayerNotificationService) { - private val tag = "SleepTimerManager" + private val tag = "CastManager" private val playerNotificationService:PlayerNotificationService = playerNotificationService private var newConnectionListener: SessionListener? = null @@ -291,6 +293,22 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic } } + inner class CustomConverter : MediaItemConverter { + override fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem { + // The MediaQueueItem you build is expected to be in the tag. + var queueItem = (mediaItem.playbackProperties!!.tag as MediaQueueItem?)!! + Log.d(tag, "Test toMediaQueueItem ${queueItem.media!!.contentUrl} | ${queueItem.playbackDuration} | ${queueItem.itemId}") + return queueItem + } + + override fun toMediaItem(mediaQueueItem: MediaQueueItem): MediaItem { + return MediaItem.Builder() + .setUri(mediaQueueItem.media!!.contentUrl) + .setTag(mediaQueueItem) + .build() + } + } + private fun listenForConnection(callback: ConnectionCallback) { // We should only ever have one of these listeners active at a time, so remove previous getSessionManager()?.removeSessionManagerListener(newConnectionListener, CastSession::class.java) @@ -302,7 +320,8 @@ class CastManager constructor(playerNotificationService:PlayerNotificationServic try { val castContext = CastContext.getSharedInstance(mainActivity) - playerNotificationService.castPlayer = CastPlayer(castContext).apply { + + playerNotificationService.castPlayer = CastPlayer(castContext, CustomConverter()).apply { setSessionAvailabilityListener(CastSessionAvailabilityListener()) addListener(PlayerListener(playerNotificationService)) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 035a6086..25ef78ca 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource +import com.google.android.exoplayer2.ui.PlayerControlView import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.upstream.* import okhttp3.OkHttpClient @@ -184,8 +185,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { currentPlayer = mPlayer - var client: OkHttpClient = OkHttpClient() - // Initialize API apiHandler = ApiHandler(ctx) @@ -220,9 +219,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { isActive = true } - - Log.d(tag, "Media Session Set") - val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken) // This is for Media Browser @@ -293,10 +289,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { var metadata = playbackSession.getMediaMetadataCompat() mediaSession.setMetadata(metadata) - var mediaItems = playbackSession.getMediaItems() - if (mPlayer == currentPlayer) { + var mediaSource:MediaSource if (playbackSession.isLocal) { @@ -316,24 +311,25 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } mPlayer.setMediaSource(mediaSource) - // Add remaining media items if multi-track - if (mediaItems.size > 1) { - mPlayer.addMediaItems(mediaItems.subList(1, mediaItems.size)) - Log.d(tag, "mPlayer total media items ${mPlayer.mediaItemCount}") - - var currentTrackIndex = playbackSession.getCurrentTrackIndex() - var currentTrackTime = playbackSession.getCurrentTrackTimeMs() - Log.d(tag, "mPlayer current track index $currentTrackIndex & current track time $currentTrackTime") - mPlayer.seekTo(currentTrackIndex, currentTrackTime) - } else { - mPlayer.seekTo(playbackSession.currentTimeMs) - } } else if (castPlayer != null) { -//// var mediaQueue = currentAudiobookStreamData!!.getCastQueue() -// // TODO: Start position will need to be adjusted if using multi-track queue -//// castPlayer?.setMediaItems(mediaQueue, 0, 0) + 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 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() @@ -349,9 +345,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { mediaSessionConnector.setPlayer(mPlayer) mPlayer } - if (currentPlaybackSession != null) { - Log.d(tag, "switchToPlayer: Initing current ab stream data") - preparePlayer(currentPlaybackSession!!, false) + currentPlaybackSession?.let { + Log.d(tag, "switchToPlayer: Preparing current playback session ${it.displayTitle}") + preparePlayer(it, false) } } @@ -404,6 +400,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { if (currentPlayer == castPlayer) { Log.d(tag, "CAST Player set on play ${currentPlayer.isLoading} || ${currentPlayer.duration} | ${currentPlayer.currentPosition}") } + currentPlayer.play() } @@ -411,6 +408,16 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { currentPlayer.pause() } + fun playPause():Boolean { + return if (currentPlayer.isPlaying) { + pause() + false + } else { + play() + true + } + } + fun seekPlayer(time: Long) { if (currentPlayer.mediaItemCount > 1) { currentPlaybackSession?.currentTime = time / 1000.0 diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt index d97cf149..43061f4b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt @@ -147,6 +147,14 @@ class AbsAudioPlayer : Plugin() { } } + @PluginMethod + fun playPause(call: PluginCall) { + Handler(Looper.getMainLooper()).post() { + var playing = playerNotificationService.playPause() + call.resolve(JSObject("{\"playing\":$playing}")) + } + } + @PluginMethod fun seekPlayer(call: PluginCall) { var time:Long = call.getString("timeMs", "0")!!.toLong() @@ -246,19 +254,19 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun requestSession(call: PluginCall) { Log.d(tag, "CAST REQUEST SESSION PLUGIN") + call.resolve() + playerNotificationService.castManager.requestSession(mainActivity, object : CastManager.RequestSessionCallback() { + override fun onError(errorCode: Int) { + Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode") + } - playerNotificationService.castManager.requestSession(mainActivity, object : CastManager.RequestSessionCallback() { - override fun onError(errorCode: Int) { - Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode") - } + override fun onCancel() { + Log.d(tag, "CAST REQUEST SESSION ON CANCEL") + } - override fun onCancel() { - Log.d(tag, "CAST REQUEST SESSION ON CANCEL") - } - - override fun onJoin(jsonSession: JSONObject?) { - Log.d(tag, "CAST REQUEST SESSION ON JOIN") - } - }) + override fun onJoin(jsonSession: JSONObject?) { + Log.d(tag, "CAST REQUEST SESSION ON JOIN") + } + }) } } diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index 896031d9..694c6693 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -109,7 +109,7 @@ export default { playbackSession: null, // Others showChapterModal: false, - showCastBtn: false, + showCastBtn: true, showFullscreen: false, totalDuration: 0, currentPlaybackRate: 1, @@ -455,15 +455,9 @@ export default { } this.seek(time) }, - playPauseClick() { + async playPauseClick() { if (this.isLoading) return - if (this.isPaused) { - console.log('playPause PLAY') - this.play() - } else { - console.log('playPause PAUSE') - this.pause() - } + this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing }, play() { AbsAudioPlayer.playPlayer() diff --git a/components/covers/BookCover.vue b/components/covers/BookCover.vue index 5259e617..60b04f80 100644 --- a/components/covers/BookCover.vue +++ b/components/covers/BookCover.vue @@ -120,7 +120,7 @@ export default { return this.media.coverPath || this.placeholderUrl }, hasCover() { - return !!this.media.coverPath + return !!this.media.coverPath || this.localCover }, sizeMultiplier() { var baseSize = this.squareAspectRatio ? 192 : 120