Skip to content

Commit fa030e4

Browse files
committed
Add implementations for handleSetDeviceVolume, handleIncreaseDeviceVolume, handleDecreaseDeviceVolume and handleSetDeviceMuted to PillarboxCastPlayer
These implementations are based on AndroidX Media3's implementations from androidx/media@405365c
1 parent 3f66e8f commit fa030e4

File tree

3 files changed

+72
-33
lines changed

3 files changed

+72
-33
lines changed

pillarbox-cast/src/main/java/ch/srgssr/pillarbox/cast/PillarboxCastPlayer.kt

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ import ch.srgssr.pillarbox.cast.extension.getPlaybackState
3939
import ch.srgssr.pillarbox.cast.extension.getRepeatMode
4040
import ch.srgssr.pillarbox.cast.extension.getTracks
4141
import ch.srgssr.pillarbox.cast.extension.getVolume
42-
import ch.srgssr.pillarbox.cast.extension.isMuted
4342
import ch.srgssr.pillarbox.player.PillarboxDsl
4443
import ch.srgssr.pillarbox.player.PillarboxPlayer
44+
import com.google.android.gms.cast.Cast
4545
import com.google.android.gms.cast.CastStatusCodes
4646
import com.google.android.gms.cast.MediaError
4747
import com.google.android.gms.cast.MediaInfo
@@ -57,6 +57,8 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient.ProgressLis
5757
import com.google.android.gms.common.api.PendingResult
5858
import com.google.common.util.concurrent.Futures
5959
import com.google.common.util.concurrent.ListenableFuture
60+
import java.io.IOException
61+
import kotlin.math.roundToInt
6062
import kotlin.time.Duration.Companion.milliseconds
6163

6264
/**
@@ -114,10 +116,21 @@ class PillarboxCastPlayer internal constructor(
114116
applicationLooper: Looper = Util.getCurrentOrMainLooper(),
115117
clock: Clock = Clock.DEFAULT
116118
) : SimpleBasePlayer(applicationLooper) {
119+
private val castListener = CastListener()
120+
private val positionSupplier = PosSupplier(0)
117121
private val sessionListener = SessionListener()
118122
private val analyticsCollector = DefaultAnalyticsCollector(clock).apply { addListener(EventLogger()) }
119123
private val mediaRouter = if (isMediaRouter2Available()) MediaRouter2Wrapper(context) else null
120124

125+
private var castSession: CastSession? = null
126+
set(value) {
127+
field?.removeCastListener(castListener)
128+
value?.addCastListener(castListener)
129+
field = value
130+
131+
remoteMediaClient = value?.remoteMediaClient
132+
}
133+
121134
private var deviceInfo = if (isMediaRouter2Available()) checkNotNull(mediaRouter).fetchDeviceInfo() else DEVICE_INFO_REMOTE_EMPTY
122135
set(value) {
123136
if (field != value) {
@@ -131,8 +144,6 @@ class PillarboxCastPlayer internal constructor(
131144
private var playlistTracker: MediaQueueTracker? = null
132145
private var trackSelectionParameters: TrackSelectionParameters = TrackSelectionParameters.DEFAULT
133146

134-
private val positionSupplier: PosSupplier = PosSupplier(0)
135-
136147
private var remoteMediaClient: RemoteMediaClient? = null
137148
set(value) {
138149
if (field != value) {
@@ -158,7 +169,7 @@ class PillarboxCastPlayer internal constructor(
158169

159170
init {
160171
castContext.sessionManager.addSessionManagerListener(sessionListener, CastSession::class.java)
161-
remoteMediaClient = castContext.sessionManager.currentCastSession?.remoteMediaClient
172+
castSession = castContext.sessionManager.currentCastSession
162173
addListener(analyticsCollector)
163174
analyticsCollector.setPlayer(this, applicationLooper)
164175
}
@@ -183,6 +194,9 @@ class PillarboxCastPlayer internal constructor(
183194
val remoteMediaClient = remoteMediaClient ?: return State.Builder().build()
184195
val itemCount = remoteMediaClient.mediaQueue.itemCount
185196
val playlist = remoteMediaClient.createPlaylist()
197+
val deviceVolume = castSession?.let {
198+
(it.volume * MAX_VOLUME).roundToInt().coerceIn(VOLUME_RANGE)
199+
}
186200

187201
return State.Builder()
188202
.setAvailableCommands(remoteMediaClient.getAvailableCommands(seekBackIncrementMs, seekForwardIncrementMs))
@@ -200,14 +214,17 @@ class PillarboxCastPlayer internal constructor(
200214
.setShuffleModeEnabled(false)
201215
.setRepeatMode(remoteMediaClient.getRepeatMode())
202216
.setVolume(remoteMediaClient.getVolume().toFloat())
203-
.setIsDeviceMuted(remoteMediaClient.isMuted())
217+
.setIsDeviceMuted(castSession?.isMute ?: false)
204218
.setDeviceInfo(deviceInfo)
205219
.setMaxSeekToPreviousPositionMs(maxSeekToPreviousPositionMs)
206220
.setSeekBackIncrementMs(seekBackIncrementMs)
207221
.setSeekForwardIncrementMs(seekForwardIncrementMs)
208222
.setTrackSelectionParameters(trackSelectionParameters)
209223
.setPlaybackParameters(PlaybackParameters(remoteMediaClient.getPlaybackRate()))
210224
.setPlaylistMetadata(playlistMetadata)
225+
.apply {
226+
deviceVolume?.let(this::setDeviceVolume)
227+
}
211228
.build()
212229
}
213230

@@ -295,8 +312,23 @@ class PillarboxCastPlayer internal constructor(
295312
setStreamVolume(volume.toDouble())
296313
}
297314

298-
override fun handleSetDeviceMuted(muted: Boolean, flags: Int) = withRemoteClient {
299-
setStreamMute(muted)
315+
override fun handleSetDeviceVolume(
316+
@IntRange(from = 0) deviceVolume: Int,
317+
flags: @C.VolumeFlags Int,
318+
) = withCastSession("handleSetDeviceVolume") {
319+
volume = deviceVolume.coerceIn(VOLUME_RANGE) / MAX_VOLUME.toDouble()
320+
}
321+
322+
override fun handleIncreaseDeviceVolume(flags: @C.VolumeFlags Int): ListenableFuture<*> {
323+
return handleSetDeviceVolume(deviceVolume + 1, flags)
324+
}
325+
326+
override fun handleDecreaseDeviceVolume(flags: @C.VolumeFlags Int): ListenableFuture<*> {
327+
return handleSetDeviceVolume(deviceVolume - 1, flags)
328+
}
329+
330+
override fun handleSetDeviceMuted(muted: Boolean, flags: @C.VolumeFlags Int) = withCastSession("handleSetDeviceMuted") {
331+
isMute = muted
300332
}
301333

302334
override fun handleSetTrackSelectionParameters(trackSelectionParameters: TrackSelectionParameters) = withRemoteClient {
@@ -436,6 +468,16 @@ class PillarboxCastPlayer internal constructor(
436468
return Futures.immediateVoidFuture()
437469
}
438470

471+
private fun withCastSession(method: String, command: CastSession.() -> Unit): ListenableFuture<*> {
472+
try {
473+
castSession?.command()
474+
} catch (exception: IOException) {
475+
Log.w(TAG, "Ignoring $method due to exception", exception)
476+
}
477+
478+
return Futures.immediateVoidFuture()
479+
}
480+
439481
private fun getCastRepeatMode(repeatMode: @Player.RepeatMode Int): Int {
440482
return when (repeatMode) {
441483
REPEAT_MODE_ALL -> MediaStatus.REPEAT_MODE_REPEAT_ALL
@@ -505,7 +547,7 @@ class PillarboxCastPlayer internal constructor(
505547

506548
override fun onSessionEnded(session: CastSession, error: Int) {
507549
Log.i(TAG, "onSessionEnded ${session.sessionId} with error = $error")
508-
remoteMediaClient = null
550+
castSession = null
509551
}
510552

511553
override fun onSessionEnding(session: CastSession) {
@@ -518,7 +560,7 @@ class PillarboxCastPlayer internal constructor(
518560

519561
override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {
520562
Log.i(TAG, "onSessionResumed ${session.sessionId} wasSuspended = $wasSuspended")
521-
remoteMediaClient = session.remoteMediaClient
563+
castSession = session
522564
}
523565

524566
override fun onSessionResuming(session: CastSession, sessionId: String) {
@@ -531,7 +573,7 @@ class PillarboxCastPlayer internal constructor(
531573

532574
override fun onSessionStarted(session: CastSession, sessionId: String) {
533575
Log.i(TAG, "onSessionStarted ${session.sessionId} sessionId = $sessionId")
534-
remoteMediaClient = session.remoteMediaClient
576+
castSession = session
535577
}
536578

537579
override fun onSessionStarting(session: CastSession) {
@@ -540,7 +582,7 @@ class PillarboxCastPlayer internal constructor(
540582

541583
override fun onSessionSuspended(session: CastSession, reason: Int) {
542584
Log.i(TAG, "onSessionSuspended ${session.sessionId} with reason = $reason")
543-
remoteMediaClient = null
585+
castSession = null
544586
}
545587
}
546588

@@ -572,7 +614,7 @@ class PillarboxCastPlayer internal constructor(
572614

573615
val remoteController = controllers[1]
574616
val deviceInfo = DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
575-
.setMaxVolume(remoteController.volumeMax)
617+
.setMaxVolume(MAX_VOLUME)
576618
.setRoutingControllerId(remoteController.id)
577619
.build()
578620

@@ -592,9 +634,26 @@ class PillarboxCastPlayer internal constructor(
592634
}
593635
}
594636

637+
private inner class CastListener : Cast.Listener() {
638+
override fun onVolumeChanged() {
639+
Log.d(TAG, "onVolumeChanged")
640+
invalidateState()
641+
}
642+
}
643+
595644
private companion object {
596645
private const val TAG = "CastSimplePlayer"
597-
private val DEVICE_INFO_REMOTE_EMPTY = DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).build()
646+
647+
/**
648+
* @see androidx.media3.cast.CastPlayer.MAX_VOLUME
649+
*/
650+
private const val MAX_VOLUME = 20
651+
652+
private val DEVICE_INFO_REMOTE_EMPTY = DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
653+
.setMaxVolume(MAX_VOLUME)
654+
.build()
655+
656+
private val VOLUME_RANGE = 0..MAX_VOLUME
598657

599658
private fun getIdleReasonString(idleReason: Int): String {
600659
return when (idleReason) {

pillarbox-cast/src/main/java/ch/srgssr/pillarbox/cast/extension/RemoteMediaClient.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ package ch.srgssr.pillarbox.cast.extension
1010
import androidx.media3.common.C
1111
import androidx.media3.common.PlaybackParameters
1212
import androidx.media3.common.Player
13-
import androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS
1413
import androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS
1514
import androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM
1615
import androidx.media3.common.Player.COMMAND_GET_TIMELINE
@@ -109,10 +108,6 @@ internal fun RemoteMediaClient.getVolume(): Double {
109108
return mediaStatus?.streamVolume ?: 0.0
110109
}
111110

112-
internal fun RemoteMediaClient.isMuted(): Boolean {
113-
return mediaStatus?.isMute == true
114-
}
115-
116111
internal fun RemoteMediaClient.getTracks(): Tracks {
117112
val mediaTracks = mediaInfo?.mediaTracks ?: emptyList<MediaTrack>()
118113
return if (mediaTracks.isEmpty()) {
@@ -162,7 +157,6 @@ internal fun RemoteMediaClient.getAvailableCommands(
162157
.addIf(COMMAND_SEEK_TO_PREVIOUS, hasPreviousItem || canSeek)
163158
.addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPreviousItem)
164159
.addIf(COMMAND_SET_VOLUME, isCommandSupported(MediaStatus.COMMAND_SET_VOLUME))
165-
.addIf(COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS, isCommandSupported(MediaStatus.COMMAND_TOGGLE_MUTE))
166160
.addIf(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, canSeek)
167161
.addIf(COMMAND_SEEK_BACK, canSeekBack)
168162
.addIf(COMMAND_SEEK_FORWARD, canSeekForward)

pillarbox-cast/src/test/java/ch/srgssr/pillarbox/cast/extension/RemoteMediaClientTest.kt

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -164,20 +164,6 @@ class RemoteMediaClientTest {
164164
assertEquals(0.5, remoteMediaClient.getVolume())
165165
}
166166

167-
@Test
168-
fun `isMuted returns false when mediaStatus is null`() {
169-
every { remoteMediaClient.mediaStatus } returns null
170-
assertEquals(false, remoteMediaClient.isMuted())
171-
}
172-
173-
@Test
174-
fun `isMuted returns isMute`() {
175-
val mediaStatus = mockk<MediaStatus>()
176-
every { remoteMediaClient.mediaStatus } returns mediaStatus
177-
every { mediaStatus.isMute } returns true
178-
assertEquals(true, remoteMediaClient.isMuted())
179-
}
180-
181167
@Test
182168
fun `getTracks returns EMPTY when mediaInfo is null`() {
183169
every { remoteMediaClient.mediaInfo } returns null

0 commit comments

Comments
 (0)