Better Cast

Now there won't be any need of ugly cast mini player as cast will be able to respond to UI events of app like seek, changing song, pause
This commit is contained in:
Prathamesh More 2022-05-13 00:18:55 +05:30
parent 70e323eb0a
commit 4b4aadcc5b
7 changed files with 127 additions and 205 deletions

View file

@ -1,7 +1,6 @@
package code.name.monkey.retromusic.activities.base
import android.os.Bundle
import code.name.monkey.retromusic.cast.CastHelper
import code.name.monkey.retromusic.cast.RetroSessionManagerListener
import code.name.monkey.retromusic.cast.RetroWebServer
import code.name.monkey.retromusic.helper.MusicPlayerRemote
@ -30,20 +29,7 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() {
override fun onSessionStarted(castSession: CastSession, p1: String) {
invalidateOptionsMenu()
mCastSession = castSession
loadCastQueue()
MusicPlayerRemote.isCasting = true
setAllowDragging(false)
collapsePanel()
}
override fun onSessionEnding(castSession: CastSession) {
MusicPlayerRemote.isCasting = false
castSession.remoteMediaClient?.let {
val position = it.mediaQueue.indexOfItemWithId(it.currentItem?.itemId ?: 0)
val progress = it.approximateStreamPosition
MusicPlayerRemote.position = position
MusicPlayerRemote.seekTo(progress.toInt())
}
MusicPlayerRemote.switchToRemotePlayback(castSession)
}
override fun onSessionEnded(castSession: CastSession, p1: Int) {
@ -51,7 +37,7 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() {
if (mCastSession == castSession) {
mCastSession = null
}
setAllowDragging(true)
MusicPlayerRemote.switchToLocalPlayback()
webServer.stop()
}
@ -59,14 +45,7 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() {
invalidateOptionsMenu()
mCastSession = castSession
webServer.start()
mCastSession?.remoteMediaClient?.let {
loadCastQueue(it.mediaQueue.indexOfItemWithId(it.currentItem?.itemId ?: 0),
it.approximateStreamPosition)
}
MusicPlayerRemote.isCasting = true
setAllowDragging(false)
collapsePanel()
MusicPlayerRemote.switchToRemotePlayback(castSession)
}
override fun onSessionSuspended(castSession: CastSession, p1: Int) {
@ -74,8 +53,7 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() {
if (mCastSession == castSession) {
mCastSession = null
}
MusicPlayerRemote.isCasting = false
setAllowDragging(true)
MusicPlayerRemote.switchToLocalPlayback()
webServer.stop()
}
}
@ -121,27 +99,4 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() {
mCastSession = null
}
}
fun loadCastQueue(
position: Int = MusicPlayerRemote.position,
progress: Long = MusicPlayerRemote.songProgressMillis.toLong(),
) {
mCastSession?.let {
if (MusicPlayerRemote.playingQueue.isNotEmpty()) {
CastHelper.castQueue(
it,
MusicPlayerRemote.playingQueue,
position,
progress
)
}
}
}
override fun onQueueChanged() {
super.onQueueChanged()
if (playServicesAvailable) {
loadCastQueue()
}
}
}

View file

@ -393,9 +393,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() {
animate: Boolean = false,
isBottomNavVisible: Boolean = bottomNavigationView.isVisible,
) {
val heightOfBar =
windowInsets.safeGetBottomInsets() +
if (MusicPlayerRemote.isCasting) dip(R.dimen.cast_mini_player_height) else dip(R.dimen.mini_player_height)
val heightOfBar = windowInsets.safeGetBottomInsets() + dip(R.dimen.mini_player_height)
val heightOfBarWithTabs = heightOfBar + dip(R.dimen.bottom_nav_height)
if (hide) {
bottomSheetBehavior.peekHeight = -windowInsets.safeGetBottomInsets()

View file

@ -6,12 +6,11 @@ import code.name.monkey.retromusic.cast.RetroWebServer.Companion.PART_COVER_ART
import code.name.monkey.retromusic.cast.RetroWebServer.Companion.PART_SONG
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.RetroUtil
import com.google.android.gms.cast.*
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED
import com.google.android.gms.cast.MediaMetadata
import com.google.android.gms.cast.MediaMetadata.*
import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.common.images.WebImage
import org.json.JSONObject
import java.net.MalformedURLException
import java.net.URL
@ -21,39 +20,7 @@ object CastHelper {
private const val CAST_MUSIC_METADATA_ALBUM_ID = "metadata_album_id"
private const val CAST_URL_PROTOCOL = "http"
fun castSong(castSession: CastSession, song: Song) {
try {
val remoteMediaClient = castSession.remoteMediaClient
val mediaLoadOptions = MediaLoadOptions.Builder().apply {
setPlayPosition(0)
setAutoplay(true)
}.build()
remoteMediaClient?.load(song.toMediaInfo()!!, mediaLoadOptions)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun castQueue(castSession: CastSession, songs: List<Song>, position: Int, progress: Long) {
try {
val remoteMediaClient = castSession.remoteMediaClient
remoteMediaClient?.queueLoad(
songs.toMediaInfoList(),
if (position != -1) position else 0,
MediaStatus.REPEAT_MODE_REPEAT_OFF,
progress,
JSONObject()
)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun List<Song>.toMediaInfoList(): Array<MediaQueueItem> {
return map { MediaQueueItem.Builder(it.toMediaInfo()!!).build() }.toTypedArray()
}
private fun Song.toMediaInfo(): MediaInfo? {
fun Song.toMediaInfo(): MediaInfo? {
val song = this
val baseUrl: URL
try {

View file

@ -30,6 +30,7 @@ import code.name.monkey.retromusic.repository.SongRepository
import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.getExternalStorageDirectory
import com.google.android.gms.cast.framework.CastSession
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
@ -43,14 +44,6 @@ object MusicPlayerRemote : KoinComponent {
private val songRepository by inject<SongRepository>()
var isCasting: Boolean = false
set(value) {
field = value
if (value) {
musicService?.quit()
}
}
@JvmStatic
val isPlaying: Boolean
get() = musicService != null && musicService!!.isPlaying
@ -472,6 +465,14 @@ object MusicPlayerRemote : KoinComponent {
.dropLastWhile { it.isEmpty() }.toTypedArray()[1]
}
fun switchToRemotePlayback(castSession: CastSession) {
musicService?.switchToRemotePlayback(castSession)
}
fun switchToLocalPlayback() {
musicService?.switchToLocalPlayback()
}
class ServiceBinder internal constructor(private val mCallback: ServiceConnection?) :
ServiceConnection {

View file

@ -0,0 +1,109 @@
package code.name.monkey.retromusic.service
import code.name.monkey.retromusic.cast.CastHelper.toMediaInfo
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.service.playback.Playback
import com.google.android.gms.cast.MediaLoadOptions
import com.google.android.gms.cast.MediaSeekOptions
import com.google.android.gms.cast.MediaStatus
import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.media.RemoteMediaClient
class CastPlayer(castSession: CastSession) : Playback,
RemoteMediaClient.Callback() {
override val isInitialized: Boolean = true
private val remoteMediaClient: RemoteMediaClient? = castSession.remoteMediaClient
init {
remoteMediaClient?.registerCallback(this)
}
private var isActuallyPlaying = false
override val isPlaying: Boolean
get() {
return remoteMediaClient?.isPlaying == true || isActuallyPlaying
}
override val audioSessionId: Int = 0
private var callbacks: Playback.PlaybackCallbacks? = null
override fun setDataSource(
song: Song,
force: Boolean,
completion: (success: Boolean) -> Unit,
) {
try {
val mediaLoadOptions =
MediaLoadOptions.Builder().setPlayPosition(0).setAutoplay(true).build()
remoteMediaClient?.load(song.toMediaInfo()!!, mediaLoadOptions)
completion(true)
} catch (e: Exception) {
e.printStackTrace()
completion(false)
}
}
override fun setNextDataSource(path: String?) {}
override fun setCallbacks(callbacks: Playback.PlaybackCallbacks) {
this.callbacks = callbacks
}
override fun start(): Boolean {
isActuallyPlaying = true
remoteMediaClient?.play()
return true
}
override fun stop() {
isActuallyPlaying = false
remoteMediaClient?.stop()
}
override fun release() {
stop()
}
override fun pause(): Boolean {
isActuallyPlaying = false
remoteMediaClient?.pause()
return true
}
override fun duration(): Int {
return remoteMediaClient?.streamDuration?.toInt() ?: 0
}
override fun position(): Int {
return remoteMediaClient?.approximateStreamPosition?.toInt() ?: 0
}
override fun seek(whereto: Int): Int {
remoteMediaClient?.seek(MediaSeekOptions.Builder().setPosition(whereto.toLong()).build())
return whereto
}
override fun setVolume(vol: Float) = true
override fun setAudioSessionId(sessionId: Int) = true
override fun setCrossFadeDuration(duration: Int) {}
override fun onStatusUpdated() {
when (remoteMediaClient?.playerState) {
MediaStatus.PLAYER_STATE_IDLE -> {
val idleReason = remoteMediaClient.idleReason
if (idleReason == MediaStatus.IDLE_REASON_FINISHED) {
callbacks?.onTrackEnded()
}
}
MediaStatus.PLAYER_STATE_PLAYING, MediaStatus.PLAYER_STATE_PAUSED -> {
callbacks?.onPlayStateChanged()
}
}
}
}

View file

@ -1,102 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container_all"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="0dp">
<RelativeLayout
android:id="@+id/container_current"
android:layout_width="match_parent"
android:layout_height="@dimen/cast_mini_controller_height">
<View
android:id="@+id/center"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_centerVertical="true"
android:visibility="invisible" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/icon_container"
android:layout_width="@dimen/cast_mini_controller_image_size"
android:layout_height="@dimen/cast_mini_controller_image_size"
android:layout_margin="8dp"
app:cardCornerRadius="10dp">
<ImageView
android:id="@+id/icon_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:scaleType="centerCrop"
android:src="@drawable/default_audio_art" />
</com.google.android.material.card.MaterialCardView>
<ImageView
android:id="@+id/button_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/cast_mini_controller_control_button_margin"
android:layout_toStartOf="@+id/button_1" />
<ImageView
android:id="@+id/button_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/cast_mini_controller_control_button_margin"
android:layout_toStartOf="@+id/button_2" />
<ImageView
android:id="@+id/button_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/title_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/center"
android:layout_marginStart="15dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="-3dp"
android:layout_toStartOf="@+id/button_0"
android:layout_toEndOf="@+id/icon_container"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/castTitleTextAppearance" />
<TextView
android:id="@+id/subtitle_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/center"
android:layout_alignStart="@+id/title_view"
android:layout_alignEnd="@+id/title_view"
android:layout_marginTop="3dp"
android:layout_toStartOf="@+id/button_0"
android:layout_toEndOf="@+id/icon_container"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/castSubtitleTextAppearance" />
<!-- Note: The ProgressBar is deliberately laid out in absolute LTR -->
<ProgressBar
android:id="@+id/progressBar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="5dp"
android:layout_alignParentTop="true"
android:layoutDirection="ltr"
android:progressDrawable="@drawable/cast_mini_controller_progress_drawable" />
</RelativeLayout>
</LinearLayout>

View file

@ -11,8 +11,8 @@
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
android:background="?attr/colorSurface"
app:defaultNavHost="true"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:layout="@layout/fragment_home" />
@ -39,12 +39,6 @@
android:layout_height="@dimen/mini_player_height"
tools:layout="@layout/fragment_mini_player" />
<fragment
android:id="@+id/castMiniController"
class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
<code.name.monkey.retromusic.views.BottomNavigationBarTinted