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