Skip to content

Commit dcacfc7

Browse files
committed
Implement getVolume()/setVolume() on CastPlayer
1 parent 4d68243 commit dcacfc7

File tree

2 files changed

+170
-8
lines changed

2 files changed

+170
-8
lines changed

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

+70-7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import android.view.SurfaceView;
3636
import android.view.TextureView;
3737
import androidx.annotation.IntRange;
38+
import androidx.annotation.NonNull;
3839
import androidx.annotation.Nullable;
3940
import androidx.annotation.RequiresApi;
4041
import androidx.annotation.VisibleForTesting;
@@ -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,33 @@ 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+
setVolumeAndNotifyIfChanged(volume);
796+
listeners.flushEvents();
797+
PendingResult<MediaChannelResult> pendingResult = remoteMediaClient.setStreamVolume(volume);
798+
this.volume.pendingResultCallback =
799+
new ResultCallback<MediaChannelResult>() {
800+
@Override
801+
public void onResult(@NonNull MediaChannelResult result) {
802+
if (remoteMediaClient != null) {
803+
updateVolumeAndNotifyIfChanged(this);
804+
listeners.flushEvents();
805+
}
806+
}
807+
};
808+
pendingResult.setResultCallback(this.volume.pendingResultCallback);
809+
}
787810

788-
/** This method is not supported and returns 1. */
789811
@Override
790812
public float getVolume() {
791-
return 1;
813+
return volume.value;
792814
}
793815

794816
/** This method is not supported and does nothing. */
@@ -971,6 +993,7 @@ private void updateInternalStateAndNotifyIfChanged() {
971993
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
972994
updateVolumeAndNotifyIfChanged();
973995
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
996+
updateVolumeAndNotifyIfChanged(/* resultCallback= */ null);
974997
updatePlaybackRateAndNotifyIfChanged(/* resultCallback= */ null);
975998
boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged();
976999
Timeline currentTimeline = getCurrentTimeline();
@@ -1094,6 +1117,14 @@ private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback<?> resu
10941117
}
10951118
}
10961119

1120+
@RequiresNonNull("remoteMediaClient")
1121+
private void updateVolumeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
1122+
if (volume.acceptsUpdate(resultCallback)) {
1123+
setVolumeAndNotifyIfChanged(fetchVolume(remoteMediaClient));
1124+
volume.clearPendingResultCallback();
1125+
}
1126+
}
1127+
10971128
/**
10981129
* Updates the timeline and notifies {@link Player.Listener event listeners} if required.
10991130
*
@@ -1229,14 +1260,29 @@ private boolean updateTracksAndSelectionsAndNotifyIfChanged() {
12291260

12301261
private void updateAvailableCommandsAndNotifyIfChanged() {
12311262
Commands previousAvailableCommands = availableCommands;
1232-
availableCommands = Util.getAvailableCommands(/* player= */ this, PERMANENT_AVAILABLE_COMMANDS);
1263+
availableCommands =
1264+
Util.getAvailableCommands(/* player= */ this, PERMANENT_AVAILABLE_COMMANDS)
1265+
.buildUpon()
1266+
.addIf(COMMAND_GET_VOLUME, isSetVolumeCommandAvailable())
1267+
.addIf(COMMAND_SET_VOLUME, isSetVolumeCommandAvailable())
1268+
.build();
12331269
if (!availableCommands.equals(previousAvailableCommands)) {
12341270
listeners.queueEvent(
12351271
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
12361272
listener -> listener.onAvailableCommandsChanged(availableCommands));
12371273
}
12381274
}
12391275

1276+
private boolean isSetVolumeCommandAvailable() {
1277+
if (remoteMediaClient != null) {
1278+
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
1279+
if (mediaStatus != null) {
1280+
return mediaStatus.isMediaCommandSupported(MediaStatus.COMMAND_SET_VOLUME);
1281+
}
1282+
}
1283+
return false;
1284+
}
1285+
12401286
private void setMediaItemsInternal(
12411287
List<MediaItem> mediaItems,
12421288
int startIndex,
@@ -1347,6 +1393,15 @@ private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode)
13471393
}
13481394
}
13491395

1396+
private void setVolumeAndNotifyIfChanged(float volume) {
1397+
if (this.volume.value != volume) {
1398+
this.volume.value = volume;
1399+
listeners.queueEvent(
1400+
Player.EVENT_VOLUME_CHANGED, listener -> listener.onVolumeChanged(volume));
1401+
updateAvailableCommandsAndNotifyIfChanged();
1402+
}
1403+
}
1404+
13501405
private void setPlaybackParametersAndNotifyIfChanged(PlaybackParameters playbackParameters) {
13511406
if (this.playbackParameters.value.equals(playbackParameters)) {
13521407
return;
@@ -1470,6 +1525,15 @@ private static int fetchPlaybackState(RemoteMediaClient remoteMediaClient) {
14701525
}
14711526
}
14721527

1528+
private static float fetchVolume(RemoteMediaClient remoteMediaClient) {
1529+
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
1530+
if (mediaStatus == null) {
1531+
// No media session active, yet.
1532+
return 1f;
1533+
}
1534+
return (float) mediaStatus.getStreamVolume();
1535+
}
1536+
14731537
private static int fetchCurrentWindowIndex(
14741538
@Nullable RemoteMediaClient remoteMediaClient, Timeline timeline) {
14751539
if (remoteMediaClient == null) {
@@ -1734,8 +1798,7 @@ public DeviceInfo fetchDeviceInfo() {
17341798
// There's only one remote routing controller. It's safe to assume it's the Cast routing
17351799
// controller.
17361800
RoutingController remoteController = controllers.get(1);
1737-
// TODO b/364580007 - Populate volume information, and implement Player volume-related
1738-
// methods.
1801+
// TODO b/364580007 - Populate min volume information.
17391802
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
17401803
.setMaxVolume(MAX_VOLUME)
17411804
.setRoutingControllerId(remoteController.getId())

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

+100-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import static androidx.media3.common.Player.COMMAND_GET_VOLUME;
2727
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
2828
import static androidx.media3.common.Player.COMMAND_PREPARE;
29+
import static androidx.media3.common.Player.COMMAND_RELEASE;
2930
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
3031
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
3132
import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
@@ -48,6 +49,7 @@
4849
import static androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED;
4950
import static com.google.common.truth.Truth.assertThat;
5051
import static org.mockito.ArgumentMatchers.any;
52+
import static org.mockito.ArgumentMatchers.anyDouble;
5153
import static org.mockito.ArgumentMatchers.anyInt;
5254
import static org.mockito.ArgumentMatchers.anyLong;
5355
import static org.mockito.ArgumentMatchers.eq;
@@ -139,6 +141,7 @@ public void setUp() {
139141
// Make the remote media client present the same default values as ExoPlayer:
140142
when(mockRemoteMediaClient.isPaused()).thenReturn(true);
141143
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
144+
when(mockMediaStatus.getStreamVolume()).thenReturn(1.0);
142145
when(mockMediaStatus.getPlaybackRate()).thenReturn(1.0d);
143146
mediaItemConverter = new DefaultMediaItemConverter();
144147
castPlayer = new CastPlayer(mockCastContext, mediaItemConverter);
@@ -390,6 +393,60 @@ public void repeatMode_changesOnStatusUpdates() {
390393
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
391394
}
392395

396+
@Test
397+
public void setVolume_masksRemoteState() {
398+
when(mockRemoteMediaClient.setStreamVolume(anyDouble())).thenReturn(mockPendingResult);
399+
assertThat(castPlayer.getVolume()).isEqualTo(1f);
400+
401+
castPlayer.setVolume(0.5f);
402+
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
403+
assertThat(castPlayer.getVolume()).isEqualTo(0.5f);
404+
verify(mockListener).onVolumeChanged(0.5f);
405+
406+
// There is a status update in the middle, which should be hidden by masking.
407+
when(mockMediaStatus.getStreamVolume()).thenReturn(0.75);
408+
remoteMediaClientCallback.onStatusUpdated();
409+
verifyNoMoreInteractions(mockListener);
410+
411+
// Upon result, the mediaStatus now exposes the new volume.
412+
when(mockMediaStatus.getStreamVolume()).thenReturn(0.5);
413+
setResultCallbackArgumentCaptor
414+
.getValue()
415+
.onResult(mock(RemoteMediaClient.MediaChannelResult.class));
416+
verifyNoMoreInteractions(mockListener);
417+
}
418+
419+
@Test
420+
public void setVolume_updatesUponResultChange() {
421+
when(mockRemoteMediaClient.setStreamVolume(anyDouble())).thenReturn(mockPendingResult);
422+
423+
castPlayer.setVolume(0.5f);
424+
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
425+
assertThat(castPlayer.getVolume()).isEqualTo(0.5f);
426+
verify(mockListener).onVolumeChanged(0.5f);
427+
428+
// There is a status update in the middle, which should be hidden by masking.
429+
when(mockMediaStatus.getStreamVolume()).thenReturn(0.75);
430+
remoteMediaClientCallback.onStatusUpdated();
431+
verifyNoMoreInteractions(mockListener);
432+
433+
// Upon result, the volume is 0.75. The state should reflect that.
434+
setResultCallbackArgumentCaptor
435+
.getValue()
436+
.onResult(mock(RemoteMediaClient.MediaChannelResult.class));
437+
verify(mockListener).onVolumeChanged(0.75f);
438+
assertThat(castPlayer.getVolume()).isEqualTo(0.75f);
439+
}
440+
441+
@Test
442+
public void volume_changesOnStatusUpdates() {
443+
assertThat(castPlayer.getVolume()).isEqualTo(1f);
444+
when(mockMediaStatus.getStreamVolume()).thenReturn(0.75);
445+
remoteMediaClientCallback.onStatusUpdated();
446+
verify(mockListener).onVolumeChanged(0.75f);
447+
assertThat(castPlayer.getVolume()).isEqualTo(0.75f);
448+
}
449+
393450
@Test
394451
public void setMediaItems_callsRemoteMediaClient() {
395452
List<MediaItem> mediaItems = new ArrayList<>();
@@ -1410,7 +1467,49 @@ public void isCommandAvailable_isTrueForAvailableCommands() {
14101467
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isTrue();
14111468
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isFalse();
14121469
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TEXT)).isFalse();
1413-
assertThat(castPlayer.isCommandAvailable(Player.COMMAND_RELEASE)).isTrue();
1470+
assertThat(castPlayer.isCommandAvailable(COMMAND_RELEASE)).isTrue();
1471+
}
1472+
1473+
@Test
1474+
public void isCommandAvailable_setVolumeIsSupported() {
1475+
when(mockMediaStatus.isMediaCommandSupported(MediaStatus.COMMAND_SET_VOLUME)).thenReturn(true);
1476+
1477+
int[] mediaQueueItemIds = new int[] {1, 2};
1478+
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
1479+
1480+
castPlayer.addMediaItems(mediaItems);
1481+
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
1482+
1483+
assertThat(castPlayer.isCommandAvailable(COMMAND_PLAY_PAUSE)).isTrue();
1484+
assertThat(castPlayer.isCommandAvailable(COMMAND_PREPARE)).isTrue();
1485+
assertThat(castPlayer.isCommandAvailable(COMMAND_STOP)).isTrue();
1486+
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_DEFAULT_POSITION)).isTrue();
1487+
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)).isTrue();
1488+
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)).isFalse();
1489+
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)).isTrue();
1490+
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)).isTrue();
1491+
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_NEXT)).isTrue();
1492+
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)).isTrue();
1493+
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_BACK)).isTrue();
1494+
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_FORWARD)).isTrue();
1495+
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)).isTrue();
1496+
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)).isFalse();
1497+
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_REPEAT_MODE)).isTrue();
1498+
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)).isTrue();
1499+
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TIMELINE)).isTrue();
1500+
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_METADATA)).isTrue();
1501+
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_PLAYLIST_METADATA)).isTrue();
1502+
assertThat(castPlayer.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue();
1503+
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)).isTrue();
1504+
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isFalse();
1505+
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_VOLUME)).isTrue();
1506+
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isTrue();
1507+
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VOLUME)).isTrue();
1508+
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_DEVICE_VOLUME)).isTrue();
1509+
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isTrue();
1510+
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isFalse();
1511+
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TEXT)).isFalse();
1512+
assertThat(castPlayer.isCommandAvailable(COMMAND_RELEASE)).isTrue();
14141513
}
14151514

14161515
@Test

0 commit comments

Comments
 (0)