From 776ad20a507048edd848e99b4b3f9c5a27c42a7a Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 26 Sep 2018 06:55:17 -0700 Subject: [PATCH] Add a MediaQueue abstraction to the cast extension ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=214598078 --- .../exoplayer2/castdemo/PlayerManager.java | 3 +- extensions/cast/build.gradle | 2 + .../exoplayer2/ext/cast/CastPlayer.java | 35 +-- .../exoplayer2/ext/cast/MediaItem.java | 293 ++++++++++++++++++ .../exoplayer2/ext/cast/MediaItemQueue.java | 85 +++++ .../exoplayer2/ext/cast/RemotePlayer.java | 45 +++ .../exoplayer2/ext/cast/MediaItemTest.java | 92 ++++++ 7 files changed, 528 insertions(+), 27 deletions(-) create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/RemotePlayer.java create mode 100644 extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index d188469de8..0c69e40164 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.ext.cast.RemotePlayer; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -51,7 +52,7 @@ import java.util.ArrayList; /** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ /* package */ final class PlayerManager - implements EventListener, CastPlayer.SessionAvailabilityListener { + implements EventListener, RemotePlayer.SessionAvailabilityListener { /** * Listener for changes in the media queue playback position. diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index bee73cac12..ab14b4034a 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -32,6 +32,8 @@ android { dependencies { api 'com.google.android.gms:play-services-cast-framework:16.0.1' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') testImplementation project(modulePrefix + 'testutils') diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index f630ea1628..ecaa56427f 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -52,35 +52,18 @@ import java.util.concurrent.CopyOnWriteArraySet; * {@link Player} implementation that communicates with a Cast receiver app. * *

The behavior of this class depends on the underlying Cast session, which is obtained from the - * Cast context passed to {@link #CastPlayer}. To keep track of the session, - * {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be - * implemented and attached to the player.

+ * Cast context passed to {@link #CastPlayer}. To keep track of the session, {@link + * #isCastSessionAvailable()} can be queried and {@link RemotePlayer.SessionAvailabilityListener} + * can be implemented and attached to the player. * - *

If no session is available, the player state will remain unchanged and calls to methods that + *

If no session is available, the player state will remain unchanged and calls to methods that * alter it will be ignored. Querying the player state is possible even when no session is - * available, in which case, the last observed receiver app state is reported.

+ * available, in which case, the last observed receiver app state is reported. * - *

Methods should be called on the application's main thread.

+ *

Methods should be called on the application's main thread. */ public final class CastPlayer implements Player { - /** - * Listener of changes in the cast session availability. - */ - public interface SessionAvailabilityListener { - - /** - * Called when a cast session becomes available to the player. - */ - void onCastSessionAvailable(); - - /** - * Called when the cast session becomes unavailable. - */ - void onCastSessionUnavailable(); - - } - private static final String TAG = "CastPlayer"; private static final int RENDERER_COUNT = 3; @@ -106,7 +89,7 @@ public final class CastPlayer implements Player { // Listeners. private final CopyOnWriteArraySet listeners; - private SessionAvailabilityListener sessionAvailabilityListener; + private RemotePlayer.SessionAvailabilityListener sessionAvailabilityListener; // Internal state. private CastTimeline currentTimeline; @@ -276,9 +259,9 @@ public final class CastPlayer implements Player { /** * Sets a listener for updates on the cast session availability. * - * @param listener The {@link SessionAvailabilityListener}. + * @param listener The {@link RemotePlayer.SessionAvailabilityListener}. */ - public void setSessionAvailabilityListener(SessionAvailabilityListener listener) { + public void setSessionAvailabilityListener(RemotePlayer.SessionAvailabilityListener listener) { sessionAvailabilityListener = listener; } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java new file mode 100644 index 0000000000..67428e8af4 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2018 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.ext.cast; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; + +/** Representation of an item that can be played by a media player. */ +public final class MediaItem { + + /** A builder for {@link MediaItem} instances. */ + public static final class Builder { + + @Nullable private UUID uuid; + private String title; + private String description; + private MediaItem.UriBundle media; + @Nullable private Object attachment; + private List drmSchemes; + private long startPositionUs; + private long endPositionUs; + private String mimeType; + + /** Creates an builder with default field values. */ + public Builder() { + clearInternal(); + } + + /** See {@link MediaItem#uuid}. */ + public Builder setUuid(UUID uuid) { + this.uuid = uuid; + return this; + } + + /** See {@link MediaItem#title}. */ + public Builder setTitle(String title) { + this.title = title; + return this; + } + + /** See {@link MediaItem#description}. */ + public Builder setDescription(String description) { + this.description = description; + return this; + } + + /** Equivalent to {@link #setMedia(UriBundle) setMedia(new UriBundle(uri))}. */ + public Builder setMedia(String uri) { + return setMedia(new UriBundle(uri)); + } + + /** See {@link MediaItem#media}. */ + public Builder setMedia(UriBundle media) { + this.media = media; + return this; + } + + /** See {@link MediaItem#attachment}. */ + public Builder setAttachment(Object attachment) { + this.attachment = attachment; + return this; + } + + /** See {@link MediaItem#drmSchemes}. */ + public Builder setDrmSchemes(List drmSchemes) { + this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes)); + return this; + } + + /** See {@link MediaItem#startPositionUs}. */ + public Builder setStartPositionUs(long startPositionUs) { + this.startPositionUs = startPositionUs; + return this; + } + + /** See {@link MediaItem#endPositionUs}. */ + public Builder setEndPositionUs(long endPositionUs) { + Assertions.checkArgument(endPositionUs != C.TIME_END_OF_SOURCE); + this.endPositionUs = endPositionUs; + return this; + } + + /** See {@link MediaItem#mimeType}. */ + public Builder setMimeType(String mimeType) { + this.mimeType = mimeType; + return this; + } + + /** + * Equivalent to {@link #build()}, except it also calls {@link #clear()} after creating the + * {@link MediaItem}. + */ + public MediaItem buildAndClear() { + MediaItem item = build(); + clearInternal(); + return item; + } + + /** Returns the builder to default values. */ + public Builder clear() { + clearInternal(); + return this; + } + + /** + * Returns a new {@link MediaItem} instance with the current builder values. This method also + * clears any values passed to {@link #setUuid(UUID)}. + */ + public MediaItem build() { + UUID uuid = this.uuid; + this.uuid = null; + return new MediaItem( + uuid != null ? uuid : UUID.randomUUID(), + title, + description, + media, + attachment, + drmSchemes, + startPositionUs, + endPositionUs, + mimeType); + } + + @EnsuresNonNull({"title", "description", "media", "drmSchemes", "mimeType"}) + private void clearInternal(@UnknownInitialization Builder this) { + uuid = null; + title = ""; + description = ""; + media = UriBundle.EMPTY; + attachment = null; + drmSchemes = Collections.emptyList(); + startPositionUs = C.TIME_UNSET; + endPositionUs = C.TIME_UNSET; + mimeType = ""; + } + } + + /** 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(""); + + /** A URI. */ + public final Uri uri; + + /** The headers to attach to any request for the given URI. */ + public final Map requestHeaders; + + /** + * Creates an instance from the given string with no request headers. + * + * @param uriString See {@link #uri}. + */ + public UriBundle(String uriString) { + this(Uri.parse(uriString), 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)); + } + } + + /** + * Represents a DRM protection scheme, and optionally provides information about how to acquire + * the license for the media. + */ + public static final class DrmScheme { + + /** The UUID of the protection scheme. */ + public final UUID uuid; + + /** + * A optional {@link UriBundle} for the license server. If no license server is provided, the + * server must be provided by the media. + */ + @Nullable public final UriBundle licenseServerUri; + + /** + * Creates an instance. + * + * @param uuid See {@link #uuid}. + * @param licenseServerUri See {@link #licenseServerUri}. + */ + public DrmScheme(UUID uuid, @Nullable UriBundle licenseServerUri) { + this.uuid = uuid; + this.licenseServerUri = licenseServerUri; + } + } + + /** + * A UUID that identifies this item, potentially across different devices. The default value is + * obtained by calling {@link UUID#randomUUID()}. + */ + public final UUID uuid; + + /** The title of the item. The default value is an empty string. */ + public final String title; + + /** A description for the item. The default value is an empty string. */ + public final String description; + + /** + * A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}. + */ + public final UriBundle media; + + /** + * An optional opaque object to attach to the media item. Handling of this attachment is + * implementation specific. The default value is null. + */ + @Nullable public final Object attachment; + + /** + * Immutable list of {@link DrmScheme} instances sorted in decreasing order of preference. The + * default value is an empty list. + */ + public final List drmSchemes; + + /** + * The position in microseconds at which playback of this media item should start. {@link + * C#TIME_UNSET} if playback should start at the default position. The default value is {@link + * C#TIME_UNSET}. + */ + public final long startPositionUs; + + /** + * The position in microseconds at which playback of this media item should end. {@link + * C#TIME_UNSET} if playback should end at the end of the media. The default value is {@link + * C#TIME_UNSET}. + */ + public final long endPositionUs; + + /** + * The mime type of this media item. The default value is an empty string. + * + *

The usage of this mime type is optional and player implementation specific. + */ + public final String mimeType; + + // TODO: Add support for sideloaded tracks, artwork, icon, and subtitle. + + private MediaItem( + UUID uuid, + String title, + String description, + UriBundle media, + @Nullable Object attachment, + List drmSchemes, + long startPositionUs, + long endPositionUs, + String mimeType) { + this.uuid = uuid; + this.title = title; + this.description = description; + this.media = media; + this.attachment = attachment; + this.drmSchemes = drmSchemes; + this.startPositionUs = startPositionUs; + this.endPositionUs = endPositionUs; + this.mimeType = mimeType; + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java new file mode 100644 index 0000000000..184e347e1c --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 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.ext.cast; + +/** Represents a sequence of {@link MediaItem MediaItems}. */ +public interface MediaItemQueue { + + /** + * Returns the item at the given index. + * + * @param index The index of the item to retrieve. + * @return The item at the given index. + * @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}. + */ + MediaItem get(int index); + + /** Returns the number of items in this queue. */ + int getSize(); + + /** + * Appends the given sequence of items to the queue. + * + * @param items The sequence of items to append. + */ + void add(MediaItem... items); + + /** + * Adds the given sequence of items to the queue at the given position, so that the first of + * {@code items} is placed at the given index. + * + * @param index The index at which {@code items} will be inserted. + * @param items The sequence of items to append. + * @throws IndexOutOfBoundsException If {@code index < 0 || index > getSize()}. + */ + void add(int index, MediaItem... items); + + /** + * Moves an existing item within the playlist. + * + *

Calling this method is equivalent to removing the item at position {@code indexFrom} and + * immediately inserting it at position {@code indexTo}. If the moved item is being played at the + * moment of the invocation, playback will stick with the moved item. + * + * @param indexFrom The index of the item to move. + * @param indexTo The index at which the item will be placed after this operation. + * @throws IndexOutOfBoundsException If for either index, {@code index < 0 || index >= getSize()}. + */ + void move(int indexFrom, int indexTo); + + /** + * Removes an item from the queue. + * + * @param index The index of the item to remove from the queue. + * @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}. + */ + void remove(int index); + + /** + * Removes a range of items from the queue. + * + *

Does nothing if an empty range ({@code from == exclusiveTo}) is passed. + * + * @param from The inclusive index at which the range to remove starts. + * @param exclusiveTo The exclusive index at which the range to remove ends. + * @throws IndexOutOfBoundsException If {@code from < 0 || exclusiveTo > getSize() || from > + * exclusiveTo}. + */ + void removeRange(int from, int exclusiveTo); + + /** Removes all items in the queue. */ + void clear(); +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/RemotePlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/RemotePlayer.java new file mode 100644 index 0000000000..98d5895db8 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/RemotePlayer.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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.ext.cast; + +import com.google.android.exoplayer2.Player; + +/** A {@link Player} for playing media remotely using the Google Cast framework. */ +public interface RemotePlayer extends Player { + + /** Listener of changes in the cast session availability. */ + interface SessionAvailabilityListener { + + /** Called when a cast session becomes available to the player. */ + void onCastSessionAvailable(); + + /** Called when the cast session becomes unavailable. */ + void onCastSessionUnavailable(); + } + + /** Returns whether a cast session is available. */ + boolean isCastSessionAvailable(); + + /** + * Sets a listener for updates on the cast session availability. + * + * @param listener The {@link SessionAvailabilityListener}. + */ + void setSessionAvailabilityListener(SessionAvailabilityListener listener); + + /** Returns the {@link MediaItemQueue} associated to this player. */ + MediaItemQueue getMediaItemQueue(); +} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java new file mode 100644 index 0000000000..3c7ec6cab3 --- /dev/null +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2018 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.ext.cast; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.UUID; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Test for {@link MediaItem}. */ +@RunWith(RobolectricTestRunner.class) +public class MediaItemTest { + + @Test + public void buildMediaItem_resetsUuid() { + MediaItem.Builder builder = new MediaItem.Builder(); + UUID uuid = new UUID(1, 1); + MediaItem item1 = builder.setUuid(uuid).build(); + MediaItem item2 = builder.build(); + MediaItem item3 = builder.build(); + assertThat(item1.uuid).isEqualTo(uuid); + assertThat(item2.uuid).isNotEqualTo(uuid); + assertThat(item3.uuid).isNotEqualTo(item2.uuid); + assertThat(item3.uuid).isNotEqualTo(uuid); + } + + @Test + public void buildMediaItem_doesNotChangeState() { + MediaItem.Builder builder = new MediaItem.Builder(); + MediaItem item1 = + builder + .setMedia("http://example.com") + .setTitle("title") + .setMimeType(MimeTypes.AUDIO_MP4) + .setStartPositionUs(3) + .setEndPositionUs(4) + .build(); + MediaItem item2 = builder.build(); + assertThat(item1.title).isEqualTo(item2.title); + assertThat(item1.media.uri).isEqualTo(item2.media.uri); + assertThat(item1.mimeType).isEqualTo(item2.mimeType); + assertThat(item1.startPositionUs).isEqualTo(item2.startPositionUs); + assertThat(item1.endPositionUs).isEqualTo(item2.endPositionUs); + } + + @Test + public void buildMediaItem_assertDefaultValues() { + assertDefaultValues(new MediaItem.Builder().build()); + } + + @Test + public void buildMediaItem_testClear() { + MediaItem.Builder builder = new MediaItem.Builder(); + builder + .setMedia("http://example.com") + .setTitle("title") + .setMimeType(MimeTypes.AUDIO_MP4) + .setStartPositionUs(3) + .setEndPositionUs(4) + .buildAndClear(); + assertDefaultValues(builder.build()); + } + + private static void assertDefaultValues(MediaItem item) { + assertThat(item.title).isEmpty(); + assertThat(item.description).isEmpty(); + assertThat(item.media.uri).isEqualTo(Uri.EMPTY); + assertThat(item.attachment).isNull(); + assertThat(item.drmSchemes).isEmpty(); + assertThat(item.startPositionUs).isEqualTo(C.TIME_UNSET); + assertThat(item.endPositionUs).isEqualTo(C.TIME_UNSET); + assertThat(item.mimeType).isEmpty(); + } +}