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 package code.name.monkey.retromusic.activities.base
import android.os.Bundle import android.os.Bundle
import code.name.monkey.retromusic.cast.CastHelper
import code.name.monkey.retromusic.cast.RetroSessionManagerListener import code.name.monkey.retromusic.cast.RetroSessionManagerListener
import code.name.monkey.retromusic.cast.RetroWebServer import code.name.monkey.retromusic.cast.RetroWebServer
import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.MusicPlayerRemote
@ -30,20 +29,7 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() {
override fun onSessionStarted(castSession: CastSession, p1: String) { override fun onSessionStarted(castSession: CastSession, p1: String) {
invalidateOptionsMenu() invalidateOptionsMenu()
mCastSession = castSession mCastSession = castSession
loadCastQueue() MusicPlayerRemote.switchToRemotePlayback(castSession)
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())
}
} }
override fun onSessionEnded(castSession: CastSession, p1: Int) { override fun onSessionEnded(castSession: CastSession, p1: Int) {
@ -51,7 +37,7 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() {
if (mCastSession == castSession) { if (mCastSession == castSession) {
mCastSession = null mCastSession = null
} }
setAllowDragging(true) MusicPlayerRemote.switchToLocalPlayback()
webServer.stop() webServer.stop()
} }
@ -59,14 +45,7 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() {
invalidateOptionsMenu() invalidateOptionsMenu()
mCastSession = castSession mCastSession = castSession
webServer.start() webServer.start()
mCastSession?.remoteMediaClient?.let { MusicPlayerRemote.switchToRemotePlayback(castSession)
loadCastQueue(it.mediaQueue.indexOfItemWithId(it.currentItem?.itemId ?: 0),
it.approximateStreamPosition)
}
MusicPlayerRemote.isCasting = true
setAllowDragging(false)
collapsePanel()
} }
override fun onSessionSuspended(castSession: CastSession, p1: Int) { override fun onSessionSuspended(castSession: CastSession, p1: Int) {
@ -74,8 +53,7 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() {
if (mCastSession == castSession) { if (mCastSession == castSession) {
mCastSession = null mCastSession = null
} }
MusicPlayerRemote.isCasting = false MusicPlayerRemote.switchToLocalPlayback()
setAllowDragging(true)
webServer.stop() webServer.stop()
} }
} }
@ -121,27 +99,4 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() {
mCastSession = null 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, animate: Boolean = false,
isBottomNavVisible: Boolean = bottomNavigationView.isVisible, isBottomNavVisible: Boolean = bottomNavigationView.isVisible,
) { ) {
val heightOfBar = val heightOfBar = windowInsets.safeGetBottomInsets() + dip(R.dimen.mini_player_height)
windowInsets.safeGetBottomInsets() +
if (MusicPlayerRemote.isCasting) dip(R.dimen.cast_mini_player_height) else dip(R.dimen.mini_player_height)
val heightOfBarWithTabs = heightOfBar + dip(R.dimen.bottom_nav_height) val heightOfBarWithTabs = heightOfBar + dip(R.dimen.bottom_nav_height)
if (hide) { if (hide) {
bottomSheetBehavior.peekHeight = -windowInsets.safeGetBottomInsets() 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.cast.RetroWebServer.Companion.PART_SONG
import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.RetroUtil 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.MediaInfo.STREAM_TYPE_BUFFERED
import com.google.android.gms.cast.MediaMetadata
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 com.google.android.gms.common.images.WebImage
import org.json.JSONObject
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL 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_MUSIC_METADATA_ALBUM_ID = "metadata_album_id"
private const val CAST_URL_PROTOCOL = "http" private const val CAST_URL_PROTOCOL = "http"
fun castSong(castSession: CastSession, song: Song) { fun Song.toMediaInfo(): MediaInfo? {
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? {
val song = this val song = this
val baseUrl: URL val baseUrl: URL
try { 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.service.MusicService
import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.getExternalStorageDirectory 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.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
@ -43,14 +44,6 @@ object MusicPlayerRemote : KoinComponent {
private val songRepository by inject<SongRepository>() private val songRepository by inject<SongRepository>()
var isCasting: Boolean = false
set(value) {
field = value
if (value) {
musicService?.quit()
}
}
@JvmStatic @JvmStatic
val isPlaying: Boolean val isPlaying: Boolean
get() = musicService != null && musicService!!.isPlaying get() = musicService != null && musicService!!.isPlaying
@ -472,6 +465,14 @@ object MusicPlayerRemote : KoinComponent {
.dropLastWhile { it.isEmpty() }.toTypedArray()[1] .dropLastWhile { it.isEmpty() }.toTypedArray()[1]
} }
fun switchToRemotePlayback(castSession: CastSession) {
musicService?.switchToRemotePlayback(castSession)
}
fun switchToLocalPlayback() {
musicService?.switchToLocalPlayback()
}
class ServiceBinder internal constructor(private val mCallback: ServiceConnection?) : class ServiceBinder internal constructor(private val mCallback: ServiceConnection?) :
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:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:defaultNavHost="true"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
app:defaultNavHost="true"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:layout="@layout/fragment_home" /> tools:layout="@layout/fragment_home" />
@ -39,12 +39,6 @@
android:layout_height="@dimen/mini_player_height" android:layout_height="@dimen/mini_player_height"
tools:layout="@layout/fragment_mini_player" /> 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> </FrameLayout>
<code.name.monkey.retromusic.views.BottomNavigationBarTinted <code.name.monkey.retromusic.views.BottomNavigationBarTinted