Allow playback of MediaItems with LocalConfiguration

When initiated by MediaController, it should be possible for `MediaSession` to pass `MediaItems` to the `Player` if they have `LocalConfiguration`. In such case, it is not required to override `MediaSession.Callback.onAddMediaItems`, because the new current default implementation will handle it.

However, in other cases, MediaItem.toBundle() will continue to strip the LocalConfiguration information.

Issue: androidx/media#282

#minor-release

PiperOrigin-RevId: 537993460
This commit is contained in:
jbibik 2023-06-05 22:13:42 +00:00 committed by Tofunmi Adigun-Hameed
parent 7956c80f73
commit d9764c18ad
8 changed files with 442 additions and 36 deletions

View File

@ -48,6 +48,10 @@
* Muxers: * Muxers:
* IMA extension: * IMA extension:
* Session: * Session:
* Add default implementation to `MediaSession.Callback.onAddMediaItems` to
allow requested `MediaItems` to be passed onto `Player` if they have
`LocalConfiguration` (e.g. URI)
([#282](https://github.com/androidx/media/issues/282)).
* UI: * UI:
* Downloads: * Downloads:
* OkHttp Extension: * OkHttp Extension:

View File

@ -1087,7 +1087,7 @@ public final class MediaItem implements Bundleable {
} }
/** Properties for local playback. */ /** Properties for local playback. */
public static final class LocalConfiguration { public static final class LocalConfiguration implements Bundleable {
/** The {@link Uri}. */ /** The {@link Uri}. */
public final Uri uri; public final Uri uri;
@ -1183,6 +1183,82 @@ public final class MediaItem implements Bundleable {
result = 31 * result + (tag == null ? 0 : tag.hashCode()); result = 31 * result + (tag == null ? 0 : tag.hashCode());
return result; return result;
} }
// Bundleable implementation.
private static final String FIELD_URI = Util.intToStringMaxRadix(0);
private static final String FIELD_MIME_TYPE = Util.intToStringMaxRadix(1);
private static final String FIELD_DRM_CONFIGURATION = Util.intToStringMaxRadix(2);
private static final String FIELD_ADS_CONFIGURATION = Util.intToStringMaxRadix(3);
private static final String FIELD_STREAM_KEYS = Util.intToStringMaxRadix(4);
private static final String FIELD_CUSTOM_CACHE_KEY = Util.intToStringMaxRadix(5);
private static final String FIELD_SUBTITLE_CONFIGURATION = Util.intToStringMaxRadix(6);
/**
* {@inheritDoc}
*
* <p>It omits the {@link #tag} field. The {@link #tag} of an instance restored from such a
* bundle by {@link #CREATOR} will be {@code null}.
*/
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelable(FIELD_URI, uri);
if (mimeType != null) {
bundle.putString(FIELD_MIME_TYPE, mimeType);
}
if (drmConfiguration != null) {
bundle.putBundle(FIELD_DRM_CONFIGURATION, drmConfiguration.toBundle());
}
if (adsConfiguration != null) {
bundle.putBundle(FIELD_ADS_CONFIGURATION, adsConfiguration.toBundle());
}
if (!streamKeys.isEmpty()) {
bundle.putParcelableArrayList(FIELD_STREAM_KEYS, new ArrayList<>(streamKeys));
}
if (customCacheKey != null) {
bundle.putString(FIELD_CUSTOM_CACHE_KEY, customCacheKey);
}
if (!subtitleConfigurations.isEmpty()) {
bundle.putParcelableArrayList(
FIELD_SUBTITLE_CONFIGURATION, BundleableUtil.toBundleArrayList(subtitleConfigurations));
}
return bundle;
}
/** Object that can restore {@link LocalConfiguration} from a {@link Bundle}. */
@UnstableApi
public static final Creator<LocalConfiguration> CREATOR = LocalConfiguration::fromBundle;
@UnstableApi
private static LocalConfiguration fromBundle(Bundle bundle) {
@Nullable Bundle drmBundle = bundle.getBundle(FIELD_DRM_CONFIGURATION);
DrmConfiguration drmConfiguration =
drmBundle == null ? null : DrmConfiguration.CREATOR.fromBundle(drmBundle);
@Nullable Bundle adsBundle = bundle.getBundle(FIELD_ADS_CONFIGURATION);
AdsConfiguration adsConfiguration =
adsBundle == null ? null : AdsConfiguration.CREATOR.fromBundle(adsBundle);
@Nullable List<StreamKey> streamKeysList = bundle.getParcelableArrayList(FIELD_STREAM_KEYS);
List<StreamKey> streamKeys =
streamKeysList == null ? ImmutableList.of() : ImmutableList.copyOf(streamKeysList);
@Nullable
List<Bundle> subtitleBundles = bundle.getParcelableArrayList(FIELD_SUBTITLE_CONFIGURATION);
ImmutableList<SubtitleConfiguration> subtitleConfiguration =
subtitleBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(SubtitleConfiguration.CREATOR, subtitleBundles);
return new LocalConfiguration(
checkNotNull(bundle.getParcelable(FIELD_URI)),
bundle.getString(FIELD_MIME_TYPE),
drmConfiguration,
adsConfiguration,
streamKeys,
bundle.getString(FIELD_CUSTOM_CACHE_KEY),
subtitleConfiguration,
/* tag= */ null);
}
} }
/** Live playback configuration. */ /** Live playback configuration. */
@ -2167,16 +2243,10 @@ public final class MediaItem implements Bundleable {
private static final String FIELD_MEDIA_METADATA = Util.intToStringMaxRadix(2); private static final String FIELD_MEDIA_METADATA = Util.intToStringMaxRadix(2);
private static final String FIELD_CLIPPING_PROPERTIES = Util.intToStringMaxRadix(3); private static final String FIELD_CLIPPING_PROPERTIES = Util.intToStringMaxRadix(3);
private static final String FIELD_REQUEST_METADATA = Util.intToStringMaxRadix(4); private static final String FIELD_REQUEST_METADATA = Util.intToStringMaxRadix(4);
private static final String FIELD_LOCAL_CONFIGURATION = Util.intToStringMaxRadix(5);
/**
* {@inheritDoc}
*
* <p>It omits the {@link #localConfiguration} field. The {@link #localConfiguration} of an
* instance restored by {@link #CREATOR} will always be {@code null}.
*/
@UnstableApi @UnstableApi
@Override private Bundle toBundle(boolean includeLocalConfiguration) {
public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
if (!mediaId.equals(DEFAULT_MEDIA_ID)) { if (!mediaId.equals(DEFAULT_MEDIA_ID)) {
bundle.putString(FIELD_MEDIA_ID, mediaId); bundle.putString(FIELD_MEDIA_ID, mediaId);
@ -2193,9 +2263,33 @@ public final class MediaItem implements Bundleable {
if (!requestMetadata.equals(RequestMetadata.EMPTY)) { if (!requestMetadata.equals(RequestMetadata.EMPTY)) {
bundle.putBundle(FIELD_REQUEST_METADATA, requestMetadata.toBundle()); bundle.putBundle(FIELD_REQUEST_METADATA, requestMetadata.toBundle());
} }
if (includeLocalConfiguration && localConfiguration != null) {
bundle.putBundle(FIELD_LOCAL_CONFIGURATION, localConfiguration.toBundle());
}
return bundle; return bundle;
} }
/**
* {@inheritDoc}
*
* <p>It omits the {@link #localConfiguration} field. The {@link #localConfiguration} of an
* instance restored from such a bundle by {@link #CREATOR} will be {@code null}.
*/
@UnstableApi
@Override
public Bundle toBundle() {
return toBundle(/* includeLocalConfiguration= */ false);
}
/**
* Returns a {@link Bundle} representing the information stored in this {@link #MediaItem} object,
* while including the {@link #localConfiguration} field if it is not null (otherwise skips it).
*/
@UnstableApi
public Bundle toBundleIncludeLocalConfiguration() {
return toBundle(/* includeLocalConfiguration= */ true);
}
/** /**
* An object that can restore {@link MediaItem} from a {@link Bundle}. * An object that can restore {@link MediaItem} from a {@link Bundle}.
* *
@ -2234,10 +2328,17 @@ public final class MediaItem implements Bundleable {
} else { } else {
requestMetadata = RequestMetadata.CREATOR.fromBundle(requestMetadataBundle); requestMetadata = RequestMetadata.CREATOR.fromBundle(requestMetadataBundle);
} }
@Nullable Bundle localConfigurationBundle = bundle.getBundle(FIELD_LOCAL_CONFIGURATION);
LocalConfiguration localConfiguration;
if (localConfigurationBundle == null) {
localConfiguration = null;
} else {
localConfiguration = LocalConfiguration.CREATOR.fromBundle(localConfigurationBundle);
}
return new MediaItem( return new MediaItem(
mediaId, mediaId,
clippingConfiguration, clippingConfiguration,
/* localConfiguration= */ null, localConfiguration,
liveConfiguration, liveConfiguration,
mediaMetadata, mediaMetadata,
requestMetadata); requestMetadata);

View File

@ -22,6 +22,7 @@ import android.os.Bundle;
import android.util.SparseArray; import android.util.SparseArray;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable; import androidx.media3.common.Bundleable;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.util.ArrayList; import java.util.ArrayList;
@ -36,10 +37,21 @@ public final class BundleableUtil {
/** Converts a list of {@link Bundleable} to a list {@link Bundle}. */ /** Converts a list of {@link Bundleable} to a list {@link Bundle}. */
public static <T extends Bundleable> ImmutableList<Bundle> toBundleList(List<T> bundleableList) { public static <T extends Bundleable> ImmutableList<Bundle> toBundleList(List<T> bundleableList) {
return toBundleList(bundleableList, Bundleable::toBundle);
}
/**
* Converts a list of {@link Bundleable} to a list {@link Bundle}
*
* @param bundleableList list of Bundleable items to be converted
* @param customToBundleFunc function that specifies how to bundle up each {@link Bundleable}
*/
public static <T extends Bundleable> ImmutableList<Bundle> toBundleList(
List<T> bundleableList, Function<T, Bundle> customToBundleFunc) {
ImmutableList.Builder<Bundle> builder = ImmutableList.builder(); ImmutableList.Builder<Bundle> builder = ImmutableList.builder();
for (int i = 0; i < bundleableList.size(); i++) { for (int i = 0; i < bundleableList.size(); i++) {
Bundleable bundleable = bundleableList.get(i); T bundleable = bundleableList.get(i);
builder.add(bundleable.toBundle()); builder.add(customToBundleFunc.apply(bundleable));
} }
return builder.build(); return builder.build();
} }

View File

@ -665,6 +665,68 @@ public class MediaItemTest {
assertThat(liveConfigurationFromBundle).isEqualTo(liveConfiguration); assertThat(liveConfigurationFromBundle).isEqualTo(liveConfiguration);
} }
@Test
public void
createDefaultLocalConfigurationInstance_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
Bundle localConfigurationBundle = mediaItem.localConfiguration.toBundle();
// Check that default values are skipped when bundling, only Uri field (="0") is present
assertThat(localConfigurationBundle.keySet()).containsExactly("0");
MediaItem.LocalConfiguration restoredLocalConfiguration =
MediaItem.LocalConfiguration.CREATOR.fromBundle(localConfigurationBundle);
assertThat(restoredLocalConfiguration).isEqualTo(mediaItem.localConfiguration);
assertThat(restoredLocalConfiguration.streamKeys).isEmpty();
assertThat(restoredLocalConfiguration.subtitleConfigurations).isEmpty();
}
@Test
public void createLocalConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() {
Map<String, String> requestHeaders = new HashMap<>();
requestHeaders.put("Referer", "http://www.google.com");
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(URI_STRING)
.setMimeType(MimeTypes.APPLICATION_MP4)
.setCustomCacheKey("key")
.setSubtitleConfigurations(
ImmutableList.of(
new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_STRING + "/en"))
.setMimeType(MimeTypes.APPLICATION_TTML)
.setLanguage("en")
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setRoleFlags(C.ROLE_FLAG_ALTERNATE)
.setLabel("label")
.setId("id")
.build()))
.setDrmConfiguration(
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
.setLicenseUri(Uri.parse(URI_STRING))
.setLicenseRequestHeaders(requestHeaders)
.setMultiSession(true)
.setForceDefaultLicenseUri(true)
.setPlayClearContentWithoutKey(true)
.setForcedSessionTrackTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO))
.setKeySetId(new byte[] {1, 2, 3})
.build())
.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(Uri.parse(URI_STRING)).build())
.build();
MediaItem.LocalConfiguration localConfiguration = mediaItem.localConfiguration;
MediaItem.LocalConfiguration localConfigurationFromBundle =
MediaItem.LocalConfiguration.CREATOR.fromBundle(localConfiguration.toBundle());
MediaItem.LocalConfiguration localConfigurationFromMediaItemBundle =
MediaItem.CREATOR.fromBundle(mediaItem.toBundleIncludeLocalConfiguration())
.localConfiguration;
assertThat(localConfigurationFromBundle).isEqualTo(localConfiguration);
assertThat(localConfigurationFromMediaItemBundle).isEqualTo(localConfiguration);
}
@Test @Test
public void builderSetLiveConfiguration() { public void builderSetLiveConfiguration() {
MediaItem mediaItem = MediaItem mediaItem =
@ -892,13 +954,25 @@ public class MediaItemTest {
} }
@Test @Test
public void roundTripViaBundle_withLocalConfiguration_dropsLocalConfiguration() { public void
roundTripViaDefaultBundle_mediaItemContainsLocalConfiguration_dropsLocalConfiguration() {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build(); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
assertThat(mediaItem.localConfiguration).isNotNull(); assertThat(mediaItem.localConfiguration).isNotNull();
assertThat(MediaItem.CREATOR.fromBundle(mediaItem.toBundle()).localConfiguration).isNull(); assertThat(MediaItem.CREATOR.fromBundle(mediaItem.toBundle()).localConfiguration).isNull();
} }
@Test
public void
roundTripViaBundleIncludeLocalConfiguration_mediaItemContainsLocalConfiguration_restoresLocalConfiguration() {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
MediaItem restoredMediaItem =
MediaItem.CREATOR.fromBundle(mediaItem.toBundleIncludeLocalConfiguration());
assertThat(mediaItem.localConfiguration).isNotNull();
assertThat(restoredMediaItem.localConfiguration).isEqualTo(mediaItem.localConfiguration);
}
@Test @Test
public void createDefaultMediaItemInstance_checksDefaultValues() { public void createDefaultMediaItemInstance_checksDefaultValues() {
MediaItem mediaItem = new MediaItem.Builder().build(); MediaItem mediaItem = new MediaItem.Builder().build();

View File

@ -724,7 +724,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
} }
dispatchRemoteSessionTaskWithPlayerCommand( dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) -> iSession.setMediaItem(controllerStub, seq, mediaItem.toBundle())); (iSession, seq) ->
iSession.setMediaItem(
controllerStub, seq, mediaItem.toBundleIncludeLocalConfiguration()));
setMediaItemsInternal( setMediaItemsInternal(
Collections.singletonList(mediaItem), Collections.singletonList(mediaItem),
@ -742,7 +744,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchRemoteSessionTaskWithPlayerCommand( dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) -> (iSession, seq) ->
iSession.setMediaItemWithStartPosition( iSession.setMediaItemWithStartPosition(
controllerStub, seq, mediaItem.toBundle(), startPositionMs)); controllerStub,
seq,
mediaItem.toBundleIncludeLocalConfiguration(),
startPositionMs));
setMediaItemsInternal( setMediaItemsInternal(
Collections.singletonList(mediaItem), Collections.singletonList(mediaItem),
@ -760,7 +765,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchRemoteSessionTaskWithPlayerCommand( dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) -> (iSession, seq) ->
iSession.setMediaItemWithResetPosition( iSession.setMediaItemWithResetPosition(
controllerStub, seq, mediaItem.toBundle(), resetPosition)); controllerStub, seq, mediaItem.toBundleIncludeLocalConfiguration(), resetPosition));
setMediaItemsInternal( setMediaItemsInternal(
Collections.singletonList(mediaItem), Collections.singletonList(mediaItem),
@ -780,7 +785,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
iSession.setMediaItems( iSession.setMediaItems(
controllerStub, controllerStub,
seq, seq,
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)))); new BundleListRetriever(
BundleableUtil.toBundleList(
mediaItems, MediaItem::toBundleIncludeLocalConfiguration))));
setMediaItemsInternal( setMediaItemsInternal(
mediaItems, mediaItems,
@ -800,7 +807,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
iSession.setMediaItemsWithResetPosition( iSession.setMediaItemsWithResetPosition(
controllerStub, controllerStub,
seq, seq,
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)), new BundleListRetriever(
BundleableUtil.toBundleList(
mediaItems, MediaItem::toBundleIncludeLocalConfiguration)),
resetPosition)); resetPosition));
setMediaItemsInternal( setMediaItemsInternal(
@ -821,7 +830,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
iSession.setMediaItemsWithStartIndex( iSession.setMediaItemsWithStartIndex(
controllerStub, controllerStub,
seq, seq,
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)), new BundleListRetriever(
BundleableUtil.toBundleList(
mediaItems, MediaItem::toBundleIncludeLocalConfiguration)),
startIndex, startIndex,
startPositionMs)); startPositionMs));
@ -860,7 +871,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
} }
dispatchRemoteSessionTaskWithPlayerCommand( dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) -> iSession.addMediaItem(controllerStub, seq, mediaItem.toBundle())); (iSession, seq) ->
iSession.addMediaItem(
controllerStub, seq, mediaItem.toBundleIncludeLocalConfiguration()));
addMediaItemsInternal( addMediaItemsInternal(
getCurrentTimeline().getWindowCount(), Collections.singletonList(mediaItem)); getCurrentTimeline().getWindowCount(), Collections.singletonList(mediaItem));
@ -875,7 +888,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchRemoteSessionTaskWithPlayerCommand( dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) -> (iSession, seq) ->
iSession.addMediaItemWithIndex(controllerStub, seq, index, mediaItem.toBundle())); iSession.addMediaItemWithIndex(
controllerStub, seq, index, mediaItem.toBundleIncludeLocalConfiguration()));
addMediaItemsInternal(index, Collections.singletonList(mediaItem)); addMediaItemsInternal(index, Collections.singletonList(mediaItem));
} }
@ -891,7 +905,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
iSession.addMediaItems( iSession.addMediaItems(
controllerStub, controllerStub,
seq, seq,
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)))); new BundleListRetriever(
BundleableUtil.toBundleList(
mediaItems, MediaItem::toBundleIncludeLocalConfiguration))));
addMediaItemsInternal(getCurrentTimeline().getWindowCount(), mediaItems); addMediaItemsInternal(getCurrentTimeline().getWindowCount(), mediaItems);
} }
@ -909,7 +925,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
controllerStub, controllerStub,
seq, seq,
index, index,
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)))); new BundleListRetriever(
BundleableUtil.toBundleList(
mediaItems, MediaItem::toBundleIncludeLocalConfiguration))));
addMediaItemsInternal(index, mediaItems); addMediaItemsInternal(index, mediaItems);
} }
@ -1193,9 +1211,11 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchRemoteSessionTaskWithPlayerCommand( dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) -> { (iSession, seq) -> {
if (checkNotNull(connectedToken).getInterfaceVersion() >= 2) { if (checkNotNull(connectedToken).getInterfaceVersion() >= 2) {
iSession.replaceMediaItem(controllerStub, seq, index, mediaItem.toBundle()); iSession.replaceMediaItem(
controllerStub, seq, index, mediaItem.toBundleIncludeLocalConfiguration());
} else { } else {
iSession.addMediaItemWithIndex(controllerStub, seq, index + 1, mediaItem.toBundle()); iSession.addMediaItemWithIndex(
controllerStub, seq, index + 1, mediaItem.toBundleIncludeLocalConfiguration());
iSession.removeMediaItem(controllerStub, seq, index); iSession.removeMediaItem(controllerStub, seq, index);
} }
}); });
@ -1213,7 +1233,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchRemoteSessionTaskWithPlayerCommand( dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) -> { (iSession, seq) -> {
IBinder mediaItemsBundleBinder = IBinder mediaItemsBundleBinder =
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)); new BundleListRetriever(
BundleableUtil.toBundleList(
mediaItems, MediaItem::toBundleIncludeLocalConfiguration));
if (checkNotNull(connectedToken).getInterfaceVersion() >= 2) { if (checkNotNull(connectedToken).getInterfaceVersion() >= 2) {
iSession.replaceMediaItems( iSession.replaceMediaItems(
controllerStub, seq, fromIndex, toIndex, mediaItemsBundleBinder); controllerStub, seq, fromIndex, toIndex, mediaItemsBundleBinder);

View File

@ -1129,10 +1129,15 @@ public class MediaSession {
* prepare or play media (for instance when browsing the catalogue and then selecting an item * prepare or play media (for instance when browsing the catalogue and then selecting an item
* for preparation from Android Auto that is using the legacy Media1 library). * for preparation from Android Auto that is using the legacy Media1 library).
* *
* <p>Note that the requested {@linkplain MediaItem media items} don't have a {@link * <p>By default, if and only if each of the provided {@linkplain MediaItem media items} has a
* MediaItem.LocalConfiguration} (for example, a URI) and need to be updated to make them * set {@link MediaItem.LocalConfiguration} (for example, a URI), then the callback returns the
* playable by the underlying {@link Player}. Typically, this implementation should be able to * list unaltered. Otherwise, the default implementation returns an {@link
* identify the correct item by its {@link MediaItem#mediaId} and/or the {@link * UnsupportedOperationException}.
*
* <p>If the requested {@linkplain MediaItem media items} don't have a {@link
* MediaItem.LocalConfiguration}, they need to be updated to make them playable by the
* underlying {@link Player}. Typically, this callback would be overridden with implementation
* that identifies the correct item by its {@link MediaItem#mediaId} and/or the {@link
* MediaItem#requestMetadata}. * MediaItem#requestMetadata}.
* *
* <p>Return a {@link ListenableFuture} with the resolved {@link MediaItem media items}. You can * <p>Return a {@link ListenableFuture} with the resolved {@link MediaItem media items}. You can
@ -1167,8 +1172,13 @@ public class MediaSession {
*/ */
default ListenableFuture<List<MediaItem>> onAddMediaItems( default ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) { MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
for (MediaItem mediaItem : mediaItems) {
if (mediaItem.localConfiguration == null) {
return Futures.immediateFailedFuture(new UnsupportedOperationException()); return Futures.immediateFailedFuture(new UnsupportedOperationException());
} }
}
return Futures.immediateFuture(mediaItems);
}
/** /**
* Called when a controller requested to set {@linkplain MediaItem media items} to the playlist * Called when a controller requested to set {@linkplain MediaItem media items} to the playlist
@ -1181,11 +1191,16 @@ public class MediaSession {
* the catalogue and then selecting an item for preparation from Android Auto that is using the * the catalogue and then selecting an item for preparation from Android Auto that is using the
* legacy Media1 library). * legacy Media1 library).
* *
* <p>Note that the requested {@linkplain MediaItem media items} in the * <p>By default, if and only if each of the provided {@linkplain MediaItem media items} has a
* MediaItemsWithStartPosition don't have a {@link MediaItem.LocalConfiguration} (for example, a * set {@link MediaItem.LocalConfiguration} (for example, a URI), then the callback returns the
* URI) and need to be updated to make them playable by the underlying {@link Player}. * list unaltered. Otherwise, the default implementation returns an {@link
* Typically, this implementation should be able to identify the correct item by its {@link * UnsupportedOperationException}.
* MediaItem#mediaId} and/or the {@link MediaItem#requestMetadata}. *
* <p>If the requested {@linkplain MediaItem media items} don't have a {@link
* MediaItem.LocalConfiguration}, they need to be updated to make them playable by the
* underlying {@link Player}. Typically, this callback would be overridden with implementation
* that identifies the correct item by its {@link MediaItem#mediaId} and/or the {@link
* MediaItem#requestMetadata}.
* *
* <p>Return a {@link ListenableFuture} with the resolved {@linkplain * <p>Return a {@link ListenableFuture} with the resolved {@linkplain
* MediaItemsWithStartPosition media items and starting index and position}. You can also return * MediaItemsWithStartPosition media items and starting index and position}. You can also return

View File

@ -377,6 +377,85 @@ public class MediaSessionCallbackTest {
assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem)); assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem));
} }
@Test
public void
onAddMediaItemsDefault_withSetMediaItemIncludeLocalConfiguration_mediaItemDoesntContainLocalConfiguration_noItemsSet()
throws Exception {
MediaItem mediaItemWithoutLocalConfiguration = createMediaItem("mediaId");
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.setMediaItemIncludeLocalConfiguration(mediaItemWithoutLocalConfiguration);
Thread.sleep(NO_RESPONSE_TIMEOUT_MS);
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION))
.isFalse();
assertThat(player.mediaItems).isEmpty();
}
@Test
public void
onAddMediaItemsDefault_withSetMediaItemsIncludeLocalConfiguration_mediaItemsDontContainLocalConfiguration_noItemsSet()
throws Exception {
MediaItem mediaItemWithoutLocalConfiguration1 = createMediaItem("mediaId1");
MediaItem mediaItemWithoutLocalConfiguration2 = createMediaItem("mediaId2");
List<MediaItem> mediaItemsWithoutLocalConfiguration =
ImmutableList.of(mediaItemWithoutLocalConfiguration1, mediaItemWithoutLocalConfiguration2);
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.setMediaItemsIncludeLocalConfiguration(mediaItemsWithoutLocalConfiguration);
Thread.sleep(NO_RESPONSE_TIMEOUT_MS);
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION))
.isFalse();
assertThat(player.mediaItems).isEmpty();
}
@Test
public void
onAddMediaItemsDefault_withSetMediaItemIncludeLocalConfiguration_mediaItemContainsLocalConfiguration_itemSet()
throws Exception {
MediaItem mediaItem = createMediaItem("mediaId");
MediaItem mediaItemWithLocalConfiguration = updateMediaItemWithLocalConfiguration(mediaItem);
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.setMediaItemIncludeLocalConfiguration(mediaItemWithLocalConfiguration);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
assertThat(player.mediaItems).containsExactly(mediaItemWithLocalConfiguration);
}
@Test
public void
onAddMediaItemsDefault_withSetMediaItemsIncludeLocalConfiguration_mediaItemsContainLocalConfiguration_itemsSet()
throws Exception {
MediaItem mediaItem1 = createMediaItem("mediaId1");
MediaItem mediaItem2 = createMediaItem("mediaId2");
List<MediaItem> fullMediaItems =
updateMediaItemsWithLocalConfiguration(ImmutableList.of(mediaItem1, mediaItem2));
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.setMediaItemsIncludeLocalConfiguration(fullMediaItems);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
assertThat(player.mediaItems).containsExactlyElementsIn(fullMediaItems).inOrder();
}
@Test @Test
public void onAddMediaItems_withSetMediaItemWithStartPosition() throws Exception { public void onAddMediaItems_withSetMediaItemWithStartPosition() throws Exception {
MediaItem mediaItem = createMediaItem("mediaId"); MediaItem mediaItem = createMediaItem("mediaId");
@ -551,6 +630,83 @@ public class MediaSessionCallbackTest {
assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem)); assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem));
} }
@Test
public void
onAddMediaItems_withAddMediaItemIncludeLocalConfiguration_mediaItemDoesntContainLocalConfiguration_noItemsAdded()
throws Exception {
MediaItem mediaItemWithoutLocalConfiguration = createMediaItem("mediaId");
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.addMediaItemIncludeLocalConfiguration(mediaItemWithoutLocalConfiguration);
Thread.sleep(NO_RESPONSE_TIMEOUT_MS);
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS)).isFalse();
assertThat(player.mediaItems).isEmpty();
}
@Test
public void
onAddMediaItems_withAddMediaItemsIncludeLocalConfiguration_mediaItemsDontContainLocalConfiguration_noItemsAdded()
throws Exception {
MediaItem mediaItemWithoutLocalConfiguration1 = createMediaItem("mediaId1");
MediaItem mediaItemWithoutLocalConfiguration2 = createMediaItem("mediaId2");
List<MediaItem> mediaItemsWithoutLocalConfiguration =
ImmutableList.of(mediaItemWithoutLocalConfiguration1, mediaItemWithoutLocalConfiguration2);
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.addMediaItemsIncludeLocalConfiguration(mediaItemsWithoutLocalConfiguration);
Thread.sleep(NO_RESPONSE_TIMEOUT_MS);
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS)).isFalse();
assertThat(player.mediaItems).isEmpty();
}
@Test
public void
onAddMediaItems_withAddMediaItemIncludeLocalConfiguration_mediaItemContainsLocalConfiguration_itemAdded()
throws Exception {
MediaItem mediaItem = createMediaItem("mediaId");
MediaItem mediaItemWithLocalConfiguration = updateMediaItemWithLocalConfiguration(mediaItem);
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.addMediaItemIncludeLocalConfiguration(mediaItemWithLocalConfiguration);
player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(player.mediaItems).containsExactly(mediaItemWithLocalConfiguration);
}
@Test
public void
onAddMediaItems_withAddMediaItemsIncludeLocalConfiguration_mediaItemsContainLocalConfiguration_itemsAdded()
throws Exception {
MediaItem mediaItem1 = createMediaItem("mediaId1");
MediaItem mediaItem2 = createMediaItem("mediaId2");
List<MediaItem> fullMediaItems =
updateMediaItemsWithLocalConfiguration(ImmutableList.of(mediaItem1, mediaItem2));
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.addMediaItemsIncludeLocalConfiguration(fullMediaItems);
player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(player.mediaItems).containsAtLeastElementsIn(fullMediaItems).inOrder();
}
@Test @Test
public void onAddMediaItems_withAddMediaItemWithIndex() throws Exception { public void onAddMediaItems_withAddMediaItemWithIndex() throws Exception {
MediaItem existingItem = createMediaItem("existingItem"); MediaItem existingItem = createMediaItem("existingItem");

View File

@ -144,6 +144,10 @@ public class RemoteMediaController {
binder.setMediaItem(controllerId, mediaItem.toBundle()); binder.setMediaItem(controllerId, mediaItem.toBundle());
} }
public void setMediaItemIncludeLocalConfiguration(MediaItem mediaItem) throws RemoteException {
binder.setMediaItem(controllerId, mediaItem.toBundleIncludeLocalConfiguration());
}
public void setMediaItem(MediaItem mediaItem, long startPositionMs) throws RemoteException { public void setMediaItem(MediaItem mediaItem, long startPositionMs) throws RemoteException {
binder.setMediaItemWithStartPosition(controllerId, mediaItem.toBundle(), startPositionMs); binder.setMediaItemWithStartPosition(controllerId, mediaItem.toBundle(), startPositionMs);
} }
@ -156,6 +160,13 @@ public class RemoteMediaController {
binder.setMediaItems(controllerId, BundleableUtil.toBundleList(mediaItems)); binder.setMediaItems(controllerId, BundleableUtil.toBundleList(mediaItems));
} }
public void setMediaItemsIncludeLocalConfiguration(List<MediaItem> mediaItems)
throws RemoteException {
binder.setMediaItems(
controllerId,
BundleableUtil.toBundleList(mediaItems, MediaItem::toBundleIncludeLocalConfiguration));
}
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition)
throws RemoteException { throws RemoteException {
binder.setMediaItemsWithResetPosition( binder.setMediaItemsWithResetPosition(
@ -186,6 +197,10 @@ public class RemoteMediaController {
binder.addMediaItem(controllerId, mediaItem.toBundle()); binder.addMediaItem(controllerId, mediaItem.toBundle());
} }
public void addMediaItemIncludeLocalConfiguration(MediaItem mediaItem) throws RemoteException {
binder.addMediaItem(controllerId, mediaItem.toBundleIncludeLocalConfiguration());
}
public void addMediaItem(int index, MediaItem mediaItem) throws RemoteException { public void addMediaItem(int index, MediaItem mediaItem) throws RemoteException {
binder.addMediaItemWithIndex(controllerId, index, mediaItem.toBundle()); binder.addMediaItemWithIndex(controllerId, index, mediaItem.toBundle());
} }
@ -194,6 +209,13 @@ public class RemoteMediaController {
binder.addMediaItems(controllerId, BundleableUtil.toBundleList(mediaItems)); binder.addMediaItems(controllerId, BundleableUtil.toBundleList(mediaItems));
} }
public void addMediaItemsIncludeLocalConfiguration(List<MediaItem> mediaItems)
throws RemoteException {
binder.addMediaItems(
controllerId,
BundleableUtil.toBundleList(mediaItems, MediaItem::toBundleIncludeLocalConfiguration));
}
public void addMediaItems(int index, List<MediaItem> mediaItems) throws RemoteException { public void addMediaItems(int index, List<MediaItem> mediaItems) throws RemoteException {
binder.addMediaItemsWithIndex(controllerId, index, BundleableUtil.toBundleList(mediaItems)); binder.addMediaItemsWithIndex(controllerId, index, BundleableUtil.toBundleList(mediaItems));
} }