Skip to content

Commit 704d120

Browse files
committed
Implement isDeviceMuted()/setDeviceMuted() on CastPlayer
1 parent d5cca67 commit 704d120

File tree

2 files changed

+134
-11
lines changed

2 files changed

+134
-11
lines changed

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

+69-7
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ public final class CastPlayer extends BasePlayer {
156156
private final StateHolder<Boolean> playWhenReady;
157157
private final StateHolder<Integer> repeatMode;
158158
private final StateHolder<Float> volume;
159+
private final StateHolder<Boolean> deviceMuted;
159160
private final StateHolder<PlaybackParameters> playbackParameters;
160161
@Nullable private RemoteMediaClient remoteMediaClient;
161162
private CastTimeline currentTimeline;
@@ -269,6 +270,7 @@ public CastPlayer(
269270
playWhenReady = new StateHolder<>(false);
270271
repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
271272
volume = new StateHolder<>(1f);
273+
deviceMuted = new StateHolder<>(false);
272274
playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT);
273275
playbackState = STATE_IDLE;
274276
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
@@ -857,10 +859,9 @@ public int getDeviceVolume() {
857859
return 0;
858860
}
859861

860-
/** This method is not supported and always returns {@code false}. */
861862
@Override
862863
public boolean isDeviceMuted() {
863-
return false;
864+
return deviceMuted.value;
864865
}
865866

866867
/**
@@ -901,11 +902,33 @@ public void decreaseDeviceVolume(@C.VolumeFlags int flags) {}
901902
*/
902903
@Deprecated
903904
@Override
904-
public void setDeviceMuted(boolean muted) {}
905+
public void setDeviceMuted(boolean muted) {
906+
setDeviceMuted(muted, 0);
907+
}
905908

906-
/** This method is not supported and does nothing. */
907909
@Override
908-
public void setDeviceMuted(boolean muted, @C.VolumeFlags int flags) {}
910+
public void setDeviceMuted(boolean muted, @C.VolumeFlags int flags) {
911+
if (remoteMediaClient == null) {
912+
return;
913+
}
914+
// We update the local state and send the message to the receiver app, which will cause the
915+
// operation to be perceived as synchronous by the user. When the operation reports a result,
916+
// the local state will be updated to reflect the state reported by the Cast SDK.
917+
setDeviceMutedAndNotifyIfChanged(muted);
918+
listeners.flushEvents();
919+
PendingResult<MediaChannelResult> pendingResult = remoteMediaClient.setStreamMute(muted);
920+
this.deviceMuted.pendingResultCallback =
921+
new ResultCallback<MediaChannelResult>() {
922+
@Override
923+
public void onResult(@NonNull MediaChannelResult result) {
924+
if (remoteMediaClient != null) {
925+
updateDeviceMutedAndNotifyIfChanged(this);
926+
listeners.flushEvents();
927+
}
928+
}
929+
};
930+
pendingResult.setResultCallback(this.deviceMuted.pendingResultCallback);
931+
}
909932

910933
/** This method is not supported and does nothing. */
911934
@Override
@@ -930,6 +953,7 @@ private void updateInternalStateAndNotifyIfChanged() {
930953
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
931954
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
932955
updateVolumeAndNotifyIfChanged(/* resultCallback= */ null);
956+
updateDeviceMutedAndNotifyIfChanged(/* resultCallback= */ null);
933957
updatePlaybackRateAndNotifyIfChanged(/* resultCallback= */ null);
934958
boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged();
935959
Timeline currentTimeline = getCurrentTimeline();
@@ -1053,6 +1077,14 @@ private void updateVolumeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCa
10531077
}
10541078
}
10551079

1080+
@RequiresNonNull("remoteMediaClient")
1081+
private void updateDeviceMutedAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
1082+
if (deviceMuted.acceptsUpdate(resultCallback)) {
1083+
setDeviceMutedAndNotifyIfChanged(fetchDeviceMuted(remoteMediaClient));
1084+
deviceMuted.clearPendingResultCallback();
1085+
}
1086+
}
1087+
10561088
/**
10571089
* Updates the timeline and notifies {@link Player.Listener event listeners} if required.
10581090
*
@@ -1192,6 +1224,8 @@ private void updateAvailableCommandsAndNotifyIfChanged() {
11921224
Util.getAvailableCommands(/* player= */ this, PERMANENT_AVAILABLE_COMMANDS)
11931225
.buildUpon()
11941226
.addIf(COMMAND_SET_VOLUME, isSetVolumeCommandAvailable())
1227+
.addIf(COMMAND_ADJUST_DEVICE_VOLUME, isToggleMuteCommandAvailable())
1228+
.addIf(COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS, isToggleMuteCommandAvailable())
11951229
.build();
11961230
if (!availableCommands.equals(previousAvailableCommands)) {
11971231
listeners.queueEvent(
@@ -1210,6 +1244,16 @@ private boolean isSetVolumeCommandAvailable() {
12101244
return false;
12111245
}
12121246

1247+
private boolean isToggleMuteCommandAvailable() {
1248+
if (remoteMediaClient != null) {
1249+
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
1250+
if (mediaStatus != null) {
1251+
return mediaStatus.isMediaCommandSupported(MediaStatus.COMMAND_TOGGLE_MUTE);
1252+
}
1253+
}
1254+
return false;
1255+
}
1256+
12131257
private void setMediaItemsInternal(
12141258
List<MediaItem> mediaItems,
12151259
int startIndex,
@@ -1318,6 +1362,16 @@ private void setVolumeAndNotifyIfChanged(float volume) {
13181362
}
13191363
}
13201364

1365+
private void setDeviceMutedAndNotifyIfChanged(boolean muted) {
1366+
if (this.deviceMuted.value != muted) {
1367+
this.deviceMuted.value = muted;
1368+
listeners.queueEvent(
1369+
Player.EVENT_DEVICE_VOLUME_CHANGED,
1370+
listener -> listener.onDeviceVolumeChanged(getDeviceVolume(), muted));
1371+
updateAvailableCommandsAndNotifyIfChanged();
1372+
}
1373+
}
1374+
13211375
private void setPlaybackParametersAndNotifyIfChanged(PlaybackParameters playbackParameters) {
13221376
if (this.playbackParameters.value.equals(playbackParameters)) {
13231377
return;
@@ -1441,6 +1495,15 @@ private static float fetchVolume(RemoteMediaClient remoteMediaClient) {
14411495
return (float) mediaStatus.getStreamVolume();
14421496
}
14431497

1498+
private static boolean fetchDeviceMuted(RemoteMediaClient remoteMediaClient) {
1499+
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
1500+
if (mediaStatus == null) {
1501+
// No media session active, yet.
1502+
return false;
1503+
}
1504+
return mediaStatus.isMute();
1505+
}
1506+
14441507
private static int fetchCurrentWindowIndex(
14451508
@Nullable RemoteMediaClient remoteMediaClient, Timeline timeline) {
14461509
if (remoteMediaClient == null) {
@@ -1705,8 +1768,7 @@ public DeviceInfo fetchDeviceInfo() {
17051768
// There's only one remote routing controller. It's safe to assume it's the Cast routing
17061769
// controller.
17071770
RoutingController remoteController = controllers.get(1);
1708-
// TODO b/364580007 - Populate volume information, and implement Player volume-related
1709-
// methods.
1771+
// TODO b/364580007 - Populate min volume information.
17101772
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
17111773
.setMaxVolume(remoteController.getVolumeMax())
17121774
.setRoutingControllerId(remoteController.getId())

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

+65-4
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;
@@ -48,6 +50,7 @@
4850
import static androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED;
4951
import static com.google.common.truth.Truth.assertThat;
5052
import static org.mockito.ArgumentMatchers.any;
53+
import static org.mockito.ArgumentMatchers.anyBoolean;
5154
import static org.mockito.ArgumentMatchers.anyDouble;
5255
import static org.mockito.ArgumentMatchers.anyInt;
5356
import static org.mockito.ArgumentMatchers.anyLong;
@@ -137,6 +140,7 @@ public void setUp() {
137140
when(mockRemoteMediaClient.isPaused()).thenReturn(true);
138141
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
139142
when(mockMediaStatus.getStreamVolume()).thenReturn(1.0);
143+
when(mockMediaStatus.isMute()).thenReturn(false);
140144
when(mockMediaStatus.getPlaybackRate()).thenReturn(1.0d);
141145
mediaItemConverter = new DefaultMediaItemConverter();
142146
castPlayer = new CastPlayer(mockCastContext, mediaItemConverter);
@@ -440,6 +444,60 @@ public void volume_changesOnStatusUpdates() {
440444
assertThat(castPlayer.getVolume()).isEqualTo(0.75f);
441445
}
442446

447+
@Test
448+
public void setDeviceMuted_masksRemoteState() {
449+
when(mockRemoteMediaClient.setStreamMute(anyBoolean())).thenReturn(mockPendingResult);
450+
assertThat(castPlayer.isDeviceMuted()).isFalse();
451+
452+
castPlayer.setDeviceMuted(true, 0);
453+
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
454+
assertThat(castPlayer.isDeviceMuted()).isTrue();
455+
verify(mockListener).onDeviceVolumeChanged(0, true);
456+
457+
// There is a status update in the middle, which should be hidden by masking.
458+
when(mockMediaStatus.isMute()).thenReturn(false);
459+
remoteMediaClientCallback.onStatusUpdated();
460+
verifyNoMoreInteractions(mockListener);
461+
462+
// Upon result, the mediaStatus now exposes the new muted state.
463+
when(mockMediaStatus.isMute()).thenReturn(true);
464+
setResultCallbackArgumentCaptor
465+
.getValue()
466+
.onResult(mock(RemoteMediaClient.MediaChannelResult.class));
467+
verifyNoMoreInteractions(mockListener);
468+
}
469+
470+
@Test
471+
public void setDeviceMuted_updatesUponResultChange() {
472+
when(mockRemoteMediaClient.setStreamMute(anyBoolean())).thenReturn(mockPendingResult);
473+
474+
castPlayer.setDeviceMuted(true, 0);
475+
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
476+
assertThat(castPlayer.isDeviceMuted()).isTrue();
477+
verify(mockListener).onDeviceVolumeChanged(0, true);
478+
479+
// There is a status update in the middle, which should be hidden by masking.
480+
when(mockMediaStatus.isMute()).thenReturn(false);
481+
remoteMediaClientCallback.onStatusUpdated();
482+
verifyNoMoreInteractions(mockListener);
483+
484+
// Upon result, the device is not muted. The state should reflect that.
485+
setResultCallbackArgumentCaptor
486+
.getValue()
487+
.onResult(mock(RemoteMediaClient.MediaChannelResult.class));
488+
verify(mockListener).onDeviceVolumeChanged(0, false);
489+
assertThat(castPlayer.isDeviceMuted()).isFalse();
490+
}
491+
492+
@Test
493+
public void isDeviceMuted_changesOnStatusUpdates() {
494+
assertThat(castPlayer.isDeviceMuted()).isFalse();
495+
when(mockMediaStatus.isMute()).thenReturn(true);
496+
remoteMediaClientCallback.onStatusUpdated();
497+
verify(mockListener).onDeviceVolumeChanged(0, true);
498+
assertThat(castPlayer.isDeviceMuted()).isTrue();
499+
}
500+
443501
@Test
444502
public void setMediaItems_callsRemoteMediaClient() {
445503
List<MediaItem> mediaItems = new ArrayList<>();
@@ -1458,14 +1516,16 @@ public void isCommandAvailable_isTrueForAvailableCommands() {
14581516
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VOLUME)).isFalse();
14591517
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_DEVICE_VOLUME)).isFalse();
14601518
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isFalse();
1519+
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)).isFalse();
14611520
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isFalse();
14621521
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TEXT)).isFalse();
1463-
assertThat(castPlayer.isCommandAvailable(Player.COMMAND_RELEASE)).isTrue();
1522+
assertThat(castPlayer.isCommandAvailable(COMMAND_RELEASE)).isTrue();
14641523
}
14651524

14661525
@Test
1467-
public void isCommandAvailable_setVolumeIsSupported() {
1526+
public void isCommandAvailable_withVolumeCommands() {
14681527
when(mockMediaStatus.isMediaCommandSupported(MediaStatus.COMMAND_SET_VOLUME)).thenReturn(true);
1528+
when(mockMediaStatus.isMediaCommandSupported(MediaStatus.COMMAND_TOGGLE_MUTE)).thenReturn(true);
14691529

14701530
int[] mediaQueueItemIds = new int[] {1, 2};
14711531
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
@@ -1499,10 +1559,11 @@ public void isCommandAvailable_setVolumeIsSupported() {
14991559
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isFalse();
15001560
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VOLUME)).isTrue();
15011561
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_DEVICE_VOLUME)).isFalse();
1502-
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isFalse();
1562+
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isTrue();
1563+
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)).isTrue();
15031564
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isFalse();
15041565
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TEXT)).isFalse();
1505-
assertThat(castPlayer.isCommandAvailable(Player.COMMAND_RELEASE)).isTrue();
1566+
assertThat(castPlayer.isCommandAvailable(COMMAND_RELEASE)).isTrue();
15061567
}
15071568

15081569
@Test

0 commit comments

Comments
 (0)