diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 9cca7eaf4c..90d2fd8576 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -37,6 +37,8 @@
frames is dequeued without reading the 'end of stream' sample.
([#11079](https://github.com/google/ExoPlayer/issues/11079)).
* Add `Builder` for `DeviceInfo` and deprecate existing constructor.
+ * Add `DeviceInfo.routingControllerId` to specify the routing controller
+ ID for remote playbacks.
* Session:
* Deprecate 4 volume-controlling methods in `Player` and add overloaded
methods which allow users to specify volume flags:
diff --git a/api.txt b/api.txt
index 92b67b5971..1981a65991 100644
--- a/api.txt
+++ b/api.txt
@@ -181,6 +181,7 @@ package androidx.media3.common {
field @IntRange(from=0) public final int maxVolume;
field @IntRange(from=0) public final int minVolume;
field @androidx.media3.common.DeviceInfo.PlaybackType public final int playbackType;
+ field @Nullable public final String routingControllerId;
}
public static final class DeviceInfo.Builder {
@@ -188,6 +189,7 @@ package androidx.media3.common {
method public androidx.media3.common.DeviceInfo build();
method public androidx.media3.common.DeviceInfo.Builder setMaxVolume(@IntRange(from=0) int);
method public androidx.media3.common.DeviceInfo.Builder setMinVolume(@IntRange(from=0) int);
+ method public androidx.media3.common.DeviceInfo.Builder setRoutingControllerId(@Nullable String);
}
@IntDef({androidx.media3.common.DeviceInfo.PLAYBACK_TYPE_LOCAL, androidx.media3.common.DeviceInfo.PLAYBACK_TYPE_REMOTE}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface DeviceInfo.PlaybackType {
diff --git a/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java b/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java
index 5a8e76c120..62daf34a41 100644
--- a/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java
+++ b/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java
@@ -17,6 +17,7 @@ package androidx.media3.common;
import static java.lang.annotation.ElementType.TYPE_USE;
+import android.media.MediaRouter2;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
@@ -57,6 +58,7 @@ public final class DeviceInfo implements Bundleable {
private int minVolume;
private int maxVolume;
+ @Nullable private String routingControllerId;
/**
* Creates the builder.
@@ -93,6 +95,28 @@ public final class DeviceInfo implements Bundleable {
return this;
}
+ /**
+ * Sets the {@linkplain MediaRouter2.RoutingController#getId() routing controller id} of the
+ * associated {@link MediaRouter2.RoutingController}.
+ *
+ *
This id allows mapping this device information to a routing controller, which provides
+ * information about the media route and allows controlling its volume.
+ *
+ *
The set value must be null if {@link DeviceInfo#playbackType} is {@link
+ * #PLAYBACK_TYPE_LOCAL}.
+ *
+ * @param routingControllerId The {@linkplain MediaRouter2.RoutingController#getId() routing
+ * controller id} of the associated {@link MediaRouter2.RoutingController}, or null to leave
+ * it unspecified.
+ * @return This builder.
+ */
+ @CanIgnoreReturnValue
+ public Builder setRoutingControllerId(@Nullable String routingControllerId) {
+ Assertions.checkArgument(playbackType != PLAYBACK_TYPE_LOCAL || routingControllerId == null);
+ this.routingControllerId = routingControllerId;
+ return this;
+ }
+
/** Builds the {@link DeviceInfo}. */
public DeviceInfo build() {
Assertions.checkArgument(minVolume <= maxVolume);
@@ -108,6 +132,15 @@ public final class DeviceInfo implements Bundleable {
/** The maximum volume that the device supports, or {@code 0} if unspecified. */
@IntRange(from = 0)
public final int maxVolume;
+ /**
+ * The {@linkplain MediaRouter2.RoutingController#getId() routing controller id} of the associated
+ * {@link MediaRouter2.RoutingController}, or null if unset or {@link #playbackType} is {@link
+ * #PLAYBACK_TYPE_LOCAL}.
+ *
+ *
This id allows mapping this device information to a routing controller, which provides
+ * information about the media route and allows controlling its volume.
+ */
+ @Nullable public final String routingControllerId;
/**
* @deprecated Use {@link Builder} instead.
@@ -125,6 +158,7 @@ public final class DeviceInfo implements Bundleable {
this.playbackType = builder.playbackType;
this.minVolume = builder.minVolume;
this.maxVolume = builder.maxVolume;
+ this.routingControllerId = builder.routingControllerId;
}
@Override
@@ -138,7 +172,8 @@ public final class DeviceInfo implements Bundleable {
DeviceInfo other = (DeviceInfo) obj;
return playbackType == other.playbackType
&& minVolume == other.minVolume
- && maxVolume == other.maxVolume;
+ && maxVolume == other.maxVolume
+ && Util.areEqual(routingControllerId, other.routingControllerId);
}
@Override
@@ -147,6 +182,7 @@ public final class DeviceInfo implements Bundleable {
result = 31 * result + playbackType;
result = 31 * result + minVolume;
result = 31 * result + maxVolume;
+ result = 31 * result + (routingControllerId == null ? 0 : routingControllerId.hashCode());
return result;
}
@@ -155,6 +191,7 @@ public final class DeviceInfo implements Bundleable {
private static final String FIELD_PLAYBACK_TYPE = Util.intToStringMaxRadix(0);
private static final String FIELD_MIN_VOLUME = Util.intToStringMaxRadix(1);
private static final String FIELD_MAX_VOLUME = Util.intToStringMaxRadix(2);
+ private static final String FIELD_ROUTING_CONTROLLER_ID = Util.intToStringMaxRadix(3);
@UnstableApi
@Override
@@ -169,6 +206,9 @@ public final class DeviceInfo implements Bundleable {
if (maxVolume != 0) {
bundle.putInt(FIELD_MAX_VOLUME, maxVolume);
}
+ if (routingControllerId != null) {
+ bundle.putString(FIELD_ROUTING_CONTROLLER_ID, routingControllerId);
+ }
return bundle;
}
@@ -180,9 +220,11 @@ public final class DeviceInfo implements Bundleable {
bundle.getInt(FIELD_PLAYBACK_TYPE, /* defaultValue= */ PLAYBACK_TYPE_LOCAL);
int minVolume = bundle.getInt(FIELD_MIN_VOLUME, /* defaultValue= */ 0);
int maxVolume = bundle.getInt(FIELD_MAX_VOLUME, /* defaultValue= */ 0);
+ @Nullable String routingControllerId = bundle.getString(FIELD_ROUTING_CONTROLLER_ID);
return new DeviceInfo.Builder(playbackType)
.setMinVolume(minVolume)
.setMaxVolume(maxVolume)
+ .setRoutingControllerId(routingControllerId)
.build();
};
}
diff --git a/libraries/common/src/test/java/androidx/media3/common/DeviceInfoTest.java b/libraries/common/src/test/java/androidx/media3/common/DeviceInfoTest.java
index b6cd9cb035..8c742f0524 100644
--- a/libraries/common/src/test/java/androidx/media3/common/DeviceInfoTest.java
+++ b/libraries/common/src/test/java/androidx/media3/common/DeviceInfoTest.java
@@ -31,6 +31,7 @@ public class DeviceInfoTest {
new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
.setMinVolume(1)
.setMaxVolume(9)
+ .setRoutingControllerId("route")
.build();
assertThat(DeviceInfo.CREATOR.fromBundle(deviceInfo.toBundle())).isEqualTo(deviceInfo);
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java
index 0e8659e322..88e0658bba 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java
@@ -1453,7 +1453,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
controllerCompat.getFlags(),
controllerCompat.isSessionReady(),
controllerCompat.getRatingType(),
- getInstance().getTimeDiffMs());
+ getInstance().getTimeDiffMs(),
+ getRoutingControllerId(controllerCompat));
Pair<@NullableType Integer, @NullableType Integer> reasons =
calculateDiscontinuityAndTransitionReason(
legacyPlayerInfo,
@@ -1642,6 +1643,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
listeners.flushEvents();
}
+ @Nullable
+ private static String getRoutingControllerId(MediaControllerCompat controllerCompat) {
+ if (Util.SDK_INT < 30) {
+ return null;
+ }
+ android.media.session.MediaController fwkController =
+ (android.media.session.MediaController) controllerCompat.getMediaController();
+ @Nullable
+ android.media.session.MediaController.PlaybackInfo playbackInfo =
+ fwkController.getPlaybackInfo();
+ if (playbackInfo == null) {
+ return null;
+ }
+ return playbackInfo.getVolumeControlId();
+ }
+
private static void ignoreFuture(Future unused) {
// Ignore return value of the future because legacy session cannot get result back.
}
@@ -1816,7 +1833,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
long sessionFlags,
boolean isSessionReady,
@RatingCompat.Style int ratingType,
- long timeDiffMs) {
+ long timeDiffMs,
+ @Nullable String routingControllerId) {
QueueTimeline currentTimeline;
MediaMetadata mediaMetadata;
int currentMediaItemIndex;
@@ -1963,7 +1981,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
newLegacyPlayerInfo.mediaMetadataCompat,
timeDiffMs);
boolean isPlaying = MediaUtils.convertToIsPlaying(newLegacyPlayerInfo.playbackStateCompat);
- DeviceInfo deviceInfo = MediaUtils.convertToDeviceInfo(newLegacyPlayerInfo.playbackInfoCompat);
+ DeviceInfo deviceInfo =
+ MediaUtils.convertToDeviceInfo(newLegacyPlayerInfo.playbackInfoCompat, routingControllerId);
int deviceVolume = MediaUtils.convertToDeviceVolume(newLegacyPlayerInfo.playbackInfoCompat);
boolean deviceMuted = MediaUtils.convertToIsDeviceMuted(newLegacyPlayerInfo.playbackInfoCompat);
long seekBackIncrementMs = oldControllerInfo.playerInfo.seekBackIncrementMs;
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java
index 5f6adff951..785a2ff604 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java
@@ -1331,7 +1331,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Converts {@link MediaControllerCompat.PlaybackInfo} to {@link DeviceInfo}. */
public static DeviceInfo convertToDeviceInfo(
- @Nullable MediaControllerCompat.PlaybackInfo playbackInfoCompat) {
+ @Nullable MediaControllerCompat.PlaybackInfo playbackInfoCompat,
+ @Nullable String routingControllerId) {
if (playbackInfoCompat == null) {
return DeviceInfo.UNKNOWN;
}
@@ -1341,6 +1342,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
? DeviceInfo.PLAYBACK_TYPE_REMOTE
: DeviceInfo.PLAYBACK_TYPE_LOCAL)
.setMaxVolume(playbackInfoCompat.getMaxVolume())
+ .setRoutingControllerId(routingControllerId)
.build();
}
diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java
index 8086c31d83..25f131ce6e 100644
--- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java
+++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java
@@ -1028,7 +1028,9 @@ import java.util.List;
Handler handler = new Handler(getApplicationLooper());
int currentVolume = getDeviceVolumeWithCommandCheck();
int legacyVolumeFlag = C.VOLUME_FLAG_SHOW_UI;
- return new VolumeProviderCompat(volumeControlType, getDeviceInfo().maxVolume, currentVolume) {
+ DeviceInfo deviceInfo = getDeviceInfo();
+ return new VolumeProviderCompat(
+ volumeControlType, deviceInfo.maxVolume, currentVolume, deviceInfo.routingControllerId) {
@Override
public void onSetVolumeTo(int volume) {
postOrRun(
diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl
index 196306d789..b51384d961 100644
--- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl
+++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl
@@ -29,7 +29,7 @@ interface IRemoteMediaSessionCompat {
Bundle getSessionToken(String sessionTag);
void release(String sessionTag);
void setPlaybackToLocal(String sessionTag, int stream);
- void setPlaybackToRemote(String sessionTag, int volumeControl, int maxVolume, int currentVolume);
+ void setPlaybackToRemote(String sessionTag, int volumeControl, int maxVolume, int currentVolume, @nullable String routingControllerId);
void setPlaybackState(String sessionTag, in Bundle stateBundle);
void setMetadata(String sessionTag, in Bundle metadataBundle);
void setQueue(String sessionTag, in Bundle queueBundle);
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java
index 299ed7f7c2..b88dff7b8b 100644
--- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java
@@ -246,7 +246,8 @@ public class MediaControllerListenerWithMediaSessionCompatTest {
session.setPlaybackToRemote(
/* volumeControl= */ VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE,
/* maxVolume= */ 100,
- /* currentVolume= */ 50);
+ /* currentVolume= */ 50,
+ /* routingSessionId= */ "route");
MediaController controller = controllerTestRule.createController(session.getSessionToken());
CountDownLatch latch = new CountDownLatch(2);
AtomicReference audioAttributesParamRef = new AtomicReference<>();
@@ -305,15 +306,18 @@ public class MediaControllerListenerWithMediaSessionCompatTest {
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
+ String testRoutingSessionId = Util.SDK_INT >= 30 ? "route" : null;
session.setPlaybackToRemote(
/* volumeControl= */ VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE,
/* maxVolume= */ 100,
- /* currentVolume= */ 50);
+ /* currentVolume= */ 50,
+ testRoutingSessionId);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(deviceInfoParamRef.get().playbackType).isEqualTo(DeviceInfo.PLAYBACK_TYPE_REMOTE);
assertThat(deviceInfoParamRef.get().maxVolume).isEqualTo(100);
+ assertThat(deviceInfoParamRef.get().routingControllerId).isEqualTo(testRoutingSessionId);
assertThat(deviceInfoGetterRef.get()).isEqualTo(deviceInfoParamRef.get());
assertThat(deviceInfoOnEventsRef.get()).isEqualTo(deviceInfoGetterRef.get());
assertThat(getEventsAsList(onEvents.get())).contains(Player.EVENT_DEVICE_VOLUME_CHANGED);
@@ -348,7 +352,8 @@ public class MediaControllerListenerWithMediaSessionCompatTest {
session.setPlaybackToRemote(
/* volumeControl= */ VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE,
/* maxVolume= */ 100,
- /* currentVolume= */ 50);
+ /* currentVolume= */ 50,
+ /* routingSessionId= */ "route");
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(deviceVolumeParam.get()).isEqualTo(50);
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java
index 4df4269499..ee779997b8 100644
--- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java
@@ -1548,6 +1548,7 @@ public class MediaControllerWithMediaSessionCompatTest {
int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
int maxVolume = 100;
int currentVolume = 45;
+ String routingSessionId = Util.SDK_INT >= 30 ? "route" : null;
AtomicReference deviceInfoRef = new AtomicReference<>();
CountDownLatch latchForDeviceInfo = new CountDownLatch(1);
@@ -1572,11 +1573,12 @@ public class MediaControllerWithMediaSessionCompatTest {
MediaController controller = controllerTestRule.createController(session.getSessionToken());
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
- session.setPlaybackToRemote(volumeControlType, maxVolume, currentVolume);
+ session.setPlaybackToRemote(volumeControlType, maxVolume, currentVolume, routingSessionId);
assertThat(latchForDeviceInfo.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(latchForDeviceVolume.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(deviceInfoRef.get().maxVolume).isEqualTo(maxVolume);
+ assertThat(deviceInfoRef.get().routingControllerId).isEqualTo(routingSessionId);
}
@Test
@@ -1588,7 +1590,8 @@ public class MediaControllerWithMediaSessionCompatTest {
session.setPlaybackToRemote(
VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE,
/* maxVolume= */ 100,
- /* currentVolume= */ 45);
+ /* currentVolume= */ 45,
+ /* routingSessionId= */ "route");
int testLocalStreamType = AudioManager.STREAM_ALARM;
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java
index aa4072ad5b..be08fe3b9a 100644
--- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java
+++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java
@@ -120,11 +120,15 @@ public class MediaSessionCompatProviderService extends Service {
@Override
public void setPlaybackToRemote(
- String sessionTag, int volumeControl, int maxVolume, int currentVolume)
+ String sessionTag,
+ int volumeControl,
+ int maxVolume,
+ int currentVolume,
+ @Nullable String routingControllerId)
throws RemoteException {
MediaSessionCompat session = sessionMap.get(sessionTag);
session.setPlaybackToRemote(
- new VolumeProviderCompat(volumeControl, maxVolume, currentVolume) {
+ new VolumeProviderCompat(volumeControl, maxVolume, currentVolume, routingControllerId) {
@Override
public void onSetVolumeTo(int volume) {
setCurrentVolume(volume);
diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java
index 870fd32b0c..e190bc39dc 100644
--- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java
+++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java
@@ -114,12 +114,13 @@ public class RemoteMediaSessionCompat {
}
/**
- * Since we cannot pass VolumeProviderCompat directly, we pass volumeControl, maxVolume,
- * currentVolume instead.
+ * Since we cannot pass VolumeProviderCompat directly, we pass the individual parameters instead.
*/
- public void setPlaybackToRemote(int volumeControl, int maxVolume, int currentVolume)
+ public void setPlaybackToRemote(
+ int volumeControl, int maxVolume, int currentVolume, @Nullable String routingControllerId)
throws RemoteException {
- binder.setPlaybackToRemote(sessionTag, volumeControl, maxVolume, currentVolume);
+ binder.setPlaybackToRemote(
+ sessionTag, volumeControl, maxVolume, currentVolume, routingControllerId);
}
public void setPlaybackState(PlaybackStateCompat state) throws RemoteException {