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:
parent
7956c80f73
commit
d9764c18ad
@ -48,6 +48,10 @@
|
||||
* Muxers:
|
||||
* IMA extension:
|
||||
* 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:
|
||||
* Downloads:
|
||||
* OkHttp Extension:
|
||||
|
@ -1087,7 +1087,7 @@ public final class MediaItem implements Bundleable {
|
||||
}
|
||||
|
||||
/** Properties for local playback. */
|
||||
public static final class LocalConfiguration {
|
||||
public static final class LocalConfiguration implements Bundleable {
|
||||
|
||||
/** The {@link Uri}. */
|
||||
public final Uri uri;
|
||||
@ -1183,6 +1183,82 @@ public final class MediaItem implements Bundleable {
|
||||
result = 31 * result + (tag == null ? 0 : tag.hashCode());
|
||||
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. */
|
||||
@ -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_CLIPPING_PROPERTIES = Util.intToStringMaxRadix(3);
|
||||
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
|
||||
@Override
|
||||
public Bundle toBundle() {
|
||||
private Bundle toBundle(boolean includeLocalConfiguration) {
|
||||
Bundle bundle = new Bundle();
|
||||
if (!mediaId.equals(DEFAULT_MEDIA_ID)) {
|
||||
bundle.putString(FIELD_MEDIA_ID, mediaId);
|
||||
@ -2193,9 +2263,33 @@ public final class MediaItem implements Bundleable {
|
||||
if (!requestMetadata.equals(RequestMetadata.EMPTY)) {
|
||||
bundle.putBundle(FIELD_REQUEST_METADATA, requestMetadata.toBundle());
|
||||
}
|
||||
if (includeLocalConfiguration && localConfiguration != null) {
|
||||
bundle.putBundle(FIELD_LOCAL_CONFIGURATION, localConfiguration.toBundle());
|
||||
}
|
||||
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}.
|
||||
*
|
||||
@ -2234,10 +2328,17 @@ public final class MediaItem implements Bundleable {
|
||||
} else {
|
||||
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(
|
||||
mediaId,
|
||||
clippingConfiguration,
|
||||
/* localConfiguration= */ null,
|
||||
localConfiguration,
|
||||
liveConfiguration,
|
||||
mediaMetadata,
|
||||
requestMetadata);
|
||||
|
@ -22,6 +22,7 @@ import android.os.Bundle;
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Bundleable;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.util.ArrayList;
|
||||
@ -36,10 +37,21 @@ public final class BundleableUtil {
|
||||
|
||||
/** Converts a list of {@link Bundleable} to a list {@link Bundle}. */
|
||||
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();
|
||||
for (int i = 0; i < bundleableList.size(); i++) {
|
||||
Bundleable bundleable = bundleableList.get(i);
|
||||
builder.add(bundleable.toBundle());
|
||||
T bundleable = bundleableList.get(i);
|
||||
builder.add(customToBundleFunc.apply(bundleable));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
@ -665,6 +665,68 @@ public class MediaItemTest {
|
||||
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
|
||||
public void builderSetLiveConfiguration() {
|
||||
MediaItem mediaItem =
|
||||
@ -892,13 +954,25 @@ public class MediaItemTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripViaBundle_withLocalConfiguration_dropsLocalConfiguration() {
|
||||
public void
|
||||
roundTripViaDefaultBundle_mediaItemContainsLocalConfiguration_dropsLocalConfiguration() {
|
||||
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
|
||||
|
||||
assertThat(mediaItem.localConfiguration).isNotNull();
|
||||
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
|
||||
public void createDefaultMediaItemInstance_checksDefaultValues() {
|
||||
MediaItem mediaItem = new MediaItem.Builder().build();
|
||||
|
@ -724,7 +724,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
}
|
||||
|
||||
dispatchRemoteSessionTaskWithPlayerCommand(
|
||||
(iSession, seq) -> iSession.setMediaItem(controllerStub, seq, mediaItem.toBundle()));
|
||||
(iSession, seq) ->
|
||||
iSession.setMediaItem(
|
||||
controllerStub, seq, mediaItem.toBundleIncludeLocalConfiguration()));
|
||||
|
||||
setMediaItemsInternal(
|
||||
Collections.singletonList(mediaItem),
|
||||
@ -742,7 +744,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
dispatchRemoteSessionTaskWithPlayerCommand(
|
||||
(iSession, seq) ->
|
||||
iSession.setMediaItemWithStartPosition(
|
||||
controllerStub, seq, mediaItem.toBundle(), startPositionMs));
|
||||
controllerStub,
|
||||
seq,
|
||||
mediaItem.toBundleIncludeLocalConfiguration(),
|
||||
startPositionMs));
|
||||
|
||||
setMediaItemsInternal(
|
||||
Collections.singletonList(mediaItem),
|
||||
@ -760,7 +765,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
dispatchRemoteSessionTaskWithPlayerCommand(
|
||||
(iSession, seq) ->
|
||||
iSession.setMediaItemWithResetPosition(
|
||||
controllerStub, seq, mediaItem.toBundle(), resetPosition));
|
||||
controllerStub, seq, mediaItem.toBundleIncludeLocalConfiguration(), resetPosition));
|
||||
|
||||
setMediaItemsInternal(
|
||||
Collections.singletonList(mediaItem),
|
||||
@ -780,7 +785,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
iSession.setMediaItems(
|
||||
controllerStub,
|
||||
seq,
|
||||
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems))));
|
||||
new BundleListRetriever(
|
||||
BundleableUtil.toBundleList(
|
||||
mediaItems, MediaItem::toBundleIncludeLocalConfiguration))));
|
||||
|
||||
setMediaItemsInternal(
|
||||
mediaItems,
|
||||
@ -800,7 +807,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
iSession.setMediaItemsWithResetPosition(
|
||||
controllerStub,
|
||||
seq,
|
||||
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)),
|
||||
new BundleListRetriever(
|
||||
BundleableUtil.toBundleList(
|
||||
mediaItems, MediaItem::toBundleIncludeLocalConfiguration)),
|
||||
resetPosition));
|
||||
|
||||
setMediaItemsInternal(
|
||||
@ -821,7 +830,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
iSession.setMediaItemsWithStartIndex(
|
||||
controllerStub,
|
||||
seq,
|
||||
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)),
|
||||
new BundleListRetriever(
|
||||
BundleableUtil.toBundleList(
|
||||
mediaItems, MediaItem::toBundleIncludeLocalConfiguration)),
|
||||
startIndex,
|
||||
startPositionMs));
|
||||
|
||||
@ -860,7 +871,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
}
|
||||
|
||||
dispatchRemoteSessionTaskWithPlayerCommand(
|
||||
(iSession, seq) -> iSession.addMediaItem(controllerStub, seq, mediaItem.toBundle()));
|
||||
(iSession, seq) ->
|
||||
iSession.addMediaItem(
|
||||
controllerStub, seq, mediaItem.toBundleIncludeLocalConfiguration()));
|
||||
|
||||
addMediaItemsInternal(
|
||||
getCurrentTimeline().getWindowCount(), Collections.singletonList(mediaItem));
|
||||
@ -875,7 +888,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
dispatchRemoteSessionTaskWithPlayerCommand(
|
||||
(iSession, seq) ->
|
||||
iSession.addMediaItemWithIndex(controllerStub, seq, index, mediaItem.toBundle()));
|
||||
iSession.addMediaItemWithIndex(
|
||||
controllerStub, seq, index, mediaItem.toBundleIncludeLocalConfiguration()));
|
||||
|
||||
addMediaItemsInternal(index, Collections.singletonList(mediaItem));
|
||||
}
|
||||
@ -891,7 +905,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
iSession.addMediaItems(
|
||||
controllerStub,
|
||||
seq,
|
||||
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems))));
|
||||
new BundleListRetriever(
|
||||
BundleableUtil.toBundleList(
|
||||
mediaItems, MediaItem::toBundleIncludeLocalConfiguration))));
|
||||
|
||||
addMediaItemsInternal(getCurrentTimeline().getWindowCount(), mediaItems);
|
||||
}
|
||||
@ -909,7 +925,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
controllerStub,
|
||||
seq,
|
||||
index,
|
||||
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems))));
|
||||
new BundleListRetriever(
|
||||
BundleableUtil.toBundleList(
|
||||
mediaItems, MediaItem::toBundleIncludeLocalConfiguration))));
|
||||
|
||||
addMediaItemsInternal(index, mediaItems);
|
||||
}
|
||||
@ -1193,9 +1211,11 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
dispatchRemoteSessionTaskWithPlayerCommand(
|
||||
(iSession, seq) -> {
|
||||
if (checkNotNull(connectedToken).getInterfaceVersion() >= 2) {
|
||||
iSession.replaceMediaItem(controllerStub, seq, index, mediaItem.toBundle());
|
||||
iSession.replaceMediaItem(
|
||||
controllerStub, seq, index, mediaItem.toBundleIncludeLocalConfiguration());
|
||||
} else {
|
||||
iSession.addMediaItemWithIndex(controllerStub, seq, index + 1, mediaItem.toBundle());
|
||||
iSession.addMediaItemWithIndex(
|
||||
controllerStub, seq, index + 1, mediaItem.toBundleIncludeLocalConfiguration());
|
||||
iSession.removeMediaItem(controllerStub, seq, index);
|
||||
}
|
||||
});
|
||||
@ -1213,7 +1233,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
dispatchRemoteSessionTaskWithPlayerCommand(
|
||||
(iSession, seq) -> {
|
||||
IBinder mediaItemsBundleBinder =
|
||||
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems));
|
||||
new BundleListRetriever(
|
||||
BundleableUtil.toBundleList(
|
||||
mediaItems, MediaItem::toBundleIncludeLocalConfiguration));
|
||||
if (checkNotNull(connectedToken).getInterfaceVersion() >= 2) {
|
||||
iSession.replaceMediaItems(
|
||||
controllerStub, seq, fromIndex, toIndex, mediaItemsBundleBinder);
|
||||
|
@ -1129,10 +1129,15 @@ public class MediaSession {
|
||||
* 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).
|
||||
*
|
||||
* <p>Note that the requested {@linkplain MediaItem media items} don't have a {@link
|
||||
* MediaItem.LocalConfiguration} (for example, a URI) and need to be updated to make them
|
||||
* playable by the underlying {@link Player}. Typically, this implementation should be able to
|
||||
* identify the correct item by its {@link MediaItem#mediaId} and/or the {@link
|
||||
* <p>By default, if and only if each of the provided {@linkplain MediaItem media items} has a
|
||||
* set {@link MediaItem.LocalConfiguration} (for example, a URI), then the callback returns the
|
||||
* list unaltered. Otherwise, the default implementation returns an {@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}.
|
||||
*
|
||||
* <p>Return a {@link ListenableFuture} with the resolved {@link MediaItem media items}. You can
|
||||
@ -1167,7 +1172,12 @@ public class MediaSession {
|
||||
*/
|
||||
default ListenableFuture<List<MediaItem>> onAddMediaItems(
|
||||
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
|
||||
return Futures.immediateFailedFuture(new UnsupportedOperationException());
|
||||
for (MediaItem mediaItem : mediaItems) {
|
||||
if (mediaItem.localConfiguration == null) {
|
||||
return Futures.immediateFailedFuture(new UnsupportedOperationException());
|
||||
}
|
||||
}
|
||||
return Futures.immediateFuture(mediaItems);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1181,11 +1191,16 @@ public class MediaSession {
|
||||
* the catalogue and then selecting an item for preparation from Android Auto that is using the
|
||||
* legacy Media1 library).
|
||||
*
|
||||
* <p>Note that the requested {@linkplain MediaItem media items} in the
|
||||
* MediaItemsWithStartPosition don't have a {@link MediaItem.LocalConfiguration} (for example, a
|
||||
* URI) and need to be updated to make them playable by the underlying {@link Player}.
|
||||
* Typically, this implementation should be able to identify the correct item by its {@link
|
||||
* MediaItem#mediaId} and/or the {@link MediaItem#requestMetadata}.
|
||||
* <p>By default, if and only if each of the provided {@linkplain MediaItem media items} has a
|
||||
* set {@link MediaItem.LocalConfiguration} (for example, a URI), then the callback returns the
|
||||
* list unaltered. Otherwise, the default implementation returns an {@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}.
|
||||
*
|
||||
* <p>Return a {@link ListenableFuture} with the resolved {@linkplain
|
||||
* MediaItemsWithStartPosition media items and starting index and position}. You can also return
|
||||
|
@ -377,6 +377,85 @@ public class MediaSessionCallbackTest {
|
||||
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
|
||||
public void onAddMediaItems_withSetMediaItemWithStartPosition() throws Exception {
|
||||
MediaItem mediaItem = createMediaItem("mediaId");
|
||||
@ -551,6 +630,83 @@ public class MediaSessionCallbackTest {
|
||||
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
|
||||
public void onAddMediaItems_withAddMediaItemWithIndex() throws Exception {
|
||||
MediaItem existingItem = createMediaItem("existingItem");
|
||||
|
@ -144,6 +144,10 @@ public class RemoteMediaController {
|
||||
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 {
|
||||
binder.setMediaItemWithStartPosition(controllerId, mediaItem.toBundle(), startPositionMs);
|
||||
}
|
||||
@ -156,6 +160,13 @@ public class RemoteMediaController {
|
||||
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)
|
||||
throws RemoteException {
|
||||
binder.setMediaItemsWithResetPosition(
|
||||
@ -186,6 +197,10 @@ public class RemoteMediaController {
|
||||
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 {
|
||||
binder.addMediaItemWithIndex(controllerId, index, mediaItem.toBundle());
|
||||
}
|
||||
@ -194,6 +209,13 @@ public class RemoteMediaController {
|
||||
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 {
|
||||
binder.addMediaItemsWithIndex(controllerId, index, BundleableUtil.toBundleList(mediaItems));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user