Skip to content

Commit 034c8ef

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 756b646 commit 034c8ef

File tree

3 files changed

+73
-33
lines changed

3 files changed

+73
-33
lines changed

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

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ import ch.srgssr.pillarbox.cast.extension.getPlaybackState
4040
import ch.srgssr.pillarbox.cast.extension.getRepeatMode
4141
import ch.srgssr.pillarbox.cast.extension.getTracks
4242
import ch.srgssr.pillarbox.cast.extension.getVolume
43-
import ch.srgssr.pillarbox.cast.extension.isMuted
4443
import ch.srgssr.pillarbox.player.PillarboxDsl
4544
import ch.srgssr.pillarbox.player.PillarboxPlayer
45+
import com.google.android.gms.cast.Cast
4646
import com.google.android.gms.cast.CastStatusCodes
4747
import com.google.android.gms.cast.MediaError
4848
import com.google.android.gms.cast.MediaInfo
@@ -58,6 +58,8 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient.ProgressLis
5858
import com.google.android.gms.common.api.PendingResult
5959
import com.google.common.util.concurrent.Futures
6060
import com.google.common.util.concurrent.ListenableFuture
61+
import java.io.IOException
62+
import kotlin.math.roundToInt
6163
import kotlin.time.Duration.Companion.milliseconds
6264

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

126+
private var castSession: CastSession? = null
127+
set(value) {
128+
field?.removeCastListener(castListener)
129+
value?.addCastListener(castListener)
130+
field = value
131+
132+
remoteMediaClient = value?.remoteMediaClient
133+
}
134+
122135
private var deviceInfo = if (isMediaRouter2Available()) checkNotNull(mediaRouter).fetchDeviceInfo() else DEVICE_INFO_REMOTE_EMPTY
123136
set(value) {
124137
if (field != value) {
@@ -131,8 +144,6 @@ class PillarboxCastPlayer internal constructor(
131144
private var sessionAvailabilityListener: SessionAvailabilityListener? = null
132145
private var playlistTracker: MediaQueueTracker? = null
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
}
@@ -190,6 +201,10 @@ class PillarboxCastPlayer internal constructor(
190201
} else {
191202
TrackSelectionParameters.DEFAULT
192203
}
204+
val deviceVolume = castSession?.let {
205+
(it.volume * MAX_VOLUME).roundToInt().coerceIn(VOLUME_RANGE)
206+
}
207+
193208
return State.Builder()
194209
.setAvailableCommands(remoteMediaClient.getAvailableCommands(seekBackIncrementMs, seekForwardIncrementMs))
195210
.setPlaybackState(if (playlist.isNotEmpty()) remoteMediaClient.getPlaybackState() else STATE_IDLE)
@@ -206,7 +221,7 @@ class PillarboxCastPlayer internal constructor(
206221
.setShuffleModeEnabled(false)
207222
.setRepeatMode(remoteMediaClient.getRepeatMode())
208223
.setVolume(remoteMediaClient.getVolume().toFloat())
209-
.setIsDeviceMuted(remoteMediaClient.isMuted())
224+
.setIsDeviceMuted(castSession?.isMute ?: false)
210225
.setDeviceInfo(deviceInfo)
211226
.setMaxSeekToPreviousPositionMs(maxSeekToPreviousPositionMs)
212227
.setSeekBackIncrementMs(seekBackIncrementMs)
@@ -215,6 +230,9 @@ class PillarboxCastPlayer internal constructor(
215230
.setPlaybackParameters(PlaybackParameters(remoteMediaClient.getPlaybackRate()))
216231
.setPlaylistMetadata(playlistMetadata)
217232
.setIsLoading(isLoading && playlist.isNotEmpty())
233+
.apply {
234+
deviceVolume?.let(this::setDeviceVolume)
235+
}
218236
.build()
219237
}
220238

@@ -302,8 +320,23 @@ class PillarboxCastPlayer internal constructor(
302320
setStreamVolume(volume.toDouble())
303321
}
304322

305-
override fun handleSetDeviceMuted(muted: Boolean, flags: Int) = withRemoteClient {
306-
setStreamMute(muted)
323+
override fun handleSetDeviceVolume(
324+
@IntRange(from = 0) deviceVolume: Int,
325+
flags: @C.VolumeFlags Int,
326+
) = withCastSession("handleSetDeviceVolume") {
327+
volume = deviceVolume.coerceIn(VOLUME_RANGE) / MAX_VOLUME.toDouble()
328+
}
329+
330+
override fun handleIncreaseDeviceVolume(flags: @C.VolumeFlags Int): ListenableFuture<*> {
331+
return handleSetDeviceVolume(deviceVolume + 1, flags)
332+
}
333+
334+
override fun handleDecreaseDeviceVolume(flags: @C.VolumeFlags Int): ListenableFuture<*> {
335+
return handleSetDeviceVolume(deviceVolume - 1, flags)
336+
}
337+
338+
override fun handleSetDeviceMuted(muted: Boolean, flags: @C.VolumeFlags Int) = withCastSession("handleSetDeviceMuted") {
339+
isMute = muted
307340
}
308341

309342
override fun handleSetTrackSelectionParameters(trackSelectionParameters: TrackSelectionParameters) = withRemoteClient {
@@ -442,6 +475,16 @@ class PillarboxCastPlayer internal constructor(
442475
return Futures.immediateVoidFuture()
443476
}
444477

478+
private fun withCastSession(method: String, command: CastSession.() -> Unit): ListenableFuture<*> {
479+
try {
480+
castSession?.command()
481+
} catch (exception: IOException) {
482+
Log.w(TAG, "Ignoring $method due to exception", exception)
483+
}
484+
485+
return Futures.immediateVoidFuture()
486+
}
487+
445488
private fun getCastRepeatMode(repeatMode: @Player.RepeatMode Int): Int {
446489
return when (repeatMode) {
447490
REPEAT_MODE_ALL -> MediaStatus.REPEAT_MODE_REPEAT_ALL
@@ -511,7 +554,7 @@ class PillarboxCastPlayer internal constructor(
511554

512555
override fun onSessionEnded(session: CastSession, error: Int) {
513556
Log.i(TAG, "onSessionEnded ${session.sessionId} with error = $error")
514-
remoteMediaClient = null
557+
castSession = null
515558
}
516559

517560
override fun onSessionEnding(session: CastSession) {
@@ -525,7 +568,7 @@ class PillarboxCastPlayer internal constructor(
525568

526569
override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {
527570
Log.i(TAG, "onSessionResumed ${session.sessionId} wasSuspended = $wasSuspended")
528-
remoteMediaClient = session.remoteMediaClient
571+
castSession = session
529572
}
530573

531574
override fun onSessionResuming(session: CastSession, sessionId: String) {
@@ -538,7 +581,7 @@ class PillarboxCastPlayer internal constructor(
538581

539582
override fun onSessionStarted(session: CastSession, sessionId: String) {
540583
Log.i(TAG, "onSessionStarted ${session.sessionId} sessionId = $sessionId")
541-
remoteMediaClient = session.remoteMediaClient
584+
castSession = session
542585
}
543586

544587
override fun onSessionStarting(session: CastSession) {
@@ -547,7 +590,7 @@ class PillarboxCastPlayer internal constructor(
547590

548591
override fun onSessionSuspended(session: CastSession, reason: Int) {
549592
Log.i(TAG, "onSessionSuspended ${session.sessionId} with reason = $reason")
550-
remoteMediaClient = null
593+
castSession = null
551594
}
552595
}
553596

@@ -579,7 +622,7 @@ class PillarboxCastPlayer internal constructor(
579622

580623
val remoteController = controllers[1]
581624
val deviceInfo = DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
582-
.setMaxVolume(remoteController.volumeMax)
625+
.setMaxVolume(MAX_VOLUME)
583626
.setRoutingControllerId(remoteController.id)
584627
.build()
585628

@@ -599,9 +642,26 @@ class PillarboxCastPlayer internal constructor(
599642
}
600643
}
601644

645+
private inner class CastListener : Cast.Listener() {
646+
override fun onVolumeChanged() {
647+
Log.d(TAG, "onVolumeChanged")
648+
invalidateState()
649+
}
650+
}
651+
602652
private companion object {
603653
private const val TAG = "CastSimplePlayer"
604-
private val DEVICE_INFO_REMOTE_EMPTY = DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).build()
654+
655+
/**
656+
* @see androidx.media3.cast.CastPlayer.MAX_VOLUME
657+
*/
658+
private const val MAX_VOLUME = 20
659+
660+
private val DEVICE_INFO_REMOTE_EMPTY = DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
661+
.setMaxVolume(MAX_VOLUME)
662+
.build()
663+
664+
private val VOLUME_RANGE = 0..MAX_VOLUME
605665

606666
private fun createTrackSelectionParametersFromSelectedTracks(tracks: Tracks): TrackSelectionParameters {
607667
return TrackSelectionParameters.DEFAULT.buildUpon().apply {

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
@@ -108,10 +107,6 @@ internal fun RemoteMediaClient.getVolume(): Double {
108107
return mediaStatus?.streamVolume ?: 0.0
109108
}
110109

111-
internal fun RemoteMediaClient.isMuted(): Boolean {
112-
return mediaStatus?.isMute == true
113-
}
114-
115110
internal fun RemoteMediaClient.getTracks(): Tracks {
116111
val mediaTracks = mediaInfo?.mediaTracks ?: emptyList<MediaTrack>()
117112
return if (mediaTracks.isEmpty()) {
@@ -163,7 +158,6 @@ internal fun RemoteMediaClient.getAvailableCommands(
163158
.addIf(COMMAND_SEEK_TO_PREVIOUS, hasPreviousItem || canSeek)
164159
.addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPreviousItem)
165160
.addIf(COMMAND_SET_VOLUME, isCommandSupported(MediaStatus.COMMAND_SET_VOLUME))
166-
.addIf(COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS, isCommandSupported(MediaStatus.COMMAND_TOGGLE_MUTE))
167161
.addIf(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, canSeek)
168162
.addIf(COMMAND_SEEK_BACK, canSeekBack)
169163
.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
@@ -169,20 +169,6 @@ class RemoteMediaClientTest {
169169
assertEquals(0.5, remoteMediaClient.getVolume())
170170
}
171171

172-
@Test
173-
fun `isMuted returns false when mediaStatus is null`() {
174-
every { remoteMediaClient.mediaStatus } returns null
175-
assertEquals(false, remoteMediaClient.isMuted())
176-
}
177-
178-
@Test
179-
fun `isMuted returns isMute`() {
180-
val mediaStatus = mockk<MediaStatus>()
181-
every { remoteMediaClient.mediaStatus } returns mediaStatus
182-
every { mediaStatus.isMute } returns true
183-
assertEquals(true, remoteMediaClient.isMuted())
184-
}
185-
186172
@Test
187173
fun `getTracks returns EMPTY when mediaInfo is null`() {
188174
every { remoteMediaClient.mediaInfo } returns null

0 commit comments

Comments
 (0)