Add a MediaQueue abstraction to the cast extension

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=214598078
This commit is contained in:
aquilescanta 2018-09-26 06:55:17 -07:00 committed by Oliver Woodman
parent a8efa27fab
commit 776ad20a50
7 changed files with 528 additions and 27 deletions

View File

@ -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.

View File

@ -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')

View File

@ -52,35 +52,18 @@ import java.util.concurrent.CopyOnWriteArraySet;
* {@link Player} implementation that communicates with a Cast receiver app.
*
* <p>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.</p>
* 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.
*
* <p> If no session is available, the player state will remain unchanged and calls to methods that
* <p>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.</p>
* available, in which case, the last observed receiver app state is reported.
*
* <p>Methods should be called on the application's main thread.</p>
* <p>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<EventListener> 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;
}

View File

@ -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<MediaItem.DrmScheme> 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<MediaItem.DrmScheme> 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<String, String> 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<String, String> 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<DrmScheme> 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.
*
* <p>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<DrmScheme> 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;
}
}

View File

@ -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.
*
* <p>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.
*
* <p>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();
}

View File

@ -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();
}

View File

@ -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();
}
}