From 630abd837c074434e445380297aa0a83e12f2273 Mon Sep 17 00:00:00 2001 From: jbibik Date: Mon, 5 Jun 2023 22:13:42 +0000 Subject: [PATCH] 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 (cherry picked from commit bcddaf27654ed342ce70fc7a270d478953c2fb80) --- .../google/android/exoplayer2/MediaItem.java | 116 ++++++++++++++++-- .../exoplayer2/util/BundleableUtil.java | 16 ++- .../android/exoplayer2/MediaItemTest.java | 76 +++++++++++- 3 files changed, 195 insertions(+), 13 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java index 41ce77bf5f1..334f5ac25a7 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java @@ -1054,7 +1054,7 @@ public Bundle toBundle() { } /** Properties for local playback. */ - public static final class LocalConfiguration { + public static final class LocalConfiguration implements Bundleable { /** The {@link Uri}. */ public final Uri uri; @@ -1150,6 +1150,79 @@ public int hashCode() { 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} + * + *

It omits the {@link #tag} field. The {@link #tag} of an instance restored from such a + * bundle by {@link #CREATOR} will be {@code null}. + */ + @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}. */ + public static final Creator CREATOR = LocalConfiguration::fromBundle; + + 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 streamKeysList = bundle.getParcelableArrayList(FIELD_STREAM_KEYS); + List streamKeys = + streamKeysList == null ? ImmutableList.of() : ImmutableList.copyOf(streamKeysList); + @Nullable + List subtitleBundles = bundle.getParcelableArrayList(FIELD_SUBTITLE_CONFIGURATION); + ImmutableList 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. */ @@ -2118,15 +2191,9 @@ public int hashCode() { 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} - * - *

It omits the {@link #localConfiguration} field. The {@link #localConfiguration} of an - * instance restored by {@link #CREATOR} will always be {@code null}. - */ - @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); @@ -2143,9 +2210,31 @@ public Bundle toBundle() { 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} + * + *

It omits the {@link #localConfiguration} field. The {@link #localConfiguration} of an + * instance restored from such a bundle by {@link #CREATOR} will be {@code null}. + */ + @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). + */ + public Bundle toBundleIncludeLocalConfiguration() { + return toBundle(/* includeLocalConfiguration= */ true); + } + /** * An object that can restore {@link MediaItem} from a {@link Bundle}. * @@ -2184,10 +2273,17 @@ private static MediaItem fromBundle(Bundle bundle) { } 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); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/BundleableUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/BundleableUtil.java index 39c5198e9ab..3aa14195f5c 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/BundleableUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/BundleableUtil.java @@ -22,6 +22,7 @@ import android.util.SparseArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Bundleable; +import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; @@ -35,10 +36,21 @@ public final class BundleableUtil { /** Converts a list of {@link Bundleable} to a list {@link Bundle}. */ public static ImmutableList toBundleList(List 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 ImmutableList toBundleList( + List bundleableList, Function customToBundleFunc) { ImmutableList.Builder 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(); } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java b/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java index e36f3a73a53..146722beb02 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java @@ -667,6 +667,68 @@ public void createLiveConfigurationInstance_roundTripViaBundle_yieldsEqualInstan 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 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 = @@ -894,13 +956,25 @@ public void roundTripViaBundle_withoutLocalConfiguration_yieldsEqualInstance() { } @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();