From d1bbd3507a818e14be965c300938f9d51f8b7836 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 2 Mar 2020 15:56:44 +0000 Subject: [PATCH] add media item to create media sources This change adds the createMediaSource(MediaItem mediaItem) method to the MediaSourceFactory interface. It doesn't deprecate createMediaSource(Uri uri) to keep the cl smaller. Deprecation and removing calls to the deprecated method from within the library and extension follow in a separate CL. PiperOrigin-RevId: 298352442 --- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../google/android/exoplayer2/MediaItem.java | 448 ++++++++++++++++++ .../source/ExtractorMediaSource.java | 29 +- .../exoplayer2/source/MediaSourceFactory.java | 35 +- .../source/ProgressiveMediaSource.java | 30 +- .../android/exoplayer2/MediaItemTest.java | 137 ++++++ .../source/dash/DashMediaSource.java | 49 +- .../exoplayer2/source/hls/HlsMediaSource.java | 49 +- .../source/smoothstreaming/SsMediaSource.java | 52 +- 9 files changed, 765 insertions(+), 73 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/MediaItem.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/MediaItemTest.java diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 4791ca0e4c..71c9c28c96 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; @@ -74,6 +75,7 @@ import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.Util; @@ -636,9 +638,12 @@ public class PlayerActivity extends AppCompatActivity @Override @NonNull - public MediaSource createMediaSource(@NonNull Uri uri) { + public MediaSource createMediaSource(@NonNull MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); return PlayerActivity.this.createLeafMediaSource( - uri, /* extension=*/ null, drmSessionManager); + mediaItem.playbackProperties.sourceUri, + mediaItem.playbackProperties.extension, + drmSessionManager); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaItem.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaItem.java new file mode 100644 index 0000000000..018ce87e74 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaItem.java @@ -0,0 +1,448 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** Representation of a media item. */ +public final class MediaItem { + + /** + * Creates a {@link MediaItem} for the given source uri. + * + * @param sourceUri The source uri. + * @return An {@link MediaItem} for the given source uri. + */ + public static MediaItem fromUri(String sourceUri) { + return new MediaItem.Builder().setSourceUri(sourceUri).build(); + } + + /** + * Creates a {@link MediaItem} for the given {@link Uri source uri}. + * + * @param sourceUri The {@link Uri source uri}. + * @return An {@link MediaItem} for the given source uri. + */ + public static MediaItem fromUri(Uri sourceUri) { + return new MediaItem.Builder().setSourceUri(sourceUri).build(); + } + + /** A builder for {@link MediaItem} instances. */ + public static final class Builder { + + @Nullable private String mediaId; + @Nullable private Uri sourceUri; + @Nullable private String extension; + @Nullable private UriBundle drmLicenseUri; + @Nullable private UUID drmUuid; + private boolean drmMultiSession; + private List streamKeys; + @Nullable private Object tag; + + /** Creates a builder. */ + public Builder() { + streamKeys = Collections.emptyList(); + } + + /** + * Sets the optional media id which identifies the media item. If not specified, {@code + * #setSourceUri} must be called and the string representation of {@link + * PlaybackProperties#sourceUri} is used as the media id. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setMediaId(@Nullable String mediaId) { + this.mediaId = mediaId; + return this; + } + + /** + * Sets the optional source uri. If not specified, {@link #setMediaId(String)} must be called. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setSourceUri(@Nullable String sourceUri) { + return setSourceUri(sourceUri == null ? null : Uri.parse(sourceUri)); + } + + /** + * Sets the optional source {@link Uri}. If not specified, {@link #setMediaId(String)} must be + * called. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setSourceUri(@Nullable Uri sourceUri) { + this.sourceUri = sourceUri; + return this; + } + + /** + * Sets the optional extension of the item. + * + *

The extension can be used to disambiguate media items that have a uri which does not allow + * to infer the actual media type. + * + *

If a {@link PlaybackProperties#sourceUri} is set, the extension is used to create a {@link + * PlaybackProperties} object. Otherwise it will be ignored. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setExtension(@Nullable String extension) { + this.extension = extension; + return this; + } + + /** + * Sets the optional license server {@link UriBundle}. If a license uri is set, the {@link + * DrmConfiguration#uuid} needs to be specified as well. + * + *

if a {@link PlaybackProperties#sourceUri} is set, the drm license uri is used to create a + * {@link PlaybackProperties} object. Otherwise it will be ignored. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setDrmLicenseUri(@Nullable UriBundle licenseUri) { + drmLicenseUri = licenseUri; + return this; + } + + /** + * Sets the optional license server {@link Uri}. If a license uri is set, the {@link + * DrmConfiguration#uuid} needs to be specified as well. + * + *

If a {@link PlaybackProperties#sourceUri} is set, the drm license uri is used to create a + * {@link PlaybackProperties} object. Otherwise it will be ignored. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setDrmLicenseUri(@Nullable Uri licenseUri) { + drmLicenseUri = licenseUri == null ? null : new UriBundle(licenseUri); + return this; + } + + /** + * Sets the optional license server uri as a {@link String}. If a license uri is set, the {@link + * DrmConfiguration#uuid} needs to be specified as well. + * + *

If a {@link PlaybackProperties#sourceUri} is set, the drm license uri is used to create a + * {@link PlaybackProperties} object. Otherwise it will be ignored. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setDrmLicenseUri(@Nullable String licenseUri) { + drmLicenseUri = licenseUri == null ? null : new UriBundle(Uri.parse(licenseUri)); + return this; + } + + /** + * Sets the {@link UUID} of the protection scheme. If a drm system uuid is set, the {@link + * DrmConfiguration#licenseUri} needs to be set as well. + * + *

If a {@link PlaybackProperties#sourceUri} is set, the drm system uuid is used to create a + * {@link PlaybackProperties} object. Otherwise it will be ignored. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setDrmUuid(@Nullable UUID uuid) { + drmUuid = uuid; + return this; + } + + /** + * Sets whether the drm configuration is multi session enabled. + * + *

If a {@link PlaybackProperties#sourceUri} is set, the drm multi session flag is used to + * create a {@link PlaybackProperties} object. Otherwise it will be ignored. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setDrmMultiSession(boolean multiSession) { + drmMultiSession = multiSession; + return this; + } + + /** + * Sets the optional stream keys by which the manifest is filtered (only used for adaptive + * streams). + * + *

If a {@link PlaybackProperties#sourceUri} is set, the stream keys are used to create a + * {@link PlaybackProperties} object. Otherwise it will be ignored. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setStreamKeys(@Nullable List streamKeys) { + this.streamKeys = + streamKeys != null && !streamKeys.isEmpty() + ? Collections.unmodifiableList(new ArrayList<>(streamKeys)) + : Collections.emptyList(); + return this; + } + + /** + * Sets the optional tag for custom attributes. The tag for the media source which will be + * published in the {@link com.google.android.exoplayer2.Timeline} of the source as {@link + * com.google.android.exoplayer2.Timeline.Window#tag}. + * + *

If a {@link PlaybackProperties#sourceUri} is set, the tag is used to create a {@link + * PlaybackProperties} object. Otherwise it will be ignored. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setTag(@Nullable Object tag) { + this.tag = tag; + return this; + } + + /** + * Returns a new {@link MediaItem} instance with the current builder values. + * + * @throws IllegalStateException If a required property is not set. + */ + public MediaItem build() { + Assertions.checkState(drmLicenseUri == null || drmUuid != null); + @Nullable PlaybackProperties playbackProperties = null; + if (sourceUri != null) { + playbackProperties = + new PlaybackProperties( + sourceUri, + extension, + drmUuid != null + ? new DrmConfiguration(drmUuid, drmLicenseUri, drmMultiSession) + : null, + streamKeys, + tag); + mediaId = mediaId != null ? mediaId : sourceUri.toString(); + } + return new MediaItem(Assertions.checkNotNull(mediaId), playbackProperties); + } + } + + /** Bundles a resource's URI with headers to attach to any request to that URI. */ + public static final class UriBundle { + + /** An empty {@link UriBundle}. */ + public static final UriBundle EMPTY = new UriBundle(Uri.EMPTY); + + /** A URI. */ + public final Uri uri; + + /** The headers to attach to any request for the given URI. */ + public final Map requestHeaders; + + /** + * Creates an instance with no request headers. + * + * @param uri See {@link #uri}. + */ + public UriBundle(Uri uri) { + this(uri, Collections.emptyMap()); + } + + /** + * Creates an instance with the given URI and request headers. + * + * @param uri See {@link #uri}. + * @param requestHeaders See {@link #requestHeaders}. + */ + public UriBundle(Uri uri, Map requestHeaders) { + this.uri = uri; + this.requestHeaders = Collections.unmodifiableMap(new HashMap<>(requestHeaders)); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + UriBundle uriBundle = (UriBundle) other; + return uri.equals(uriBundle.uri) && requestHeaders.equals(uriBundle.requestHeaders); + } + + @Override + public int hashCode() { + int result = uri.hashCode(); + result = 31 * result + requestHeaders.hashCode(); + return result; + } + } + + /** DRM configuration for a media item. */ + public static final class DrmConfiguration { + + /** The UUID of the protection scheme. */ + public final UUID uuid; + + /** + * Optional license server {@link Uri}. If {@code null} then the license server must be + * specified by the media. + */ + @Nullable public final UriBundle licenseUri; + + /** Whether the drm configuration is multi session enabled. */ + public final boolean multiSession; + + /** + * Creates an instance. + * + * @param uuid See {@link #uuid}. + * @param licenseUri See {@link #licenseUri}. + * @param multiSession See {@link #multiSession}. + */ + public DrmConfiguration(UUID uuid, @Nullable UriBundle licenseUri, boolean multiSession) { + this.uuid = uuid; + this.licenseUri = licenseUri; + this.multiSession = multiSession; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + DrmConfiguration other = (DrmConfiguration) obj; + return uuid.equals(other.uuid) + && Util.areEqual(licenseUri, other.licenseUri) + && multiSession == other.multiSession; + } + + @Override + public int hashCode() { + int result = uuid.hashCode(); + result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0); + result = 31 * result + (multiSession ? 1 : 0); + return result; + } + } + + /** Properties for local playback. */ + public static final class PlaybackProperties { + + /** The source {@link Uri}. */ + public final Uri sourceUri; + + /** + * The optional extension of the item, or {@code null} if unspecified. + * + *

The extension can be used to disambiguate media items that have a uri which does not allow + * to infer the actual media type. + */ + @Nullable public final String extension; + + /** Optional {@link DrmConfiguration} for the media. */ + @Nullable public final DrmConfiguration drmConfiguration; + + /** Optional stream keys by which the manifest is filtered. */ + public final List streamKeys; + + /** + * Optional tag for custom attributes. The tag for the media source which will be published in + * the {@link com.google.android.exoplayer2.Timeline} of the source as {@link + * com.google.android.exoplayer2.Timeline.Window#tag}. + */ + @Nullable public final Object tag; + + public PlaybackProperties( + Uri sourceUri, + @Nullable String extension, + @Nullable DrmConfiguration drmConfiguration, + List streamKeys, + @Nullable Object tag) { + this.sourceUri = sourceUri; + this.extension = extension; + this.drmConfiguration = drmConfiguration; + this.streamKeys = streamKeys; + this.tag = tag; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PlaybackProperties other = (PlaybackProperties) obj; + + return sourceUri.equals(other.sourceUri) + && Util.areEqual(extension, other.extension) + && Util.areEqual(drmConfiguration, other.drmConfiguration) + && Util.areEqual(streamKeys, other.streamKeys) + && Util.areEqual(tag, other.tag); + } + + @Override + public int hashCode() { + int result = sourceUri.hashCode(); + result = 31 * result + (extension == null ? 0 : extension.hashCode()); + result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode()); + result = 31 * result + streamKeys.hashCode(); + result = 31 * result + (tag == null ? 0 : tag.hashCode()); + return result; + } + } + + /** Identifies the media item. */ + public final String mediaId; + + /** Optional playback properties. */ + @Nullable public final PlaybackProperties playbackProperties; + + private MediaItem(String mediaId, @Nullable PlaybackProperties playbackProperties) { + this.mediaId = mediaId; + this.playbackProperties = playbackProperties; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MediaItem other = (MediaItem) o; + + return Util.areEqual(mediaId, other.mediaId) + && Util.areEqual(playbackProperties, other.playbackProperties); + } + + @Override + public int hashCode() { + int result = mediaId.hashCode(); + result = 31 * result + (playbackProperties != null ? playbackProperties.hashCode() : 0); + return result; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 97ee365b72..49478d0bf0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -111,13 +112,10 @@ public final class ExtractorMediaSource extends CompositeMediaSource { } /** - * Sets a tag for the media source which will be published in the {@link - * com.google.android.exoplayer2.Timeline} of the source as {@link - * com.google.android.exoplayer2.Timeline.Window#tag}. - * - * @param tag A tag for the media source. - * @return This factory, for convenience. + * @deprecated Use {@link MediaItem.PlaybackProperties#tag} and {@link + * #createMediaSource(MediaItem)} instead. */ + @Deprecated public Factory setTag(@Nullable Object tag) { this.tag = tag; return this; @@ -172,19 +170,32 @@ public final class ExtractorMediaSource extends CompositeMediaSource { /** * Returns a new {@link ExtractorMediaSource} using the current parameters. * - * @param uri The {@link Uri}. + * @param uri The {@link Uri uri}. * @return The new {@link ExtractorMediaSource}. */ @Override public ExtractorMediaSource createMediaSource(Uri uri) { + return createMediaSource(new MediaItem.Builder().setSourceUri(uri).build()); + } + + /** + * Returns a new {@link ExtractorMediaSource} using the current parameters. + * + * @param mediaItem The {@link MediaItem}. + * @return The new {@link ExtractorMediaSource}. + * @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}. + */ + @Override + public ExtractorMediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); return new ExtractorMediaSource( - uri, + mediaItem.playbackProperties.sourceUri, dataSourceFactory, extractorsFactory, loadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, - tag); + mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java index 11f1c5ed61..9e886ab50d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.StreamKey; @@ -26,12 +27,8 @@ import java.util.List; /** Factory for creating {@link MediaSource}s from URIs. */ public interface MediaSourceFactory { - /** - * Sets a list of {@link StreamKey StreamKeys} by which the manifest is filtered. - * - * @param streamKeys A list of {@link StreamKey StreamKeys}. - * @return This factory, for convenience. - */ + /** @deprecated Use {@link MediaItem.PlaybackProperties#streamKeys} instead. */ + @Deprecated default MediaSourceFactory setStreamKeys(@Nullable List streamKeys) { return this; } @@ -44,18 +41,28 @@ public interface MediaSourceFactory { */ MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager); - /** - * Creates a new {@link MediaSource} with the specified {@code uri}. - * - * @param uri The URI to play. - * @return The new {@link MediaSource media source}. - */ - MediaSource createMediaSource(Uri uri); - /** * Returns the {@link C.ContentType content types} supported by media sources created by this * factory. */ @C.ContentType int[] getSupportedTypes(); + + /** + * Creates a new {@link MediaSource} with the specified {@link MediaItem}. + * + * @param mediaItem The media item to play. + * @return The new {@link MediaSource media source}. + */ + MediaSource createMediaSource(MediaItem mediaItem); + + /** + * Creates a new {@link MediaSource} with the specified {@code uri}. + * + * @param uri The URI to play. + * @return The new {@link MediaSource media source}. + */ + default MediaSource createMediaSource(Uri uri) { + return createMediaSource(new MediaItem.Builder().setSourceUri(uri).build()); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index a1611d6723..dd7a0e743c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -29,6 +30,7 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; /** * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}. @@ -106,13 +108,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource } /** - * Sets a tag for the media source which will be published in the {@link - * com.google.android.exoplayer2.Timeline} of the source as {@link - * com.google.android.exoplayer2.Timeline.Window#tag}. - * - * @param tag A tag for the media source. - * @return This factory, for convenience. + * @deprecated Use {@link MediaItem.PlaybackProperties#tag} and {@link + * #createMediaSource(MediaItem)} instead. */ + @Deprecated public Factory setTag(@Nullable Object tag) { this.tag = tag; return this; @@ -168,20 +167,33 @@ public final class ProgressiveMediaSource extends BaseMediaSource /** * Returns a new {@link ProgressiveMediaSource} using the current parameters. * - * @param uri The {@link Uri}. + * @param uri The {@link Uri uri}. * @return The new {@link ProgressiveMediaSource}. */ @Override public ProgressiveMediaSource createMediaSource(Uri uri) { + return createMediaSource(new MediaItem.Builder().setSourceUri(uri).build()); + } + + /** + * Returns a new {@link ProgressiveMediaSource} using the current parameters. + * + * @param mediaItem The {@link MediaItem}. + * @return The new {@link ProgressiveMediaSource}. + * @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}. + */ + @Override + public ProgressiveMediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); return new ProgressiveMediaSource( - uri, + mediaItem.playbackProperties.sourceUri, dataSourceFactory, extractorsFactory, drmSessionManager, loadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, - tag); + mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaItemTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaItemTest.java new file mode 100644 index 0000000000..a74c5df741 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaItemTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.offline.StreamKey; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link MediaItem MediaItems}. */ +@RunWith(AndroidJUnit4.class) +public class MediaItemTest { + + private static final String URI_STRING = "http://www.google.com"; + + @Test + public void builder_needsSourceUriOrMediaId() { + assertThrows(NullPointerException.class, () -> new MediaItem.Builder().build()); + } + + @Test + public void builderWithUri_setsSourceUri() { + Uri uri = Uri.parse(URI_STRING); + + MediaItem mediaItem = MediaItem.fromUri(uri); + + assertThat(mediaItem.playbackProperties.sourceUri.toString()).isEqualTo(URI_STRING); + assertThat(mediaItem.mediaId).isEqualTo(URI_STRING); + } + + @Test + public void builderWithUriAsString_setsSourceUri() { + MediaItem mediaItem = MediaItem.fromUri(URI_STRING); + + assertThat(mediaItem.playbackProperties.sourceUri.toString()).isEqualTo(URI_STRING); + assertThat(mediaItem.mediaId).isEqualTo(URI_STRING); + } + + @Test + public void builderSetExtension_isNullByDefault() { + MediaItem mediaItem = MediaItem.fromUri(URI_STRING); + + assertThat(mediaItem.playbackProperties.extension).isNull(); + } + + @Test + public void builderSetExtension_setsExtension() { + MediaItem mediaItem = + new MediaItem.Builder().setSourceUri(URI_STRING).setExtension("mpd").build(); + + assertThat(mediaItem.playbackProperties.extension).isEqualTo("mpd"); + } + + @Test + public void builderSetDrmConfig_isNullByDefault() { + // Null value by default. + MediaItem mediaItem = new MediaItem.Builder().setSourceUri(URI_STRING).build(); + assertThat(mediaItem.playbackProperties.drmConfiguration).isNull(); + } + + @Test + public void builderSetDrmConfig_setsAllProperties() { + MediaItem.UriBundle licenseUri = new MediaItem.UriBundle(Uri.parse(URI_STRING)); + + MediaItem mediaItem = + new MediaItem.Builder() + .setSourceUri(URI_STRING) + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmLicenseUri(licenseUri) + .setDrmMultiSession(/* multiSession= */ true) + .build(); + + assertThat(mediaItem.playbackProperties.drmConfiguration).isNotNull(); + assertThat(mediaItem.playbackProperties.drmConfiguration.uuid).isEqualTo(C.WIDEVINE_UUID); + assertThat(mediaItem.playbackProperties.drmConfiguration.licenseUri).isEqualTo(licenseUri); + assertThat(mediaItem.playbackProperties.drmConfiguration.multiSession).isTrue(); + } + + @Test + public void builderSetDrmUuid_notCalled_throwsIllegalStateException() { + assertThrows( + IllegalStateException.class, + () -> + new MediaItem.Builder() + .setSourceUri(URI_STRING) + // missing uuid + .setDrmLicenseUri(new MediaItem.UriBundle(Uri.parse(URI_STRING))) + .build()); + } + + @Test + public void builderSetStreamKeys_setsStreamKeys() { + List streamKeys = new ArrayList<>(); + streamKeys.add(new StreamKey(1, 0, 0)); + streamKeys.add(new StreamKey(0, 1, 1)); + + MediaItem mediaItem = + new MediaItem.Builder().setSourceUri(URI_STRING).setStreamKeys(streamKeys).build(); + + assertThat(mediaItem.playbackProperties.streamKeys).isEqualTo(streamKeys); + } + + @Test + public void builderSetTag_isNullByDefault() { + MediaItem mediaItem = new MediaItem.Builder().setSourceUri(URI_STRING).build(); + + assertThat(mediaItem.playbackProperties.tag).isNull(); + } + + @Test + public void builderSetTag_setsTag() { + Object tag = new Object(); + + MediaItem mediaItem = new MediaItem.Builder().setSourceUri(URI_STRING).setTag(tag).build(); + + assertThat(mediaItem.playbackProperties.tag).isEqualTo(tag); + } +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 0e0a8c929f..b9f08e9a31 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -23,6 +23,7 @@ import android.util.SparseArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DrmSession; @@ -63,6 +64,7 @@ import java.io.InputStreamReader; import java.nio.charset.Charset; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.TimeZone; @@ -88,7 +90,7 @@ public final class DashMediaSource extends BaseMediaSource { private long livePresentationDelayMs; private boolean livePresentationDelayOverridesManifest; @Nullable private ParsingLoadable.Parser manifestParser; - @Nullable private List streamKeys; + private List streamKeys; @Nullable private Object tag; /** @@ -119,24 +121,28 @@ public final class DashMediaSource extends BaseMediaSource { loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + streamKeys = Collections.emptyList(); } /** - * Sets a tag for the media source which will be published in the {@link - * com.google.android.exoplayer2.Timeline} of the source as {@link - * com.google.android.exoplayer2.Timeline.Window#tag}. - * - * @param tag A tag for the media source. - * @return This factory, for convenience. + * @deprecated Use {@link MediaItem.PlaybackProperties#tag} and {@link + * #createMediaSource(MediaItem)} instead. */ + @Deprecated public Factory setTag(@Nullable Object tag) { this.tag = tag; return this; } + /** + * @deprecated Use {@link MediaItem.PlaybackProperties#streamKeys} and {@link + * #createMediaSource(MediaItem)} instead. + */ + @SuppressWarnings("deprecation") + @Deprecated @Override public Factory setStreamKeys(@Nullable List streamKeys) { - this.streamKeys = streamKeys != null && !streamKeys.isEmpty() ? streamKeys : null; + this.streamKeys = streamKeys != null ? streamKeys : Collections.emptyList(); return this; } @@ -304,21 +310,38 @@ public final class DashMediaSource extends BaseMediaSource { /** * Returns a new {@link DashMediaSource} using the current parameters. * - * @param manifestUri The manifest {@link Uri}. + * @param uri The {@link Uri uri}. * @return The new {@link DashMediaSource}. */ @Override - public DashMediaSource createMediaSource(Uri manifestUri) { + public DashMediaSource createMediaSource(Uri uri) { + return createMediaSource(new MediaItem.Builder().setSourceUri(uri).build()); + } + + /** + * Returns a new {@link DashMediaSource} using the current parameters. + * + * @param mediaItem The media item of the dash stream. + * @return The new {@link DashMediaSource}. + * @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}. + */ + @Override + public DashMediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); @Nullable ParsingLoadable.Parser manifestParser = this.manifestParser; if (manifestParser == null) { manifestParser = new DashManifestParser(); } - if (streamKeys != null) { + List streamKeys = + !mediaItem.playbackProperties.streamKeys.isEmpty() + ? mediaItem.playbackProperties.streamKeys + : this.streamKeys; + if (!streamKeys.isEmpty()) { manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys); } return new DashMediaSource( /* manifest= */ null, - Assertions.checkNotNull(manifestUri), + mediaItem.playbackProperties.sourceUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, @@ -327,7 +350,7 @@ public final class DashMediaSource extends BaseMediaSource { loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest, - tag); + mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag); } @Override diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 96be633812..01b42c194c 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -23,6 +23,7 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.Extractor; @@ -52,6 +53,7 @@ import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; +import java.util.Collections; import java.util.List; /** An HLS {@link MediaSource}. */ @@ -98,7 +100,7 @@ public final class HlsMediaSource extends BaseMediaSource private boolean allowChunklessPreparation; @MetadataType private int metadataType; private boolean useSessionKeys; - @Nullable private List streamKeys; + private List streamKeys; @Nullable private Object tag; /** @@ -127,16 +129,14 @@ public final class HlsMediaSource extends BaseMediaSource loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); metadataType = METADATA_TYPE_ID3; + streamKeys = Collections.emptyList(); } /** - * Sets a tag for the media source which will be published in the {@link - * com.google.android.exoplayer2.Timeline} of the source as {@link - * com.google.android.exoplayer2.Timeline.Window#tag}. - * - * @param tag A tag for the media source. - * @return This factory, for convenience. + * @deprecated Use {@link MediaItem.PlaybackProperties#tag} and {@link + * #createMediaSource(MediaItem)} instead. */ + @Deprecated public Factory setTag(@Nullable Object tag) { this.tag = tag; return this; @@ -298,9 +298,15 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + /** + * @deprecated Use {@link MediaItem.PlaybackProperties#streamKeys} and {@link + * #createMediaSource(MediaItem)} instead. + */ + @SuppressWarnings("deprecation") + @Deprecated @Override public Factory setStreamKeys(@Nullable List streamKeys) { - this.streamKeys = streamKeys != null && !streamKeys.isEmpty() ? streamKeys : null; + this.streamKeys = streamKeys != null ? streamKeys : Collections.emptyList(); return this; } @@ -308,6 +314,7 @@ public final class HlsMediaSource extends BaseMediaSource * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, * MediaSourceEventListener)} instead. */ + @SuppressWarnings("deprecation") @Deprecated public HlsMediaSource createMediaSource( Uri playlistUri, @@ -323,17 +330,35 @@ public final class HlsMediaSource extends BaseMediaSource /** * Returns a new {@link HlsMediaSource} using the current parameters. * + * @param uri The {@link Uri uri}. * @return The new {@link HlsMediaSource}. */ @Override - public HlsMediaSource createMediaSource(Uri playlistUri) { + public HlsMediaSource createMediaSource(Uri uri) { + return createMediaSource(new MediaItem.Builder().setSourceUri(uri).build()); + } + + /** + * Returns a new {@link HlsMediaSource} using the current parameters. + * + * @param mediaItem The {@link MediaItem}. + * @return The new {@link HlsMediaSource}. + * @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}. + */ + @Override + public HlsMediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); HlsPlaylistParserFactory playlistParserFactory = this.playlistParserFactory; - if (streamKeys != null) { + List streamKeys = + !mediaItem.playbackProperties.streamKeys.isEmpty() + ? mediaItem.playbackProperties.streamKeys + : this.streamKeys; + if (!streamKeys.isEmpty()) { playlistParserFactory = new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys); } return new HlsMediaSource( - playlistUri, + mediaItem.playbackProperties.sourceUri, hlsDataSourceFactory, extractorFactory, compositeSequenceableLoaderFactory, @@ -344,7 +369,7 @@ public final class HlsMediaSource extends BaseMediaSource allowChunklessPreparation, metadataType, useSessionKeys, - tag); + mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag); } @Override diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 6836b5cd42..b18bad63a2 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -21,6 +21,7 @@ import android.os.SystemClock; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -53,6 +54,7 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** A SmoothStreaming {@link MediaSource}. */ @@ -74,7 +76,7 @@ public final class SsMediaSource extends BaseMediaSource private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; @Nullable private ParsingLoadable.Parser manifestParser; - @Nullable private List streamKeys; + private List streamKeys; @Nullable private Object tag; /** @@ -105,15 +107,14 @@ public final class SsMediaSource extends BaseMediaSource loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + streamKeys = Collections.emptyList(); } /** - * Sets a tag for the media source which will be published in the {@link Timeline} of the source - * as {@link Timeline.Window#tag}. - * - * @param tag A tag for the media source. - * @return This factory, for convenience. + * @deprecated Use {@link MediaItem.PlaybackProperties#tag} and {@link + * #createMediaSource(MediaItem)} instead. */ + @Deprecated public Factory setTag(@Nullable Object tag) { this.tag = tag; return this; @@ -204,12 +205,29 @@ public final class SsMediaSource extends BaseMediaSource return this; } + /** + * @deprecated Use {@link MediaItem.PlaybackProperties#streamKeys} and {@link + * #createMediaSource(MediaItem)} instead. + */ + @SuppressWarnings("deprecation") + @Deprecated @Override - public Factory setStreamKeys(List streamKeys) { - this.streamKeys = streamKeys != null && !streamKeys.isEmpty() ? streamKeys : null; + public Factory setStreamKeys(@Nullable List streamKeys) { + this.streamKeys = streamKeys != null ? streamKeys : Collections.emptyList(); return this; } + /** + * Returns a new {@link SsMediaSource} using the current parameters. + * + * @param uri The {@link Uri uri}. + * @return The new {@link SsMediaSource}. + */ + @Override + public SsMediaSource createMediaSource(Uri uri) { + return createMediaSource(new MediaItem.Builder().setSourceUri(uri).build()); + } + /** * Returns a new {@link SsMediaSource} using the current parameters and the specified sideloaded * manifest. @@ -220,7 +238,7 @@ public final class SsMediaSource extends BaseMediaSource */ public SsMediaSource createMediaSource(SsManifest manifest) { Assertions.checkArgument(!manifest.isLive); - if (streamKeys != null) { + if (!streamKeys.isEmpty()) { manifest = manifest.copy(streamKeys); } return new SsMediaSource( @@ -271,21 +289,27 @@ public final class SsMediaSource extends BaseMediaSource /** * Returns a new {@link SsMediaSource} using the current parameters. * - * @param manifestUri The manifest {@link Uri}. + * @param mediaItem The {@link MediaItem}. * @return The new {@link SsMediaSource}. + * @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}. */ @Override - public SsMediaSource createMediaSource(Uri manifestUri) { + public SsMediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); @Nullable ParsingLoadable.Parser manifestParser = this.manifestParser; if (manifestParser == null) { manifestParser = new SsManifestParser(); } - if (streamKeys != null) { + List streamKeys = + !mediaItem.playbackProperties.streamKeys.isEmpty() + ? mediaItem.playbackProperties.streamKeys + : this.streamKeys; + if (!streamKeys.isEmpty()) { manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys); } return new SsMediaSource( /* manifest= */ null, - Assertions.checkNotNull(manifestUri), + mediaItem.playbackProperties.sourceUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, @@ -293,7 +317,7 @@ public final class SsMediaSource extends BaseMediaSource drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, - tag); + mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag); } @Override