Skip to content

Commit 0853a47

Browse files
Merge pull request #2279 from MGaetan89:cast_volume_management
PiperOrigin-RevId: 762426809
2 parents 297dc5e + 3665ff2 commit 0853a47

File tree

3 files changed

+159
-14
lines changed

3 files changed

+159
-14
lines changed

RELEASENOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
* MIDI extension:
7373
* Leanback extension:
7474
* Cast extension:
75+
* Add support for `setVolume()`, and `getVolume()`
76+
([#2279](https://github.com/androidx/media/pull/2279)).
7577
* Test Utilities:
7678
* Add `advance(player).untilPositionAtLeast` and `untilMediaItemIndex` to
7779
`TestPlayerRunHelper` in order to advance the player until a specified

libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ public final class CastPlayer extends BasePlayer {
113113
public static final DeviceInfo DEVICE_INFO_REMOTE_EMPTY =
114114
new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).setMaxVolume(MAX_VOLUME).build();
115115

116-
private static final Range<Integer> VOLUME_RANGE = new Range<>(0, MAX_VOLUME);
116+
private static final Range<Integer> RANGE_DEVICE_VOLUME = new Range<>(0, MAX_VOLUME);
117+
private static final Range<Float> RANGE_VOLUME = new Range<>(0.f, 1.f);
117118

118119
static {
119120
MediaLibraryInfo.registerModule("media3.cast");
@@ -178,6 +179,7 @@ public final class CastPlayer extends BasePlayer {
178179
private final StateHolder<Integer> repeatMode;
179180
private boolean isMuted;
180181
private int deviceVolume;
182+
private final StateHolder<Float> volume;
181183
private final StateHolder<PlaybackParameters> playbackParameters;
182184
@Nullable private CastSession castSession;
183185
@Nullable private RemoteMediaClient remoteMediaClient;
@@ -293,6 +295,7 @@ public CastPlayer(
293295
playWhenReady = new StateHolder<>(false);
294296
repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
295297
deviceVolume = MAX_VOLUME;
298+
volume = new StateHolder<>(1f);
296299
playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT);
297300
playbackState = STATE_IDLE;
298301
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
@@ -781,14 +784,34 @@ public AudioAttributes getAudioAttributes() {
781784
return AudioAttributes.DEFAULT;
782785
}
783786

784-
/** This method is not supported and does nothing. */
785787
@Override
786-
public void setVolume(float volume) {}
788+
public void setVolume(float volume) {
789+
if (remoteMediaClient == null) {
790+
return;
791+
}
792+
// We update the local state and send the message to the receiver app, which will cause the
793+
// operation to be perceived as synchronous by the user. When the operation reports a result,
794+
// the local state will be updated to reflect the state reported by the Cast SDK.
795+
volume = RANGE_VOLUME.clamp(volume);
796+
setVolumeAndNotifyIfChanged(volume);
797+
listeners.flushEvents();
798+
PendingResult<MediaChannelResult> pendingResult = remoteMediaClient.setStreamVolume(volume);
799+
this.volume.pendingResultCallback =
800+
new ResultCallback<MediaChannelResult>() {
801+
@Override
802+
public void onResult(MediaChannelResult result) {
803+
if (remoteMediaClient != null) {
804+
updateVolumeAndNotifyIfChanged(this);
805+
listeners.flushEvents();
806+
}
807+
}
808+
};
809+
pendingResult.setResultCallback(this.volume.pendingResultCallback);
810+
}
787811

788-
/** This method is not supported and returns 1. */
789812
@Override
790813
public float getVolume() {
791-
return 1;
814+
return volume.value;
792815
}
793816

794817
/** This method is not supported and does nothing. */
@@ -880,7 +903,7 @@ public void setDeviceVolume(@IntRange(from = 0) int volume, @C.VolumeFlags int f
880903
if (castSession == null) {
881904
return;
882905
}
883-
volume = VOLUME_RANGE.clamp(volume);
906+
volume = RANGE_DEVICE_VOLUME.clamp(volume);
884907
try {
885908
// See [Internal ref: b/399691860] for context on why we don't use
886909
// RemoteMediaClient.setStreamVolume.
@@ -969,8 +992,9 @@ private void updateInternalStateAndNotifyIfChanged() {
969992
? getCurrentTimeline().getPeriod(oldWindowIndex, period, /* setIds= */ true).uid
970993
: null;
971994
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
972-
updateVolumeAndNotifyIfChanged();
995+
updateDeviceVolumeAndNotifyIfChanged();
973996
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
997+
updateVolumeAndNotifyIfChanged(/* resultCallback= */ null);
974998
updatePlaybackRateAndNotifyIfChanged(/* resultCallback= */ null);
975999
boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged();
9761000
Timeline currentTimeline = getCurrentTimeline();
@@ -1079,13 +1103,23 @@ private void updatePlaybackRateAndNotifyIfChanged(@Nullable ResultCallback<?> re
10791103
}
10801104

10811105
@RequiresNonNull("castSession")
1082-
private void updateVolumeAndNotifyIfChanged() {
1106+
private void updateDeviceVolumeAndNotifyIfChanged() {
10831107
if (castSession != null) {
1084-
int deviceVolume = VOLUME_RANGE.clamp((int) Math.round(castSession.getVolume() * MAX_VOLUME));
1108+
int deviceVolume =
1109+
RANGE_DEVICE_VOLUME.clamp((int) Math.round(castSession.getVolume() * MAX_VOLUME));
10851110
setDeviceVolumeAndNotifyIfChanged(deviceVolume, castSession.isMute());
10861111
}
10871112
}
10881113

1114+
@RequiresNonNull("remoteMediaClient")
1115+
private void updateVolumeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
1116+
if (volume.acceptsUpdate(resultCallback)) {
1117+
float remoteVolume = RANGE_VOLUME.clamp(fetchVolume(remoteMediaClient));
1118+
setVolumeAndNotifyIfChanged(remoteVolume);
1119+
volume.clearPendingResultCallback();
1120+
}
1121+
}
1122+
10891123
@RequiresNonNull("remoteMediaClient")
10901124
private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
10911125
if (repeatMode.acceptsUpdate(resultCallback)) {
@@ -1229,14 +1263,29 @@ private boolean updateTracksAndSelectionsAndNotifyIfChanged() {
12291263

12301264
private void updateAvailableCommandsAndNotifyIfChanged() {
12311265
Commands previousAvailableCommands = availableCommands;
1232-
availableCommands = Util.getAvailableCommands(/* player= */ this, PERMANENT_AVAILABLE_COMMANDS);
1266+
availableCommands =
1267+
Util.getAvailableCommands(/* player= */ this, PERMANENT_AVAILABLE_COMMANDS)
1268+
.buildUpon()
1269+
.addIf(COMMAND_GET_VOLUME, isSetVolumeCommandAvailable())
1270+
.addIf(COMMAND_SET_VOLUME, isSetVolumeCommandAvailable())
1271+
.build();
12331272
if (!availableCommands.equals(previousAvailableCommands)) {
12341273
listeners.queueEvent(
12351274
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
12361275
listener -> listener.onAvailableCommandsChanged(availableCommands));
12371276
}
12381277
}
12391278

1279+
private boolean isSetVolumeCommandAvailable() {
1280+
if (remoteMediaClient != null) {
1281+
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
1282+
if (mediaStatus != null) {
1283+
return mediaStatus.isMediaCommandSupported(MediaStatus.COMMAND_SET_VOLUME);
1284+
}
1285+
}
1286+
return false;
1287+
}
1288+
12401289
private void setMediaItemsInternal(
12411290
List<MediaItem> mediaItems,
12421291
int startIndex,
@@ -1347,6 +1396,15 @@ private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode)
13471396
}
13481397
}
13491398

1399+
private void setVolumeAndNotifyIfChanged(float volume) {
1400+
if (this.volume.value != volume) {
1401+
this.volume.value = volume;
1402+
listeners.queueEvent(
1403+
Player.EVENT_VOLUME_CHANGED, listener -> listener.onVolumeChanged(volume));
1404+
updateAvailableCommandsAndNotifyIfChanged();
1405+
}
1406+
}
1407+
13501408
private void setPlaybackParametersAndNotifyIfChanged(PlaybackParameters playbackParameters) {
13511409
if (this.playbackParameters.value.equals(playbackParameters)) {
13521410
return;
@@ -1470,6 +1528,14 @@ private static int fetchPlaybackState(RemoteMediaClient remoteMediaClient) {
14701528
}
14711529
}
14721530

1531+
private static float fetchVolume(RemoteMediaClient remoteMediaClient) {
1532+
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
1533+
if (mediaStatus == null) {
1534+
return 1f;
1535+
}
1536+
return (float) mediaStatus.getStreamVolume();
1537+
}
1538+
14731539
private static int fetchCurrentWindowIndex(
14741540
@Nullable RemoteMediaClient remoteMediaClient, Timeline timeline) {
14751541
if (remoteMediaClient == null) {
@@ -1734,8 +1800,6 @@ public DeviceInfo fetchDeviceInfo() {
17341800
// There's only one remote routing controller. It's safe to assume it's the Cast routing
17351801
// controller.
17361802
RoutingController remoteController = controllers.get(1);
1737-
// TODO b/364580007 - Populate volume information, and implement Player volume-related
1738-
// methods.
17391803
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
17401804
.setMaxVolume(MAX_VOLUME)
17411805
.setRoutingControllerId(remoteController.getId())
@@ -1774,7 +1838,7 @@ private final class CastListener extends Cast.Listener {
17741838

17751839
@Override
17761840
public void onVolumeChanged() {
1777-
updateVolumeAndNotifyIfChanged();
1841+
updateDeviceVolumeAndNotifyIfChanged();
17781842
listeners.flushEvents();
17791843
}
17801844
}

libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package androidx.media3.cast;
1717

1818
import static androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME;
19+
import static androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS;
1920
import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS;
2021
import static androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES;
2122
import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM;
@@ -26,6 +27,7 @@
2627
import static androidx.media3.common.Player.COMMAND_GET_VOLUME;
2728
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
2829
import static androidx.media3.common.Player.COMMAND_PREPARE;
30+
import static androidx.media3.common.Player.COMMAND_RELEASE;
2931
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
3032
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
3133
import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
@@ -36,6 +38,7 @@
3638
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
3739
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
3840
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME;
41+
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS;
3942
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
4043
import static androidx.media3.common.Player.COMMAND_SET_PLAYLIST_METADATA;
4144
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
@@ -48,6 +51,7 @@
4851
import static androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED;
4952
import static com.google.common.truth.Truth.assertThat;
5053
import static org.mockito.ArgumentMatchers.any;
54+
import static org.mockito.ArgumentMatchers.anyDouble;
5155
import static org.mockito.ArgumentMatchers.anyInt;
5256
import static org.mockito.ArgumentMatchers.anyLong;
5357
import static org.mockito.ArgumentMatchers.eq;
@@ -139,6 +143,7 @@ public void setUp() {
139143
// Make the remote media client present the same default values as ExoPlayer:
140144
when(mockRemoteMediaClient.isPaused()).thenReturn(true);
141145
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
146+
when(mockMediaStatus.getStreamVolume()).thenReturn(1.0);
142147
when(mockMediaStatus.getPlaybackRate()).thenReturn(1.0d);
143148
mediaItemConverter = new DefaultMediaItemConverter();
144149
castPlayer = new CastPlayer(mockCastContext, mediaItemConverter);
@@ -390,6 +395,60 @@ public void repeatMode_changesOnStatusUpdates() {
390395
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
391396
}
392397

398+
@Test
399+
public void setVolume_masksRemoteState() {
400+
when(mockRemoteMediaClient.setStreamVolume(anyDouble())).thenReturn(mockPendingResult);
401+
assertThat(castPlayer.getVolume()).isEqualTo(1f);
402+
403+
castPlayer.setVolume(0.5f);
404+
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
405+
assertThat(castPlayer.getVolume()).isEqualTo(0.5f);
406+
verify(mockListener).onVolumeChanged(0.5f);
407+
408+
// There is a status update in the middle, which should be hidden by masking.
409+
when(mockMediaStatus.getStreamVolume()).thenReturn(0.75);
410+
remoteMediaClientCallback.onStatusUpdated();
411+
verifyNoMoreInteractions(mockListener);
412+
413+
// Upon result, the mediaStatus now exposes the new volume.
414+
when(mockMediaStatus.getStreamVolume()).thenReturn(0.5);
415+
setResultCallbackArgumentCaptor
416+
.getValue()
417+
.onResult(mock(RemoteMediaClient.MediaChannelResult.class));
418+
verifyNoMoreInteractions(mockListener);
419+
}
420+
421+
@Test
422+
public void setVolume_updatesUponResultChange() {
423+
when(mockRemoteMediaClient.setStreamVolume(anyDouble())).thenReturn(mockPendingResult);
424+
425+
castPlayer.setVolume(0.5f);
426+
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
427+
assertThat(castPlayer.getVolume()).isEqualTo(0.5f);
428+
verify(mockListener).onVolumeChanged(0.5f);
429+
430+
// There is a status update in the middle, which should be hidden by masking.
431+
when(mockMediaStatus.getStreamVolume()).thenReturn(0.75);
432+
remoteMediaClientCallback.onStatusUpdated();
433+
verifyNoMoreInteractions(mockListener);
434+
435+
// Upon result, the volume is 0.75. The state should reflect that.
436+
setResultCallbackArgumentCaptor
437+
.getValue()
438+
.onResult(mock(RemoteMediaClient.MediaChannelResult.class));
439+
verify(mockListener).onVolumeChanged(0.75f);
440+
assertThat(castPlayer.getVolume()).isEqualTo(0.75f);
441+
}
442+
443+
@Test
444+
public void volume_changesOnStatusUpdates() {
445+
assertThat(castPlayer.getVolume()).isEqualTo(1f);
446+
when(mockMediaStatus.getStreamVolume()).thenReturn(0.75);
447+
remoteMediaClientCallback.onStatusUpdated();
448+
verify(mockListener).onVolumeChanged(0.75f);
449+
assertThat(castPlayer.getVolume()).isEqualTo(0.75f);
450+
}
451+
393452
@Test
394453
public void setMediaItems_callsRemoteMediaClient() {
395454
List<MediaItem> mediaItems = new ArrayList<>();
@@ -1410,7 +1469,27 @@ public void isCommandAvailable_isTrueForAvailableCommands() {
14101469
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isTrue();
14111470
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isFalse();
14121471
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TEXT)).isFalse();
1413-
assertThat(castPlayer.isCommandAvailable(Player.COMMAND_RELEASE)).isTrue();
1472+
assertThat(castPlayer.isCommandAvailable(COMMAND_RELEASE)).isTrue();
1473+
}
1474+
1475+
@Test
1476+
public void isCommandAvailable_setVolumeIsSupported() {
1477+
when(mockMediaStatus.isMediaCommandSupported(MediaStatus.COMMAND_SET_VOLUME)).thenReturn(true);
1478+
1479+
int[] mediaQueueItemIds = new int[] {1, 2};
1480+
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
1481+
1482+
castPlayer.addMediaItems(mediaItems);
1483+
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
1484+
1485+
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isFalse();
1486+
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_VOLUME)).isTrue();
1487+
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isTrue();
1488+
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VOLUME)).isTrue();
1489+
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_DEVICE_VOLUME)).isTrue();
1490+
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS)).isTrue();
1491+
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isTrue();
1492+
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)).isTrue();
14141493
}
14151494

14161495
@Test

0 commit comments

Comments
 (0)