2929import android .media .RouteDiscoveryPreference ;
3030import android .os .Handler ;
3131import android .os .Looper ;
32+ import android .util .Range ;
3233import android .view .Surface ;
3334import android .view .SurfaceHolder ;
3435import android .view .SurfaceView ;
5960import androidx .media3 .common .util .Size ;
6061import androidx .media3 .common .util .UnstableApi ;
6162import androidx .media3 .common .util .Util ;
63+ import com .google .android .gms .cast .Cast ;
6264import com .google .android .gms .cast .CastStatusCodes ;
6365import com .google .android .gms .cast .MediaInfo ;
6466import com .google .android .gms .cast .MediaQueueItem ;
7375import com .google .android .gms .common .api .PendingResult ;
7476import com .google .android .gms .common .api .ResultCallback ;
7577import com .google .common .collect .ImmutableList ;
78+ import java .io .IOException ;
7679import java .util .List ;
7780import java .util .Objects ;
7881import org .checkerframework .checker .nullness .qual .RequiresNonNull ;
9396@ UnstableApi
9497public final class CastPlayer extends BasePlayer {
9598
99+ /**
100+ * Maximum volume to use for {@link #getDeviceVolume()} and {@link #setDeviceVolume}.
101+ *
102+ * <p>These methods are implemented around {@link CastSession#setVolume} and {@link
103+ * CastSession#getVolume} which operate on a {@code [0, 1]} range. So this value allows us to
104+ * convert to and from the int-based volume scale that {@link #getDeviceVolume()} uses.
105+ */
106+ private static final int MAX_VOLUME = 20 ;
107+
96108 /**
97109 * A {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote} {@link DeviceInfo} with a null {@link
98110 * DeviceInfo#routingControllerId}.
99111 */
100112 public static final DeviceInfo DEVICE_INFO_REMOTE_EMPTY =
101- new DeviceInfo .Builder (DeviceInfo .PLAYBACK_TYPE_REMOTE ).build ();
113+ new DeviceInfo .Builder (DeviceInfo .PLAYBACK_TYPE_REMOTE ).setMaxVolume (MAX_VOLUME ).build ();
114+
115+ private static final Range <Integer > VOLUME_RANGE = new Range <>(0 , MAX_VOLUME );
102116
103117 static {
104118 MediaLibraryInfo .registerModule ("media3.cast" );
@@ -113,6 +127,11 @@ public final class CastPlayer extends BasePlayer {
113127 COMMAND_STOP ,
114128 COMMAND_SEEK_TO_DEFAULT_POSITION ,
115129 COMMAND_SEEK_TO_MEDIA_ITEM ,
130+ COMMAND_GET_DEVICE_VOLUME ,
131+ COMMAND_ADJUST_DEVICE_VOLUME ,
132+ COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS ,
133+ COMMAND_SET_DEVICE_VOLUME ,
134+ COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS ,
116135 COMMAND_SET_REPEAT_MODE ,
117136 COMMAND_SET_SPEED_AND_PITCH ,
118137 COMMAND_GET_CURRENT_MEDIA_ITEM ,
@@ -144,6 +163,8 @@ public final class CastPlayer extends BasePlayer {
144163 @ Nullable private final Api30Impl api30Impl ;
145164
146165 // Result callbacks.
166+ private final Cast .Listener castListener ;
167+
147168 private final StatusListener statusListener ;
148169 private final SeekResultCallback seekResultCallback ;
149170
@@ -154,7 +175,10 @@ public final class CastPlayer extends BasePlayer {
154175 // Internal state.
155176 private final StateHolder <Boolean > playWhenReady ;
156177 private final StateHolder <Integer > repeatMode ;
178+ private boolean isMuted ;
179+ private int deviceVolume ;
157180 private final StateHolder <PlaybackParameters > playbackParameters ;
181+ @ Nullable private CastSession castSession ;
158182 @ Nullable private RemoteMediaClient remoteMediaClient ;
159183 private CastTimeline currentTimeline ;
160184 private Tracks currentTracks ;
@@ -257,6 +281,7 @@ public CastPlayer(
257281 this .maxSeekToPreviousPositionMs = maxSeekToPreviousPositionMs ;
258282 timelineTracker = new CastTimelineTracker (mediaItemConverter );
259283 period = new Timeline .Period ();
284+ castListener = new CastListener ();
260285 statusListener = new StatusListener ();
261286 seekResultCallback = new SeekResultCallback ();
262287 listeners =
@@ -266,6 +291,7 @@ public CastPlayer(
266291 (listener , flags ) -> listener .onEvents (/* player= */ this , new Events (flags )));
267292 playWhenReady = new StateHolder <>(false );
268293 repeatMode = new StateHolder <>(REPEAT_MODE_OFF );
294+ deviceVolume = MAX_VOLUME ;
269295 playbackParameters = new StateHolder <>(PlaybackParameters .DEFAULT );
270296 playbackState = STATE_IDLE ;
271297 currentTimeline = CastTimeline .EMPTY_CAST_TIMELINE ;
@@ -278,8 +304,7 @@ public CastPlayer(
278304
279305 SessionManager sessionManager = castContext .getSessionManager ();
280306 sessionManager .addSessionManagerListener (statusListener , CastSession .class );
281- CastSession session = sessionManager .getCurrentCastSession ();
282- setRemoteMediaClient (session != null ? session .getRemoteMediaClient () : null );
307+ setCastSession (sessionManager .getCurrentCastSession ());
283308 updateInternalStateAndNotifyIfChanged ();
284309 if (SDK_INT >= 30 && context != null ) {
285310 api30Impl = new Api30Impl (context );
@@ -829,61 +854,94 @@ public DeviceInfo getDeviceInfo() {
829854 return deviceInfo ;
830855 }
831856
832- /** This method is not supported and always returns {@code 0}. */
833857 @ Override
834858 public int getDeviceVolume () {
835- return 0 ;
859+ return deviceVolume ;
836860 }
837861
838- /** This method is not supported and always returns {@code false}. */
839862 @ Override
840863 public boolean isDeviceMuted () {
841- return false ;
864+ return isMuted ;
842865 }
843866
844867 /**
845868 * @deprecated Use {@link #setDeviceVolume(int, int)} instead.
846869 */
847870 @ Deprecated
848871 @ Override
849- public void setDeviceVolume (int volume ) {}
872+ public void setDeviceVolume (@ IntRange (from = 0 ) int volume ) {
873+ setDeviceVolume (volume , /* flags= */ 0 );
874+ }
850875
851- /** This method is not supported and does nothing. */
852876 @ Override
853- public void setDeviceVolume (int volume , @ C .VolumeFlags int flags ) {}
877+ public void setDeviceVolume (@ IntRange (from = 0 ) int volume , @ C .VolumeFlags int flags ) {
878+ if (castSession == null ) {
879+ return ;
880+ }
881+ volume = VOLUME_RANGE .clamp (volume );
882+ try {
883+ // See [Internal ref: b/399691860] for context on why we don't use
884+ // RemoteMediaClient.setStreamVolume.
885+ castSession .setVolume ((float ) volume / MAX_VOLUME );
886+ } catch (IOException e ) {
887+ Log .w (TAG , "Ignoring setDeviceVolume due to exception" , e );
888+ return ;
889+ }
890+ setDeviceVolumeAndNotifyIfChanged (volume , isMuted );
891+ listeners .flushEvents ();
892+ }
854893
855894 /**
856895 * @deprecated Use {@link #increaseDeviceVolume(int)} instead.
857896 */
858897 @ Deprecated
859898 @ Override
860- public void increaseDeviceVolume () {}
899+ public void increaseDeviceVolume () {
900+ increaseDeviceVolume (/* flags= */ 0 );
901+ }
861902
862- /** This method is not supported and does nothing. */
863903 @ Override
864- public void increaseDeviceVolume (@ C .VolumeFlags int flags ) {}
904+ public void increaseDeviceVolume (@ C .VolumeFlags int flags ) {
905+ setDeviceVolume (getDeviceVolume () + 1 , flags );
906+ }
865907
866908 /**
867- * @deprecated Use {@link #decreaseDeviceVolume(int)} instead.
909+ * @deprecated Use {@link #decreaseDeviceVolume(int)} (int)} instead.
868910 */
869911 @ Deprecated
870912 @ Override
871- public void decreaseDeviceVolume () {}
913+ public void decreaseDeviceVolume () {
914+ decreaseDeviceVolume (/* flags= */ 0 );
915+ }
872916
873- /** This method is not supported and does nothing. */
874917 @ Override
875- public void decreaseDeviceVolume (@ C .VolumeFlags int flags ) {}
918+ public void decreaseDeviceVolume (@ C .VolumeFlags int flags ) {
919+ setDeviceVolume (getDeviceVolume () - 1 , flags );
920+ }
876921
877922 /**
878923 * @deprecated Use {@link #setDeviceMuted(boolean, int)} instead.
879924 */
880925 @ Deprecated
881926 @ Override
882- public void setDeviceMuted (boolean muted ) {}
927+ public void setDeviceMuted (boolean muted ) {
928+ setDeviceMuted (muted , /* flags= */ 0 );
929+ }
883930
884- /** This method is not supported and does nothing. */
885931 @ Override
886- public void setDeviceMuted (boolean muted , @ C .VolumeFlags int flags ) {}
932+ public void setDeviceMuted (boolean muted , @ C .VolumeFlags int flags ) {
933+ if (castSession == null ) {
934+ return ;
935+ }
936+ try {
937+ castSession .setMute (muted );
938+ } catch (IOException e ) {
939+ Log .w (TAG , "Ignoring setDeviceMuted due to exception" , e );
940+ return ;
941+ }
942+ setDeviceVolumeAndNotifyIfChanged (deviceVolume , muted );
943+ listeners .flushEvents ();
944+ }
887945
888946 /** This method is not supported and does nothing. */
889947 @ Override
@@ -906,6 +964,7 @@ private void updateInternalStateAndNotifyIfChanged() {
906964 ? getCurrentTimeline ().getPeriod (oldWindowIndex , period , /* setIds= */ true ).uid
907965 : null ;
908966 updatePlayerStateAndNotifyIfChanged (/* resultCallback= */ null );
967+ updateVolumeAndNotifyIfChanged ();
909968 updateRepeatModeAndNotifyIfChanged (/* resultCallback= */ null );
910969 updatePlaybackRateAndNotifyIfChanged (/* resultCallback= */ null );
911970 boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged ();
@@ -1014,6 +1073,14 @@ private void updatePlaybackRateAndNotifyIfChanged(@Nullable ResultCallback<?> re
10141073 }
10151074 }
10161075
1076+ @ RequiresNonNull ("castSession" )
1077+ private void updateVolumeAndNotifyIfChanged () {
1078+ if (castSession != null ) {
1079+ int deviceVolume = VOLUME_RANGE .clamp ((int ) Math .round (castSession .getVolume () * MAX_VOLUME ));
1080+ setDeviceVolumeAndNotifyIfChanged (deviceVolume , castSession .isMute ());
1081+ }
1082+ }
1083+
10171084 @ RequiresNonNull ("remoteMediaClient" )
10181085 private void updateRepeatModeAndNotifyIfChanged (@ Nullable ResultCallback <?> resultCallback ) {
10191086 if (repeatMode .acceptsUpdate (resultCallback )) {
@@ -1255,6 +1322,17 @@ private PositionInfo getCurrentPositionInfo() {
12551322 /* adIndexInAdGroup= */ C .INDEX_UNSET );
12561323 }
12571324
1325+ private void setDeviceVolumeAndNotifyIfChanged (
1326+ @ IntRange (from = 0 ) int deviceVolume , boolean isMuted ) {
1327+ if (this .deviceVolume != deviceVolume || this .isMuted != isMuted ) {
1328+ this .deviceVolume = deviceVolume ;
1329+ this .isMuted = isMuted ;
1330+ listeners .queueEvent (
1331+ Player .EVENT_DEVICE_VOLUME_CHANGED ,
1332+ listener -> listener .onDeviceVolumeChanged (deviceVolume , isMuted ));
1333+ }
1334+ }
1335+
12581336 private void setRepeatModeAndNotifyIfChanged (@ Player .RepeatMode int repeatMode ) {
12591337 if (this .repeatMode .value != repeatMode ) {
12601338 this .repeatMode .value = repeatMode ;
@@ -1307,7 +1385,16 @@ private void setPlayerStateAndNotifyIfChanged(
13071385 }
13081386 }
13091387
1310- private void setRemoteMediaClient (@ Nullable RemoteMediaClient remoteMediaClient ) {
1388+ private void setCastSession (@ Nullable CastSession castSession ) {
1389+ if (this .castSession != null ) {
1390+ this .castSession .removeCastListener (castListener );
1391+ }
1392+ if (castSession != null ) {
1393+ castSession .addCastListener (castListener );
1394+ }
1395+ this .castSession = castSession ;
1396+ RemoteMediaClient remoteMediaClient =
1397+ castSession != null ? castSession .getRemoteMediaClient () : null ;
13111398 if (this .remoteMediaClient == remoteMediaClient ) {
13121399 // Do nothing.
13131400 return ;
@@ -1468,22 +1555,22 @@ public void onAdBreakStatusUpdated() {}
14681555
14691556 @ Override
14701557 public void onSessionStarted (CastSession castSession , String s ) {
1471- setRemoteMediaClient (castSession . getRemoteMediaClient () );
1558+ setCastSession (castSession );
14721559 }
14731560
14741561 @ Override
14751562 public void onSessionResumed (CastSession castSession , boolean b ) {
1476- setRemoteMediaClient (castSession . getRemoteMediaClient () );
1563+ setCastSession (castSession );
14771564 }
14781565
14791566 @ Override
14801567 public void onSessionEnded (CastSession castSession , int i ) {
1481- setRemoteMediaClient (null );
1568+ setCastSession (null );
14821569 }
14831570
14841571 @ Override
14851572 public void onSessionSuspended (CastSession castSession , int i ) {
1486- setRemoteMediaClient (null );
1573+ setCastSession (null );
14871574 }
14881575
14891576 @ Override
@@ -1645,6 +1732,7 @@ public DeviceInfo fetchDeviceInfo() {
16451732 // TODO b/364580007 - Populate volume information, and implement Player volume-related
16461733 // methods.
16471734 return new DeviceInfo .Builder (DeviceInfo .PLAYBACK_TYPE_REMOTE )
1735+ .setMaxVolume (MAX_VOLUME )
16481736 .setRoutingControllerId (remoteController .getId ())
16491737 .build ();
16501738 }
@@ -1676,4 +1764,13 @@ public void onStop(RoutingController controller) {
16761764 }
16771765 }
16781766 }
1767+
1768+ private final class CastListener extends Cast .Listener {
1769+
1770+ @ Override
1771+ public void onVolumeChanged () {
1772+ updateVolumeAndNotifyIfChanged ();
1773+ listeners .flushEvents ();
1774+ }
1775+ }
16791776}
0 commit comments