From 38248af5cf28b38a0613360cc799f3d97ac910e4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 30 May 2022 12:30:02 +0000 Subject: [PATCH] Add RequestMetadata to MediaItem These fields can be used to transport additional request properties when the requester doesn't know the details needed for the actual playback (i.e. the LocalConfiguration). PiperOrigin-RevId: 451857093 --- .../google/android/exoplayer2/MediaItem.java | 176 +++++++++++++++++- .../android/exoplayer2/MediaItemTest.java | 30 +++ 2 files changed, 201 insertions(+), 5 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 6e90fa6349..56e850709b 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 @@ -85,6 +85,7 @@ public final class MediaItem implements Bundleable { // TODO: Change this to LiveConfiguration once all the deprecated individual setters // are removed. private LiveConfiguration.Builder liveConfiguration; + private RequestMetadata requestMetadata; /** Creates a builder. */ @SuppressWarnings("deprecation") // Temporarily uses DrmConfiguration.Builder() constructor. @@ -94,6 +95,7 @@ public final class MediaItem implements Bundleable { streamKeys = Collections.emptyList(); subtitleConfigurations = ImmutableList.of(); liveConfiguration = new LiveConfiguration.Builder(); + requestMetadata = RequestMetadata.EMPTY; } private Builder(MediaItem mediaItem) { @@ -102,6 +104,7 @@ public final class MediaItem implements Bundleable { mediaId = mediaItem.mediaId; mediaMetadata = mediaItem.mediaMetadata; liveConfiguration = mediaItem.liveConfiguration.buildUpon(); + requestMetadata = mediaItem.requestMetadata; @Nullable LocalConfiguration localConfiguration = mediaItem.localConfiguration; if (localConfiguration != null) { customCacheKey = localConfiguration.customCacheKey; @@ -500,6 +503,12 @@ public final class MediaItem implements Bundleable { return this; } + /** Sets the request metadata. */ + public Builder setRequestMetadata(RequestMetadata requestMetadata) { + this.requestMetadata = requestMetadata; + return this; + } + /** Returns a new {@link MediaItem} instance with the current builder values. */ @SuppressWarnings("deprecation") // Using PlaybackProperties while it exists. public MediaItem build() { @@ -524,7 +533,8 @@ public final class MediaItem implements Bundleable { clippingConfiguration.buildClippingProperties(), localConfiguration, liveConfiguration.build(), - mediaMetadata != null ? mediaMetadata : MediaMetadata.EMPTY); + mediaMetadata != null ? mediaMetadata : MediaMetadata.EMPTY, + requestMetadata); } } @@ -1691,6 +1701,144 @@ public final class MediaItem implements Bundleable { } } + /** + * Metadata that helps the player to understand a playback request represented by a {@link + * MediaItem}. + * + *

This metadata is most useful for cases where playback requests are forwarded to other player + * instances (e.g. from a {@link android.media.session.MediaController}) and the player creating + * the request doesn't know the required {@link LocalConfiguration} for playback. + */ + public static final class RequestMetadata implements Bundleable { + + /** Empty request metadata. */ + public static final RequestMetadata EMPTY = new Builder().build(); + + /** Builder for {@link RequestMetadata} instances. */ + public static final class Builder { + + @Nullable private Uri mediaUri; + @Nullable private String searchQuery; + @Nullable private Bundle extras; + + /** Constructs an instance. */ + public Builder() {} + + private Builder(RequestMetadata requestMetadata) { + this.mediaUri = requestMetadata.mediaUri; + this.searchQuery = requestMetadata.searchQuery; + this.extras = requestMetadata.extras; + } + + /** Sets the URI of the requested media, or null if not known or applicable. */ + public Builder setMediaUri(@Nullable Uri mediaUri) { + this.mediaUri = mediaUri; + return this; + } + + /** Sets the search query for the requested media, or null if not applicable. */ + public Builder setSearchQuery(@Nullable String searchQuery) { + this.searchQuery = searchQuery; + return this; + } + + /** Sets optional extras {@link Bundle}. */ + public Builder setExtras(@Nullable Bundle extras) { + this.extras = extras; + return this; + } + + /** Builds the request metadata. */ + public RequestMetadata build() { + return new RequestMetadata(this); + } + } + + /** The URI of the requested media, or null if not known or applicable. */ + @Nullable public final Uri mediaUri; + + /** The search query for the requested media, or null if not applicable. */ + @Nullable public final String searchQuery; + + /** + * Optional extras {@link Bundle}. + * + *

Given the complexities of checking the equality of two {@link Bundle}s, this is not + * considered in the {@link #equals(Object)} or {@link #hashCode()}. + */ + @Nullable public final Bundle extras; + + private RequestMetadata(Builder builder) { + this.mediaUri = builder.mediaUri; + this.searchQuery = builder.searchQuery; + this.extras = builder.extras; + } + + /** Returns a {@link Builder} initialized with the values of this instance. */ + public Builder buildUpon() { + return new Builder(this); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof RequestMetadata)) { + return false; + } + RequestMetadata that = (RequestMetadata) o; + return Util.areEqual(mediaUri, that.mediaUri) && Util.areEqual(searchQuery, that.searchQuery); + } + + @Override + public int hashCode() { + int result = mediaUri == null ? 0 : mediaUri.hashCode(); + result = 31 * result + (searchQuery == null ? 0 : searchQuery.hashCode()); + return result; + } + + // Bundleable implementation. + + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({FIELD_MEDIA_URI, FIELD_SEARCH_QUERY, FIELD_EXTRAS}) + private @interface FieldNumber {} + + private static final int FIELD_MEDIA_URI = 0; + private static final int FIELD_SEARCH_QUERY = 1; + private static final int FIELD_EXTRAS = 2; + + @Override + public Bundle toBundle() { + Bundle bundle = new Bundle(); + if (mediaUri != null) { + bundle.putParcelable(keyForField(FIELD_MEDIA_URI), mediaUri); + } + if (searchQuery != null) { + bundle.putString(keyForField(FIELD_SEARCH_QUERY), searchQuery); + } + if (extras != null) { + bundle.putBundle(keyForField(FIELD_EXTRAS), extras); + } + return bundle; + } + + /** Object that can restore {@link RequestMetadata} from a {@link Bundle}. */ + public static final Creator CREATOR = + bundle -> + new RequestMetadata.Builder() + .setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI))) + .setSearchQuery(bundle.getString(keyForField(FIELD_SEARCH_QUERY))) + .setExtras(bundle.getBundle(keyForField(FIELD_EXTRAS))) + .build(); + + private static String keyForField(@RequestMetadata.FieldNumber int field) { + return Integer.toString(field, Character.MAX_RADIX); + } + } + /** * The default media ID that is used if the media ID is not explicitly set by {@link * Builder#setMediaId(String)}. @@ -1726,6 +1874,9 @@ public final class MediaItem implements Bundleable { */ @Deprecated public final ClippingProperties clippingProperties; + /** The media {@link RequestMetadata}. */ + public final RequestMetadata requestMetadata; + // Using PlaybackProperties and ClippingProperties until they're deleted. @SuppressWarnings("deprecation") private MediaItem( @@ -1733,7 +1884,8 @@ public final class MediaItem implements Bundleable { ClippingProperties clippingConfiguration, @Nullable PlaybackProperties localConfiguration, LiveConfiguration liveConfiguration, - MediaMetadata mediaMetadata) { + MediaMetadata mediaMetadata, + RequestMetadata requestMetadata) { this.mediaId = mediaId; this.localConfiguration = localConfiguration; this.playbackProperties = localConfiguration; @@ -1741,6 +1893,7 @@ public final class MediaItem implements Bundleable { this.mediaMetadata = mediaMetadata; this.clippingConfiguration = clippingConfiguration; this.clippingProperties = clippingConfiguration; + this.requestMetadata = requestMetadata; } /** Returns a {@link Builder} initialized with the values of this instance. */ @@ -1763,7 +1916,8 @@ public final class MediaItem implements Bundleable { && clippingConfiguration.equals(other.clippingConfiguration) && Util.areEqual(localConfiguration, other.localConfiguration) && Util.areEqual(liveConfiguration, other.liveConfiguration) - && Util.areEqual(mediaMetadata, other.mediaMetadata); + && Util.areEqual(mediaMetadata, other.mediaMetadata) + && Util.areEqual(requestMetadata, other.requestMetadata); } @Override @@ -1773,6 +1927,7 @@ public final class MediaItem implements Bundleable { result = 31 * result + liveConfiguration.hashCode(); result = 31 * result + clippingConfiguration.hashCode(); result = 31 * result + mediaMetadata.hashCode(); + result = 31 * result + requestMetadata.hashCode(); return result; } @@ -1785,7 +1940,8 @@ public final class MediaItem implements Bundleable { FIELD_MEDIA_ID, FIELD_LIVE_CONFIGURATION, FIELD_MEDIA_METADATA, - FIELD_CLIPPING_PROPERTIES + FIELD_CLIPPING_PROPERTIES, + FIELD_REQUEST_METADATA }) private @interface FieldNumber {} @@ -1793,6 +1949,7 @@ public final class MediaItem implements Bundleable { private static final int FIELD_LIVE_CONFIGURATION = 1; private static final int FIELD_MEDIA_METADATA = 2; private static final int FIELD_CLIPPING_PROPERTIES = 3; + private static final int FIELD_REQUEST_METADATA = 4; /** * {@inheritDoc} @@ -1807,6 +1964,7 @@ public final class MediaItem implements Bundleable { bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle()); bundle.putBundle(keyForField(FIELD_MEDIA_METADATA), mediaMetadata.toBundle()); bundle.putBundle(keyForField(FIELD_CLIPPING_PROPERTIES), clippingConfiguration.toBundle()); + bundle.putBundle(keyForField(FIELD_REQUEST_METADATA), requestMetadata.toBundle()); return bundle; } @@ -1843,12 +2001,20 @@ public final class MediaItem implements Bundleable { } else { clippingConfiguration = ClippingConfiguration.CREATOR.fromBundle(clippingConfigurationBundle); } + @Nullable Bundle requestMetadataBundle = bundle.getBundle(keyForField(FIELD_REQUEST_METADATA)); + RequestMetadata requestMetadata; + if (requestMetadataBundle == null) { + requestMetadata = RequestMetadata.EMPTY; + } else { + requestMetadata = RequestMetadata.CREATOR.fromBundle(requestMetadataBundle); + } return new MediaItem( mediaId, clippingConfiguration, /* localConfiguration= */ null, liveConfiguration, - mediaMetadata); + mediaMetadata, + requestMetadata); } private static String keyForField(@FieldNumber int field) { 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 2b57645f54..8b2ac0e054 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 @@ -19,7 +19,9 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import android.net.Uri; +import android.os.Bundle; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.MediaItem.RequestMetadata; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.util.MimeTypes; import com.google.common.collect.ImmutableList; @@ -581,6 +583,24 @@ public class MediaItemTest { assertThat(mediaItem.liveConfiguration.maxOffsetMs).isEqualTo(1234); } + @Test + public void builder_setRequestMetadata_setsRequestMetadata() { + Bundle extras = new Bundle(); + extras.putString("key", "value"); + RequestMetadata requestMetadata = + new RequestMetadata.Builder() + .setMediaUri(Uri.parse("http://test.test")) + .setSearchQuery("Play media!") + .setExtras(extras) + .build(); + + MediaItem mediaItem = + new MediaItem.Builder().setMediaId("mediaID").setRequestMetadata(requestMetadata).build(); + + assertThat(mediaItem.requestMetadata).isEqualTo(requestMetadata); + assertThat(mediaItem.requestMetadata.extras.getString("key")).isEqualTo("value"); + } + @Test @SuppressWarnings("deprecation") // Testing deprecated setter methods public void buildUpon_individualSetters_equalsToOriginal() { @@ -679,6 +699,11 @@ public class MediaItemTest { .setLabel("label") .setId("id") .build())) + .setRequestMetadata( + new RequestMetadata.Builder() + .setMediaUri(Uri.parse("http://test.test")) + .setSearchQuery("search") + .build()) .setTag(new Object()) .build(); @@ -706,6 +731,11 @@ public class MediaItemTest { .setClipRelativeToDefaultPosition(true) .setClipRelativeToLiveWindow(true) .setClipStartsAtKeyFrame(true) + .setRequestMetadata( + new RequestMetadata.Builder() + .setMediaUri(Uri.parse("http://test.test")) + .setSearchQuery("search") + .build()) .build(); assertThat(mediaItem.localConfiguration).isNull();