-
+
+
diff --git a/extensions/cast/README.md b/extensions/cast/README.md
index cc72c5f9bc..1c0d7ac56f 100644
--- a/extensions/cast/README.md
+++ b/extensions/cast/README.md
@@ -5,7 +5,7 @@
The cast extension is a [Player][] implementation that controls playback on a
Cast receiver app.
-[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.html
+[Player]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/Player.html
## Getting the extension ##
diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle
index 0baa074d4a..4dc463ff81 100644
--- a/extensions/cast/build.gradle
+++ b/extensions/cast/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -24,32 +23,21 @@ android {
}
defaultConfig {
- minSdkVersion 14
+ minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
- consumerProguardFiles 'proguard-rules.txt'
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
api 'com.google.android.gms:play-services-cast-framework:16.1.2'
- compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
- compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
+ implementation 'androidx.annotation:annotation:1.0.2'
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
- testImplementation project(modulePrefix + 'testutils')
- testImplementation 'junit:junit:' + junitVersion
- testImplementation 'org.mockito:mockito-core:' + mockitoVersion
- testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
- // These dependencies are necessary to force the supportLibraryVersion of
- // com.android.support:support-v4, com.android.support:appcompat-v7 and
- // com.android.support:mediarouter-v7 to be used. Else older versions are
- // used, for example via:
- // com.google.android.gms:play-services-cast-framework:15.0.1
- // |-- com.android.support:mediarouter-v7:26.1.0
- api 'com.android.support:support-v4:' + supportLibraryVersion
- api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
- api 'com.android.support:recyclerview-v7:' + supportLibraryVersion
}
ext {
diff --git a/extensions/cast/proguard-rules.txt b/extensions/cast/proguard-rules.txt
deleted file mode 100644
index bc94b33c1c..0000000000
--- a/extensions/cast/proguard-rules.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-# Proguard rules specific to the Cast extension.
-
-# DefaultCastOptionsProvider is commonly referred to only by the app's manifest.
--keep class com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider
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 71322de87e..14bb433d2b 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
@@ -16,8 +16,8 @@
package com.google.android.exoplayer2.ext.cast;
import android.os.Looper;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.BasePlayer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
@@ -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 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 extends BasePlayer {
- /**
- * 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;
@@ -591,7 +574,9 @@ public final class CastPlayer extends BasePlayer {
CastTimeline oldTimeline = currentTimeline;
MediaStatus status = getMediaStatus();
currentTimeline =
- status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE;
+ status != null
+ ? timelineTracker.getCastTimeline(remoteMediaClient)
+ : CastTimeline.EMPTY_CAST_TIMELINE;
return !oldTimeline.equals(currentTimeline);
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
index 4939e62a2b..800c19047b 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
@@ -15,24 +15,66 @@
*/
package com.google.android.exoplayer2.ext.cast;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
+import android.util.SparseArray;
import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaQueueItem;
import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
/**
* A {@link Timeline} for Cast media queues.
*/
/* package */ final class CastTimeline extends Timeline {
+ /** Holds {@link Timeline} related data for a Cast media item. */
+ public static final class ItemData {
+
+ /** Holds no media information. */
+ public static final ItemData EMPTY = new ItemData();
+
+ /** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
+ public final long durationUs;
+ /**
+ * The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public final long defaultPositionUs;
+
+ private ItemData() {
+ this(/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */ C.TIME_UNSET);
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param durationUs See {@link #durationsUs}.
+ * @param defaultPositionUs See {@link #defaultPositionUs}.
+ */
+ public ItemData(long durationUs, long defaultPositionUs) {
+ this.durationUs = durationUs;
+ this.defaultPositionUs = defaultPositionUs;
+ }
+
+ /** Returns an instance with the given {@link #durationsUs}. */
+ public ItemData copyWithDurationUs(long durationUs) {
+ if (durationUs == this.durationUs) {
+ return this;
+ }
+ return new ItemData(durationUs, defaultPositionUs);
+ }
+
+ /** Returns an instance with the given {@link #defaultPositionsUs}. */
+ public ItemData copyWithDefaultPositionUs(long defaultPositionUs) {
+ if (defaultPositionUs == this.defaultPositionUs) {
+ return this;
+ }
+ return new ItemData(durationUs, defaultPositionUs);
+ }
+ }
+
+ /** {@link Timeline} for a cast queue that has no items. */
public static final CastTimeline EMPTY_CAST_TIMELINE =
- new CastTimeline(Collections.emptyList(), Collections.emptyMap());
+ new CastTimeline(new int[0], new SparseArray<>());
private final SparseIntArray idsToIndex;
private final int[] ids;
@@ -40,28 +82,23 @@ import java.util.Map;
private final long[] defaultPositionsUs;
/**
- * @param items A list of cast media queue items to represent.
- * @param contentIdToDurationUsMap A map of content id to duration in microseconds.
+ * Creates a Cast timeline from the given data.
+ *
+ * @param itemIds The ids of the items in the timeline.
+ * @param itemIdToData Maps item ids to {@link ItemData}.
*/
- public CastTimeline(List items, Map contentIdToDurationUsMap) {
- int itemCount = items.size();
- int index = 0;
+ public CastTimeline(int[] itemIds, SparseArray itemIdToData) {
+ int itemCount = itemIds.length;
idsToIndex = new SparseIntArray(itemCount);
- ids = new int[itemCount];
+ ids = Arrays.copyOf(itemIds, itemCount);
durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount];
- for (MediaQueueItem item : items) {
- int itemId = item.getItemId();
- ids[index] = itemId;
- idsToIndex.put(itemId, index);
- MediaInfo mediaInfo = item.getMedia();
- String contentId = mediaInfo.getContentId();
- durationsUs[index] =
- contentIdToDurationUsMap.containsKey(contentId)
- ? contentIdToDurationUsMap.get(contentId)
- : CastUtils.getStreamDurationUs(mediaInfo);
- defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
- index++;
+ for (int i = 0; i < ids.length; i++) {
+ int id = ids[i];
+ idsToIndex.put(id, i);
+ ItemData data = itemIdToData.get(id, ItemData.EMPTY);
+ durationsUs[i] = data.durationUs;
+ defaultPositionsUs[i] = data.defaultPositionUs;
}
}
@@ -108,7 +145,7 @@ import java.util.Map;
}
@Override
- public Object getUidOfPeriod(int periodIndex) {
+ public Integer getUidOfPeriod(int periodIndex) {
return ids[periodIndex];
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java
index 412bfb476d..40c93a115a 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java
@@ -15,53 +15,84 @@
*/
package com.google.android.exoplayer2.ext.cast;
-import com.google.android.gms.cast.MediaInfo;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
-import java.util.HashMap;
+import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import java.util.HashSet;
-import java.util.List;
/**
- * Creates {@link CastTimeline}s from cast receiver app media status.
+ * Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
*
* This class keeps track of the duration reported by the current item to fill any missing
* durations in the media queue items [See internal: b/65152553].
*/
/* package */ final class CastTimelineTracker {
- private final HashMap contentIdToDurationUsMap;
- private final HashSet scratchContentIdSet;
+ private final SparseArray itemIdToData;
public CastTimelineTracker() {
- contentIdToDurationUsMap = new HashMap<>();
- scratchContentIdSet = new HashSet<>();
+ itemIdToData = new SparseArray<>();
}
/**
- * Returns a {@link CastTimeline} that represent the given {@code status}.
+ * Returns a {@link CastTimeline} that represents the state of the given {@code
+ * remoteMediaClient}.
*
- * @param status The Cast media status.
- * @return A {@link CastTimeline} that represent the given {@code status}.
+ * Returned timelines may contain values obtained from {@code remoteMediaClient} in previous
+ * invocations of this method.
+ *
+ * @param remoteMediaClient The Cast media client.
+ * @return A {@link CastTimeline} that represents the given {@code remoteMediaClient} status.
*/
- public CastTimeline getCastTimeline(MediaStatus status) {
- MediaInfo mediaInfo = status.getMediaInfo();
- List items = status.getQueueItems();
- removeUnusedDurationEntries(items);
-
- if (mediaInfo != null) {
- String contentId = mediaInfo.getContentId();
- long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
- contentIdToDurationUsMap.put(contentId, durationUs);
+ public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {
+ int[] itemIds = remoteMediaClient.getMediaQueue().getItemIds();
+ if (itemIds.length > 0) {
+ // Only remove unused items when there is something in the queue to avoid removing all entries
+ // if the remote media client clears the queue temporarily. See [Internal ref: b/128825216].
+ removeUnusedItemDataEntries(itemIds);
}
- return new CastTimeline(items, contentIdToDurationUsMap);
+
+ // TODO: Reset state when the app instance changes [Internal ref: b/129672468].
+ MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
+ if (mediaStatus == null) {
+ return CastTimeline.EMPTY_CAST_TIMELINE;
+ }
+
+ int currentItemId = mediaStatus.getCurrentItemId();
+ long durationUs = CastUtils.getStreamDurationUs(mediaStatus.getMediaInfo());
+ itemIdToData.put(
+ currentItemId,
+ itemIdToData
+ .get(currentItemId, CastTimeline.ItemData.EMPTY)
+ .copyWithDurationUs(durationUs));
+
+ for (MediaQueueItem item : mediaStatus.getQueueItems()) {
+ int itemId = item.getItemId();
+ itemIdToData.put(
+ itemId,
+ itemIdToData
+ .get(itemId, CastTimeline.ItemData.EMPTY)
+ .copyWithDefaultPositionUs((long) (item.getStartTime() * C.MICROS_PER_SECOND)));
+ }
+
+ return new CastTimeline(itemIds, itemIdToData);
}
- private void removeUnusedDurationEntries(List items) {
- scratchContentIdSet.clear();
- for (MediaQueueItem item : items) {
- scratchContentIdSet.add(item.getMedia().getContentId());
+ private void removeUnusedItemDataEntries(int[] itemIds) {
+ HashSet scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2);
+ for (int id : itemIds) {
+ scratchItemIds.add(id);
+ }
+
+ int index = 0;
+ while (index < itemIdToData.size()) {
+ if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
+ itemIdToData.removeAt(index);
+ } else {
+ index++;
+ }
}
- contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
}
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
index 997857f6b5..d1660c3306 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
@@ -31,11 +31,13 @@ import com.google.android.gms.cast.MediaTrack;
* unknown or not applicable.
*
* @param mediaInfo The media info to get the duration from.
- * @return The duration in microseconds.
+ * @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable.
*/
public static long getStreamDurationUs(MediaInfo mediaInfo) {
- long durationMs =
- mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION;
+ if (mediaInfo == null) {
+ return C.TIME_UNSET;
+ }
+ long durationMs = mediaInfo.getStreamDuration();
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
}
@@ -109,6 +111,7 @@ import com.google.android.gms.cast.MediaTrack;
/* codecs= */ null,
/* bitrate= */ Format.NO_VALUE,
/* selectionFlags= */ 0,
+ /* roleFlags= */ 0,
mediaTrack.getLanguage());
}
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..adb8e59070
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java
@@ -0,0 +1,368 @@
+/*
+ * 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 androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+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;
+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.parse(uri)))}. */
+ public Builder setMedia(String uri) {
+ return setMedia(new UriBundle(Uri.parse(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(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;
+ }
+ }
+
+ /**
+ * 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;
+
+ /**
+ * 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 licenseServer;
+
+ /**
+ * Creates an instance.
+ *
+ * @param uuid See {@link #uuid}.
+ * @param licenseServer See {@link #licenseServer}.
+ */
+ public DrmScheme(UUID uuid, @Nullable UriBundle licenseServer) {
+ this.uuid = uuid;
+ this.licenseServer = licenseServer;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+
+ DrmScheme drmScheme = (DrmScheme) other;
+ return uuid.equals(drmScheme.uuid) && Util.areEqual(licenseServer, drmScheme.licenseServer);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = uuid.hashCode();
+ result = 31 * result + (licenseServer != null ? licenseServer.hashCode() : 0);
+ return result;
+ }
+ }
+
+ /**
+ * 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.
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ MediaItem mediaItem = (MediaItem) other;
+ return startPositionUs == mediaItem.startPositionUs
+ && endPositionUs == mediaItem.endPositionUs
+ && uuid.equals(mediaItem.uuid)
+ && title.equals(mediaItem.title)
+ && description.equals(mediaItem.description)
+ && media.equals(mediaItem.media)
+ && Util.areEqual(attachment, mediaItem.attachment)
+ && drmSchemes.equals(mediaItem.drmSchemes)
+ && mimeType.equals(mediaItem.mimeType);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = uuid.hashCode();
+ result = 31 * result + title.hashCode();
+ result = 31 * result + description.hashCode();
+ result = 31 * result + media.hashCode();
+ result = 31 * result + (attachment != null ? attachment.hashCode() : 0);
+ result = 31 * result + drmSchemes.hashCode();
+ result = 31 * result + (int) (startPositionUs ^ (startPositionUs >>> 32));
+ result = 31 * result + (int) (endPositionUs ^ (endPositionUs >>> 32));
+ result = 31 * result + mimeType.hashCode();
+ return result;
+ }
+
+ 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/SessionAvailabilityListener.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/SessionAvailabilityListener.java
new file mode 100644
index 0000000000..c686c496c6
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/SessionAvailabilityListener.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+/** 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();
+}
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java
index 4c60e7c0b3..69b25e4456 100644
--- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java
@@ -15,23 +15,23 @@
*/
package com.google.android.exoplayer2.ext.cast;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
-import java.util.ArrayList;
+import com.google.android.gms.cast.framework.media.MediaQueue;
+import com.google.android.gms.cast.framework.media.RemoteMediaClient;
+import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
-import org.robolectric.RobolectricTestRunner;
/** Tests for {@link CastTimelineTracker}. */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
public class CastTimelineTrackerTest {
- private static final long DURATION_1_MS = 1000;
private static final long DURATION_2_MS = 2000;
private static final long DURATION_3_MS = 3000;
private static final long DURATION_4_MS = 4000;
@@ -39,91 +39,89 @@ public class CastTimelineTrackerTest {
/** Tests that duration of the current media info is correctly propagated to the timeline. */
@Test
- public void testGetCastTimeline() {
- MediaInfo mediaInfo;
- MediaStatus status =
- mockMediaStatus(
- new int[] {1, 2, 3},
- new String[] {"contentId1", "contentId2", "contentId3"},
- new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
-
+ public void testGetCastTimelinePersistsDuration() {
CastTimelineTracker tracker = new CastTimelineTracker();
- mediaInfo = getMediaInfo("contentId1", DURATION_1_MS);
- Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
- TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
- mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
- Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
+ RemoteMediaClient remoteMediaClient =
+ mockRemoteMediaClient(
+ /* itemIds= */ new int[] {1, 2, 3, 4, 5},
+ /* currentItemId= */ 2,
+ /* currentDurationMs= */ DURATION_2_MS);
TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(status),
- C.msToUs(DURATION_1_MS),
+ tracker.getCastTimeline(remoteMediaClient),
C.TIME_UNSET,
- C.msToUs(DURATION_3_MS));
+ C.msToUs(DURATION_2_MS),
+ C.TIME_UNSET,
+ C.TIME_UNSET,
+ C.TIME_UNSET);
- mediaInfo = getMediaInfo("contentId2", DURATION_2_MS);
- Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
+ remoteMediaClient =
+ mockRemoteMediaClient(
+ /* itemIds= */ new int[] {1, 2, 3},
+ /* currentItemId= */ 3,
+ /* currentDurationMs= */ DURATION_3_MS);
TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(status),
- C.msToUs(DURATION_1_MS),
+ tracker.getCastTimeline(remoteMediaClient),
+ C.TIME_UNSET,
C.msToUs(DURATION_2_MS),
C.msToUs(DURATION_3_MS));
- MediaStatus newStatus =
- mockMediaStatus(
- new int[] {4, 1, 5, 3},
- new String[] {"contentId4", "contentId1", "contentId5", "contentId3"},
- new long[] {
- MediaInfo.UNKNOWN_DURATION,
- MediaInfo.UNKNOWN_DURATION,
- DURATION_5_MS,
- MediaInfo.UNKNOWN_DURATION
- });
- mediaInfo = getMediaInfo("contentId5", DURATION_5_MS);
- Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
+ remoteMediaClient =
+ mockRemoteMediaClient(
+ /* itemIds= */ new int[] {1, 3},
+ /* currentItemId= */ 3,
+ /* currentDurationMs= */ DURATION_3_MS);
TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(newStatus),
- C.TIME_UNSET,
- C.msToUs(DURATION_1_MS),
- C.msToUs(DURATION_5_MS),
- C.msToUs(DURATION_3_MS));
+ tracker.getCastTimeline(remoteMediaClient), C.TIME_UNSET, C.msToUs(DURATION_3_MS));
- mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
- Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
+ remoteMediaClient =
+ mockRemoteMediaClient(
+ /* itemIds= */ new int[] {1, 2, 3, 4, 5},
+ /* currentItemId= */ 4,
+ /* currentDurationMs= */ DURATION_4_MS);
TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(newStatus),
+ tracker.getCastTimeline(remoteMediaClient),
C.TIME_UNSET,
- C.msToUs(DURATION_1_MS),
- C.msToUs(DURATION_5_MS),
- C.msToUs(DURATION_3_MS));
-
- mediaInfo = getMediaInfo("contentId4", DURATION_4_MS);
- Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
- TimelineAsserts.assertPeriodDurations(
- tracker.getCastTimeline(newStatus),
+ C.TIME_UNSET,
+ C.msToUs(DURATION_3_MS),
C.msToUs(DURATION_4_MS),
- C.msToUs(DURATION_1_MS),
- C.msToUs(DURATION_5_MS),
- C.msToUs(DURATION_3_MS));
+ C.TIME_UNSET);
+
+ remoteMediaClient =
+ mockRemoteMediaClient(
+ /* itemIds= */ new int[] {1, 2, 3, 4, 5},
+ /* currentItemId= */ 5,
+ /* currentDurationMs= */ DURATION_5_MS);
+ TimelineAsserts.assertPeriodDurations(
+ tracker.getCastTimeline(remoteMediaClient),
+ C.TIME_UNSET,
+ C.TIME_UNSET,
+ C.msToUs(DURATION_3_MS),
+ C.msToUs(DURATION_4_MS),
+ C.msToUs(DURATION_5_MS));
}
- private static MediaStatus mockMediaStatus(
- int[] itemIds, String[] contentIds, long[] durationsMs) {
- ArrayList items = new ArrayList<>();
- for (int i = 0; i < contentIds.length; i++) {
- MediaInfo mediaInfo = getMediaInfo(contentIds[i], durationsMs[i]);
- MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
- Mockito.when(item.getMedia()).thenReturn(mediaInfo);
- Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
- items.add(item);
- }
+ private static RemoteMediaClient mockRemoteMediaClient(
+ int[] itemIds, int currentItemId, long currentDurationMs) {
+ RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
MediaStatus status = Mockito.mock(MediaStatus.class);
- Mockito.when(status.getQueueItems()).thenReturn(items);
- return status;
+ Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList());
+ Mockito.when(remoteMediaClient.getMediaStatus()).thenReturn(status);
+ Mockito.when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
+ Mockito.when(status.getCurrentItemId()).thenReturn(currentItemId);
+ MediaQueue mediaQueue = mockMediaQueue(itemIds);
+ Mockito.when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
+ return remoteMediaClient;
}
- private static MediaInfo getMediaInfo(String contentId, long durationMs) {
- return new MediaInfo.Builder(contentId)
+ private static MediaQueue mockMediaQueue(int[] itemIds) {
+ MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
+ Mockito.when(mediaQueue.getItemIds()).thenReturn(itemIds);
+ return mediaQueue;
+ }
+
+ private static MediaInfo getMediaInfo(long durationMs) {
+ return new MediaInfo.Builder(/*contentId= */ "")
.setStreamDuration(durationMs)
.setContentType(MimeTypes.APPLICATION_MP4)
.setStreamType(MediaInfo.STREAM_TYPE_NONE)
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..9cdc073b06
--- /dev/null
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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 androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test for {@link MediaItem}. */
+@RunWith(AndroidJUnit4.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
+ .setUuid(new UUID(0, 1))
+ .setMedia("http://example.com")
+ .setTitle("title")
+ .setMimeType(MimeTypes.AUDIO_MP4)
+ .setStartPositionUs(3)
+ .setEndPositionUs(4)
+ .build();
+ MediaItem item2 = builder.setUuid(new UUID(0, 1)).build();
+ assertThat(item1).isEqualTo(item2);
+ }
+
+ @Test
+ public void buildMediaItem_assertDefaultValues() {
+ assertDefaultValues(new MediaItem.Builder().build());
+ }
+
+ @Test
+ public void buildAndClear_assertDefaultValues() {
+ 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());
+ }
+
+ @Test
+ public void equals_withEqualDrmSchemes_returnsTrue() {
+ MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem mediaItem1 =
+ builder
+ .setUuid(new UUID(0, 1))
+ .setMedia("www.google.com")
+ .setDrmSchemes(createDummyDrmSchemes(1))
+ .buildAndClear();
+ MediaItem mediaItem2 =
+ builder
+ .setUuid(new UUID(0, 1))
+ .setMedia("www.google.com")
+ .setDrmSchemes(createDummyDrmSchemes(1))
+ .buildAndClear();
+ assertThat(mediaItem1).isEqualTo(mediaItem2);
+ }
+
+ @Test
+ public void equals_withDifferentDrmRequestHeaders_returnsFalse() {
+ MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem mediaItem1 =
+ builder
+ .setUuid(new UUID(0, 1))
+ .setMedia("www.google.com")
+ .setDrmSchemes(createDummyDrmSchemes(1))
+ .buildAndClear();
+ MediaItem mediaItem2 =
+ builder
+ .setUuid(new UUID(0, 1))
+ .setMedia("www.google.com")
+ .setDrmSchemes(createDummyDrmSchemes(2))
+ .buildAndClear();
+ assertThat(mediaItem1).isNotEqualTo(mediaItem2);
+ }
+
+ 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();
+ }
+
+ private static List createDummyDrmSchemes(int seed) {
+ HashMap requestHeaders1 = new HashMap<>();
+ requestHeaders1.put("key1", "value1");
+ requestHeaders1.put("key2", "value1");
+ MediaItem.UriBundle uriBundle1 =
+ new MediaItem.UriBundle(Uri.parse("www.uri1.com"), requestHeaders1);
+ MediaItem.DrmScheme drmScheme1 = new MediaItem.DrmScheme(C.WIDEVINE_UUID, uriBundle1);
+ HashMap requestHeaders2 = new HashMap<>();
+ requestHeaders2.put("key3", "value3");
+ requestHeaders2.put("key4", "valueWithSeed" + seed);
+ MediaItem.UriBundle uriBundle2 =
+ new MediaItem.UriBundle(Uri.parse("www.uri2.com"), requestHeaders2);
+ MediaItem.DrmScheme drmScheme2 = new MediaItem.DrmScheme(C.PLAYREADY_UUID, uriBundle2);
+ return Arrays.asList(drmScheme1, drmScheme2);
+ }
+}
diff --git a/extensions/cast/src/test/resources/robolectric.properties b/extensions/cast/src/test/resources/robolectric.properties
deleted file mode 100644
index 2f3210368e..0000000000
--- a/extensions/cast/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-manifest=src/test/AndroidManifest.xml
diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md
index f1f6d68c81..dc64b862b6 100644
--- a/extensions/cronet/README.md
+++ b/extensions/cronet/README.md
@@ -2,7 +2,7 @@
The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
-[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
+[HttpDataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
## Getting the extension ##
@@ -52,4 +52,4 @@ respectively.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle
index 520edfe1d1..76972a3530 100644
--- a/extensions/cronet/build.gradle
+++ b/extensions/cronet/build.gradle
@@ -16,10 +16,9 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
- minSdkVersion 16
+ minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
@@ -27,12 +26,14 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
- api 'org.chromium.net:cronet-embedded:71.3578.98'
+ api 'org.chromium.net:cronet-embedded:73.3683.76'
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'androidx.annotation:annotation:1.0.2'
testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils-robolectric')
}
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
index ab10f41d8f..a9995af0e4 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
@@ -16,10 +16,11 @@
package com.google.android.exoplayer2.ext.cronet;
import android.net.Uri;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
@@ -493,6 +494,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
throw new IOException("HTTP request with non-empty body must set Content-Type");
}
+ if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
+ requestBuilder.addHeader(
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
+ }
// Set the Range header.
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
StringBuilder rangeValue = new StringBuilder();
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
index d832e4625d..93edb4e893 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2.ext.cronet;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
index 829b53f863..270c1f6323 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
@@ -16,7 +16,7 @@
package com.google.android.exoplayer2.ext.cronet;
import android.content.Context;
-import android.support.annotation.IntDef;
+import androidx.annotation.IntDef;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java
index 117518a1eb..244ba9083b 100644
--- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java
+++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java
@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@@ -28,10 +29,9 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
/** Tests for {@link ByteArrayUploadDataProvider}. */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
public final class ByteArrayUploadDataProviderTest {
private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
index 7d47b0da64..7c4c03dd87 100644
--- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
+++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
@@ -31,6 +31,7 @@ import static org.mockito.Mockito.when;
import android.net.Uri;
import android.os.ConditionVariable;
import android.os.SystemClock;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
@@ -62,10 +63,9 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
/** Tests for {@link CronetDataSource}. */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
public final class CronetDataSourceTest {
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
diff --git a/extensions/cronet/src/test/resources/robolectric.properties b/extensions/cronet/src/test/resources/robolectric.properties
deleted file mode 100644
index 2f3210368e..0000000000
--- a/extensions/cronet/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-manifest=src/test/AndroidManifest.xml
diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md
index 52dacf8166..5b68f1e352 100644
--- a/extensions/ffmpeg/README.md
+++ b/extensions/ffmpeg/README.md
@@ -147,11 +147,11 @@ then implement your own logic to use the renderer for a given track.
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
[#2781]: https://github.com/google/ExoPlayer/issues/2781
-[Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension
+[Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension
## Links ##
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle
index 1630b6f775..ffecdcd16f 100644
--- a/extensions/ffmpeg/build.gradle
+++ b/extensions/ffmpeg/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -33,12 +32,15 @@ android {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'androidx.annotation:annotation:1.0.2'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ testImplementation project(modulePrefix + 'testutils-robolectric')
}
ext {
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
index f0b30baa8a..c5d80aa32b 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
@@ -16,7 +16,7 @@
package com.google.android.exoplayer2.ext.ffmpeg;
import android.os.Handler;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
index c5b76002fa..7c5864420a 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2.ext.ffmpeg;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
index e5018a49b3..58109c1666 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
@@ -15,10 +15,11 @@
*/
package com.google.android.exoplayer2.ext.ffmpeg;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
/**
@@ -30,6 +31,8 @@ public final class FfmpegLibrary {
ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg");
}
+ private static final String TAG = "FfmpegLibrary";
+
private static final LibraryLoader LOADER =
new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
@@ -69,7 +72,14 @@ public final class FfmpegLibrary {
return false;
}
String codecName = getCodecName(mimeType, encoding);
- return codecName != null && ffmpegHasDecoder(codecName);
+ if (codecName == null) {
+ return false;
+ }
+ if (!ffmpegHasDecoder(codecName)) {
+ Log.w(TAG, "No " + codecName + " decoder available. Check the FFmpeg build configuration.");
+ return false;
+ }
+ return true;
}
/**
diff --git a/extensions/ffmpeg/src/test/AndroidManifest.xml b/extensions/ffmpeg/src/test/AndroidManifest.xml
new file mode 100644
index 0000000000..d53bca4ca2
--- /dev/null
+++ b/extensions/ffmpeg/src/test/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/extensions/ffmpeg/src/test/java/com/google/android/exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.java b/extensions/ffmpeg/src/test/java/com/google/android/exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.java
new file mode 100644
index 0000000000..a52d1b1d7a
--- /dev/null
+++ b/extensions/ffmpeg/src/test/java/com/google/android/exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2019 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.ffmpeg;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}. */
+@RunWith(AndroidJUnit4.class)
+public final class DefaultRenderersFactoryTest {
+
+ @Test
+ public void createRenderers_instantiatesVpxRenderer() {
+ DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
+ FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);
+ }
+}
diff --git a/extensions/flac/README.md b/extensions/flac/README.md
index 54701eea1d..78035f4d87 100644
--- a/extensions/flac/README.md
+++ b/extensions/flac/README.md
@@ -95,4 +95,4 @@ player, then implement your own logic to use the renderer for a given track.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle
index e5261902c6..06a5888404 100644
--- a/extensions/flac/build.gradle
+++ b/extensions/flac/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -34,13 +33,15 @@ android {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core')
- androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
+ implementation 'androidx.annotation:annotation:1.0.2'
androidTestImplementation project(modulePrefix + 'testutils')
+ androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
}
diff --git a/extensions/flac/src/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml
index cfc90117ac..39b92aa217 100644
--- a/extensions/flac/src/androidTest/AndroidManifest.xml
+++ b/extensions/flac/src/androidTest/AndroidManifest.xml
@@ -18,6 +18,9 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.flac.test">
+
+
+
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
index f8e61a0609..435279fc45 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
@@ -16,22 +16,26 @@
package com.google.android.exoplayer2.ext.flac;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
-import android.test.InstrumentationTestCase;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
+import org.junit.Before;
+import org.junit.runner.RunWith;
/** Unit test for {@link FlacBinarySearchSeeker}. */
-public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
+@RunWith(AndroidJUnit4.class)
+public final class FlacBinarySearchSeekerTest {
private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac";
private static final int DURATION_US = 2_741_000;
- @Override
- protected void setUp() throws Exception {
- super.setUp();
+ @Before
+ public void setUp() {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
@@ -39,7 +43,8 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
throws IOException, FlacDecoderException, InterruptedException {
- byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
+ byte[] data =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
@@ -57,7 +62,8 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
public void testSetSeekTargetUs_returnsSeekPending()
throws IOException, FlacDecoderException, InterruptedException {
- byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
+ byte[] data =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java
index 58ab260277..6008d99448 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java
@@ -16,11 +16,13 @@
package com.google.android.exoplayer2.ext.flac;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
-import android.support.annotation.Nullable;
-import android.test.InstrumentationTestCase;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
@@ -38,9 +40,12 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.List;
import java.util.Random;
+import org.junit.Before;
+import org.junit.runner.RunWith;
/** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */
-public final class FlacExtractorSeekTest extends InstrumentationTestCase {
+@RunWith(AndroidJUnit4.class)
+public final class FlacExtractorSeekTest {
private static final String NO_SEEKTABLE_FLAC = "bear_no_seek.flac";
private static final int DURATION_US = 2_741_000;
@@ -54,18 +59,18 @@ public final class FlacExtractorSeekTest extends InstrumentationTestCase {
private PositionHolder positionHolder;
private long totalInputLength;
- @Override
- protected void setUp() throws Exception {
- super.setUp();
+ @Before
+ public void setUp() throws Exception {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
expectedOutput = new FakeExtractorOutput();
- extractAllSamplesFromFileToExpectedOutput(getInstrumentation().getContext(), NO_SEEKTABLE_FLAC);
+ extractAllSamplesFromFileToExpectedOutput(
+ ApplicationProvider.getApplicationContext(), NO_SEEKTABLE_FLAC);
expectedTrackOutput = expectedOutput.trackOutputs.get(0);
dataSource =
- new DefaultDataSourceFactory(getInstrumentation().getContext(), "UserAgent")
+ new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent")
.createDataSource();
totalInputLength = readInputLength();
positionHolder = new PositionHolder();
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
index 29a597daa4..d9cbac6ad5 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
@@ -15,17 +15,20 @@
*/
package com.google.android.exoplayer2.ext.flac;
-import android.test.InstrumentationTestCase;
+import static org.junit.Assert.fail;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import org.junit.Before;
+import org.junit.runner.RunWith;
-/**
- * Unit test for {@link FlacExtractor}.
- */
-public class FlacExtractorTest extends InstrumentationTestCase {
+/** Unit test for {@link FlacExtractor}. */
+@RunWith(AndroidJUnit4.class)
+public class FlacExtractorTest {
- @Override
- protected void setUp() throws Exception {
- super.setUp();
+ @Before
+ public void setUp() throws Exception {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
@@ -33,11 +36,11 @@ public class FlacExtractorTest extends InstrumentationTestCase {
public void testExtractFlacSample() throws Exception {
ExtractorAsserts.assertBehavior(
- FlacExtractor::new, "bear.flac", getInstrumentation().getContext());
+ FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext());
}
public void testExtractFlacSampleWithId3Header() throws Exception {
ExtractorAsserts.assertBehavior(
- FlacExtractor::new, "bear_with_id3.flac", getInstrumentation().getContext());
+ FlacExtractor::new, "bear_with_id3.flac", ApplicationProvider.getApplicationContext());
}
}
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
index 99ddba55c4..1cd9483178 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
@@ -15,21 +15,21 @@
*/
package com.google.android.exoplayer2.ext.flac;
-import static androidx.test.InstrumentationRegistry.getContext;
import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
-import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
@@ -56,7 +56,7 @@ public class FlacPlaybackTest {
private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable =
- new TestPlaybackRunnable(Uri.parse(uri), getContext());
+ new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
thread.join();
@@ -83,12 +83,12 @@ public class FlacPlaybackTest {
Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
- player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
+ player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
MediaSource mediaSource =
- new ExtractorMediaSource.Factory(
- new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
- .setExtractorsFactory(MatroskaExtractor.FACTORY)
+ new ProgressiveMediaSource.Factory(
+ new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
+ MatroskaExtractor.FACTORY)
.createMediaSource(uri);
player.prepare(mediaSource);
player.setPlayWhenReady(true);
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
index 8f5dcef16b..bb72e114fe 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
@@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.flac;
import static com.google.android.exoplayer2.util.Util.getPcmEncoding;
-import android.support.annotation.IntDef;
-import android.support.annotation.Nullable;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
@@ -94,7 +94,7 @@ public final class FlacExtractor implements Extractor {
/** Constructs an instance with flags = 0. */
public FlacExtractor() {
- this(0);
+ this(/* flags= */ 0);
}
/**
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
index 424fcbb285..ac7646cc4b 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
@@ -42,7 +42,9 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
- public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
+ public LibflacAudioRenderer(
+ Handler eventHandler,
+ AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java
index 79c4452928..611197bbe5 100644
--- a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java
+++ b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.flac;
import static com.google.common.truth.Truth.assertThat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
@@ -27,6 +28,7 @@ import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
+import com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.PsExtractor;
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
@@ -35,10 +37,9 @@ import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
/** Unit test for {@link DefaultExtractorsFactory}. */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
public final class DefaultExtractorsFactoryTest {
@Test
@@ -59,6 +60,7 @@ public final class DefaultExtractorsFactoryTest {
Mp3Extractor.class,
AdtsExtractor.class,
Ac3Extractor.class,
+ Ac4Extractor.class,
TsExtractor.class,
FlvExtractor.class,
OggExtractor.class,
diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultRenderersFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultRenderersFactoryTest.java
new file mode 100644
index 0000000000..fb20ff1114
--- /dev/null
+++ b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultRenderersFactoryTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2019 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.flac;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link DefaultRenderersFactoryTest} with {@link LibflacAudioRenderer}. */
+@RunWith(AndroidJUnit4.class)
+public final class DefaultRenderersFactoryTest {
+
+ @Test
+ public void createRenderers_instantiatesVpxRenderer() {
+ DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
+ LibflacAudioRenderer.class, C.TRACK_TYPE_AUDIO);
+ }
+}
diff --git a/extensions/flac/src/test/resources/robolectric.properties b/extensions/flac/src/test/resources/robolectric.properties
deleted file mode 100644
index 2f3210368e..0000000000
--- a/extensions/flac/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-manifest=src/test/AndroidManifest.xml
diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md
index 5dab885436..1874ff77d7 100644
--- a/extensions/gvr/README.md
+++ b/extensions/gvr/README.md
@@ -37,4 +37,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.gvr.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle
index 234f551896..50acd6c040 100644
--- a/extensions/gvr/build.gradle
+++ b/extensions/gvr/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -27,11 +26,14 @@ android {
minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation project(modulePrefix + 'library-ui')
+ implementation 'androidx.annotation:annotation:1.0.2'
api 'com.google.vr:sdk-base:1.190.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}
diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
index eca31c98e4..02e4328ec7 100644
--- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
+++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2.ext.gvr;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
@@ -38,9 +38,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
private static final int FRAMES_PER_OUTPUT_BUFFER = 1024;
private static final int OUTPUT_CHANNEL_COUNT = 2;
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.
+ private static final int NO_SURROUND_FORMAT = GvrAudioSurround.SurroundFormat.INVALID;
private int sampleRateHz;
private int channelCount;
+ private int pendingGvrAudioSurroundFormat;
@Nullable private GvrAudioSurround gvrAudioSurround;
private ByteBuffer buffer;
private boolean inputEnded;
@@ -57,6 +59,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER;
+ pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
}
/**
@@ -92,33 +95,28 @@ public final class GvrAudioProcessor implements AudioProcessor {
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
- maybeReleaseGvrAudioSurround();
- int surroundFormat;
switch (channelCount) {
case 1:
- surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
break;
case 2:
- surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
break;
case 4:
- surroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
break;
case 6:
- surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
break;
case 9:
- surroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
break;
case 16:
- surroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
+ pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
break;
default:
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
- gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
- FRAMES_PER_OUTPUT_BUFFER);
- gvrAudioSurround.updateNativeOrientation(w, x, y, z);
if (buffer == EMPTY_BUFFER) {
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
.order(ByteOrder.nativeOrder());
@@ -128,7 +126,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public boolean isActive() {
- return gvrAudioSurround != null;
+ return pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT || gvrAudioSurround != null;
}
@Override
@@ -156,14 +154,17 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public void queueEndOfStream() {
- Assertions.checkNotNull(gvrAudioSurround);
+ if (gvrAudioSurround != null) {
+ gvrAudioSurround.triggerProcessing();
+ }
inputEnded = true;
- gvrAudioSurround.triggerProcessing();
}
@Override
public ByteBuffer getOutput() {
- Assertions.checkNotNull(gvrAudioSurround);
+ if (gvrAudioSurround == null) {
+ return EMPTY_BUFFER;
+ }
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
buffer.position(0).limit(writtenBytes);
return buffer;
@@ -171,13 +172,20 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public boolean isEnded() {
- Assertions.checkNotNull(gvrAudioSurround);
- return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
+ return inputEnded
+ && (gvrAudioSurround == null || gvrAudioSurround.getAvailableOutputSize() == 0);
}
@Override
public void flush() {
- if (gvrAudioSurround != null) {
+ if (pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT) {
+ maybeReleaseGvrAudioSurround();
+ gvrAudioSurround =
+ new GvrAudioSurround(
+ pendingGvrAudioSurroundFormat, sampleRateHz, channelCount, FRAMES_PER_OUTPUT_BUFFER);
+ gvrAudioSurround.updateNativeOrientation(w, x, y, z);
+ pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
+ } else if (gvrAudioSurround != null) {
gvrAudioSurround.flush();
}
inputEnded = false;
@@ -191,13 +199,13 @@ public final class GvrAudioProcessor implements AudioProcessor {
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER;
+ pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
}
private void maybeReleaseGvrAudioSurround() {
- if (this.gvrAudioSurround != null) {
- GvrAudioSurround gvrAudioSurround = this.gvrAudioSurround;
- this.gvrAudioSurround = null;
+ if (gvrAudioSurround != null) {
gvrAudioSurround.release();
+ gvrAudioSurround = null;
}
}
diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java
new file mode 100644
index 0000000000..2c912c17f2
--- /dev/null
+++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java
@@ -0,0 +1,355 @@
+/*
+ * 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.gvr;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.SurfaceTexture;
+import android.opengl.Matrix;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import androidx.annotation.BinderThread;
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import android.view.ContextThemeWrapper;
+import android.view.MotionEvent;
+import android.view.Surface;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.ui.PlayerControlView;
+import com.google.android.exoplayer2.ui.spherical.GlViewGroup;
+import com.google.android.exoplayer2.ui.spherical.PointerRenderer;
+import com.google.android.exoplayer2.ui.spherical.SceneRenderer;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import com.google.vr.ndk.base.DaydreamApi;
+import com.google.vr.sdk.base.AndroidCompat;
+import com.google.vr.sdk.base.Eye;
+import com.google.vr.sdk.base.GvrActivity;
+import com.google.vr.sdk.base.GvrView;
+import com.google.vr.sdk.base.HeadTransform;
+import com.google.vr.sdk.base.Viewport;
+import com.google.vr.sdk.controller.Controller;
+import com.google.vr.sdk.controller.ControllerManager;
+import javax.microedition.khronos.egl.EGLConfig;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/** Base activity for VR 360 video playback. */
+public abstract class GvrPlayerActivity extends GvrActivity {
+
+ private static final int EXIT_FROM_VR_REQUEST_CODE = 42;
+
+ private final Handler mainHandler;
+
+ @Nullable private Player player;
+ @MonotonicNonNull private GlViewGroup glView;
+ @MonotonicNonNull private ControllerManager controllerManager;
+ @MonotonicNonNull private SurfaceTexture surfaceTexture;
+ @MonotonicNonNull private Surface surface;
+ @MonotonicNonNull private SceneRenderer scene;
+ @MonotonicNonNull private PlayerControlView playerControl;
+
+ public GvrPlayerActivity() {
+ mainHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setScreenAlwaysOn(true);
+
+ GvrView gvrView = new GvrView(this);
+ // Since videos typically have fewer pixels per degree than the phones, reducing the render
+ // target scaling factor reduces the work required to render the scene.
+ gvrView.setRenderTargetScale(.5f);
+
+ // If a custom theme isn't specified, the Context's theme is used. For VR Activities, this is
+ // the old Android default theme rather than a modern theme. Override this with a custom theme.
+ Context theme = new ContextThemeWrapper(this, R.style.VrTheme);
+ glView = new GlViewGroup(theme, R.layout.vr_ui);
+
+ playerControl = Assertions.checkNotNull(glView.findViewById(R.id.controller));
+ playerControl.setShowVrButton(true);
+ playerControl.setVrButtonListener(v -> exit());
+
+ PointerRenderer pointerRenderer = new PointerRenderer();
+ scene = new SceneRenderer();
+ Renderer renderer = new Renderer(scene, glView, pointerRenderer);
+
+ // Attach glView to gvrView in order to properly handle UI events.
+ gvrView.addView(glView, 0);
+
+ // Standard GvrView configuration
+ gvrView.setEGLConfigChooser(
+ 8, 8, 8, 8, // RGBA bits.
+ 16, // Depth bits.
+ 0); // Stencil bits.
+ gvrView.setRenderer(renderer);
+ setContentView(gvrView);
+
+ // Most Daydream phones can render a 4k video at 60fps in sustained performance mode. These
+ // options can be tweaked along with the render target scale.
+ if (gvrView.setAsyncReprojectionEnabled(true)) {
+ AndroidCompat.setSustainedPerformanceMode(this, true);
+ }
+
+ // Handle the user clicking on the 'X' in the top left corner. Since this is done when the user
+ // has taken the headset out of VR, it should launch the app's exit flow directly rather than
+ // using the transition flow.
+ gvrView.setOnCloseButtonListener(this::finish);
+
+ ControllerManager.EventListener listener =
+ new ControllerManager.EventListener() {
+ @Override
+ public void onApiStatusChanged(int status) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onRecentered() {
+ // TODO if in cardboard mode call gvrView.recenterHeadTracker();
+ glView.post(() -> Util.castNonNull(playerControl).show());
+ }
+ };
+ controllerManager = new ControllerManager(this, listener);
+
+ Controller controller = controllerManager.getController();
+ ControllerEventListener controllerEventListener =
+ new ControllerEventListener(controller, pointerRenderer, glView);
+ controller.setEventListener(controllerEventListener);
+ }
+
+ /**
+ * Sets the {@link Player} to use.
+ *
+ * @param newPlayer The {@link Player} to use, or {@code null} to detach the current player.
+ */
+ protected void setPlayer(@Nullable Player newPlayer) {
+ Assertions.checkNotNull(scene);
+ if (player == newPlayer) {
+ return;
+ }
+ if (player != null) {
+ Player.VideoComponent videoComponent = player.getVideoComponent();
+ if (videoComponent != null) {
+ if (surface != null) {
+ videoComponent.clearVideoSurface(surface);
+ }
+ videoComponent.clearVideoFrameMetadataListener(scene);
+ videoComponent.clearCameraMotionListener(scene);
+ }
+ }
+ player = newPlayer;
+ if (player != null) {
+ Player.VideoComponent videoComponent = player.getVideoComponent();
+ if (videoComponent != null) {
+ videoComponent.setVideoFrameMetadataListener(scene);
+ videoComponent.setCameraMotionListener(scene);
+ videoComponent.setVideoSurface(surface);
+ }
+ }
+ Assertions.checkNotNull(playerControl).setPlayer(player);
+ }
+
+ /**
+ * Sets the default stereo mode. If the played video doesn't contain a stereo mode the default one
+ * is used.
+ *
+ * @param stereoMode A {@link C.StereoMode} value.
+ */
+ protected void setDefaultStereoMode(@C.StereoMode int stereoMode) {
+ Assertions.checkNotNull(scene).setDefaultStereoMode(stereoMode);
+ }
+
+ @CallSuper
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent unused) {
+ if (requestCode == EXIT_FROM_VR_REQUEST_CODE && resultCode == RESULT_OK) {
+ finish();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Util.castNonNull(controllerManager).start();
+ }
+
+ @Override
+ protected void onPause() {
+ Util.castNonNull(controllerManager).stop();
+ super.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ setPlayer(null);
+ releaseSurface(surfaceTexture, surface);
+ super.onDestroy();
+ }
+
+ /** Tries to exit gracefully from VR using a VR transition dialog. */
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ protected void exit() {
+ // This needs to use GVR's exit transition to avoid disorienting the user.
+ DaydreamApi api = DaydreamApi.create(this);
+ if (api != null) {
+ api.exitFromVr(this, EXIT_FROM_VR_REQUEST_CODE, null);
+ // Eventually, the Activity's onActivityResult will be called.
+ api.close();
+ } else {
+ finish();
+ }
+ }
+
+ /** Toggles PlayerControl visibility. */
+ @UiThread
+ protected void togglePlayerControlVisibility() {
+ if (Assertions.checkNotNull(playerControl).isVisible()) {
+ playerControl.hide();
+ } else {
+ playerControl.show();
+ }
+ }
+
+ // Called on GL thread.
+ private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
+ mainHandler.post(
+ () -> {
+ SurfaceTexture oldSurfaceTexture = this.surfaceTexture;
+ Surface oldSurface = this.surface;
+ this.surfaceTexture = surfaceTexture;
+ this.surface = new Surface(surfaceTexture);
+ if (player != null) {
+ Player.VideoComponent videoComponent = player.getVideoComponent();
+ if (videoComponent != null) {
+ videoComponent.setVideoSurface(surface);
+ }
+ }
+ releaseSurface(oldSurfaceTexture, oldSurface);
+ });
+ }
+
+ private static void releaseSurface(
+ @Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {
+ if (oldSurfaceTexture != null) {
+ oldSurfaceTexture.release();
+ }
+ if (oldSurface != null) {
+ oldSurface.release();
+ }
+ }
+
+ private class Renderer implements GvrView.StereoRenderer {
+ private static final float Z_NEAR = .1f;
+ private static final float Z_FAR = 100;
+
+ private final float[] viewProjectionMatrix = new float[16];
+ private final SceneRenderer scene;
+ private final GlViewGroup glView;
+ private final PointerRenderer pointerRenderer;
+
+ public Renderer(SceneRenderer scene, GlViewGroup glView, PointerRenderer pointerRenderer) {
+ this.scene = scene;
+ this.glView = glView;
+ this.pointerRenderer = pointerRenderer;
+ }
+
+ @Override
+ public void onNewFrame(HeadTransform headTransform) {}
+
+ @Override
+ public void onDrawEye(Eye eye) {
+ Matrix.multiplyMM(
+ viewProjectionMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, eye.getEyeView(), 0);
+ scene.drawFrame(viewProjectionMatrix, eye.getType() == Eye.Type.RIGHT);
+ if (glView.isVisible()) {
+ glView.getRenderer().draw(viewProjectionMatrix);
+ pointerRenderer.draw(viewProjectionMatrix);
+ }
+ }
+
+ @Override
+ public void onFinishFrame(Viewport viewport) {}
+
+ @Override
+ public void onSurfaceCreated(EGLConfig config) {
+ onSurfaceTextureAvailable(scene.init());
+ glView.getRenderer().init();
+ pointerRenderer.init();
+ }
+
+ @Override
+ public void onSurfaceChanged(int width, int height) {}
+
+ @Override
+ public void onRendererShutdown() {
+ glView.getRenderer().shutdown();
+ pointerRenderer.shutdown();
+ scene.shutdown();
+ }
+ }
+
+ private class ControllerEventListener extends Controller.EventListener {
+
+ private final Controller controller;
+ private final PointerRenderer pointerRenderer;
+ private final GlViewGroup glView;
+ private final float[] controllerOrientationMatrix;
+ private boolean clickButtonDown;
+ private boolean appButtonDown;
+
+ public ControllerEventListener(
+ Controller controller, PointerRenderer pointerRenderer, GlViewGroup glView) {
+ this.controller = controller;
+ this.pointerRenderer = pointerRenderer;
+ this.glView = glView;
+ controllerOrientationMatrix = new float[16];
+ }
+
+ @Override
+ @BinderThread
+ public void onUpdate() {
+ controller.update();
+ controller.orientation.toRotationMatrix(controllerOrientationMatrix);
+ pointerRenderer.setControllerOrientation(controllerOrientationMatrix);
+
+ if (clickButtonDown || controller.clickButtonState) {
+ int action;
+ if (clickButtonDown != controller.clickButtonState) {
+ clickButtonDown = controller.clickButtonState;
+ action = clickButtonDown ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP;
+ } else {
+ action = MotionEvent.ACTION_MOVE;
+ }
+ glView.post(
+ () -> {
+ float[] angles = controller.orientation.toYawPitchRollRadians(new float[3]);
+ boolean clickedOnView = glView.simulateClick(action, angles[0], angles[1]);
+ if (action == MotionEvent.ACTION_DOWN && !clickedOnView) {
+ togglePlayerControlVisibility();
+ }
+ });
+ } else if (!appButtonDown && controller.appButtonState) {
+ glView.post(GvrPlayerActivity.this::togglePlayerControlVisibility);
+ }
+ appButtonDown = controller.appButtonState;
+ }
+ }
+}
diff --git a/extensions/gvr/src/main/res/layout/vr_ui.xml b/extensions/gvr/src/main/res/layout/vr_ui.xml
new file mode 100644
index 0000000000..e84ee31fe6
--- /dev/null
+++ b/extensions/gvr/src/main/res/layout/vr_ui.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/demos/main/src/main/res/layout/start_download_dialog.xml b/extensions/gvr/src/main/res/values-v21/styles.xml
similarity index 77%
rename from demos/main/src/main/res/layout/start_download_dialog.xml
rename to extensions/gvr/src/main/res/values-v21/styles.xml
index acb9af5d97..276db1b42d 100644
--- a/demos/main/src/main/res/layout/start_download_dialog.xml
+++ b/extensions/gvr/src/main/res/values-v21/styles.xml
@@ -13,7 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/gvr/src/main/res/values/styles.xml b/extensions/gvr/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..ab5fde106a
--- /dev/null
+++ b/extensions/gvr/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/extensions/ima/README.md b/extensions/ima/README.md
index e13cd85590..4ed6a5428a 100644
--- a/extensions/ima/README.md
+++ b/extensions/ima/README.md
@@ -5,7 +5,7 @@ The IMA extension is an [AdsLoader][] implementation wrapping the
alongside content.
[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/
-[AdsLoader]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/ads/AdsLoader.html
+[AdsLoader]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/source/ads/AdsLoader.html
## Getting the extension ##
@@ -61,4 +61,4 @@ playback.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle
index 4d6302c898..a91bbbd981 100644
--- a/extensions/ima/build.gradle
+++ b/extensions/ima/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -28,23 +27,14 @@ android {
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
- api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.6'
+ api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
implementation project(modulePrefix + 'library-core')
- implementation 'com.google.android.gms:play-services-ads:17.1.2'
- // These dependencies are necessary to force the supportLibraryVersion of
- // com.android.support:support-v4 and com.android.support:customtabs to be
- // used. Else older versions are used, for example via:
- // com.google.android.gms:play-services-ads:17.1.2
- // |-- com.android.support:customtabs:26.1.0
- implementation 'com.android.support:support-v4:' + supportLibraryVersion
- implementation 'com.android.support:customtabs:' + supportLibraryVersion
- testImplementation 'com.google.truth:truth:' + truthVersion
- testImplementation 'junit:junit:' + junitVersion
- testImplementation 'org.mockito:mockito-core:' + mockitoVersion
- testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
testImplementation project(modulePrefix + 'testutils-robolectric')
}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
index 3aeefb5441..465ad51ac5 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
@@ -19,8 +19,9 @@ import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import android.os.SystemClock;
-import android.support.annotation.IntDef;
-import android.support.annotation.Nullable;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import android.view.View;
import android.view.ViewGroup;
import com.google.ads.interactivemedia.v3.api.Ad;
@@ -216,7 +217,7 @@ public final class ImaAdsLoader
return this;
}
- // @VisibleForTesting
+ @VisibleForTesting
/* package */ Builder setImaFactory(ImaFactory imaFactory) {
this.imaFactory = Assertions.checkNotNull(imaFactory);
return this;
@@ -755,7 +756,8 @@ public final class ImaAdsLoader
// until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered
// just after an ad group isn't incorrectly attributed to the next ad group.
int nextAdGroupIndex =
- adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs));
+ adPlaybackState.getAdGroupIndexAfterPositionUs(
+ C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) {
long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]);
if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) {
@@ -1389,7 +1391,7 @@ public final class ImaAdsLoader
}
/** Factory for objects provided by the IMA SDK. */
- // @VisibleForTesting
+ @VisibleForTesting
/* package */ interface ImaFactory {
/** @see ImaSdkSettings */
ImaSdkSettings createImaSdkSettings();
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
index dabae2de4b..1e1935c63a 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
@@ -22,10 +22,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.net.Uri;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdEvent;
@@ -54,11 +56,9 @@ import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
/** Test for {@link ImaAdsLoader}. */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
public class ImaAdsLoaderTest {
private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND;
@@ -95,8 +95,8 @@ public class ImaAdsLoaderTest {
adDisplayContainer,
fakeAdsRequest,
fakeAdsLoader);
- adViewGroup = new FrameLayout(RuntimeEnvironment.application);
- adOverlayView = new View(RuntimeEnvironment.application);
+ adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext());
+ adOverlayView = new View(ApplicationProvider.getApplicationContext());
adViewProvider =
new AdsLoader.AdViewProvider() {
@Override
@@ -237,7 +237,7 @@ public class ImaAdsLoaderTest {
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints));
imaAdsLoader =
- new ImaAdsLoader.Builder(RuntimeEnvironment.application)
+ new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setImaFactory(testImaFactory)
.setImaSdkSettings(imaSdkSettings)
.buildForAdTag(TEST_URI);
diff --git a/extensions/ima/src/test/resources/robolectric.properties b/extensions/ima/src/test/resources/robolectric.properties
deleted file mode 100644
index 2f3210368e..0000000000
--- a/extensions/ima/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-manifest=src/test/AndroidManifest.xml
diff --git a/extensions/jobdispatcher/build.gradle b/extensions/jobdispatcher/build.gradle
index a0e3f8e0c8..d7f19d2545 100644
--- a/extensions/jobdispatcher/build.gradle
+++ b/extensions/jobdispatcher/build.gradle
@@ -18,7 +18,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -29,6 +28,8 @@ android {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
index b7818546f9..d79dead0d7 100644
--- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
+++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
@@ -57,6 +57,7 @@ import com.google.android.exoplayer2.util.Util;
*/
public final class JobDispatcherScheduler implements Scheduler {
+ private static final boolean DEBUG = false;
private static final String TAG = "JobDispatcherScheduler";
private static final String KEY_SERVICE_ACTION = "service_action";
private static final String KEY_SERVICE_PACKAGE = "service_package";
@@ -78,8 +79,8 @@ public final class JobDispatcherScheduler implements Scheduler {
}
@Override
- public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) {
- Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
+ public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
+ Job job = buildJob(jobDispatcher, requirements, jobTag, servicePackage, serviceAction);
int result = jobDispatcher.schedule(job);
logd("Scheduling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
@@ -96,26 +97,18 @@ public final class JobDispatcherScheduler implements Scheduler {
FirebaseJobDispatcher dispatcher,
Requirements requirements,
String tag,
- String serviceAction,
- String servicePackage) {
+ String servicePackage,
+ String serviceAction) {
Job.Builder builder =
dispatcher
.newJobBuilder()
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called
.setTag(tag);
- switch (requirements.getRequiredNetworkType()) {
- case Requirements.NETWORK_TYPE_NONE:
- // do nothing.
- break;
- case Requirements.NETWORK_TYPE_ANY:
- builder.addConstraint(Constraint.ON_ANY_NETWORK);
- break;
- case Requirements.NETWORK_TYPE_UNMETERED:
- builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
- break;
- default:
- throw new UnsupportedOperationException();
+ if (requirements.isUnmeteredNetworkRequired()) {
+ builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
+ } else if (requirements.isNetworkRequired()) {
+ builder.addConstraint(Constraint.ON_ANY_NETWORK);
}
if (requirements.isIdleRequired()) {
@@ -129,7 +122,7 @@ public final class JobDispatcherScheduler implements Scheduler {
Bundle extras = new Bundle();
extras.putString(KEY_SERVICE_ACTION, serviceAction);
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
- extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
+ extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
builder.setExtras(extras);
return builder.build();
diff --git a/extensions/leanback/README.md b/extensions/leanback/README.md
index 4eba6552e1..b6eb085247 100644
--- a/extensions/leanback/README.md
+++ b/extensions/leanback/README.md
@@ -28,4 +28,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.leanback.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle
index 10bfef8e7c..c6f5a216ce 100644
--- a/extensions/leanback/build.gradle
+++ b/extensions/leanback/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -27,11 +26,14 @@ android {
minSdkVersion 17
targetSdkVersion project.ext.targetSdkVersion
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation('com.android.support:leanback-v17:' + supportLibraryVersion)
+ implementation 'androidx.annotation:annotation:1.0.2'
+ implementation 'androidx.leanback:leanback:1.0.0'
}
ext {
diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
index 0c9491bb1a..3f4c5d6229 100644
--- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
+++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
@@ -17,11 +17,11 @@ package com.google.android.exoplayer2.ext.leanback;
import android.content.Context;
import android.os.Handler;
-import android.support.annotation.Nullable;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.media.PlaybackGlueHost;
-import android.support.v17.leanback.media.PlayerAdapter;
-import android.support.v17.leanback.media.SurfaceHolderGlueHost;
+import androidx.annotation.Nullable;
+import androidx.leanback.R;
+import androidx.leanback.media.PlaybackGlueHost;
+import androidx.leanback.media.PlayerAdapter;
+import androidx.leanback.media.SurfaceHolderGlueHost;
import android.util.Pair;
import android.view.Surface;
import android.view.SurfaceHolder;
diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md
index bd6b59c0c1..64b55a8036 100644
--- a/extensions/mediasession/README.md
+++ b/extensions/mediasession/README.md
@@ -29,4 +29,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
* [Javadoc][]: Classes matching
`com.google.android.exoplayer2.ext.mediasession.*` belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle
index 5fb25c6382..6c6ddf4ce4 100644
--- a/extensions/mediasession/build.gradle
+++ b/extensions/mediasession/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -27,11 +26,13 @@ android {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- api 'com.android.support:support-media-compat:' + supportLibraryVersion
+ api 'androidx.media:media:1.0.1'
}
ext {
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java
deleted file mode 100644
index 7d983e14e9..0000000000
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2017 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.mediasession;
-
-import android.os.Bundle;
-import android.os.ResultReceiver;
-import android.support.v4.media.session.PlaybackStateCompat;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.util.RepeatModeUtil;
-
-/**
- * A default implementation of {@link MediaSessionConnector.PlaybackController}.
- *
- * Methods can be safely overridden by subclasses to intercept calls for given actions.
- */
-public class DefaultPlaybackController implements MediaSessionConnector.PlaybackController {
-
- /**
- * The default fast forward increment, in milliseconds.
- */
- public static final int DEFAULT_FAST_FORWARD_MS = 15000;
- /**
- * The default rewind increment, in milliseconds.
- */
- public static final int DEFAULT_REWIND_MS = 5000;
-
- private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE
- | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE
- | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
- | PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
-
- protected final long rewindIncrementMs;
- protected final long fastForwardIncrementMs;
- protected final int repeatToggleModes;
-
- /**
- * Creates a new instance.
- *
- * Equivalent to {@code DefaultPlaybackController(DefaultPlaybackController.DEFAULT_REWIND_MS,
- * DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS,
- * MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}.
- */
- public DefaultPlaybackController() {
- this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS,
- MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES);
- }
-
- /**
- * Creates a new instance with the given fast forward and rewind increments.
- * @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will
- * cause the rewind action to be disabled.
- * @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative
- * @param repeatToggleModes The available repeatToggleModes.
- */
- public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs,
- @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
- this.rewindIncrementMs = rewindIncrementMs;
- this.fastForwardIncrementMs = fastForwardIncrementMs;
- this.repeatToggleModes = repeatToggleModes;
- }
-
- @Override
- public long getSupportedPlaybackActions(Player player) {
- if (player == null || player.getCurrentTimeline().isEmpty()) {
- return 0;
- } else if (!player.isCurrentWindowSeekable()) {
- return BASE_ACTIONS;
- }
- long actions = BASE_ACTIONS | PlaybackStateCompat.ACTION_SEEK_TO;
- if (fastForwardIncrementMs > 0) {
- actions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
- }
- if (rewindIncrementMs > 0) {
- actions |= PlaybackStateCompat.ACTION_REWIND;
- }
- return actions;
- }
-
- @Override
- public void onPlay(Player player) {
- player.setPlayWhenReady(true);
- }
-
- @Override
- public void onPause(Player player) {
- player.setPlayWhenReady(false);
- }
-
- @Override
- public void onSeekTo(Player player, long position) {
- long duration = player.getDuration();
- if (duration != C.TIME_UNSET) {
- position = Math.min(position, duration);
- }
- player.seekTo(Math.max(position, 0));
- }
-
- @Override
- public void onFastForward(Player player) {
- if (fastForwardIncrementMs <= 0) {
- return;
- }
- onSeekTo(player, player.getCurrentPosition() + fastForwardIncrementMs);
- }
-
- @Override
- public void onRewind(Player player) {
- if (rewindIncrementMs <= 0) {
- return;
- }
- onSeekTo(player, player.getCurrentPosition() - rewindIncrementMs);
- }
-
- @Override
- public void onStop(Player player) {
- player.stop(true);
- }
-
- @Override
- public void onSetShuffleMode(Player player, int shuffleMode) {
- player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL
- || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP);
- }
-
- @Override
- public void onSetRepeatMode(Player player, int repeatMode) {
- int selectedExoPlayerRepeatMode = player.getRepeatMode();
- switch (repeatMode) {
- case PlaybackStateCompat.REPEAT_MODE_ALL:
- case PlaybackStateCompat.REPEAT_MODE_GROUP:
- if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL) != 0) {
- selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ALL;
- }
- break;
- case PlaybackStateCompat.REPEAT_MODE_ONE:
- if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE) != 0) {
- selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ONE;
- }
- break;
- default:
- selectedExoPlayerRepeatMode = Player.REPEAT_MODE_OFF;
- break;
- }
- player.setRepeatMode(selectedExoPlayerRepeatMode);
- }
-
- // CommandReceiver implementation.
-
- @Override
- public String[] getCommands() {
- return null;
- }
-
- @Override
- public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
- // Do nothing.
- }
-
-}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
index 9323723601..9c80fabc50 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.mediasession;
+import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
@@ -22,8 +23,9 @@ import android.os.Handler;
import android.os.Looper;
import android.os.ResultReceiver;
import android.os.SystemClock;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.LongDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
@@ -32,6 +34,8 @@ import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Pair;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ControlDispatcher;
+import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackParameters;
@@ -41,6 +45,9 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.RepeatModeUtil;
import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -61,18 +68,24 @@ import java.util.Map;
*
*
* - Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code
- * PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
- * when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
- * actions can be handled by passing one or more {@link CustomActionProvider}s in a similar
- * way.
+ * PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed to
+ * {@link #setPlaybackPreparer(PlaybackPreparer)}.
+ *
- Custom actions can be handled by passing one or more {@link CustomActionProvider}s to
+ * {@link #setCustomActionProviders(CustomActionProvider...)}.
*
- To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by
* calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator}
* is recommended for most use cases.
*
- To enable editing of the media queue, you can set a {@link QueueEditor} by calling {@link
* #setQueueEditor(QueueEditor)}.
+ *
- A {@link MediaButtonEventHandler} can be set by calling {@link
+ * #setMediaButtonEventHandler(MediaButtonEventHandler)}. By default media button events are
+ * handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
*
- An {@link ErrorMessageProvider} for providing human readable error messages and
* corresponding error codes can be set by calling {@link
* #setErrorMessageProvider(ErrorMessageProvider)}.
+ *
- A {@link MediaMetadataProvider} can be set by calling {@link
+ * #setMediaMetadataProvider(MediaMetadataProvider)}. By default the {@link
+ * DefaultMediaMetadataProvider} is used.
*
*/
public final class MediaSessionConnector {
@@ -81,28 +94,82 @@ public final class MediaSessionConnector {
ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession");
}
- /**
- * The default repeat toggle modes which is the bitmask of {@link
- * RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}.
- */
- public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
- RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
+ /** Playback actions supported by the connector. */
+ @LongDef(
+ flag = true,
+ value = {
+ PlaybackStateCompat.ACTION_PLAY_PAUSE,
+ PlaybackStateCompat.ACTION_PLAY,
+ PlaybackStateCompat.ACTION_PAUSE,
+ PlaybackStateCompat.ACTION_SEEK_TO,
+ PlaybackStateCompat.ACTION_FAST_FORWARD,
+ PlaybackStateCompat.ACTION_REWIND,
+ PlaybackStateCompat.ACTION_STOP,
+ PlaybackStateCompat.ACTION_SET_REPEAT_MODE,
+ PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PlaybackActions {}
+
+ @PlaybackActions
+ public static final long ALL_PLAYBACK_ACTIONS =
+ PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_SEEK_TO
+ | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_REWIND
+ | PlaybackStateCompat.ACTION_STOP
+ | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
+ | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
+
+ /** The default playback actions. */
+ @PlaybackActions public static final long DEFAULT_PLAYBACK_ACTIONS = ALL_PLAYBACK_ACTIONS;
+
+ /** The default fast forward increment, in milliseconds. */
+ public static final int DEFAULT_FAST_FORWARD_MS = 15000;
+ /** The default rewind increment, in milliseconds. */
+ public static final int DEFAULT_REWIND_MS = 5000;
public static final String EXTRAS_PITCH = "EXO_PITCH";
+
+ private static final long BASE_PLAYBACK_ACTIONS =
+ PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_STOP
+ | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
+ | PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
private static final int BASE_MEDIA_SESSION_FLAGS =
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
private static final int EDITOR_MEDIA_SESSION_FLAGS =
BASE_MEDIA_SESSION_FLAGS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
+ private static final MediaMetadataCompat METADATA_EMPTY =
+ new MediaMetadataCompat.Builder().build();
+
/** Receiver of media commands sent by a media controller. */
public interface CommandReceiver {
/**
- * Returns the commands the receiver handles, or {@code null} if no commands need to be handled.
+ * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. The
+ * receiver may handle the command, but is not required to do so. Changes to the player should
+ * be made via the {@link ControlDispatcher}.
+ *
+ * @param player The player connected to the media session.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
+ * @param command The command name.
+ * @param extras Optional parameters for the command, may be null.
+ * @param cb A result receiver to which a result may be sent by the command, may be null.
+ * @return Whether the receiver handled the command.
*/
- String[] getCommands();
- /** See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. */
- void onCommand(Player player, String command, Bundle extras, ResultReceiver cb);
+ boolean onCommand(
+ Player player,
+ ControlDispatcher controlDispatcher,
+ String command,
+ Bundle extras,
+ ResultReceiver cb);
}
/** Interface to which playback preparation actions are delegated. */
@@ -140,51 +207,6 @@ public final class MediaSessionConnector {
void onPrepareFromUri(Uri uri, Bundle extras);
}
- /** Interface to which playback actions are delegated. */
- public interface PlaybackController extends CommandReceiver {
-
- long ACTIONS =
- PlaybackStateCompat.ACTION_PLAY_PAUSE
- | PlaybackStateCompat.ACTION_PLAY
- | PlaybackStateCompat.ACTION_PAUSE
- | PlaybackStateCompat.ACTION_SEEK_TO
- | PlaybackStateCompat.ACTION_FAST_FORWARD
- | PlaybackStateCompat.ACTION_REWIND
- | PlaybackStateCompat.ACTION_STOP
- | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
- | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
-
- /**
- * Returns the actions which are supported by the controller. The supported actions must be a
- * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, {@link
- * PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, {@link
- * PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, {@link
- * PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP}, {@link
- * PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and {@link
- * PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}.
- *
- * @param player The player.
- * @return The bitmask of the supported media actions.
- */
- long getSupportedPlaybackActions(@Nullable Player player);
- /** See {@link MediaSessionCompat.Callback#onPlay()}. */
- void onPlay(Player player);
- /** See {@link MediaSessionCompat.Callback#onPause()}. */
- void onPause(Player player);
- /** See {@link MediaSessionCompat.Callback#onSeekTo(long)}. */
- void onSeekTo(Player player, long position);
- /** See {@link MediaSessionCompat.Callback#onFastForward()}. */
- void onFastForward(Player player);
- /** See {@link MediaSessionCompat.Callback#onRewind()}. */
- void onRewind(Player player);
- /** See {@link MediaSessionCompat.Callback#onStop()}. */
- void onStop(Player player);
- /** See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. */
- void onSetShuffleMode(Player player, int shuffleMode);
- /** See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}. */
- void onSetRepeatMode(Player player, int repeatMode);
- }
-
/**
* Handles queue navigation actions, and updates the media session queue by calling {@code
* MediaSessionCompat.setQueue()}.
@@ -202,20 +224,20 @@ public final class MediaSessionConnector {
* PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, {@link
* PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.
*
- * @param player The {@link Player}.
+ * @param player The player connected to the media session.
* @return The bitmask of the supported media actions.
*/
- long getSupportedQueueNavigatorActions(@Nullable Player player);
+ long getSupportedQueueNavigatorActions(Player player);
/**
* Called when the timeline of the player has changed.
*
- * @param player The player of which the timeline has changed.
+ * @param player The player connected to the media session.
*/
void onTimelineChanged(Player player);
/**
* Called when the current window index changed.
*
- * @param player The player of which the current window index of the timeline has changed.
+ * @param player The player connected to the media session.
*/
void onCurrentWindowIndexChanged(Player player);
/**
@@ -230,12 +252,30 @@ public final class MediaSessionConnector {
* @return The id of the active queue item.
*/
long getActiveQueueItemId(@Nullable Player player);
- /** See {@link MediaSessionCompat.Callback#onSkipToPrevious()}. */
- void onSkipToPrevious(Player player);
- /** See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}. */
- void onSkipToQueueItem(Player player, long id);
- /** See {@link MediaSessionCompat.Callback#onSkipToNext()}. */
- void onSkipToNext(Player player);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}.
+ *
+ * @param player The player connected to the media session.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
+ */
+ void onSkipToPrevious(Player player, ControlDispatcher controlDispatcher);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}.
+ *
+ * @param player The player connected to the media session.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
+ */
+ void onSkipToQueueItem(Player player, ControlDispatcher controlDispatcher, long id);
+ /**
+ * See {@link MediaSessionCompat.Callback#onSkipToNext()}.
+ *
+ * @param player The player connected to the media session.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
+ */
+ void onSkipToNext(Player player, ControlDispatcher controlDispatcher);
}
/** Handles media session queue edits. */
@@ -260,15 +300,28 @@ public final class MediaSessionConnector {
/** Callback receiving a user rating for the active media item. */
public interface RatingCallback extends CommandReceiver {
- long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING;
-
/** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}. */
void onSetRating(Player player, RatingCompat rating);
-
+
/** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat, Bundle)}. */
void onSetRating(Player player, RatingCompat rating, Bundle extras);
}
+ /** Handles a media button event. */
+ public interface MediaButtonEventHandler {
+ /**
+ * See {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
+ *
+ * @param player The {@link Player}.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
+ * @param mediaButtonEvent The {@link Intent}.
+ * @return True if the event was handled, false otherwise.
+ */
+ boolean onMediaButtonEvent(
+ Player player, ControlDispatcher controlDispatcher, Intent mediaButtonEvent);
+ }
+
/**
* Provides a {@link PlaybackStateCompat.CustomAction} to be published and handles the action when
* sent by a media controller.
@@ -277,19 +330,24 @@ public final class MediaSessionConnector {
/**
* Called when a custom action provided by this provider is sent to the media session.
*
+ * @param player The player connected to the media session.
+ * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
+ * changes to the player.
* @param action The name of the action which was sent by a media controller.
* @param extras Optional extras sent by a media controller.
*/
- void onCustomAction(String action, Bundle extras);
+ void onCustomAction(
+ Player player, ControlDispatcher controlDispatcher, String action, Bundle extras);
/**
* Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media
* session by the connector or {@code null} if this action should not be published at the given
* player state.
*
+ * @param player The player connected to the media session.
* @return The custom action to be included in the session playback state or {@code null}.
*/
- PlaybackStateCompat.CustomAction getCustomAction();
+ PlaybackStateCompat.CustomAction getCustomAction(Player player);
}
/** Provides a {@link MediaMetadataCompat} for a given player state. */
@@ -297,7 +355,7 @@ public final class MediaSessionConnector {
/**
* Gets the {@link MediaMetadataCompat} to be published to the session.
*
- * @param player The player for which to provide metadata.
+ * @param player The player connected to the media session.
* @return The {@link MediaMetadataCompat} to be published to the session.
*/
MediaMetadataCompat getMetadata(Player player);
@@ -306,143 +364,151 @@ public final class MediaSessionConnector {
/** The wrapped {@link MediaSessionCompat}. */
public final MediaSessionCompat mediaSession;
- private @Nullable final MediaMetadataProvider mediaMetadataProvider;
- private final ExoPlayerEventListener exoPlayerEventListener;
- private final MediaSessionCallback mediaSessionCallback;
- private final PlaybackController playbackController;
- private final Map commandMap;
+ private final Looper looper;
+ private final ComponentListener componentListener;
+ private final ArrayList commandReceivers;
+ private final ArrayList customCommandReceivers;
- private Player player;
+ private ControlDispatcher controlDispatcher;
private CustomActionProvider[] customActionProviders;
private Map customActionMap;
- private @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
- private @Nullable Pair customError;
- private PlaybackPreparer playbackPreparer;
- private QueueNavigator queueNavigator;
- private QueueEditor queueEditor;
- private RatingCallback ratingCallback;
+ @Nullable private MediaMetadataProvider mediaMetadataProvider;
+ @Nullable private Player player;
+ @Nullable private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
+ @Nullable private Pair customError;
+ @Nullable private Bundle customErrorExtras;
+ @Nullable private PlaybackPreparer playbackPreparer;
+ @Nullable private QueueNavigator queueNavigator;
+ @Nullable private QueueEditor queueEditor;
+ @Nullable private RatingCallback ratingCallback;
+ @Nullable private MediaButtonEventHandler mediaButtonEventHandler;
+
+ private long enabledPlaybackActions;
+ private int rewindMs;
+ private int fastForwardMs;
/**
* Creates an instance.
*
- * Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
- *
* @param mediaSession The {@link MediaSessionCompat} to connect to.
*/
public MediaSessionConnector(MediaSessionCompat mediaSession) {
- this(mediaSession, null);
- }
-
- /**
- * Creates an instance.
- *
- *
Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, new
- * DefaultMediaMetadataProvider(mediaSession.getController(), null))}.
- *
- * @param mediaSession The {@link MediaSessionCompat} to connect to.
- * @param playbackController A {@link PlaybackController} for handling playback actions.
- */
- public MediaSessionConnector(
- MediaSessionCompat mediaSession, PlaybackController playbackController) {
- this(
- mediaSession,
- playbackController,
- new DefaultMediaMetadataProvider(mediaSession.getController(), null));
- }
-
- /**
- * Creates an instance.
- *
- * @param mediaSession The {@link MediaSessionCompat} to connect to.
- * @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
- * null} if the connector should handle playback actions directly.
- * @param doMaintainMetadata Whether the connector should maintain the metadata of the session.
- * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active
- * queue item to the session metadata.
- * @deprecated Use {@link MediaSessionConnector#MediaSessionConnector(MediaSessionCompat,
- * PlaybackController, MediaMetadataProvider)}.
- */
- @Deprecated
- public MediaSessionConnector(
- MediaSessionCompat mediaSession,
- @Nullable PlaybackController playbackController,
- boolean doMaintainMetadata,
- @Nullable String metadataExtrasPrefix) {
- this(
- mediaSession,
- playbackController,
- doMaintainMetadata
- ? new DefaultMediaMetadataProvider(mediaSession.getController(), metadataExtrasPrefix)
- : null);
- }
-
- /**
- * Creates an instance.
- *
- * @param mediaSession The {@link MediaSessionCompat} to connect to.
- * @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
- * null} if the connector should handle playback actions directly.
- * @param mediaMetadataProvider A {@link MediaMetadataProvider} for providing a custom metadata
- * object to be published to the media session, or {@code null} if metadata shouldn't be
- * published.
- */
- public MediaSessionConnector(
- MediaSessionCompat mediaSession,
- @Nullable PlaybackController playbackController,
- @Nullable MediaMetadataProvider mediaMetadataProvider) {
this.mediaSession = mediaSession;
- this.playbackController =
- playbackController != null ? playbackController : new DefaultPlaybackController();
- this.mediaMetadataProvider = mediaMetadataProvider;
- mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
- mediaSessionCallback = new MediaSessionCallback();
- exoPlayerEventListener = new ExoPlayerEventListener();
+ looper = Util.getLooper();
+ componentListener = new ComponentListener();
+ commandReceivers = new ArrayList<>();
+ customCommandReceivers = new ArrayList<>();
+ controlDispatcher = new DefaultControlDispatcher();
+ customActionProviders = new CustomActionProvider[0];
customActionMap = Collections.emptyMap();
- commandMap = new HashMap<>();
- registerCommandReceiver(playbackController);
+ mediaMetadataProvider =
+ new DefaultMediaMetadataProvider(
+ mediaSession.getController(), /* metadataExtrasPrefix= */ null);
+ enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS;
+ rewindMs = DEFAULT_REWIND_MS;
+ fastForwardMs = DEFAULT_FAST_FORWARD_MS;
+ mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
+ mediaSession.setCallback(componentListener, new Handler(looper));
}
/**
* Sets the player to be connected to the media session. Must be called on the same thread that is
* used to access the player.
*
- *
The order in which any {@link CustomActionProvider}s are passed determines the order of the
- * actions published with the playback state of the session.
- *
* @param player The player to be connected to the {@code MediaSession}, or {@code null} to
* disconnect the current player.
- * @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player.
- * @param customActionProviders Optional {@link CustomActionProvider}s to publish and handle
- * custom actions.
*/
- public void setPlayer(
- @Nullable Player player,
- @Nullable PlaybackPreparer playbackPreparer,
- CustomActionProvider... customActionProviders) {
- Assertions.checkArgument(player == null || player.getApplicationLooper() == Looper.myLooper());
+ public void setPlayer(@Nullable Player player) {
+ Assertions.checkArgument(player == null || player.getApplicationLooper() == looper);
if (this.player != null) {
- this.player.removeListener(exoPlayerEventListener);
- mediaSession.setCallback(null);
+ this.player.removeListener(componentListener);
}
- unregisterCommandReceiver(this.playbackPreparer);
-
this.player = player;
- this.playbackPreparer = playbackPreparer;
- registerCommandReceiver(playbackPreparer);
-
- this.customActionProviders =
- (player != null && customActionProviders != null)
- ? customActionProviders
- : new CustomActionProvider[0];
if (player != null) {
- Handler handler = new Handler(Util.getLooper());
- mediaSession.setCallback(mediaSessionCallback, handler);
- player.addListener(exoPlayerEventListener);
+ player.addListener(componentListener);
}
invalidateMediaSessionPlaybackState();
invalidateMediaSessionMetadata();
}
+ /**
+ * Sets the {@link PlaybackPreparer}.
+ *
+ * @param playbackPreparer The {@link PlaybackPreparer}.
+ */
+ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
+ if (this.playbackPreparer != playbackPreparer) {
+ unregisterCommandReceiver(this.playbackPreparer);
+ this.playbackPreparer = playbackPreparer;
+ registerCommandReceiver(playbackPreparer);
+ invalidateMediaSessionPlaybackState();
+ }
+ }
+
+ /**
+ * Sets the {@link ControlDispatcher}.
+ *
+ * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link
+ * DefaultControlDispatcher}.
+ */
+ public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) {
+ if (this.controlDispatcher != controlDispatcher) {
+ this.controlDispatcher =
+ controlDispatcher == null ? new DefaultControlDispatcher() : controlDispatcher;
+ }
+ }
+
+ /**
+ * Sets the {@link MediaButtonEventHandler}. Pass {@code null} if the media button event should be
+ * handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
+ *
+ * @param mediaButtonEventHandler The {@link MediaButtonEventHandler}, or null to let the event be
+ * handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
+ */
+ public void setMediaButtonEventHandler(
+ @Nullable MediaButtonEventHandler mediaButtonEventHandler) {
+ this.mediaButtonEventHandler = mediaButtonEventHandler;
+ }
+
+ /**
+ * Sets the enabled playback actions.
+ *
+ * @param enabledPlaybackActions The enabled playback actions.
+ */
+ public void setEnabledPlaybackActions(@PlaybackActions long enabledPlaybackActions) {
+ enabledPlaybackActions &= ALL_PLAYBACK_ACTIONS;
+ if (this.enabledPlaybackActions != enabledPlaybackActions) {
+ this.enabledPlaybackActions = enabledPlaybackActions;
+ invalidateMediaSessionPlaybackState();
+ }
+ }
+
+ /**
+ * Sets the rewind increment in milliseconds.
+ *
+ * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the
+ * rewind button to be disabled.
+ */
+ public void setRewindIncrementMs(int rewindMs) {
+ if (this.rewindMs != rewindMs) {
+ this.rewindMs = rewindMs;
+ invalidateMediaSessionPlaybackState();
+ }
+ }
+
+ /**
+ * Sets the fast forward increment in milliseconds.
+ *
+ * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will
+ * cause the fast forward button to be disabled.
+ */
+ public void setFastForwardIncrementMs(int fastForwardMs) {
+ if (this.fastForwardMs != fastForwardMs) {
+ this.fastForwardMs = fastForwardMs;
+ invalidateMediaSessionPlaybackState();
+ }
+ }
+
/**
* Sets the optional {@link ErrorMessageProvider}.
*
@@ -519,20 +585,65 @@ public final class MediaSessionConnector {
* @param code The error code to report. Ignored when {@code message} is {@code null}.
*/
public void setCustomErrorMessage(@Nullable CharSequence message, int code) {
+ setCustomErrorMessage(message, code, /* extras= */ null);
+ }
+
+ /**
+ * Sets a custom error on the session.
+ *
+ * @param message The error string to report or {@code null} to clear the error.
+ * @param code The error code to report. Ignored when {@code message} is {@code null}.
+ * @param extras Extras to include in reported {@link PlaybackStateCompat}.
+ */
+ public void setCustomErrorMessage(
+ @Nullable CharSequence message, int code, @Nullable Bundle extras) {
customError = (message == null) ? null : new Pair<>(code, message);
+ customErrorExtras = (message == null) ? null : extras;
invalidateMediaSessionPlaybackState();
}
+ /**
+ * Sets custom action providers. The order of the {@link CustomActionProvider}s determines the
+ * order in which the actions are published.
+ *
+ * @param customActionProviders The custom action providers, or null to remove all existing custom
+ * action providers.
+ */
+ public void setCustomActionProviders(@Nullable CustomActionProvider... customActionProviders) {
+ this.customActionProviders =
+ customActionProviders == null ? new CustomActionProvider[0] : customActionProviders;
+ invalidateMediaSessionPlaybackState();
+ }
+
+ /**
+ * Sets a provider of metadata to be published to the media session. Pass {@code null} if no
+ * metadata should be published.
+ *
+ * @param mediaMetadataProvider The provider of metadata to publish, or {@code null} if no
+ * metadata should be published.
+ */
+ public void setMediaMetadataProvider(@Nullable MediaMetadataProvider mediaMetadataProvider) {
+ if (this.mediaMetadataProvider != mediaMetadataProvider) {
+ this.mediaMetadataProvider = mediaMetadataProvider;
+ invalidateMediaSessionMetadata();
+ }
+ }
+
/**
* Updates the metadata of the media session.
*
*
Apps normally only need to call this method when the backing data for a given media item has
* changed and the metadata should be updated immediately.
+ *
+ *
The {@link MediaMetadataCompat} which is published to the session is obtained by calling
+ * {@link MediaMetadataProvider#getMetadata(Player)}.
*/
public final void invalidateMediaSessionMetadata() {
- if (mediaMetadataProvider != null && player != null) {
- mediaSession.setMetadata(mediaMetadataProvider.getMetadata(player));
- }
+ MediaMetadataCompat metadata =
+ mediaMetadataProvider != null && player != null
+ ? mediaMetadataProvider.getMetadata(player)
+ : METADATA_EMPTY;
+ mediaSession.setMetadata(metadata != null ? metadata : METADATA_EMPTY);
}
/**
@@ -544,14 +655,14 @@ public final class MediaSessionConnector {
public final void invalidateMediaSessionPlaybackState() {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
if (player == null) {
- builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
+ builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
mediaSession.setPlaybackState(builder.build());
return;
}
Map currentActions = new HashMap<>();
for (CustomActionProvider customActionProvider : customActionProviders) {
- PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction();
+ PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(player);
if (customAction != null) {
currentActions.put(customAction.getAction(), customActionProvider);
builder.addCustomAction(customAction);
@@ -560,6 +671,7 @@ public final class MediaSessionConnector {
customActionMap = Collections.unmodifiableMap(currentActions);
int playbackState = player.getPlaybackState();
+ Bundle extras = new Bundle();
ExoPlaybackException playbackError =
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
boolean reportError = playbackError != null || customError != null;
@@ -569,6 +681,9 @@ public final class MediaSessionConnector {
: mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
if (customError != null) {
builder.setErrorMessage(customError.first, customError.second);
+ if (customErrorExtras != null) {
+ extras.putAll(customErrorExtras);
+ }
} else if (playbackError != null && errorMessageProvider != null) {
Pair message = errorMessageProvider.getErrorMessage(playbackError);
builder.setErrorMessage(message.first, message.second);
@@ -577,10 +692,9 @@ public final class MediaSessionConnector {
queueNavigator != null
? queueNavigator.getActiveQueueItemId(player)
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
- Bundle extras = new Bundle();
extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
builder
- .setActions(buildPlaybackActions())
+ .setActions(buildPrepareActions() | buildPlaybackActions(player))
.setActiveQueueItemId(activeQueueItemId)
.setBufferedPosition(player.getBufferedPosition())
.setState(
@@ -605,34 +719,77 @@ public final class MediaSessionConnector {
}
}
+ /**
+ * Registers a custom command receiver for responding to commands delivered via {@link
+ * MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
+ *
+ * Commands are only dispatched to this receiver when a player is connected.
+ *
+ * @param commandReceiver The command receiver to register.
+ */
+ public void registerCustomCommandReceiver(CommandReceiver commandReceiver) {
+ if (!customCommandReceivers.contains(commandReceiver)) {
+ customCommandReceivers.add(commandReceiver);
+ }
+ }
+
+ /**
+ * Unregisters a previously registered custom command receiver.
+ *
+ * @param commandReceiver The command receiver to unregister.
+ */
+ public void unregisterCustomCommandReceiver(CommandReceiver commandReceiver) {
+ customCommandReceivers.remove(commandReceiver);
+ }
+
private void registerCommandReceiver(CommandReceiver commandReceiver) {
- if (commandReceiver != null && commandReceiver.getCommands() != null) {
- for (String command : commandReceiver.getCommands()) {
- commandMap.put(command, commandReceiver);
- }
+ if (!commandReceivers.contains(commandReceiver)) {
+ commandReceivers.add(commandReceiver);
}
}
private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
- if (commandReceiver != null && commandReceiver.getCommands() != null) {
- for (String command : commandReceiver.getCommands()) {
- commandMap.remove(command);
- }
- }
+ commandReceivers.remove(commandReceiver);
}
- private long buildPlaybackActions() {
- long actions =
- (PlaybackController.ACTIONS & playbackController.getSupportedPlaybackActions(player));
- if (playbackPreparer != null) {
- actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
+ private long buildPrepareActions() {
+ return playbackPreparer == null
+ ? 0
+ : (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
+ }
+
+ private long buildPlaybackActions(Player player) {
+ boolean enableSeeking = false;
+ boolean enableRewind = false;
+ boolean enableFastForward = false;
+ boolean enableSetRating = false;
+ Timeline timeline = player.getCurrentTimeline();
+ if (!timeline.isEmpty() && !player.isPlayingAd()) {
+ enableSeeking = player.isCurrentWindowSeekable();
+ enableRewind = enableSeeking && rewindMs > 0;
+ enableFastForward = enableSeeking && fastForwardMs > 0;
+ enableSetRating = true;
}
+
+ long playbackActions = BASE_PLAYBACK_ACTIONS;
+ if (enableSeeking) {
+ playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
+ }
+ if (enableFastForward) {
+ playbackActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
+ }
+ if (enableRewind) {
+ playbackActions |= PlaybackStateCompat.ACTION_REWIND;
+ }
+ playbackActions &= enabledPlaybackActions;
+
+ long actions = playbackActions;
if (queueNavigator != null) {
actions |=
(QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
}
- if (ratingCallback != null) {
- actions |= RatingCallback.ACTIONS;
+ if (ratingCallback != null && enableSetRating) {
+ actions |= PlaybackStateCompat.ACTION_SET_RATING;
}
return actions;
}
@@ -644,39 +801,74 @@ public final class MediaSessionConnector {
case Player.STATE_READY:
return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
case Player.STATE_ENDED:
- return PlaybackStateCompat.STATE_PAUSED;
+ return PlaybackStateCompat.STATE_STOPPED;
default:
return PlaybackStateCompat.STATE_NONE;
}
}
+ private boolean canDispatchPlaybackAction(long action) {
+ return player != null && (enabledPlaybackActions & action) != 0;
+ }
+
private boolean canDispatchToPlaybackPreparer(long action) {
return playbackPreparer != null
- && (playbackPreparer.getSupportedPrepareActions() & PlaybackPreparer.ACTIONS & action) != 0;
- }
-
- private boolean canDispatchToRatingCallback(long action) {
- return ratingCallback != null && (RatingCallback.ACTIONS & action) != 0;
- }
-
- private boolean canDispatchToPlaybackController(long action) {
- return (playbackController.getSupportedPlaybackActions(player)
- & PlaybackController.ACTIONS
- & action)
- != 0;
+ && (playbackPreparer.getSupportedPrepareActions() & action) != 0;
}
private boolean canDispatchToQueueNavigator(long action) {
- return queueNavigator != null
- && (queueNavigator.getSupportedQueueNavigatorActions(player)
- & QueueNavigator.ACTIONS
- & action)
- != 0;
+ return player != null
+ && queueNavigator != null
+ && (queueNavigator.getSupportedQueueNavigatorActions(player) & action) != 0;
+ }
+
+ private boolean canDispatchSetRating() {
+ return player != null && ratingCallback != null;
+ }
+
+ private boolean canDispatchQueueEdit() {
+ return player != null && queueEditor != null;
+ }
+
+ private boolean canDispatchMediaButtonEvent() {
+ return player != null && mediaButtonEventHandler != null;
+ }
+
+ private void stopPlayerForPrepare(boolean playWhenReady) {
+ if (player != null) {
+ player.stop();
+ player.setPlayWhenReady(playWhenReady);
+ }
+ }
+
+ private void rewind(Player player) {
+ if (player.isCurrentWindowSeekable() && rewindMs > 0) {
+ seekTo(player, player.getCurrentPosition() - rewindMs);
+ }
+ }
+
+ private void fastForward(Player player) {
+ if (player.isCurrentWindowSeekable() && fastForwardMs > 0) {
+ seekTo(player, player.getCurrentPosition() + fastForwardMs);
+ }
+ }
+
+ private void seekTo(Player player, long positionMs) {
+ seekTo(player, player.getCurrentWindowIndex(), positionMs);
+ }
+
+ private void seekTo(Player player, int windowIndex, long positionMs) {
+ long durationMs = player.getDuration();
+ if (durationMs != C.TIME_UNSET) {
+ positionMs = Math.min(positionMs, durationMs);
+ }
+ positionMs = Math.max(positionMs, 0);
+ controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
}
/**
- * Provides a default {@link MediaMetadataCompat} with properties and extras propagated from the
- * active queue item to the session metadata.
+ * Provides a default {@link MediaMetadataCompat} with properties and extras taken from the {@link
+ * MediaDescriptionCompat} of the {@link MediaSessionCompat.QueueItem} of the active queue item.
*/
public static final class DefaultMediaMetadataProvider implements MediaMetadataProvider {
@@ -699,7 +891,7 @@ public final class MediaSessionConnector {
@Override
public MediaMetadataCompat getMetadata(Player player) {
if (player.getCurrentTimeline().isEmpty()) {
- return null;
+ return METADATA_EMPTY;
}
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
if (player.isPlayingAd()) {
@@ -776,11 +968,14 @@ public final class MediaSessionConnector {
}
}
- private class ExoPlayerEventListener implements Player.EventListener {
+ private class ComponentListener extends MediaSessionCompat.Callback
+ implements Player.EventListener {
private int currentWindowIndex;
private int currentWindowCount;
+ // Player.EventListener implementation.
+
@Override
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
@@ -821,6 +1016,7 @@ public final class MediaSessionConnector {
? PlaybackStateCompat.SHUFFLE_MODE_ALL
: PlaybackStateCompat.SHUFFLE_MODE_NONE);
invalidateMediaSessionPlaybackState();
+ invalidateMediaSessionQueue();
}
@Override
@@ -843,109 +1039,150 @@ public final class MediaSessionConnector {
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
invalidateMediaSessionPlaybackState();
}
- }
- private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ // MediaSessionCompat.Callback implementation.
@Override
public void onPlay() {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PLAY)) {
- playbackController.onPlay(player);
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
+ if (player.getPlaybackState() == Player.STATE_IDLE) {
+ if (playbackPreparer != null) {
+ playbackPreparer.onPrepare();
+ }
+ } else if (player.getPlaybackState() == Player.STATE_ENDED) {
+ controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
+ }
+ controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);
}
}
@Override
public void onPause() {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PAUSE)) {
- playbackController.onPause(player);
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) {
+ controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);
}
}
@Override
- public void onSeekTo(long position) {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SEEK_TO)) {
- playbackController.onSeekTo(player, position);
+ public void onSeekTo(long positionMs) {
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SEEK_TO)) {
+ seekTo(player, positionMs);
}
}
@Override
public void onFastForward() {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_FAST_FORWARD)) {
- playbackController.onFastForward(player);
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_FAST_FORWARD)) {
+ fastForward(player);
}
}
@Override
public void onRewind() {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_REWIND)) {
- playbackController.onRewind(player);
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_REWIND)) {
+ rewind(player);
}
}
@Override
public void onStop() {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_STOP)) {
- playbackController.onStop(player);
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_STOP)) {
+ controlDispatcher.dispatchStop(player, /* reset= */ true);
}
}
@Override
- public void onSetShuffleMode(int shuffleMode) {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {
- playbackController.onSetShuffleMode(player, shuffleMode);
+ public void onSetShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {
+ boolean shuffleModeEnabled;
+ switch (shuffleMode) {
+ case PlaybackStateCompat.SHUFFLE_MODE_ALL:
+ case PlaybackStateCompat.SHUFFLE_MODE_GROUP:
+ shuffleModeEnabled = true;
+ break;
+ case PlaybackStateCompat.SHUFFLE_MODE_NONE:
+ case PlaybackStateCompat.SHUFFLE_MODE_INVALID:
+ default:
+ shuffleModeEnabled = false;
+ break;
+ }
+ controlDispatcher.dispatchSetShuffleModeEnabled(player, shuffleModeEnabled);
}
}
@Override
- public void onSetRepeatMode(int repeatMode) {
- if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
- playbackController.onSetRepeatMode(player, repeatMode);
+ public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int mediaSessionRepeatMode) {
+ if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
+ @RepeatModeUtil.RepeatToggleModes int repeatMode;
+ switch (mediaSessionRepeatMode) {
+ case PlaybackStateCompat.REPEAT_MODE_ALL:
+ case PlaybackStateCompat.REPEAT_MODE_GROUP:
+ repeatMode = Player.REPEAT_MODE_ALL;
+ break;
+ case PlaybackStateCompat.REPEAT_MODE_ONE:
+ repeatMode = Player.REPEAT_MODE_ONE;
+ break;
+ case PlaybackStateCompat.REPEAT_MODE_NONE:
+ case PlaybackStateCompat.REPEAT_MODE_INVALID:
+ default:
+ repeatMode = Player.REPEAT_MODE_OFF;
+ break;
+ }
+ controlDispatcher.dispatchSetRepeatMode(player, repeatMode);
}
}
@Override
public void onSkipToNext() {
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
- queueNavigator.onSkipToNext(player);
+ queueNavigator.onSkipToNext(player, controlDispatcher);
}
}
@Override
public void onSkipToPrevious() {
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) {
- queueNavigator.onSkipToPrevious(player);
+ queueNavigator.onSkipToPrevious(player, controlDispatcher);
}
}
@Override
public void onSkipToQueueItem(long id) {
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) {
- queueNavigator.onSkipToQueueItem(player, id);
+ queueNavigator.onSkipToQueueItem(player, controlDispatcher, id);
}
}
@Override
public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
- Map actionMap = customActionMap;
- if (actionMap.containsKey(action)) {
- actionMap.get(action).onCustomAction(action, extras);
+ if (player != null && customActionMap.containsKey(action)) {
+ customActionMap.get(action).onCustomAction(player, controlDispatcher, action, extras);
invalidateMediaSessionPlaybackState();
}
}
@Override
public void onCommand(String command, Bundle extras, ResultReceiver cb) {
- CommandReceiver commandReceiver = commandMap.get(command);
- if (commandReceiver != null) {
- commandReceiver.onCommand(player, command, extras, cb);
+ if (player != null) {
+ for (int i = 0; i < commandReceivers.size(); i++) {
+ if (commandReceivers.get(i).onCommand(player, controlDispatcher, command, extras, cb)) {
+ return;
+ }
+ }
+ for (int i = 0; i < customCommandReceivers.size(); i++) {
+ if (customCommandReceivers
+ .get(i)
+ .onCommand(player, controlDispatcher, command, extras, cb)) {
+ return;
+ }
+ }
}
}
@Override
public void onPrepare() {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
- player.stop();
- player.setPlayWhenReady(false);
+ stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepare();
}
}
@@ -953,8 +1190,7 @@ public final class MediaSessionConnector {
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
- player.stop();
- player.setPlayWhenReady(false);
+ stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
}
}
@@ -962,8 +1198,7 @@ public final class MediaSessionConnector {
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
- player.stop();
- player.setPlayWhenReady(false);
+ stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromSearch(query, extras);
}
}
@@ -971,8 +1206,7 @@ public final class MediaSessionConnector {
@Override
public void onPrepareFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
- player.stop();
- player.setPlayWhenReady(false);
+ stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromUri(uri, extras);
}
}
@@ -980,8 +1214,7 @@ public final class MediaSessionConnector {
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
- player.stop();
- player.setPlayWhenReady(true);
+ stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
}
}
@@ -989,8 +1222,7 @@ public final class MediaSessionConnector {
@Override
public void onPlayFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
- player.stop();
- player.setPlayWhenReady(true);
+ stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromSearch(query, extras);
}
}
@@ -998,45 +1230,53 @@ public final class MediaSessionConnector {
@Override
public void onPlayFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
- player.stop();
- player.setPlayWhenReady(true);
+ stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromUri(uri, extras);
}
}
@Override
public void onSetRating(RatingCompat rating) {
- if (canDispatchToRatingCallback(PlaybackStateCompat.ACTION_SET_RATING)) {
+ if (canDispatchSetRating()) {
ratingCallback.onSetRating(player, rating);
}
}
-
+
@Override
public void onSetRating(RatingCompat rating, Bundle extras) {
- if (canDispatchToRatingCallback(PlaybackStateCompat.ACTION_SET_RATING)) {
+ if (canDispatchSetRating()) {
ratingCallback.onSetRating(player, rating, extras);
}
}
@Override
public void onAddQueueItem(MediaDescriptionCompat description) {
- if (queueEditor != null) {
+ if (canDispatchQueueEdit()) {
queueEditor.onAddQueueItem(player, description);
}
}
@Override
public void onAddQueueItem(MediaDescriptionCompat description, int index) {
- if (queueEditor != null) {
+ if (canDispatchQueueEdit()) {
queueEditor.onAddQueueItem(player, description, index);
}
}
@Override
public void onRemoveQueueItem(MediaDescriptionCompat description) {
- if (queueEditor != null) {
+ if (canDispatchQueueEdit()) {
queueEditor.onRemoveQueueItem(player, description);
}
}
+
+ @Override
+ public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
+ boolean isHandled =
+ canDispatchMediaButtonEvent()
+ && mediaButtonEventHandler.onMediaButtonEvent(
+ player, controlDispatcher, mediaButtonEvent);
+ return isHandled || super.onMediaButtonEvent(mediaButtonEvent);
+ }
}
}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
index 057f59f62c..617b8781f4 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
@@ -18,17 +18,20 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.media.session.PlaybackStateCompat;
+import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.util.RepeatModeUtil;
-/**
- * Provides a custom action for toggling repeat modes.
- */
+/** Provides a custom action for toggling repeat modes. */
public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
+ /** The default repeat toggle modes. */
+ @RepeatModeUtil.RepeatToggleModes
+ public static final int DEFAULT_REPEAT_TOGGLE_MODES =
+ RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
+
private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE";
- private final Player player;
@RepeatModeUtil.RepeatToggleModes
private final int repeatToggleModes;
private final CharSequence repeatAllDescription;
@@ -37,27 +40,23 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
/**
* Creates a new instance.
- *
- * Equivalent to {@code RepeatModeActionProvider(context, player,
- * MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}.
+ *
+ *
Equivalent to {@code RepeatModeActionProvider(context, DEFAULT_REPEAT_TOGGLE_MODES)}.
*
* @param context The context.
- * @param player The player on which to toggle the repeat mode.
*/
- public RepeatModeActionProvider(Context context, Player player) {
- this(context, player, MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES);
+ public RepeatModeActionProvider(Context context) {
+ this(context, DEFAULT_REPEAT_TOGGLE_MODES);
}
/**
* Creates a new instance enabling the given repeat toggle modes.
*
* @param context The context.
- * @param player The player on which to toggle the repeat mode.
* @param repeatToggleModes The toggle modes to enable.
*/
- public RepeatModeActionProvider(Context context, Player player,
- @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
- this.player = player;
+ public RepeatModeActionProvider(
+ Context context, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
this.repeatToggleModes = repeatToggleModes;
repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description);
repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description);
@@ -65,16 +64,17 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
}
@Override
- public void onCustomAction(String action, Bundle extras) {
+ public void onCustomAction(
+ Player player, ControlDispatcher controlDispatcher, String action, Bundle extras) {
int mode = player.getRepeatMode();
int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
if (mode != proposedMode) {
- player.setRepeatMode(proposedMode);
+ controlDispatcher.dispatchSetRepeatMode(player, proposedMode);
}
}
@Override
- public PlaybackStateCompat.CustomAction getCustomAction() {
+ public PlaybackStateCompat.CustomAction getCustomAction(Player player) {
CharSequence actionLabel;
int iconResourceId;
switch (player.getRepeatMode()) {
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
index 7c00fcdf17..d076404bb4 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
@@ -17,12 +17,13 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
@@ -186,20 +187,23 @@ public final class TimelineQueueEditor
// CommandReceiver implementation.
- @NonNull
@Override
- public String[] getCommands() {
- return new String[] {COMMAND_MOVE_QUEUE_ITEM};
- }
-
- @Override
- public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
+ public boolean onCommand(
+ Player player,
+ ControlDispatcher controlDispatcher,
+ String command,
+ Bundle extras,
+ ResultReceiver cb) {
+ if (!COMMAND_MOVE_QUEUE_ITEM.equals(command)) {
+ return false;
+ }
int from = extras.getInt(EXTRA_FROM_INDEX, C.INDEX_UNSET);
int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET);
if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) {
queueDataAdapter.move(from, to);
queueMediaSource.moveMediaSource(from, to);
}
+ return true;
}
}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
index d55f8e04f0..6e61ad2fe2 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
@@ -17,17 +17,18 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
-import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.List;
/**
* An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that maps the
@@ -40,7 +41,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
private final MediaSessionCompat mediaSession;
private final Timeline.Window window;
- protected final int maxQueueSize;
+ private final int maxQueueSize;
private long activeQueueItemId;
@@ -66,6 +67,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
* @param maxQueueSize The maximum queue size.
*/
public TimelineQueueNavigator(MediaSessionCompat mediaSession, int maxQueueSize) {
+ Assertions.checkState(maxQueueSize > 0);
this.mediaSession = mediaSession;
this.maxQueueSize = maxQueueSize;
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
@@ -75,6 +77,11 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
/**
* Gets the {@link MediaDescriptionCompat} for a given timeline window index.
*
+ *
Often artworks and icons need to be loaded asynchronously. In such a case, return a {@link
+ * MediaDescriptionCompat} without the images, load your images asynchronously off the main thread
+ * and then call {@link MediaSessionConnector#invalidateMediaSessionQueue()} to make the connector
+ * update the queue by calling {@link #getMediaDescription(Player, int)} again.
+ *
* @param player The current player.
* @param windowIndex The timeline window index for which to provide a description.
* @return A {@link MediaDescriptionCompat}.
@@ -83,21 +90,25 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
@Override
public long getSupportedQueueNavigatorActions(Player player) {
- if (player == null) {
- return 0;
- }
+ boolean enableSkipTo = false;
+ boolean enablePrevious = false;
+ boolean enableNext = false;
Timeline timeline = player.getCurrentTimeline();
- if (timeline.isEmpty() || player.isPlayingAd()) {
- return 0;
+ if (!timeline.isEmpty() && !player.isPlayingAd()) {
+ timeline.getWindow(player.getCurrentWindowIndex(), window);
+ enableSkipTo = timeline.getWindowCount() > 1;
+ enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious();
+ enableNext = window.isDynamic || player.hasNext();
}
+
long actions = 0;
- if (timeline.getWindowCount() > 1) {
+ if (enableSkipTo) {
actions |= PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
}
- if (window.isSeekable || !window.isDynamic || player.hasPrevious()) {
+ if (enablePrevious) {
actions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
}
- if (window.isDynamic || player.hasNext()) {
+ if (enableNext) {
actions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
}
return actions;
@@ -124,7 +135,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
}
@Override
- public void onSkipToPrevious(Player player) {
+ public void onSkipToPrevious(Player player, ControlDispatcher controlDispatcher) {
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty() || player.isPlayingAd()) {
return;
@@ -135,26 +146,26 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
if (previousWindowIndex != C.INDEX_UNSET
&& (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS
|| (window.isDynamic && !window.isSeekable))) {
- player.seekTo(previousWindowIndex, C.TIME_UNSET);
+ controlDispatcher.dispatchSeekTo(player, previousWindowIndex, C.TIME_UNSET);
} else {
- player.seekTo(0);
+ controlDispatcher.dispatchSeekTo(player, windowIndex, 0);
}
}
@Override
- public void onSkipToQueueItem(Player player, long id) {
+ public void onSkipToQueueItem(Player player, ControlDispatcher controlDispatcher, long id) {
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty() || player.isPlayingAd()) {
return;
}
int windowIndex = (int) id;
if (0 <= windowIndex && windowIndex < timeline.getWindowCount()) {
- player.seekTo(windowIndex, C.TIME_UNSET);
+ controlDispatcher.dispatchSeekTo(player, windowIndex, C.TIME_UNSET);
}
}
@Override
- public void onSkipToNext(Player player) {
+ public void onSkipToNext(Player player, ControlDispatcher controlDispatcher) {
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty() || player.isPlayingAd()) {
return;
@@ -162,42 +173,71 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
int windowIndex = player.getCurrentWindowIndex();
int nextWindowIndex = player.getNextWindowIndex();
if (nextWindowIndex != C.INDEX_UNSET) {
- player.seekTo(nextWindowIndex, C.TIME_UNSET);
+ controlDispatcher.dispatchSeekTo(player, nextWindowIndex, C.TIME_UNSET);
} else if (timeline.getWindow(windowIndex, window).isDynamic) {
- player.seekTo(windowIndex, C.TIME_UNSET);
+ controlDispatcher.dispatchSeekTo(player, windowIndex, C.TIME_UNSET);
}
}
// CommandReceiver implementation.
@Override
- public String[] getCommands() {
- return null;
+ public boolean onCommand(
+ Player player,
+ ControlDispatcher controlDispatcher,
+ String command,
+ Bundle extras,
+ ResultReceiver cb) {
+ return false;
}
- @Override
- public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
- // Do nothing.
- }
+ // Helper methods.
private void publishFloatingQueueWindow(Player player) {
- if (player.getCurrentTimeline().isEmpty()) {
+ Timeline timeline = player.getCurrentTimeline();
+ if (timeline.isEmpty()) {
mediaSession.setQueue(Collections.emptyList());
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
return;
}
- int windowCount = player.getCurrentTimeline().getWindowCount();
+ ArrayDeque queue = new ArrayDeque<>();
+ int queueSize = Math.min(maxQueueSize, timeline.getWindowCount());
+
+ // Add the active queue item.
int currentWindowIndex = player.getCurrentWindowIndex();
- int queueSize = Math.min(maxQueueSize, windowCount);
- int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
- windowCount - queueSize);
- List queue = new ArrayList<>();
- for (int i = startIndex; i < startIndex + queueSize; i++) {
- queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(player, i), i));
+ queue.add(
+ new MediaSessionCompat.QueueItem(
+ getMediaDescription(player, currentWindowIndex), currentWindowIndex));
+
+ // Fill queue alternating with next and/or previous queue items.
+ int firstWindowIndex = currentWindowIndex;
+ int lastWindowIndex = currentWindowIndex;
+ boolean shuffleModeEnabled = player.getShuffleModeEnabled();
+ while ((firstWindowIndex != C.INDEX_UNSET || lastWindowIndex != C.INDEX_UNSET)
+ && queue.size() < queueSize) {
+ // Begin with next to have a longer tail than head if an even sized queue needs to be trimmed.
+ if (lastWindowIndex != C.INDEX_UNSET) {
+ lastWindowIndex =
+ timeline.getNextWindowIndex(
+ lastWindowIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled);
+ if (lastWindowIndex != C.INDEX_UNSET) {
+ queue.add(
+ new MediaSessionCompat.QueueItem(
+ getMediaDescription(player, lastWindowIndex), lastWindowIndex));
+ }
+ }
+ if (firstWindowIndex != C.INDEX_UNSET && queue.size() < queueSize) {
+ firstWindowIndex =
+ timeline.getPreviousWindowIndex(
+ firstWindowIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled);
+ if (firstWindowIndex != C.INDEX_UNSET) {
+ queue.addFirst(
+ new MediaSessionCompat.QueueItem(
+ getMediaDescription(player, firstWindowIndex), firstWindowIndex));
+ }
+ }
}
- mediaSession.setQueue(queue);
+ mediaSession.setQueue(new ArrayList<>(queue));
activeQueueItemId = currentWindowIndex;
}
-
}
-
diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md
index 73297b54a9..2f9893fe3b 100644
--- a/extensions/okhttp/README.md
+++ b/extensions/okhttp/README.md
@@ -3,7 +3,7 @@
The OkHttp extension is an [HttpDataSource][] implementation using Square's
[OkHttp][].
-[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
+[HttpDataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
[OkHttp]: https://square.github.io/okhttp/
## License note ##
@@ -61,4 +61,4 @@ respectively.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.okhttp.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle
index 78825a6277..db2e073c8a 100644
--- a/extensions/okhttp/build.gradle
+++ b/extensions/okhttp/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -28,11 +27,13 @@ android {
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'androidx.annotation:annotation:1.0.2'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
api 'com.squareup.okhttp3:okhttp:3.12.1'
}
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
index 778277fdbc..a749495184 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
@@ -18,9 +18,10 @@ package com.google.android.exoplayer2.ext.okhttp;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.net.Uri;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
@@ -263,7 +264,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
long position = dataSpec.position;
long length = dataSpec.length;
- boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
if (url == null) {
@@ -293,10 +293,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
if (userAgent != null) {
builder.addHeader("User-Agent", userAgent);
}
-
- if (!allowGzip) {
+ if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
builder.addHeader("Accept-Encoding", "identity");
}
+ if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
+ builder.addHeader(
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
+ }
RequestBody requestBody = null;
if (dataSpec.httpBody != null) {
requestBody = RequestBody.create(null, dataSpec.httpBody);
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
index 09f4e0b61a..d0ef35cb07 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2.ext.okhttp;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
diff --git a/extensions/opus/README.md b/extensions/opus/README.md
index 15c3e5413d..95c6807275 100644
--- a/extensions/opus/README.md
+++ b/extensions/opus/README.md
@@ -98,4 +98,4 @@ player, then implement your own logic to use the renderer for a given track.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.opus.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle
index cb12442de8..56acbdb7d3 100644
--- a/extensions/opus/build.gradle
+++ b/extensions/opus/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -34,11 +33,15 @@ android {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
+ testImplementation project(modulePrefix + 'testutils-robolectric')
+ androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
+ androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
}
ext {
diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml
index 5ba0f3c0f4..7f775f4d32 100644
--- a/extensions/opus/src/androidTest/AndroidManifest.xml
+++ b/extensions/opus/src/androidTest/AndroidManifest.xml
@@ -18,6 +18,9 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.opus.test">
+
+
+
diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
index c457514c87..dcd5f4957a 100644
--- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
+++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
@@ -15,21 +15,21 @@
*/
package com.google.android.exoplayer2.ext.opus;
-import static androidx.test.InstrumentationRegistry.getContext;
import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
-import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
@@ -56,7 +56,7 @@ public class OpusPlaybackTest {
private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable =
- new TestPlaybackRunnable(Uri.parse(uri), getContext());
+ new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
thread.join();
@@ -83,12 +83,12 @@ public class OpusPlaybackTest {
Looper.prepare();
LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
- player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
+ player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
MediaSource mediaSource =
- new ExtractorMediaSource.Factory(
- new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"))
- .setExtractorsFactory(MatroskaExtractor.FACTORY)
+ new ProgressiveMediaSource.Factory(
+ new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"),
+ MatroskaExtractor.FACTORY)
.createMediaSource(uri);
player.prepare(mediaSource);
player.setPlayWhenReady(true);
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
index e288339058..59337c0847 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
@@ -47,7 +47,9 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
- public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
+ public LibopusAudioRenderer(
+ Handler eventHandler,
+ AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
index 4cb3ce3190..285be96388 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
@@ -27,7 +27,7 @@ public final class OpusLibrary {
ExoPlayerLibraryInfo.registerModule("goog.exo.opus");
}
- private static final LibraryLoader LOADER = new LibraryLoader("opusJNI");
+ private static final LibraryLoader LOADER = new LibraryLoader("opusV2JNI");
private OpusLibrary() {}
diff --git a/extensions/opus/src/main/jni/Android.mk b/extensions/opus/src/main/jni/Android.mk
index 9d1e4fe726..0b06d9ecd8 100644
--- a/extensions/opus/src/main/jni/Android.mk
+++ b/extensions/opus/src/main/jni/Android.mk
@@ -21,10 +21,10 @@ include $(CLEAR_VARS)
LOCAL_PATH := $(WORKING_DIR)
include libopus.mk
-# build libopusJNI.so
+# build libopusV2JNI.so
include $(CLEAR_VARS)
LOCAL_PATH := $(WORKING_DIR)
-LOCAL_MODULE := libopusJNI
+LOCAL_MODULE := libopusV2JNI
LOCAL_ARM_MODE := arm
LOCAL_CPP_EXTENSION := .cc
LOCAL_SRC_FILES := opus_jni.cc
diff --git a/extensions/opus/src/test/AndroidManifest.xml b/extensions/opus/src/test/AndroidManifest.xml
new file mode 100644
index 0000000000..ac6a3bf68f
--- /dev/null
+++ b/extensions/opus/src/test/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/extensions/opus/src/test/java/com/google/android/exoplayer2/ext/opus/DefaultRenderersFactoryTest.java b/extensions/opus/src/test/java/com/google/android/exoplayer2/ext/opus/DefaultRenderersFactoryTest.java
new file mode 100644
index 0000000000..e57ad84a41
--- /dev/null
+++ b/extensions/opus/src/test/java/com/google/android/exoplayer2/ext/opus/DefaultRenderersFactoryTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2019 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.opus;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link DefaultRenderersFactoryTest} with {@link LibopusAudioRenderer}. */
+@RunWith(AndroidJUnit4.class)
+public final class DefaultRenderersFactoryTest {
+
+ @Test
+ public void createRenderers_instantiatesVpxRenderer() {
+ DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
+ LibopusAudioRenderer.class, C.TRACK_TYPE_AUDIO);
+ }
+}
diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md
index 3863dff965..a34341692b 100644
--- a/extensions/rtmp/README.md
+++ b/extensions/rtmp/README.md
@@ -3,7 +3,7 @@
The RTMP extension is a [DataSource][] implementation for playing [RTMP][]
streams using [LibRtmp Client for Android][].
-[DataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/DataSource.html
+[DataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/DataSource.html
[RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
[LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android
@@ -53,4 +53,4 @@ doesn't need to handle any other protocols, you can update any `DataSource`s and
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.rtmp.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle
index af02ee2eaa..ca734c3657 100644
--- a/extensions/rtmp/build.gradle
+++ b/extensions/rtmp/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -24,17 +23,18 @@ android {
}
defaultConfig {
- minSdkVersion 15
+ minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'net.butterflytv.utils:rtmp-client:3.0.1'
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
- testImplementation 'junit:junit:' + junitVersion
- testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ implementation 'androidx.annotation:annotation:1.0.2'
+ testImplementation project(modulePrefix + 'testutils-robolectric')
}
ext {
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
index 08c328ce81..272a8d1eb4 100644
--- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
@@ -16,7 +16,7 @@
package com.google.android.exoplayer2.ext.rtmp;
import android.net.Uri;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.upstream.BaseDataSource;
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
index d1350276f2..36abf825d6 100644
--- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2.ext.rtmp;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener;
diff --git a/extensions/rtmp/src/test/java/com/google/android/exoplayer2/ext/rtmp/DefaultDataSourceTest.java b/extensions/rtmp/src/test/java/com/google/android/exoplayer2/ext/rtmp/DefaultDataSourceTest.java
index f4753798b8..469e66a884 100644
--- a/extensions/rtmp/src/test/java/com/google/android/exoplayer2/ext/rtmp/DefaultDataSourceTest.java
+++ b/extensions/rtmp/src/test/java/com/google/android/exoplayer2/ext/rtmp/DefaultDataSourceTest.java
@@ -16,23 +16,25 @@
package com.google.android.exoplayer2.ext.rtmp;
import android.net.Uri;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
/** Unit test for {@link DefaultDataSource} with RTMP URIs. */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
public final class DefaultDataSourceTest {
@Test
public void openRtmpDataSpec_instantiatesRtmpDataSourceViaReflection() throws IOException {
DefaultDataSource dataSource =
new DefaultDataSource(
- RuntimeEnvironment.application, "userAgent", /* allowCrossProtocolRedirects= */ false);
+ ApplicationProvider.getApplicationContext(),
+ "userAgent",
+ /* allowCrossProtocolRedirects= */ false);
DataSpec dataSpec = new DataSpec(Uri.parse("rtmp://test.com/stream"));
try {
dataSource.open(dataSpec);
diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md
index 306f04d0e2..0de29eea32 100644
--- a/extensions/vp9/README.md
+++ b/extensions/vp9/README.md
@@ -34,24 +34,20 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
NDK_PATH=""
```
-* Fetch libvpx and libyuv:
+* Fetch libvpx:
```
cd "${VP9_EXT_PATH}/jni" && \
-git clone https://chromium.googlesource.com/webm/libvpx libvpx && \
-git clone https://chromium.googlesource.com/libyuv/libyuv libyuv
+git clone https://chromium.googlesource.com/webm/libvpx libvpx
```
-* Checkout the appropriate branches of libvpx and libyuv (the scripts and
- makefiles bundled in this repo are known to work only at these versions of the
- libraries - we will update this periodically as newer versions of
- libvpx/libyuv are released):
+* Checkout the appropriate branch of libvpx (the scripts and makefiles bundled
+ in this repo are known to work only at specific versions of the library - we
+ will update this periodically as newer versions of libvpx are released):
```
cd "${VP9_EXT_PATH}/jni/libvpx" && \
-git checkout tags/v1.7.0 -b v1.7.0 && \
-cd "${VP9_EXT_PATH}/jni/libyuv" && \
-git checkout 996a2bbd
+git checkout tags/v1.8.0 -b v1.8.0
```
* Run a script that generates necessary configuration files for libvpx:
@@ -78,10 +74,10 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
* Android config scripts should be re-generated by running
`generate_libvpx_android_configs.sh`
* Clean and re-build the project.
-* If you want to use your own version of libvpx or libyuv, place it in
- `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
- please note that `generate_libvpx_android_configs.sh` and the makefiles need
- to be modified to work with arbitrary versions of libvpx and libyuv.
+* If you want to use your own version of libvpx, place it in
+ `${VP9_EXT_PATH}/jni/libvpx`. Please note that
+ `generate_libvpx_android_configs.sh` and the makefiles may need to be modified
+ to work with arbitrary versions of libvpx.
## Using the extension ##
@@ -123,4 +119,4 @@ type `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` with the
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.vp9.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle
index 96c58d7a57..02b68b831d 100644
--- a/extensions/vp9/build.gradle
+++ b/extensions/vp9/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -34,13 +33,17 @@ android {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
- androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
- androidTestImplementation 'com.google.truth:truth:' + truthVersion
+ implementation 'androidx.annotation:annotation:1.0.2'
+ testImplementation project(modulePrefix + 'testutils-robolectric')
+ androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
+ androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
+ androidTestImplementation 'androidx.test.ext:truth:' + androidXTestVersion
}
ext {
diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml
index 214427c4f0..6ca2e7164a 100644
--- a/extensions/vp9/src/androidTest/AndroidManifest.xml
+++ b/extensions/vp9/src/androidTest/AndroidManifest.xml
@@ -18,6 +18,9 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.vp9.test">
+
+
+
diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
index 2eb5c87e04..5120004889 100644
--- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
+++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
@@ -15,22 +15,22 @@
*/
package com.google.android.exoplayer2.ext.vp9;
-import static androidx.test.InstrumentationRegistry.getContext;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
-import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Log;
@@ -89,7 +89,7 @@ public class VpxPlaybackTest {
private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable =
- new TestPlaybackRunnable(Uri.parse(uri), getContext());
+ new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
thread.join();
@@ -114,14 +114,14 @@ public class VpxPlaybackTest {
@Override
public void run() {
Looper.prepare();
- LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0);
+ LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0);
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
- player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector);
+ player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector);
player.addListener(this);
MediaSource mediaSource =
- new ExtractorMediaSource.Factory(
- new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"))
- .setExtractorsFactory(MatroskaExtractor.FACTORY)
+ new ProgressiveMediaSource.Factory(
+ new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"),
+ MatroskaExtractor.FACTORY)
.createMediaSource(uri);
player
.createMessage(videoRenderer)
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
index e3081cd2d2..d5da9a011d 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
@@ -15,14 +15,14 @@
*/
package com.google.android.exoplayer2.ext.vp9;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
+import static java.lang.Runtime.getRuntime;
+
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
-import android.support.annotation.CallSuper;
-import android.support.annotation.IntDef;
-import android.support.annotation.Nullable;
+import androidx.annotation.CallSuper;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import android.view.Surface;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
@@ -97,19 +97,17 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*/
public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = C.MSG_CUSTOM_BASE;
- /**
- * The number of input buffers.
- */
- private static final int NUM_INPUT_BUFFERS = 8;
+ /** The number of input buffers. */
+ private final int numInputBuffers;
/**
* The number of output buffers. The renderer may limit the minimum possible value due to
* requiring multiple output buffers to be dequeued at a time for it to make progress.
*/
- private static final int NUM_OUTPUT_BUFFERS = 8;
+ private final int numOutputBuffers;
/** The default input buffer size. */
private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
- private final boolean scaleToFit;
+ private final boolean enableRowMultiThreadMode;
private final boolean disableLoopFilter;
private final long allowedJoiningTimeMs;
private final int maxDroppedFramesToNotify;
@@ -119,7 +117,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private final TimedValueQueue formatQueue;
private final DecoderInputBuffer flagsOnlyBuffer;
private final DrmSessionManager drmSessionManager;
- private final boolean useSurfaceYuvOutput;
+ private final int threads;
private Format format;
private Format pendingFormat;
@@ -127,13 +125,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private VpxDecoder decoder;
private VpxInputBuffer inputBuffer;
private VpxOutputBuffer outputBuffer;
- private DrmSession drmSession;
- private DrmSession pendingDrmSession;
+ @Nullable private DrmSession decoderDrmSession;
+ @Nullable private DrmSession sourceDrmSession;
private @ReinitializationState int decoderReinitializationState;
private boolean decoderReceivedBuffers;
- private Bitmap bitmap;
private boolean renderedFirstFrame;
private long initialPositionUs;
private long joiningDeadlineMs;
@@ -158,16 +155,14 @@ public class LibvpxVideoRenderer extends BaseRenderer {
protected DecoderCounters decoderCounters;
/**
- * @param scaleToFit Whether video frames should be scaled to fit when rendering.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
*/
- public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs) {
- this(scaleToFit, allowedJoiningTimeMs, null, null, 0);
+ public LibvpxVideoRenderer(long allowedJoiningTimeMs) {
+ this(allowedJoiningTimeMs, null, null, 0);
}
/**
- * @param scaleToFit Whether video frames should be scaled to fit when rendering.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
@@ -176,23 +171,22 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
*/
- public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
- Handler eventHandler, VideoRendererEventListener eventListener,
+ public LibvpxVideoRenderer(
+ long allowedJoiningTimeMs,
+ Handler eventHandler,
+ VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
this(
- scaleToFit,
allowedJoiningTimeMs,
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false,
- /* disableLoopFilter= */ false,
- /* useSurfaceYuvOutput= */ false);
+ /* disableLoopFilter= */ false);
}
/**
- * @param scaleToFit Whether video frames should be scaled to fit when rendering.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
@@ -208,10 +202,51 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
- * @param useSurfaceYuvOutput Directly output YUV to the Surface via ANativeWindow.
*/
public LibvpxVideoRenderer(
- boolean scaleToFit,
+ long allowedJoiningTimeMs,
+ Handler eventHandler,
+ VideoRendererEventListener eventListener,
+ int maxDroppedFramesToNotify,
+ DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
+ boolean disableLoopFilter) {
+ this(
+ allowedJoiningTimeMs,
+ eventHandler,
+ eventListener,
+ maxDroppedFramesToNotify,
+ drmSessionManager,
+ playClearSamplesWithoutKeys,
+ disableLoopFilter,
+ /* enableRowMultiThreadMode= */ false,
+ getRuntime().availableProcessors(),
+ /* numInputBuffers= */ 4,
+ /* numOutputBuffers= */ 4);
+ }
+
+ /**
+ * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+ * can attempt to seamlessly join an ongoing playback.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
+ * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
+ * @param drmSessionManager For use with encrypted media. May be null if support for encrypted
+ * media is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisition. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
+ * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled.
+ * @param threads Number of threads libvpx will use to decode.
+ * @param numInputBuffers Number of input buffers.
+ * @param numOutputBuffers Number of output buffers.
+ */
+ public LibvpxVideoRenderer(
long allowedJoiningTimeMs,
Handler eventHandler,
VideoRendererEventListener eventListener,
@@ -219,15 +254,20 @@ public class LibvpxVideoRenderer extends BaseRenderer {
DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys,
boolean disableLoopFilter,
- boolean useSurfaceYuvOutput) {
+ boolean enableRowMultiThreadMode,
+ int threads,
+ int numInputBuffers,
+ int numOutputBuffers) {
super(C.TRACK_TYPE_VIDEO);
- this.scaleToFit = scaleToFit;
this.disableLoopFilter = disableLoopFilter;
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
this.drmSessionManager = drmSessionManager;
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
- this.useSurfaceYuvOutput = useSurfaceYuvOutput;
+ this.enableRowMultiThreadMode = enableRowMultiThreadMode;
+ this.threads = threads;
+ this.numInputBuffers = numInputBuffers;
+ this.numOutputBuffers = numOutputBuffers;
joiningDeadlineMs = C.TIME_UNSET;
clearReportedVideoSize();
formatHolder = new FormatHolder();
@@ -364,24 +404,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
clearReportedVideoSize();
clearRenderedFirstFrame();
try {
+ setSourceDrmSession(null);
releaseDecoder();
} finally {
- try {
- if (drmSession != null) {
- drmSessionManager.releaseSession(drmSession);
- }
- } finally {
- try {
- if (pendingDrmSession != null && pendingDrmSession != drmSession) {
- drmSessionManager.releaseSession(pendingDrmSession);
- }
- } finally {
- drmSession = null;
- pendingDrmSession = null;
- decoderCounters.ensureUpdated();
- eventDispatcher.disabled(decoderCounters);
- }
- }
+ eventDispatcher.disabled(decoderCounters);
}
}
@@ -433,18 +459,35 @@ public class LibvpxVideoRenderer extends BaseRenderer {
/** Releases the decoder. */
@CallSuper
protected void releaseDecoder() {
- if (decoder == null) {
- return;
- }
-
inputBuffer = null;
outputBuffer = null;
- decoder.release();
- decoder = null;
- decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false;
buffersInCodecCount = 0;
+ if (decoder != null) {
+ decoder.release();
+ decoder = null;
+ decoderCounters.decoderReleaseCount++;
+ }
+ setDecoderDrmSession(null);
+ }
+
+ private void setSourceDrmSession(@Nullable DrmSession session) {
+ DrmSession previous = sourceDrmSession;
+ sourceDrmSession = session;
+ releaseDrmSessionIfUnused(previous);
+ }
+
+ private void setDecoderDrmSession(@Nullable DrmSession session) {
+ DrmSession previous = decoderDrmSession;
+ decoderDrmSession = session;
+ releaseDrmSessionIfUnused(previous);
+ }
+
+ private void releaseDrmSessionIfUnused(@Nullable DrmSession session) {
+ if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
+ drmSessionManager.releaseSession(session);
+ }
}
/**
@@ -467,16 +510,20 @@ public class LibvpxVideoRenderer extends BaseRenderer {
throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
}
- pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
- if (pendingDrmSession == drmSession) {
- drmSessionManager.releaseSession(pendingDrmSession);
+ DrmSession session =
+ drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
+ if (session == decoderDrmSession || session == sourceDrmSession) {
+ // We already had this session. The manager must be reference counting, so release it once
+ // to get the count attributed to this renderer back down to 1.
+ drmSessionManager.releaseSession(session);
}
+ setSourceDrmSession(session);
} else {
- pendingDrmSession = null;
+ setSourceDrmSession(null);
}
}
- if (pendingDrmSession != drmSession) {
+ if (sourceDrmSession != decoderDrmSession) {
if (decoderReceivedBuffers) {
// Signal end of stream and wait for any final output buffers before re-initialization.
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
@@ -579,18 +626,14 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*/
protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException {
int bufferMode = outputBuffer.mode;
- boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null;
boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
- if (!renderRgb && !renderYuv && !renderSurface) {
+ if (!renderYuv && !renderSurface) {
dropOutputBuffer(outputBuffer);
} else {
maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
- if (renderRgb) {
- renderRgbFrame(outputBuffer, scaleToFit);
- outputBuffer.release();
- } else if (renderYuv) {
+ if (renderYuv) {
outputBufferRenderer.setOutputBuffer(outputBuffer);
// The renderer will release the buffer.
} else { // renderSurface
@@ -668,8 +711,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
this.surface = surface;
this.outputBufferRenderer = outputBufferRenderer;
if (surface != null) {
- outputMode =
- useSurfaceYuvOutput ? VpxDecoder.OUTPUT_MODE_SURFACE_YUV : VpxDecoder.OUTPUT_MODE_RGB;
+ outputMode = VpxDecoder.OUTPUT_MODE_SURFACE_YUV;
} else {
outputMode =
outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE;
@@ -704,12 +746,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
return;
}
- drmSession = pendingDrmSession;
+ setDecoderDrmSession(sourceDrmSession);
+
ExoMediaCrypto mediaCrypto = null;
- if (drmSession != null) {
- mediaCrypto = drmSession.getMediaCrypto();
+ if (decoderDrmSession != null) {
+ mediaCrypto = decoderDrmSession.getMediaCrypto();
if (mediaCrypto == null) {
- DrmSessionException drmError = drmSession.getError();
+ DrmSessionException drmError = decoderDrmSession.getError();
if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
// input format causes the session to be replaced before it's used.
@@ -727,12 +770,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
decoder =
new VpxDecoder(
- NUM_INPUT_BUFFERS,
- NUM_OUTPUT_BUFFERS,
+ numInputBuffers,
+ numOutputBuffers,
initialInputBufferSize,
mediaCrypto,
disableLoopFilter,
- useSurfaceYuvOutput);
+ enableRowMultiThreadMode,
+ threads);
decoder.setOutputMode(outputMode);
TraceUtil.endSection();
long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
@@ -922,33 +966,16 @@ public class LibvpxVideoRenderer extends BaseRenderer {
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
+ if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
- @DrmSession.State int drmSessionState = drmSession.getState();
+ @DrmSession.State int drmSessionState = decoderDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
}
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
- private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) {
- if (bitmap == null
- || bitmap.getWidth() != outputBuffer.width
- || bitmap.getHeight() != outputBuffer.height) {
- bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565);
- }
- bitmap.copyPixelsFromBuffer(outputBuffer.data);
- Canvas canvas = surface.lockCanvas(null);
- if (scale) {
- canvas.scale(
- ((float) canvas.getWidth()) / outputBuffer.width,
- ((float) canvas.getHeight()) / outputBuffer.height);
- }
- canvas.drawBitmap(bitmap, 0, 0, null);
- surface.unlockCanvasAndPost(canvas);
- }
-
private void setJoiningDeadlineMs() {
joiningDeadlineMs = allowedJoiningTimeMs > 0
? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
index 51ef8e9bcf..57e5481b55 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
@@ -31,8 +31,7 @@ import java.nio.ByteBuffer;
public static final int OUTPUT_MODE_NONE = -1;
public static final int OUTPUT_MODE_YUV = 0;
- public static final int OUTPUT_MODE_RGB = 1;
- public static final int OUTPUT_MODE_SURFACE_YUV = 2;
+ public static final int OUTPUT_MODE_SURFACE_YUV = 1;
private static final int NO_ERROR = 0;
private static final int DECODE_ERROR = 1;
@@ -52,7 +51,8 @@ import java.nio.ByteBuffer;
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
- * @param enableSurfaceYuvOutputMode Whether OUTPUT_MODE_SURFACE_YUV is allowed.
+ * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled.
+ * @param threads Number of threads libvpx will use to decode.
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
*/
public VpxDecoder(
@@ -61,7 +61,8 @@ import java.nio.ByteBuffer;
int initialInputBufferSize,
ExoMediaCrypto exoMediaCrypto,
boolean disableLoopFilter,
- boolean enableSurfaceYuvOutputMode)
+ boolean enableRowMultiThreadMode,
+ int threads)
throws VpxDecoderException {
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) {
@@ -71,7 +72,7 @@ import java.nio.ByteBuffer;
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
}
- vpxDecContext = vpxInit(disableLoopFilter, enableSurfaceYuvOutputMode);
+ vpxDecContext = vpxInit(disableLoopFilter, enableRowMultiThreadMode, threads);
if (vpxDecContext == 0) {
throw new VpxDecoderException("Failed to initialize decoder");
}
@@ -86,8 +87,8 @@ import java.nio.ByteBuffer;
/**
* Sets the output mode for frames rendered by the decoder.
*
- * @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE}, {@link #OUTPUT_MODE_RGB}
- * and {@link #OUTPUT_MODE_YUV}.
+ * @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE} and {@link
+ * #OUTPUT_MODE_YUV}.
*/
public void setOutputMode(int outputMode) {
this.outputMode = outputMode;
@@ -168,7 +169,8 @@ import java.nio.ByteBuffer;
}
}
- private native long vpxInit(boolean disableLoopFilter, boolean enableSurfaceYuvOutputMode);
+ private native long vpxInit(
+ boolean disableLoopFilter, boolean enableRowMultiThreadMode, int threads);
private native long vpxClose(long context);
private native long vpxDecode(long context, ByteBuffer encoded, int length);
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
index 854576b4b2..5a65fc56ff 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
@@ -27,7 +27,7 @@ public final class VpxLibrary {
ExoPlayerLibraryInfo.registerModule("goog.exo.vpx");
}
- private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxJNI");
+ private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxV2JNI");
private VpxLibrary() {}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
index fa0df1cfa9..22330e0a05 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
@@ -19,10 +19,8 @@ import com.google.android.exoplayer2.decoder.OutputBuffer;
import com.google.android.exoplayer2.video.ColorInfo;
import java.nio.ByteBuffer;
-/**
- * Output buffer containing video frame data, populated by {@link VpxDecoder}.
- */
-/* package */ final class VpxOutputBuffer extends OutputBuffer {
+/** Output buffer containing video frame data, populated by {@link VpxDecoder}. */
+public final class VpxOutputBuffer extends OutputBuffer {
public static final int COLORSPACE_UNKNOWN = 0;
public static final int COLORSPACE_BT601 = 1;
@@ -62,36 +60,20 @@ import java.nio.ByteBuffer;
* Initializes the buffer.
*
* @param timeUs The presentation timestamp for the buffer, in microseconds.
- * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE},
- * {@link VpxDecoder#OUTPUT_MODE_RGB} and {@link VpxDecoder#OUTPUT_MODE_YUV}.
+ * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE} and {@link
+ * VpxDecoder#OUTPUT_MODE_YUV}.
*/
public void init(long timeUs, int mode) {
this.timeUs = timeUs;
this.mode = mode;
}
- /**
- * Resizes the buffer based on the given dimensions. Called via JNI after decoding completes.
- * @return Whether the buffer was resized successfully.
- */
- public boolean initForRgbFrame(int width, int height) {
- this.width = width;
- this.height = height;
- this.yuvPlanes = null;
- if (!isSafeToMultiply(width, height) || !isSafeToMultiply(width * height, 2)) {
- return false;
- }
- int minimumRgbSize = width * height * 2;
- initData(minimumRgbSize);
- return true;
- }
-
/**
* Resizes the buffer based on the given stride. Called via JNI after decoding completes.
+ *
* @return Whether the buffer was resized successfully.
*/
- public boolean initForYuvFrame(int width, int height, int yStride, int uvStride,
- int colorspace) {
+ public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) {
this.width = width;
this.height = height;
this.colorspace = colorspace;
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java
index 837539593e..d82f5a6071 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java
@@ -17,8 +17,7 @@ package com.google.android.exoplayer2.ext.vp9;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
+import com.google.android.exoplayer2.util.GlUtil;
import java.nio.FloatBuffer;
import java.util.concurrent.atomic.AtomicReference;
import javax.microedition.khronos.egl.EGLConfig;
@@ -72,11 +71,8 @@ import javax.microedition.khronos.opengles.GL10;
+ " gl_FragColor = vec4(mColorConversion * yuv, 1.0);\n"
+ "}\n";
- private static final FloatBuffer TEXTURE_VERTICES = nativeFloatBuffer(
- -1.0f, 1.0f,
- -1.0f, -1.0f,
- 1.0f, 1.0f,
- 1.0f, -1.0f);
+ private static final FloatBuffer TEXTURE_VERTICES =
+ GlUtil.createBuffer(new float[] {-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f});
private final int[] yuvTextures = new int[3];
private final AtomicReference pendingOutputBufferReference;
@@ -114,21 +110,7 @@ import javax.microedition.khronos.opengles.GL10;
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
- // Create the GL program.
- program = GLES20.glCreateProgram();
-
- // Add the vertex and fragment shaders.
- addShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER, program);
- addShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER, program);
-
- // Link the GL program.
- GLES20.glLinkProgram(program);
- int[] result = new int[] {
- GLES20.GL_FALSE
- };
- result[0] = GLES20.GL_FALSE;
- GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, result, 0);
- abortUnless(result[0] == GLES20.GL_TRUE, GLES20.glGetProgramInfoLog(program));
+ program = GlUtil.compileProgram(VERTEX_SHADER, FRAGMENT_SHADER);
GLES20.glUseProgram(program);
int posLocation = GLES20.glGetAttribLocation(program, "in_pos");
GLES20.glEnableVertexAttribArray(posLocation);
@@ -136,11 +118,11 @@ import javax.microedition.khronos.opengles.GL10;
posLocation, 2, GLES20.GL_FLOAT, false, 0, TEXTURE_VERTICES);
texLocation = GLES20.glGetAttribLocation(program, "in_tc");
GLES20.glEnableVertexAttribArray(texLocation);
- checkNoGLES2Error();
+ GlUtil.checkGlError();
colorMatrixLocation = GLES20.glGetUniformLocation(program, "mColorConversion");
- checkNoGLES2Error();
+ GlUtil.checkGlError();
setupTextures();
- checkNoGLES2Error();
+ GlUtil.checkGlError();
}
@Override
@@ -191,11 +173,8 @@ import javax.microedition.khronos.opengles.GL10;
float crop = (float) outputBuffer.width / outputBuffer.yuvStrides[0];
// This buffer is consumed during each call to glDrawArrays. It needs to be a member variable
// rather than a local variable to ensure that it doesn't get garbage collected.
- textureCoords = nativeFloatBuffer(
- 0.0f, 0.0f,
- 0.0f, 1.0f,
- crop, 0.0f,
- crop, 1.0f);
+ textureCoords =
+ GlUtil.createBuffer(new float[] {0.0f, 0.0f, 0.0f, 1.0f, crop, 0.0f, crop, 1.0f});
GLES20.glVertexAttribPointer(
texLocation, 2, GLES20.GL_FLOAT, false, 0, textureCoords);
previousWidth = outputBuffer.width;
@@ -203,23 +182,7 @@ import javax.microedition.khronos.opengles.GL10;
}
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
- checkNoGLES2Error();
- }
-
- private void addShader(int type, String source, int program) {
- int[] result = new int[] {
- GLES20.GL_FALSE
- };
- int shader = GLES20.glCreateShader(type);
- GLES20.glShaderSource(shader, source);
- GLES20.glCompileShader(shader);
- GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, 0);
- abortUnless(result[0] == GLES20.GL_TRUE,
- GLES20.glGetShaderInfoLog(shader) + ", source: " + source);
- GLES20.glAttachShader(program, shader);
- GLES20.glDeleteShader(shader);
-
- checkNoGLES2Error();
+ GlUtil.checkGlError();
}
private void setupTextures() {
@@ -237,28 +200,6 @@ import javax.microedition.khronos.opengles.GL10;
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
}
- checkNoGLES2Error();
+ GlUtil.checkGlError();
}
-
- private void abortUnless(boolean condition, String msg) {
- if (!condition) {
- throw new RuntimeException(msg);
- }
- }
-
- private void checkNoGLES2Error() {
- int error = GLES20.glGetError();
- if (error != GLES20.GL_NO_ERROR) {
- throw new RuntimeException("GLES20 error: " + error);
- }
- }
-
- private static FloatBuffer nativeFloatBuffer(float... array) {
- FloatBuffer buffer = ByteBuffer.allocateDirect(array.length * 4).order(
- ByteOrder.nativeOrder()).asFloatBuffer();
- buffer.put(array);
- buffer.flip();
- return buffer;
- }
-
}
diff --git a/extensions/vp9/src/main/jni/Android.mk b/extensions/vp9/src/main/jni/Android.mk
index 868b869d56..cb7571a1b0 100644
--- a/extensions/vp9/src/main/jni/Android.mk
+++ b/extensions/vp9/src/main/jni/Android.mk
@@ -17,27 +17,21 @@
WORKING_DIR := $(call my-dir)
include $(CLEAR_VARS)
LIBVPX_ROOT := $(WORKING_DIR)/libvpx
-LIBYUV_ROOT := $(WORKING_DIR)/libyuv
-
-# build libyuv_static.a
-LOCAL_PATH := $(WORKING_DIR)
-LIBYUV_DISABLE_JPEG := "yes"
-include $(LIBYUV_ROOT)/Android.mk
# build libvpx.so
LOCAL_PATH := $(WORKING_DIR)
include libvpx.mk
-# build libvpxJNI.so
+# build libvpxV2JNI.so
include $(CLEAR_VARS)
LOCAL_PATH := $(WORKING_DIR)
-LOCAL_MODULE := libvpxJNI
+LOCAL_MODULE := libvpxV2JNI
LOCAL_ARM_MODE := arm
LOCAL_CPP_EXTENSION := .cc
LOCAL_SRC_FILES := vpx_jni.cc
LOCAL_LDLIBS := -llog -lz -lm -landroid
LOCAL_SHARED_LIBRARIES := libvpx
-LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures
+LOCAL_STATIC_LIBRARIES := cpufeatures
include $(BUILD_SHARED_LIBRARY)
$(call import-module,android/cpufeatures)
diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc
index f36c433b22..82c023afbc 100644
--- a/extensions/vp9/src/main/jni/vpx_jni.cc
+++ b/extensions/vp9/src/main/jni/vpx_jni.cc
@@ -30,8 +30,6 @@
#include
#include
-#include "libyuv.h" // NOLINT
-
#define VPX_CODEC_DISABLE_COMPAT 1
#include "vpx/vpx_decoder.h"
#include "vpx/vp8dx.h"
@@ -61,7 +59,6 @@
(JNIEnv* env, jobject thiz, ##__VA_ARGS__)\
// JNI references for VpxOutputBuffer class.
-static jmethodID initForRgbFrame;
static jmethodID initForYuvFrame;
static jfieldID dataField;
static jfieldID outputModeField;
@@ -393,11 +390,7 @@ class JniBufferManager {
};
struct JniCtx {
- JniCtx(bool enableBufferManager) {
- if (enableBufferManager) {
- buffer_manager = new JniBufferManager();
- }
- }
+ JniCtx() { buffer_manager = new JniBufferManager(); }
~JniCtx() {
if (native_window) {
@@ -441,11 +434,11 @@ int vpx_release_frame_buffer(void* priv, vpx_codec_frame_buffer_t* fb) {
}
DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
- jboolean enableBufferManager) {
- JniCtx* context = new JniCtx(enableBufferManager);
+ jboolean enableRowMultiThreadMode, jint threads) {
+ JniCtx* context = new JniCtx();
context->decoder = new vpx_codec_ctx_t();
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
- cfg.threads = android_getCpuCount();
+ cfg.threads = threads;
errorCode = 0;
vpx_codec_err_t err =
vpx_codec_dec_init(context->decoder, &vpx_codec_vp9_dx_algo, &cfg, 0);
@@ -454,21 +447,33 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
errorCode = err;
return 0;
}
+#ifdef VPX_CTRL_VP9_DECODE_SET_ROW_MT
+ err = vpx_codec_control(context->decoder, VP9D_SET_ROW_MT,
+ enableRowMultiThreadMode);
+ if (err) {
+ LOGE("ERROR: Failed to enable row multi thread mode, error = %d.", err);
+ }
+#endif
if (disableLoopFilter) {
- // TODO(b/71930387): Use vpx_codec_control(), not vpx_codec_control_().
- err = vpx_codec_control_(context->decoder, VP9_SET_SKIP_LOOP_FILTER, true);
+ err = vpx_codec_control(context->decoder, VP9_SET_SKIP_LOOP_FILTER, true);
if (err) {
LOGE("ERROR: Failed to shut off libvpx loop filter, error = %d.", err);
}
- }
- if (enableBufferManager) {
- err = vpx_codec_set_frame_buffer_functions(
- context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,
- context->buffer_manager);
+#ifdef VPX_CTRL_VP9_SET_LOOP_FILTER_OPT
+ } else {
+ err = vpx_codec_control(context->decoder, VP9D_SET_LOOP_FILTER_OPT, true);
if (err) {
- LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.",
+ LOGE("ERROR: Failed to enable loop filter optimization, error = %d.",
err);
}
+#endif
+ }
+ err = vpx_codec_set_frame_buffer_functions(
+ context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,
+ context->buffer_manager);
+ if (err) {
+ LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.",
+ err);
}
// Populate JNI References.
@@ -476,8 +481,6 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
"(IIIII)Z");
- initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame",
- "(II)Z");
dataField = env->GetFieldID(outputBufferClass, "data",
"Ljava/nio/ByteBuffer;");
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
@@ -529,28 +532,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
}
const int kOutputModeYuv = 0;
- const int kOutputModeRgb = 1;
- const int kOutputModeSurfaceYuv = 2;
+ const int kOutputModeSurfaceYuv = 1;
int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
- if (outputMode == kOutputModeRgb) {
- // resize buffer if required.
- jboolean initResult = env->CallBooleanMethod(jOutputBuffer, initForRgbFrame,
- img->d_w, img->d_h);
- if (env->ExceptionCheck() || !initResult) {
- return -1;
- }
-
- // get pointer to the data buffer.
- const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField);
- uint8_t* const dst =
- reinterpret_cast(env->GetDirectBufferAddress(dataObject));
-
- libyuv::I420ToRGB565(img->planes[VPX_PLANE_Y], img->stride[VPX_PLANE_Y],
- img->planes[VPX_PLANE_U], img->stride[VPX_PLANE_U],
- img->planes[VPX_PLANE_V], img->stride[VPX_PLANE_V],
- dst, img->d_w * 2, img->d_w, img->d_h);
- } else if (outputMode == kOutputModeYuv) {
+ if (outputMode == kOutputModeYuv) {
const int kColorspaceUnknown = 0;
const int kColorspaceBT601 = 1;
const int kColorspaceBT709 = 2;
@@ -608,9 +593,6 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
}
} else if (outputMode == kOutputModeSurfaceYuv &&
img->fmt != VPX_IMG_FMT_I42016) {
- if (!context->buffer_manager) {
- return -1; // enableBufferManager was not set in vpxInit.
- }
int id = *(int*)img->fb_priv;
context->buffer_manager->add_ref(id);
JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id);
diff --git a/extensions/vp9/src/test/AndroidManifest.xml b/extensions/vp9/src/test/AndroidManifest.xml
new file mode 100644
index 0000000000..a0123f17db
--- /dev/null
+++ b/extensions/vp9/src/test/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/extensions/vp9/src/test/java/com/google/android/exoplayer2/ext/vp9/DefaultRenderersFactoryTest.java b/extensions/vp9/src/test/java/com/google/android/exoplayer2/ext/vp9/DefaultRenderersFactoryTest.java
new file mode 100644
index 0000000000..33de600aa7
--- /dev/null
+++ b/extensions/vp9/src/test/java/com/google/android/exoplayer2/ext/vp9/DefaultRenderersFactoryTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2019 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.vp9;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link DefaultRenderersFactoryTest} with {@link LibvpxVideoRenderer}. */
+@RunWith(AndroidJUnit4.class)
+public final class DefaultRenderersFactoryTest {
+
+ @Test
+ public void createRenderers_instantiatesVpxRenderer() {
+ DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
+ LibvpxVideoRenderer.class, C.TRACK_TYPE_VIDEO);
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index b55575bc3b..4b9bfa8fa2 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,5 @@
## Project-wide Gradle settings.
-android.useDeprecatedNdk=true
+android.useAndroidX=true
+android.enableJetifier=true
+android.enableUnitTestBinaryResources=true
buildDir=buildout
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 5559e8ccfa..6d00e1ce97 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Tue Sep 05 13:43:42 BST 2017
+#Thu Apr 25 13:15:25 BST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
diff --git a/javadoc_combined.gradle b/javadoc_combined.gradle
index 209ad3a1a3..d2fa241a81 100644
--- a/javadoc_combined.gradle
+++ b/javadoc_combined.gradle
@@ -42,7 +42,7 @@ class CombinedJavadocPlugin implements Plugin {
if (name == "release") {
classpath +=
libraryModule.project.files(
- variant.javaCompile.classpath.files,
+ variant.javaCompileProvider.get().classpath.files,
libraryModule.project.android.getBootClasspath())
}
}
diff --git a/javadoc_library.gradle b/javadoc_library.gradle
index 65219843e3..a818ea390e 100644
--- a/javadoc_library.gradle
+++ b/javadoc_library.gradle
@@ -21,7 +21,7 @@ android.libraryVariants.all { variant ->
task("generateJavadoc", type: Javadoc) {
description = "Generates Javadoc for the ${javadocTitle}."
title = "ExoPlayer ${javadocTitle}"
- source = variant.javaCompile.source
+ source = variant.javaCompileProvider.get().source
options {
links "http://docs.oracle.com/javase/7/docs/api/"
linksOffline "https://developer.android.com/reference",
@@ -33,7 +33,7 @@ android.libraryVariants.all { variant ->
doFirst {
classpath =
files(
- variant.javaCompile.classpath.files,
+ variant.javaCompileProvider.get().classpath.files,
project.android.getBootClasspath())
}
doLast {
diff --git a/library/all/README.md b/library/all/README.md
index 8746e3afc6..43f942116e 100644
--- a/library/all/README.md
+++ b/library/all/README.md
@@ -10,4 +10,4 @@ individually. See ExoPlayer's [top level README][] for more information.
* [Javadoc][]: Note that this Javadoc is combined with that of other modules.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/library/all/build.gradle b/library/all/build.gradle
index bb832ba0ff..f78b8b2132 100644
--- a/library/all/build.gradle
+++ b/library/all/build.gradle
@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion project.ext.minSdkVersion
diff --git a/library/core/README.md b/library/core/README.md
index f31ffed131..7fa89dda8d 100644
--- a/library/core/README.md
+++ b/library/core/README.md
@@ -6,4 +6,4 @@ The core of the ExoPlayer library.
* [Javadoc][]: Note that this Javadoc is combined with that of other modules.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/library/core/build.gradle b/library/core/build.gradle
index 606033fdea..68ff8cc977 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -16,7 +16,6 @@ apply from: '../../constants.gradle'
android {
compileSdkVersion project.ext.compileSdkVersion
- buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -54,21 +53,25 @@ android {
// testCoverageEnabled = true
// }
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'androidx.annotation:annotation:1.0.2'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
- androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
+ androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
+ androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
+ androidTestImplementation 'androidx.test.ext:truth:' + androidXTestVersion
androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
- androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
- androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
- androidTestImplementation 'com.google.truth:truth:' + truthVersion
+ androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion
+ androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion
- testImplementation 'com.google.truth:truth:' + truthVersion
- testImplementation 'junit:junit:' + junitVersion
+ testImplementation 'androidx.test:core:' + androidXTestVersion
+ testImplementation 'androidx.test.ext:junit:' + androidXTestVersion
+ testImplementation 'androidx.test.ext:truth:' + androidXTestVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt
index a5c50a78f5..07ba438182 100644
--- a/library/core/proguard-rules.txt
+++ b/library/core/proguard-rules.txt
@@ -3,7 +3,7 @@
# Constructors accessed via reflection in DefaultRenderersFactory
-dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer
-keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer {
- (boolean, long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
+ (long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
}
-dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer
-keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer {
@@ -30,10 +30,36 @@
();
}
-# Constructors accessed via reflection in DownloadAction
--dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloadAction
--dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction
--dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction
+# Constructors accessed via reflection in DefaultDownloaderFactory
+-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloader
+-keepclassmembers class com.google.android.exoplayer2.source.dash.offline.DashDownloader {
+ (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);
+}
+-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloader
+-keepclassmembers class com.google.android.exoplayer2.source.hls.offline.HlsDownloader {
+ (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);
+}
+-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader
+-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader {
+ (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);
+}
+
+# Constructors accessed via reflection in DownloadHelper
+-dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory
+-keepclassmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory {
+ (com.google.android.exoplayer2.upstream.DataSource$Factory);
+ com.google.android.exoplayer2.source.dash.DashMediaSource createMediaSource(android.net.Uri);
+}
+-dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory
+-keepclassmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory {
+ (com.google.android.exoplayer2.upstream.DataSource$Factory);
+ com.google.android.exoplayer2.source.hls.HlsMediaSource createMediaSource(android.net.Uri);
+}
+-dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory
+-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
+ (com.google.android.exoplayer2.upstream.DataSource$Factory);
+ com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource createMediaSource(android.net.Uri);
+}
# Don't warn about checkerframework
-dontwarn org.checkerframework.**
diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml
index d9104b1077..e6e874a27a 100644
--- a/library/core/src/androidTest/AndroidManifest.xml
+++ b/library/core/src/androidTest/AndroidManifest.xml
@@ -18,6 +18,9 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.core.test">
+
+
+
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
index 45b784e30f..a76b5cf6c1 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
@@ -26,10 +26,10 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.FileNotFoundException;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java
index eb3bd4f91a..774f1b452c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
/** Abstract base {@link Player} which implements common implementation independent methods. */
@@ -135,4 +135,58 @@ public abstract class BasePlayer implements Player {
@RepeatMode int repeatMode = getRepeatMode();
return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode;
}
+
+ /** Holds a listener reference. */
+ protected static final class ListenerHolder {
+
+ /**
+ * The listener on which {link #invoke} will execute {@link ListenerInvocation listener
+ * invocations}.
+ */
+ public final Player.EventListener listener;
+
+ private boolean released;
+
+ public ListenerHolder(Player.EventListener listener) {
+ this.listener = listener;
+ }
+
+ /** Prevents any further {@link ListenerInvocation} to be executed on {@link #listener}. */
+ public void release() {
+ released = true;
+ }
+
+ /**
+ * Executes the given {@link ListenerInvocation} on {@link #listener}. Does nothing if {@link
+ * #release} has been called on this instance.
+ */
+ public void invoke(ListenerInvocation listenerInvocation) {
+ if (!released) {
+ listenerInvocation.invokeListener(listener);
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ return listener.equals(((ListenerHolder) other).listener);
+ }
+
+ @Override
+ public int hashCode() {
+ return listener.hashCode();
+ }
+ }
+
+ /** Parameterized invocation of a {@link Player.EventListener} method. */
+ protected interface ListenerInvocation {
+
+ /** Executes the invocation on the given {@link Player.EventListener}. */
+ void invokeListener(Player.EventListener listener);
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
index 51e724bee1..1099b14bfc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSessionManager;
@@ -37,7 +37,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
private SampleStream stream;
private Format[] streamFormats;
private long streamOffsetUs;
- private boolean readEndOfStream;
+ private long readingPositionUs;
private boolean streamIsFinal;
/**
@@ -46,7 +46,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
*/
public BaseRenderer(int trackType) {
this.trackType = trackType;
- readEndOfStream = true;
+ readingPositionUs = C.TIME_END_OF_SOURCE;
}
@Override
@@ -98,7 +98,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
throws ExoPlaybackException {
Assertions.checkState(!streamIsFinal);
this.stream = stream;
- readEndOfStream = false;
+ readingPositionUs = offsetUs;
streamFormats = formats;
streamOffsetUs = offsetUs;
onStreamChanged(formats, offsetUs);
@@ -111,7 +111,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
@Override
public final boolean hasReadStreamToEnd() {
- return readEndOfStream;
+ return readingPositionUs == C.TIME_END_OF_SOURCE;
+ }
+
+ @Override
+ public final long getReadingPositionUs() {
+ return readingPositionUs;
}
@Override
@@ -132,7 +137,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
@Override
public final void resetPosition(long positionUs) throws ExoPlaybackException {
streamIsFinal = false;
- readEndOfStream = false;
+ readingPositionUs = positionUs;
onPositionReset(positionUs, false);
}
@@ -153,6 +158,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
onDisabled();
}
+ @Override
+ public final void reset() {
+ Assertions.checkState(state == STATE_DISABLED);
+ onReset();
+ }
+
// RendererCapabilities implementation.
@Override
@@ -247,6 +258,15 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
// Do nothing.
}
+ /**
+ * Called when the renderer is reset.
+ *
+ * The default implementation is a no-op.
+ */
+ protected void onReset() {
+ // Do nothing.
+ }
+
// Methods to be called by subclasses.
/** Returns the formats of the currently enabled stream. */
@@ -288,10 +308,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
int result = stream.readData(formatHolder, buffer, formatRequired);
if (result == C.RESULT_BUFFER_READ) {
if (buffer.isEndOfStream()) {
- readEndOfStream = true;
+ readingPositionUs = C.TIME_END_OF_SOURCE;
return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;
}
buffer.timeUs += streamOffsetUs;
+ readingPositionUs = Math.max(readingPositionUs, buffer.timeUs);
} else if (result == C.RESULT_FORMAT_READ) {
Format format = formatHolder.format;
if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
@@ -317,7 +338,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* Returns whether the upstream source is ready.
*/
protected final boolean isSourceReady() {
- return readEndOfStream ? streamIsFinal : stream.isReady();
+ return hasReadStreamToEnd() ? streamIsFinal : stream.isReady();
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java
index 8810b51000..04a90b38d8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/C.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java
@@ -21,7 +21,7 @@ import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.media.MediaFormat;
-import android.support.annotation.IntDef;
+import androidx.annotation.IntDef;
import android.view.Surface;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.audio.AuxEffectInfo;
@@ -101,6 +101,9 @@ public final class C {
*/
public static final String UTF16_NAME = "UTF-16";
+ /** The name of the UTF-16 little-endian charset. */
+ public static final String UTF16LE_NAME = "UTF-16LE";
+
/**
* The name of the serif font family.
*/
@@ -143,8 +146,8 @@ public final class C {
* {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
* #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link
- * #ENCODING_E_AC3}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link
- * #ENCODING_DOLBY_TRUEHD}.
+ * #ENCODING_E_AC3}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or
+ * {@link #ENCODING_DOLBY_TRUEHD}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@@ -160,9 +163,10 @@ public final class C {
ENCODING_PCM_A_LAW,
ENCODING_AC3,
ENCODING_E_AC3,
+ ENCODING_AC4,
ENCODING_DTS,
ENCODING_DTS_HD,
- ENCODING_DOLBY_TRUEHD
+ ENCODING_DOLBY_TRUEHD,
})
public @interface Encoding {}
@@ -206,6 +210,8 @@ public final class C {
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/** @see AudioFormat#ENCODING_E_AC3 */
public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
+ /** @see AudioFormat#ENCODING_AC4 */
+ public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4;
/** @see AudioFormat#ENCODING_DTS */
public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
/** @see AudioFormat#ENCODING_DTS_HD */
@@ -536,9 +542,7 @@ public final class C {
*/
public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4
- /**
- * Represents an undetermined language as an ISO 639 alpha-3 language code.
- */
+ /** Represents an undetermined language as an ISO 639-2 language code. */
public static final String LANGUAGE_UNDETERMINED = "und";
/**
@@ -978,6 +982,79 @@ public final class C {
*/
public static final int NETWORK_TYPE_OTHER = 8;
+ /**
+ * Track role flags. Possible flag values are {@link #ROLE_FLAG_MAIN}, {@link
+ * #ROLE_FLAG_ALTERNATE}, {@link #ROLE_FLAG_SUPPLEMENTARY}, {@link #ROLE_FLAG_COMMENTARY}, {@link
+ * #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link
+ * #ROLE_FLAG_SUBTITLE}, {@link #ROLE_FLAG_SIGN}, {@link #ROLE_FLAG_DESCRIBES_VIDEO}, {@link
+ * #ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND}, {@link #ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY},
+ * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG} and {@link #ROLE_FLAG_EASY_TO_READ}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ ROLE_FLAG_MAIN,
+ ROLE_FLAG_ALTERNATE,
+ ROLE_FLAG_SUPPLEMENTARY,
+ ROLE_FLAG_COMMENTARY,
+ ROLE_FLAG_DUB,
+ ROLE_FLAG_EMERGENCY,
+ ROLE_FLAG_CAPTION,
+ ROLE_FLAG_SUBTITLE,
+ ROLE_FLAG_SIGN,
+ ROLE_FLAG_DESCRIBES_VIDEO,
+ ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND,
+ ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY,
+ ROLE_FLAG_TRANSCRIBES_DIALOG,
+ ROLE_FLAG_EASY_TO_READ
+ })
+ public @interface RoleFlags {}
+ /** Indicates a main track. */
+ public static final int ROLE_FLAG_MAIN = 1;
+ /**
+ * Indicates an alternate track. For example a video track recorded from an different view point
+ * than the main track(s).
+ */
+ public static final int ROLE_FLAG_ALTERNATE = 1 << 1;
+ /**
+ * Indicates a supplementary track, meaning the track has lower importance than the main track(s).
+ * For example a video track that provides a visual accompaniment to a main audio track.
+ */
+ public static final int ROLE_FLAG_SUPPLEMENTARY = 1 << 2;
+ /** Indicates the track contains commentary, for example from the director. */
+ public static final int ROLE_FLAG_COMMENTARY = 1 << 3;
+ /**
+ * Indicates the track is in a different language from the original, for example dubbed audio or
+ * translated captions.
+ */
+ public static final int ROLE_FLAG_DUB = 1 << 4;
+ /** Indicates the track contains information about a current emergency. */
+ public static final int ROLE_FLAG_EMERGENCY = 1 << 5;
+ /**
+ * Indicates the track contains captions. This flag may be set on video tracks to indicate the
+ * presence of burned in captions.
+ */
+ public static final int ROLE_FLAG_CAPTION = 1 << 6;
+ /**
+ * Indicates the track contains subtitles. This flag may be set on video tracks to indicate the
+ * presence of burned in subtitles.
+ */
+ public static final int ROLE_FLAG_SUBTITLE = 1 << 7;
+ /** Indicates the track contains a visual sign-language interpretation of an audio track. */
+ public static final int ROLE_FLAG_SIGN = 1 << 8;
+ /** Indicates the track contains an audio or textual description of a video track. */
+ public static final int ROLE_FLAG_DESCRIBES_VIDEO = 1 << 9;
+ /** Indicates the track contains a textual description of music and sound. */
+ public static final int ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND = 1 << 10;
+ /** Indicates the track is designed for improved intelligibility of dialogue. */
+ public static final int ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY = 1 << 11;
+ /** Indicates the track contains a transcription of spoken dialog. */
+ public static final int ROLE_FLAG_TRANSCRIBES_DIALOG = 1 << 12;
+ /** Indicates the track contains a text that has been edited for ease of reading. */
+ public static final int ROLE_FLAG_EASY_TO_READ = 1 << 13;
+
/**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving
* {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
index c109ed81c1..972f651a41 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
@@ -20,7 +20,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.PriorityTaskManager;
import com.google.android.exoplayer2.util.Util;
/**
@@ -30,12 +29,14 @@ public class DefaultLoadControl implements LoadControl {
/**
* The default minimum duration of media that the player will attempt to ensure is buffered at all
- * times, in milliseconds.
+ * times, in milliseconds. This value is only applied to playbacks without video.
*/
public static final int DEFAULT_MIN_BUFFER_MS = 15000;
/**
* The default maximum duration of media that the player will attempt to buffer, in milliseconds.
+ * For playbacks with video, this is also the default minimum duration of media that the player
+ * will attempt to ensure is buffered.
*/
public static final int DEFAULT_MAX_BUFFER_MS = 50000;
@@ -70,27 +71,26 @@ public class DefaultLoadControl implements LoadControl {
public static final class Builder {
private DefaultAllocator allocator;
- private int minBufferMs;
+ private int minBufferAudioMs;
+ private int minBufferVideoMs;
private int maxBufferMs;
private int bufferForPlaybackMs;
private int bufferForPlaybackAfterRebufferMs;
private int targetBufferBytes;
private boolean prioritizeTimeOverSizeThresholds;
- private PriorityTaskManager priorityTaskManager;
private int backBufferDurationMs;
private boolean retainBackBufferFromKeyframe;
private boolean createDefaultLoadControlCalled;
/** Constructs a new instance. */
public Builder() {
- allocator = null;
- minBufferMs = DEFAULT_MIN_BUFFER_MS;
+ minBufferAudioMs = DEFAULT_MIN_BUFFER_MS;
+ minBufferVideoMs = DEFAULT_MAX_BUFFER_MS;
maxBufferMs = DEFAULT_MAX_BUFFER_MS;
bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS;
bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
targetBufferBytes = DEFAULT_TARGET_BUFFER_BYTES;
prioritizeTimeOverSizeThresholds = DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS;
- priorityTaskManager = null;
backBufferDurationMs = DEFAULT_BACK_BUFFER_DURATION_MS;
retainBackBufferFromKeyframe = DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME;
}
@@ -129,7 +129,18 @@ public class DefaultLoadControl implements LoadControl {
int bufferForPlaybackMs,
int bufferForPlaybackAfterRebufferMs) {
Assertions.checkState(!createDefaultLoadControlCalled);
- this.minBufferMs = minBufferMs;
+ assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0");
+ assertGreaterOrEqual(
+ bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0");
+ assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs");
+ assertGreaterOrEqual(
+ minBufferMs,
+ bufferForPlaybackAfterRebufferMs,
+ "minBufferMs",
+ "bufferForPlaybackAfterRebufferMs");
+ assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs");
+ this.minBufferAudioMs = minBufferMs;
+ this.minBufferVideoMs = minBufferMs;
this.maxBufferMs = maxBufferMs;
this.bufferForPlaybackMs = bufferForPlaybackMs;
this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs;
@@ -165,19 +176,6 @@ public class DefaultLoadControl implements LoadControl {
return this;
}
- /**
- * Sets the {@link PriorityTaskManager} to use.
- *
- * @param priorityTaskManager The {@link PriorityTaskManager} to use.
- * @return This builder, for convenience.
- * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
- */
- public Builder setPriorityTaskManager(PriorityTaskManager priorityTaskManager) {
- Assertions.checkState(!createDefaultLoadControlCalled);
- this.priorityTaskManager = priorityTaskManager;
- return this;
- }
-
/**
* Sets the back buffer duration, and whether the back buffer is retained from the previous
* keyframe.
@@ -190,6 +188,7 @@ public class DefaultLoadControl implements LoadControl {
*/
public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) {
Assertions.checkState(!createDefaultLoadControlCalled);
+ assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0");
this.backBufferDurationMs = backBufferDurationMs;
this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;
return this;
@@ -197,19 +196,20 @@ public class DefaultLoadControl implements LoadControl {
/** Creates a {@link DefaultLoadControl}. */
public DefaultLoadControl createDefaultLoadControl() {
+ Assertions.checkState(!createDefaultLoadControlCalled);
createDefaultLoadControlCalled = true;
if (allocator == null) {
- allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
+ allocator = new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
}
return new DefaultLoadControl(
allocator,
- minBufferMs,
+ minBufferAudioMs,
+ minBufferVideoMs,
maxBufferMs,
bufferForPlaybackMs,
bufferForPlaybackAfterRebufferMs,
targetBufferBytes,
prioritizeTimeOverSizeThresholds,
- priorityTaskManager,
backBufferDurationMs,
retainBackBufferFromKeyframe);
}
@@ -217,18 +217,19 @@ public class DefaultLoadControl implements LoadControl {
private final DefaultAllocator allocator;
- private final long minBufferUs;
+ private final long minBufferAudioUs;
+ private final long minBufferVideoUs;
private final long maxBufferUs;
private final long bufferForPlaybackUs;
private final long bufferForPlaybackAfterRebufferUs;
private final int targetBufferBytesOverwrite;
private final boolean prioritizeTimeOverSizeThresholds;
- private final PriorityTaskManager priorityTaskManager;
private final long backBufferDurationUs;
private final boolean retainBackBufferFromKeyframe;
private int targetBufferSize;
private boolean isBuffering;
+ private boolean hasVideo;
/** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */
@SuppressWarnings("deprecation")
@@ -238,21 +239,22 @@ public class DefaultLoadControl implements LoadControl {
/** @deprecated Use {@link Builder} instead. */
@Deprecated
- @SuppressWarnings("deprecation")
public DefaultLoadControl(DefaultAllocator allocator) {
this(
allocator,
- DEFAULT_MIN_BUFFER_MS,
+ /* minBufferAudioMs= */ DEFAULT_MIN_BUFFER_MS,
+ /* minBufferVideoMs= */ DEFAULT_MAX_BUFFER_MS,
DEFAULT_MAX_BUFFER_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
DEFAULT_TARGET_BUFFER_BYTES,
- DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
+ DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS,
+ DEFAULT_BACK_BUFFER_DURATION_MS,
+ DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME);
}
/** @deprecated Use {@link Builder} instead. */
@Deprecated
- @SuppressWarnings("deprecation")
public DefaultLoadControl(
DefaultAllocator allocator,
int minBufferMs,
@@ -263,70 +265,57 @@ public class DefaultLoadControl implements LoadControl {
boolean prioritizeTimeOverSizeThresholds) {
this(
allocator,
- minBufferMs,
+ /* minBufferAudioMs= */ minBufferMs,
+ /* minBufferVideoMs= */ minBufferMs,
maxBufferMs,
bufferForPlaybackMs,
bufferForPlaybackAfterRebufferMs,
targetBufferBytes,
prioritizeTimeOverSizeThresholds,
- /* priorityTaskManager= */ null);
- }
-
- /** @deprecated Use {@link Builder} instead. */
- @Deprecated
- public DefaultLoadControl(
- DefaultAllocator allocator,
- int minBufferMs,
- int maxBufferMs,
- int bufferForPlaybackMs,
- int bufferForPlaybackAfterRebufferMs,
- int targetBufferBytes,
- boolean prioritizeTimeOverSizeThresholds,
- PriorityTaskManager priorityTaskManager) {
- this(
- allocator,
- minBufferMs,
- maxBufferMs,
- bufferForPlaybackMs,
- bufferForPlaybackAfterRebufferMs,
- targetBufferBytes,
- prioritizeTimeOverSizeThresholds,
- priorityTaskManager,
DEFAULT_BACK_BUFFER_DURATION_MS,
DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME);
}
protected DefaultLoadControl(
DefaultAllocator allocator,
- int minBufferMs,
+ int minBufferAudioMs,
+ int minBufferVideoMs,
int maxBufferMs,
int bufferForPlaybackMs,
int bufferForPlaybackAfterRebufferMs,
int targetBufferBytes,
boolean prioritizeTimeOverSizeThresholds,
- PriorityTaskManager priorityTaskManager,
int backBufferDurationMs,
boolean retainBackBufferFromKeyframe) {
assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0");
assertGreaterOrEqual(
bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0");
- assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs");
assertGreaterOrEqual(
- minBufferMs,
+ minBufferAudioMs, bufferForPlaybackMs, "minBufferAudioMs", "bufferForPlaybackMs");
+ assertGreaterOrEqual(
+ minBufferVideoMs, bufferForPlaybackMs, "minBufferVideoMs", "bufferForPlaybackMs");
+ assertGreaterOrEqual(
+ minBufferAudioMs,
bufferForPlaybackAfterRebufferMs,
- "minBufferMs",
+ "minBufferAudioMs",
"bufferForPlaybackAfterRebufferMs");
- assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs");
+ assertGreaterOrEqual(
+ minBufferVideoMs,
+ bufferForPlaybackAfterRebufferMs,
+ "minBufferVideoMs",
+ "bufferForPlaybackAfterRebufferMs");
+ assertGreaterOrEqual(maxBufferMs, minBufferAudioMs, "maxBufferMs", "minBufferAudioMs");
+ assertGreaterOrEqual(maxBufferMs, minBufferVideoMs, "maxBufferMs", "minBufferVideoMs");
assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0");
this.allocator = allocator;
- this.minBufferUs = C.msToUs(minBufferMs);
+ this.minBufferAudioUs = C.msToUs(minBufferAudioMs);
+ this.minBufferVideoUs = C.msToUs(minBufferVideoMs);
this.maxBufferUs = C.msToUs(maxBufferMs);
this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs);
this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs);
this.targetBufferBytesOverwrite = targetBufferBytes;
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
- this.priorityTaskManager = priorityTaskManager;
this.backBufferDurationUs = C.msToUs(backBufferDurationMs);
this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;
}
@@ -339,6 +328,7 @@ public class DefaultLoadControl implements LoadControl {
@Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) {
+ hasVideo = hasVideo(renderers, trackSelections);
targetBufferSize =
targetBufferBytesOverwrite == C.LENGTH_UNSET
? calculateTargetBufferSize(renderers, trackSelections)
@@ -374,8 +364,7 @@ public class DefaultLoadControl implements LoadControl {
@Override
public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
- boolean wasBuffering = isBuffering;
- long minBufferUs = this.minBufferUs;
+ long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs;
if (playbackSpeed > 1) {
// The playback speed is faster than real time, so scale up the minimum required media
// duration to keep enough media buffered for a playout duration of minBufferUs.
@@ -388,13 +377,6 @@ public class DefaultLoadControl implements LoadControl {
} else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) {
isBuffering = false;
} // Else don't change the buffering state
- if (priorityTaskManager != null && isBuffering != wasBuffering) {
- if (isBuffering) {
- priorityTaskManager.add(C.PRIORITY_PLAYBACK);
- } else {
- priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
- }
- }
return isBuffering;
}
@@ -430,15 +412,21 @@ public class DefaultLoadControl implements LoadControl {
private void reset(boolean resetAllocator) {
targetBufferSize = 0;
- if (priorityTaskManager != null && isBuffering) {
- priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
- }
isBuffering = false;
if (resetAllocator) {
allocator.reset();
}
}
+ private static boolean hasVideo(Renderer[] renderers, TrackSelectionArray trackSelectionArray) {
+ for (int i = 0; i < renderers.length; i++) {
+ if (renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO && trackSelectionArray.get(i) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private static void assertGreaterOrEqual(int value1, int value2, String name1, String name2) {
Assertions.checkArgument(value1 >= value2, name1 + " cannot be less than " + name2);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java
index ed57cec70c..89e7d857c8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.StandaloneMediaClock;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
index 50832dd5af..2a977f5bba 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
@@ -19,8 +19,8 @@ import android.content.Context;
import android.media.MediaCodec;
import android.os.Handler;
import android.os.Looper;
-import android.support.annotation.IntDef;
-import android.support.annotation.Nullable;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
@@ -268,7 +268,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
extensionRendererMode, renderersList);
buildCameraMotionRenderers(context, extensionRendererMode, renderersList);
buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList);
- return renderersList.toArray(new Renderer[renderersList.size()]);
+ return renderersList.toArray(new Renderer[0]);
}
/**
@@ -323,7 +323,6 @@ public class DefaultRenderersFactory implements RenderersFactory {
Class> clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
Constructor> constructor =
clazz.getConstructor(
- boolean.class,
long.class,
android.os.Handler.class,
com.google.android.exoplayer2.video.VideoRendererEventListener.class,
@@ -332,7 +331,6 @@ public class DefaultRenderersFactory implements RenderersFactory {
Renderer renderer =
(Renderer)
constructor.newInstance(
- true,
allowedVideoJoiningTimeMs,
eventHandler,
eventListener,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
index 6b84245141..b5f8f954bb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
@@ -15,7 +15,8 @@
*/
package com.google.android.exoplayer2;
-import android.support.annotation.IntDef;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
@@ -30,11 +31,12 @@ public final class ExoPlaybackException extends Exception {
/**
* The type of source that produced the error. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER}
- * or {@link #TYPE_UNEXPECTED}.
+ * {@link #TYPE_UNEXPECTED}, {@link #TYPE_REMOTE} or {@link #TYPE_OUT_OF_MEMORY}. Note that new
+ * types may be added in the future and error handling should handle unknown type values.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
- @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED})
+ @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED, TYPE_REMOTE, TYPE_OUT_OF_MEMORY})
public @interface Type {}
/**
* The error occurred loading data from a {@link MediaSource}.
@@ -54,11 +56,16 @@ public final class ExoPlaybackException extends Exception {
* Call {@link #getUnexpectedException()} to retrieve the underlying cause.
*/
public static final int TYPE_UNEXPECTED = 2;
-
/**
- * The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
- * {@link #TYPE_UNEXPECTED}.
+ * The error occurred in a remote component.
+ *
+ *
Call {@link #getMessage()} to retrieve the message associated with the error.
*/
+ public static final int TYPE_REMOTE = 3;
+ /** The error was an {@link OutOfMemoryError}. */
+ public static final int TYPE_OUT_OF_MEMORY = 4;
+
+ /** The {@link Type} of the playback failure. */
@Type public final int type;
/**
@@ -66,7 +73,7 @@ public final class ExoPlaybackException extends Exception {
*/
public final int rendererIndex;
- private final Throwable cause;
+ @Nullable private final Throwable cause;
/**
* Creates an instance of type {@link #TYPE_SOURCE}.
@@ -75,7 +82,7 @@ public final class ExoPlaybackException extends Exception {
* @return The created instance.
*/
public static ExoPlaybackException createForSource(IOException cause) {
- return new ExoPlaybackException(TYPE_SOURCE, cause, C.INDEX_UNSET);
+ return new ExoPlaybackException(TYPE_SOURCE, cause, /* rendererIndex= */ C.INDEX_UNSET);
}
/**
@@ -95,8 +102,28 @@ public final class ExoPlaybackException extends Exception {
* @param cause The cause of the failure.
* @return The created instance.
*/
- /* package */ static ExoPlaybackException createForUnexpected(RuntimeException cause) {
- return new ExoPlaybackException(TYPE_UNEXPECTED, cause, C.INDEX_UNSET);
+ public static ExoPlaybackException createForUnexpected(RuntimeException cause) {
+ return new ExoPlaybackException(TYPE_UNEXPECTED, cause, /* rendererIndex= */ C.INDEX_UNSET);
+ }
+
+ /**
+ * Creates an instance of type {@link #TYPE_REMOTE}.
+ *
+ * @param message The message associated with the error.
+ * @return The created instance.
+ */
+ public static ExoPlaybackException createForRemote(String message) {
+ return new ExoPlaybackException(TYPE_REMOTE, message);
+ }
+
+ /**
+ * Creates an instance of type {@link #TYPE_OUT_OF_MEMORY}.
+ *
+ * @param cause The cause of the failure.
+ * @return The created instance.
+ */
+ public static ExoPlaybackException createForOutOfMemoryError(OutOfMemoryError cause) {
+ return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause, /* rendererIndex= */ C.INDEX_UNSET);
}
private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) {
@@ -106,6 +133,13 @@ public final class ExoPlaybackException extends Exception {
this.rendererIndex = rendererIndex;
}
+ private ExoPlaybackException(@Type int type, String message) {
+ super(message);
+ this.type = type;
+ rendererIndex = C.INDEX_UNSET;
+ cause = null;
+ }
+
/**
* Retrieves the underlying error when {@link #type} is {@link #TYPE_SOURCE}.
*
@@ -113,7 +147,7 @@ public final class ExoPlaybackException extends Exception {
*/
public IOException getSourceException() {
Assertions.checkState(type == TYPE_SOURCE);
- return (IOException) cause;
+ return (IOException) Assertions.checkNotNull(cause);
}
/**
@@ -123,7 +157,7 @@ public final class ExoPlaybackException extends Exception {
*/
public Exception getRendererException() {
Assertions.checkState(type == TYPE_RENDERER);
- return (Exception) cause;
+ return (Exception) Assertions.checkNotNull(cause);
}
/**
@@ -133,7 +167,16 @@ public final class ExoPlaybackException extends Exception {
*/
public RuntimeException getUnexpectedException() {
Assertions.checkState(type == TYPE_UNEXPECTED);
- return (RuntimeException) cause;
+ return (RuntimeException) Assertions.checkNotNull(cause);
}
+ /**
+ * Retrieves the underlying error when {@link #type} is {@link #TYPE_OUT_OF_MEMORY}.
+ *
+ * @throws IllegalStateException If {@link #type} is not {@link #TYPE_OUT_OF_MEMORY}.
+ */
+ public OutOfMemoryError getOutOfMemoryError() {
+ Assertions.checkState(type == TYPE_OUT_OF_MEMORY);
+ return (OutOfMemoryError) Assertions.checkNotNull(cause);
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
index 0e8c176486..d0f9e2ae04 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -16,15 +16,15 @@
package com.google.android.exoplayer2;
import android.os.Looper;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.ClippingMediaSource;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
-import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
@@ -48,7 +48,7 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
*
A {@link MediaSource} that defines the media to be played, loads the media, and from
* which the loaded media can be read. A MediaSource is injected via {@link
* #prepare(MediaSource)} at the start of playback. The library modules provide default
- * implementations for regular media files ({@link ExtractorMediaSource}), DASH
+ * implementations for progressive media files ({@link ProgressiveMediaSource}), DASH
* (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an
* implementation for loading single media samples ({@link SingleSampleMediaSource}) that's
* most often used for side-loaded subtitle files, and implementations for building more
@@ -117,12 +117,6 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
*/
public interface ExoPlayer extends Player {
- /**
- * @deprecated Use {@link Player.EventListener} instead.
- */
- @Deprecated
- interface EventListener extends Player.EventListener {}
-
/** @deprecated Use {@link PlayerMessage.Target} instead. */
@Deprecated
interface ExoPlayerComponent extends PlayerMessage.Target {}
@@ -147,43 +141,6 @@ public interface ExoPlayer extends Player {
}
}
- /**
- * @deprecated Use {@link Player#STATE_IDLE} instead.
- */
- @Deprecated
- int STATE_IDLE = Player.STATE_IDLE;
- /**
- * @deprecated Use {@link Player#STATE_BUFFERING} instead.
- */
- @Deprecated
- int STATE_BUFFERING = Player.STATE_BUFFERING;
- /**
- * @deprecated Use {@link Player#STATE_READY} instead.
- */
- @Deprecated
- int STATE_READY = Player.STATE_READY;
- /**
- * @deprecated Use {@link Player#STATE_ENDED} instead.
- */
- @Deprecated
- int STATE_ENDED = Player.STATE_ENDED;
-
- /**
- * @deprecated Use {@link Player#REPEAT_MODE_OFF} instead.
- */
- @Deprecated
- @RepeatMode int REPEAT_MODE_OFF = Player.REPEAT_MODE_OFF;
- /**
- * @deprecated Use {@link Player#REPEAT_MODE_ONE} instead.
- */
- @Deprecated
- @RepeatMode int REPEAT_MODE_ONE = Player.REPEAT_MODE_ONE;
- /**
- * @deprecated Use {@link Player#REPEAT_MODE_ALL} instead.
- */
- @Deprecated
- @RepeatMode int REPEAT_MODE_ALL = Player.REPEAT_MODE_ALL;
-
/** Returns the {@link Looper} associated with the playback thread. */
Looper getPlaybackLooper();
@@ -196,18 +153,12 @@ public interface ExoPlayer extends Player {
/**
* Prepares the player to play the provided {@link MediaSource}. Equivalent to
* {@code prepare(mediaSource, true, true)}.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to prepare a
- * player more than once with the same piece of media, use a new instance each time.
*/
void prepare(MediaSource mediaSource);
/**
* Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback
* position the default position in the first {@link Timeline.Window}.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to prepare a
- * player more than once with the same piece of media, use a new instance each time.
*
* @param mediaSource The {@link MediaSource} to play.
* @param resetPosition Whether the playback position should be reset to the default position in
@@ -252,4 +203,34 @@ public interface ExoPlayer extends Player {
/** Returns the currently active {@link SeekParameters} of the player. */
SeekParameters getSeekParameters();
+
+ /**
+ * Sets whether the player is allowed to keep holding limited resources such as video decoders,
+ * even when in the idle state. By doing so, the player may be able to reduce latency when
+ * starting to play another piece of content for which the same resources are required.
+ *
+ *
This mode should be used with caution, since holding limited resources may prevent other
+ * players of media components from acquiring them. It should only be enabled when both
+ * of the following conditions are true:
+ *
+ *
+ * - The application that owns the player is in the foreground.
+ *
- The player is used in a way that may benefit from foreground mode. For this to be true,
+ * the same player instance must be used to play multiple pieces of content, and there must
+ * be gaps between the playbacks (i.e. {@link #stop} is called to halt one playback, and
+ * {@link #prepare} is called some time later to start a new one).
+ *
+ *
+ * Note that foreground mode is not useful for switching between content without gaps
+ * between the playbacks. For this use case {@link #stop} does not need to be called, and simply
+ * calling {@link #prepare} for the new media will cause limited resources to be retained even if
+ * foreground mode is not enabled.
+ *
+ *
If foreground mode is enabled, it's the application's responsibility to disable it when the
+ * conditions described above no longer hold.
+ *
+ * @param foregroundMode Whether the player is allowed to keep limited resources even when in the
+ * idle state.
+ */
+ void setForegroundMode(boolean foregroundMode);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
index 6c2a6f527c..59647feaa9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
@@ -17,9 +17,8 @@ package com.google.android.exoplayer2;
import android.content.Context;
import android.os.Looper;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
-import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
@@ -38,44 +37,6 @@ public final class ExoPlayerFactory {
private ExoPlayerFactory() {}
- /**
- * Creates a {@link SimpleExoPlayer} instance.
- *
- * @param context A {@link Context}.
- * @param trackSelector The {@link TrackSelector} that will be used by the instance.
- * @param loadControl The {@link LoadControl} that will be used by the instance.
- * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
- * LoadControl)}.
- */
- @Deprecated
- public static SimpleExoPlayer newSimpleInstance(
- Context context, TrackSelector trackSelector, LoadControl loadControl) {
- RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
- return newSimpleInstance(context, renderersFactory, trackSelector, loadControl);
- }
-
- /**
- * Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used.
- *
- * @param context A {@link Context}.
- * @param trackSelector The {@link TrackSelector} that will be used by the instance.
- * @param loadControl The {@link LoadControl} that will be used by the instance.
- * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
- * will not be used for DRM protected playbacks.
- * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
- * LoadControl)}.
- */
- @Deprecated
- public static SimpleExoPlayer newSimpleInstance(
- Context context,
- TrackSelector trackSelector,
- LoadControl loadControl,
- @Nullable DrmSessionManager drmSessionManager) {
- RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
- return newSimpleInstance(
- context, renderersFactory, trackSelector, loadControl, drmSessionManager);
- }
-
/**
* Creates a {@link SimpleExoPlayer} instance.
*
@@ -88,7 +49,7 @@ public final class ExoPlayerFactory {
* extension renderers are used. Note that extensions must be included in the application
* build for them to be considered available.
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
- * LoadControl)}.
+ * LoadControl, DrmSessionManager)}.
*/
@Deprecated
public static SimpleExoPlayer newSimpleInstance(
@@ -117,7 +78,7 @@ public final class ExoPlayerFactory {
* @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
* seamlessly join an ongoing playback.
* @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,
- * LoadControl)}.
+ * LoadControl, DrmSessionManager)}.
*/
@Deprecated
public static SimpleExoPlayer newSimpleInstance(
@@ -154,23 +115,6 @@ public final class ExoPlayerFactory {
return newSimpleInstance(context, new DefaultRenderersFactory(context), trackSelector);
}
- /**
- * Creates a {@link SimpleExoPlayer} instance.
- *
- * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
- * @param trackSelector The {@link TrackSelector} that will be used by the instance.
- * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector)}. The use
- * of {@link SimpleExoPlayer#setAudioAttributes(AudioAttributes, boolean)} to manage audio
- * focus will be unavailable for the {@link SimpleExoPlayer} returned by this method.
- */
- @Deprecated
- @SuppressWarnings("nullness:argument.type.incompatible")
- public static SimpleExoPlayer newSimpleInstance(
- RenderersFactory renderersFactory, TrackSelector trackSelector) {
- return newSimpleInstance(
- /* context= */ null, renderersFactory, trackSelector, new DefaultLoadControl());
- }
-
/**
* Creates a {@link SimpleExoPlayer} instance.
*
@@ -183,6 +127,38 @@ public final class ExoPlayerFactory {
return newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl());
}
+ /**
+ * Creates a {@link SimpleExoPlayer} instance.
+ *
+ * @param context A {@link Context}.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context, TrackSelector trackSelector, LoadControl loadControl) {
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
+ return newSimpleInstance(context, renderersFactory, trackSelector, loadControl);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used.
+ *
+ * @param context A {@link Context}.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ */
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager drmSessionManager) {
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
+ return newSimpleInstance(
+ context, renderersFactory, trackSelector, loadControl, drmSessionManager);
+ }
+
/**
* Creates a {@link SimpleExoPlayer} instance.
*
@@ -358,7 +334,7 @@ public final class ExoPlayerFactory {
trackSelector,
loadControl,
drmSessionManager,
- getDefaultBandwidthMeter(),
+ getDefaultBandwidthMeter(context),
analyticsCollectorFactory,
looper);
}
@@ -400,28 +376,32 @@ public final class ExoPlayerFactory {
/**
* Creates an {@link ExoPlayer} instance.
*
+ * @param context A {@link Context}.
* @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
*/
- public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) {
- return newInstance(renderers, trackSelector, new DefaultLoadControl());
+ public static ExoPlayer newInstance(
+ Context context, Renderer[] renderers, TrackSelector trackSelector) {
+ return newInstance(context, renderers, trackSelector, new DefaultLoadControl());
}
/**
* Creates an {@link ExoPlayer} instance.
*
+ * @param context A {@link Context}.
* @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
*/
- public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
- LoadControl loadControl) {
- return newInstance(renderers, trackSelector, loadControl, Util.getLooper());
+ public static ExoPlayer newInstance(
+ Context context, Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
+ return newInstance(context, renderers, trackSelector, loadControl, Util.getLooper());
}
/**
* Creates an {@link ExoPlayer} instance.
*
+ * @param context A {@link Context}.
* @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
@@ -429,13 +409,19 @@ public final class ExoPlayerFactory {
* used to call listeners on.
*/
public static ExoPlayer newInstance(
- Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Looper looper) {
- return newInstance(renderers, trackSelector, loadControl, getDefaultBandwidthMeter(), looper);
+ Context context,
+ Renderer[] renderers,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ Looper looper) {
+ return newInstance(
+ context, renderers, trackSelector, loadControl, getDefaultBandwidthMeter(context), looper);
}
/**
* Creates an {@link ExoPlayer} instance.
*
+ * @param context A {@link Context}.
* @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
@@ -443,7 +429,9 @@ public final class ExoPlayerFactory {
* @param looper The {@link Looper} which must be used for all calls to the player and which is
* used to call listeners on.
*/
+ @SuppressWarnings("unused")
public static ExoPlayer newInstance(
+ Context context,
Renderer[] renderers,
TrackSelector trackSelector,
LoadControl loadControl,
@@ -453,9 +441,9 @@ public final class ExoPlayerFactory {
renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper);
}
- private static synchronized BandwidthMeter getDefaultBandwidthMeter() {
+ private static synchronized BandwidthMeter getDefaultBandwidthMeter(Context context) {
if (singletonBandwidthMeter == null) {
- singletonBandwidthMeter = new DefaultBandwidthMeter.Builder().build();
+ singletonBandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
}
return singletonBandwidthMeter;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index de6e867514..15deb8ea47 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -19,7 +19,7 @@ import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.source.MediaSource;
@@ -37,8 +37,7 @@ import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.CopyOnWriteArrayList;
/** An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}. */
/* package */ final class ExoPlayerImpl extends BasePlayer implements ExoPlayer {
@@ -49,7 +48,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
* This empty track selector result can only be used for {@link PlaybackInfo#trackSelectorResult}
* when the player does not have any track selection made (such as when player is reset, or when
* player seeks to an unprepared period). It will not be used as result of any {@link
- * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)} operation.
+ * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)}
+ * operation.
*/
/* package */ final TrackSelectorResult emptyTrackSelectorResult;
@@ -58,9 +58,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
private final Handler eventHandler;
private final ExoPlayerImplInternal internalPlayer;
private final Handler internalPlayerHandler;
- private final CopyOnWriteArraySet listeners;
+ private final CopyOnWriteArrayList listeners;
private final Timeline.Period period;
- private final ArrayDeque pendingPlaybackInfoUpdates;
+ private final ArrayDeque pendingListenerNotifications;
private MediaSource mediaSource;
private boolean playWhenReady;
@@ -70,6 +70,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private int pendingOperationAcks;
private boolean hasPendingPrepare;
private boolean hasPendingSeek;
+ private boolean foregroundMode;
private PlaybackParameters playbackParameters;
private SeekParameters seekParameters;
private @Nullable ExoPlaybackException playbackError;
@@ -109,7 +110,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
this.playWhenReady = false;
this.repeatMode = Player.REPEAT_MODE_OFF;
this.shuffleModeEnabled = false;
- this.listeners = new CopyOnWriteArraySet<>();
+ this.listeners = new CopyOnWriteArrayList<>();
emptyTrackSelectorResult =
new TrackSelectorResult(
new RendererConfiguration[renderers.length],
@@ -126,7 +127,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
};
playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult);
- pendingPlaybackInfoUpdates = new ArrayDeque<>();
+ pendingListenerNotifications = new ArrayDeque<>();
internalPlayer =
new ExoPlayerImplInternal(
renderers,
@@ -178,12 +179,17 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void addListener(Player.EventListener listener) {
- listeners.add(listener);
+ listeners.addIfAbsent(new ListenerHolder(listener));
}
@Override
public void removeListener(Player.EventListener listener) {
- listeners.remove(listener);
+ for (ListenerHolder listenerHolder : listeners) {
+ if (listenerHolder.listener.equals(listener)) {
+ listenerHolder.release();
+ listeners.remove(listenerHolder);
+ }
+ }
}
@Override
@@ -228,8 +234,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET,
- /* seekProcessed= */ false,
- /* playWhenReadyChanged= */ false);
+ /* seekProcessed= */ false);
}
@Override
@@ -245,13 +250,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
if (this.playWhenReady != playWhenReady) {
this.playWhenReady = playWhenReady;
- updatePlaybackInfo(
- playbackInfo,
- /* positionDiscontinuity= */ false,
- /* ignored */ DISCONTINUITY_REASON_INTERNAL,
- /* ignored */ TIMELINE_CHANGE_REASON_RESET,
- /* seekProcessed= */ false,
- /* playWhenReadyChanged= */ true);
+ int playbackState = playbackInfo.playbackState;
+ notifyListeners(listener -> listener.onPlayerStateChanged(playWhenReady, playbackState));
}
}
@@ -265,9 +265,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
internalPlayer.setRepeatMode(repeatMode);
- for (Player.EventListener listener : listeners) {
- listener.onRepeatModeChanged(repeatMode);
- }
+ notifyListeners(listener -> listener.onRepeatModeChanged(repeatMode));
}
}
@@ -281,9 +279,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (this.shuffleModeEnabled != shuffleModeEnabled) {
this.shuffleModeEnabled = shuffleModeEnabled;
internalPlayer.setShuffleModeEnabled(shuffleModeEnabled);
- for (Player.EventListener listener : listeners) {
- listener.onShuffleModeEnabledChanged(shuffleModeEnabled);
- }
+ notifyListeners(listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
}
}
@@ -332,9 +328,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first);
}
internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
- for (Player.EventListener listener : listeners) {
- listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK);
- }
+ notifyListeners(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
}
@Override
@@ -366,6 +360,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
return seekParameters;
}
+ @Override
+ public void setForegroundMode(boolean foregroundMode) {
+ if (this.foregroundMode != foregroundMode) {
+ this.foregroundMode = foregroundMode;
+ internalPlayer.setForegroundMode(foregroundMode);
+ }
+ }
+
@Override
public void stop(boolean reset) {
if (reset) {
@@ -388,8 +390,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET,
- /* seekProcessed= */ false,
- /* playWhenReadyChanged= */ false);
+ /* seekProcessed= */ false);
}
@Override
@@ -400,6 +401,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
mediaSource = null;
internalPlayer.release();
eventHandler.removeCallbacksAndMessages(null);
+ playbackInfo =
+ getResetPlaybackInfo(
+ /* resetPosition= */ false,
+ /* resetState= */ false,
+ /* playbackState= */ Player.STATE_IDLE);
}
@Override
@@ -599,17 +605,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj;
if (!this.playbackParameters.equals(playbackParameters)) {
this.playbackParameters = playbackParameters;
- for (Player.EventListener listener : listeners) {
- listener.onPlaybackParametersChanged(playbackParameters);
- }
+ notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters));
}
break;
case ExoPlayerImplInternal.MSG_ERROR:
ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj;
this.playbackError = playbackError;
- for (Player.EventListener listener : listeners) {
- listener.onPlayerError(playbackError);
- }
+ notifyListeners(listener -> listener.onPlayerError(playbackError));
break;
default:
throw new IllegalStateException();
@@ -629,8 +631,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
playbackInfo.resetToNewPosition(
playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs);
}
- if ((!this.playbackInfo.timeline.isEmpty() || hasPendingPrepare)
- && playbackInfo.timeline.isEmpty()) {
+ if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) {
// Update the masking variables, which are used when the timeline becomes empty.
maskingPeriodIndex = 0;
maskingWindowIndex = 0;
@@ -649,8 +650,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
positionDiscontinuity,
positionDiscontinuityReason,
timelineChangeReason,
- seekProcessed,
- /* playWhenReadyChanged= */ false);
+ seekProcessed);
}
}
@@ -665,6 +665,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
maskingPeriodIndex = getCurrentPeriodIndex();
maskingWindowPositionMs = getCurrentPosition();
}
+ // Also reset period-based PlaybackInfo positions if resetting the state.
+ resetPosition = resetPosition || resetState;
MediaPeriodId mediaPeriodId =
resetPosition
? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window)
@@ -692,29 +694,37 @@ import java.util.concurrent.CopyOnWriteArraySet;
boolean positionDiscontinuity,
@Player.DiscontinuityReason int positionDiscontinuityReason,
@Player.TimelineChangeReason int timelineChangeReason,
- boolean seekProcessed,
- boolean playWhenReadyChanged) {
- boolean isRunningRecursiveListenerNotification = !pendingPlaybackInfoUpdates.isEmpty();
- pendingPlaybackInfoUpdates.addLast(
+ boolean seekProcessed) {
+ // Assign playback info immediately such that all getters return the right values.
+ PlaybackInfo previousPlaybackInfo = this.playbackInfo;
+ this.playbackInfo = playbackInfo;
+ notifyListeners(
new PlaybackInfoUpdate(
playbackInfo,
- /* previousPlaybackInfo= */ this.playbackInfo,
+ previousPlaybackInfo,
listeners,
trackSelector,
positionDiscontinuity,
positionDiscontinuityReason,
timelineChangeReason,
seekProcessed,
- playWhenReady,
- playWhenReadyChanged));
- // Assign playback info immediately such that all getters return the right values.
- this.playbackInfo = playbackInfo;
+ playWhenReady));
+ }
+
+ private void notifyListeners(ListenerInvocation listenerInvocation) {
+ CopyOnWriteArrayList listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
+ notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation));
+ }
+
+ private void notifyListeners(Runnable listenerNotificationRunnable) {
+ boolean isRunningRecursiveListenerNotification = !pendingListenerNotifications.isEmpty();
+ pendingListenerNotifications.addLast(listenerNotificationRunnable);
if (isRunningRecursiveListenerNotification) {
return;
}
- while (!pendingPlaybackInfoUpdates.isEmpty()) {
- pendingPlaybackInfoUpdates.peekFirst().notifyListeners();
- pendingPlaybackInfoUpdates.removeFirst();
+ while (!pendingListenerNotifications.isEmpty()) {
+ pendingListenerNotifications.peekFirst().run();
+ pendingListenerNotifications.removeFirst();
}
}
@@ -729,42 +739,40 @@ import java.util.concurrent.CopyOnWriteArraySet;
return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0;
}
- private static final class PlaybackInfoUpdate {
+ private static final class PlaybackInfoUpdate implements Runnable {
private final PlaybackInfo playbackInfo;
- private final Set listeners;
+ private final CopyOnWriteArrayList listenerSnapshot;
private final TrackSelector trackSelector;
private final boolean positionDiscontinuity;
private final @Player.DiscontinuityReason int positionDiscontinuityReason;
private final @Player.TimelineChangeReason int timelineChangeReason;
private final boolean seekProcessed;
- private final boolean playWhenReady;
- private final boolean playbackStateOrPlayWhenReadyChanged;
+ private final boolean playbackStateChanged;
private final boolean timelineOrManifestChanged;
private final boolean isLoadingChanged;
private final boolean trackSelectorResultChanged;
+ private final boolean playWhenReady;
public PlaybackInfoUpdate(
PlaybackInfo playbackInfo,
PlaybackInfo previousPlaybackInfo,
- Set listeners,
+ CopyOnWriteArrayList listeners,
TrackSelector trackSelector,
boolean positionDiscontinuity,
@Player.DiscontinuityReason int positionDiscontinuityReason,
@Player.TimelineChangeReason int timelineChangeReason,
boolean seekProcessed,
- boolean playWhenReady,
- boolean playWhenReadyChanged) {
+ boolean playWhenReady) {
this.playbackInfo = playbackInfo;
- this.listeners = listeners;
+ this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
this.trackSelector = trackSelector;
this.positionDiscontinuity = positionDiscontinuity;
this.positionDiscontinuityReason = positionDiscontinuityReason;
this.timelineChangeReason = timelineChangeReason;
this.seekProcessed = seekProcessed;
this.playWhenReady = playWhenReady;
- playbackStateOrPlayWhenReadyChanged =
- playWhenReadyChanged || previousPlaybackInfo.playbackState != playbackInfo.playbackState;
+ playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState;
timelineOrManifestChanged =
previousPlaybackInfo.timeline != playbackInfo.timeline
|| previousPlaybackInfo.manifest != playbackInfo.manifest;
@@ -773,40 +781,46 @@ import java.util.concurrent.CopyOnWriteArraySet;
previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
}
- public void notifyListeners() {
+ @Override
+ public void run() {
if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
- for (Player.EventListener listener : listeners) {
- listener.onTimelineChanged(
- playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason);
- }
+ invokeAll(
+ listenerSnapshot,
+ listener ->
+ listener.onTimelineChanged(
+ playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason));
}
if (positionDiscontinuity) {
- for (Player.EventListener listener : listeners) {
- listener.onPositionDiscontinuity(positionDiscontinuityReason);
- }
+ invokeAll(
+ listenerSnapshot,
+ listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason));
}
if (trackSelectorResultChanged) {
trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info);
- for (Player.EventListener listener : listeners) {
- listener.onTracksChanged(
- playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections);
- }
+ invokeAll(
+ listenerSnapshot,
+ listener ->
+ listener.onTracksChanged(
+ playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections));
}
if (isLoadingChanged) {
- for (Player.EventListener listener : listeners) {
- listener.onLoadingChanged(playbackInfo.isLoading);
- }
+ invokeAll(listenerSnapshot, listener -> listener.onLoadingChanged(playbackInfo.isLoading));
}
- if (playbackStateOrPlayWhenReadyChanged) {
- for (Player.EventListener listener : listeners) {
- listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
- }
+ if (playbackStateChanged) {
+ invokeAll(
+ listenerSnapshot,
+ listener -> listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState));
}
if (seekProcessed) {
- for (Player.EventListener listener : listeners) {
- listener.onSeekProcessed();
- }
+ invokeAll(listenerSnapshot, EventListener::onSeekProcessed);
}
}
}
+
+ private static void invokeAll(
+ CopyOnWriteArrayList listeners, ListenerInvocation listenerInvocation) {
+ for (ListenerHolder listenerHolder : listeners) {
+ listenerHolder.invoke(listenerInvocation);
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index c31c6b75a5..37774bccb5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -21,8 +21,8 @@ import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
@@ -44,6 +44,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
/** Implements the internal behavior of {@link ExoPlayerImpl}. */
/* package */ final class ExoPlayerImplInternal
@@ -76,9 +77,10 @@ import java.util.Collections;
private static final int MSG_TRACK_SELECTION_INVALIDATED = 11;
private static final int MSG_SET_REPEAT_MODE = 12;
private static final int MSG_SET_SHUFFLE_ENABLED = 13;
- private static final int MSG_SEND_MESSAGE = 14;
- private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15;
- private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16;
+ private static final int MSG_SET_FOREGROUND_MODE = 14;
+ private static final int MSG_SEND_MESSAGE = 15;
+ private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16;
+ private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17;
private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
@@ -114,6 +116,7 @@ import java.util.Collections;
private boolean rebuffering;
@Player.RepeatMode private int repeatMode;
private boolean shuffleModeEnabled;
+ private boolean foregroundMode;
private int pendingPrepareCount;
private SeekPosition pendingInitialSeekPosition;
@@ -215,6 +218,29 @@ import java.util.Collections;
handler.obtainMessage(MSG_SEND_MESSAGE, message).sendToTarget();
}
+ public synchronized void setForegroundMode(boolean foregroundMode) {
+ if (foregroundMode) {
+ handler.obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 1, 0).sendToTarget();
+ } else {
+ AtomicBoolean processedFlag = new AtomicBoolean();
+ handler
+ .obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 0, 0, processedFlag)
+ .sendToTarget();
+ boolean wasInterrupted = false;
+ while (!processedFlag.get() && !released) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ wasInterrupted = true;
+ }
+ }
+ if (wasInterrupted) {
+ // Restore the interrupted status.
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
public synchronized void release() {
if (released) {
return;
@@ -308,8 +334,15 @@ import java.util.Collections;
case MSG_SET_SEEK_PARAMETERS:
setSeekParametersInternal((SeekParameters) msg.obj);
break;
+ case MSG_SET_FOREGROUND_MODE:
+ setForegroundModeInternal(
+ /* foregroundMode= */ msg.arg1 != 0, /* processedFlag= */ (AtomicBoolean) msg.obj);
+ break;
case MSG_STOP:
- stopInternal(/* reset= */ msg.arg1 != 0, /* acknowledgeStop= */ true);
+ stopInternal(
+ /* forceResetRenderers= */ false,
+ /* resetPositionAndState= */ msg.arg1 != 0,
+ /* acknowledgeStop= */ true);
break;
case MSG_PERIOD_PREPARED:
handlePeriodPrepared((MediaPeriod) msg.obj);
@@ -342,19 +375,31 @@ import java.util.Collections;
maybeNotifyPlaybackInfoChanged();
} catch (ExoPlaybackException e) {
Log.e(TAG, "Playback error.", e);
- stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
+ stopInternal(
+ /* forceResetRenderers= */ true,
+ /* resetPositionAndState= */ false,
+ /* acknowledgeStop= */ false);
maybeNotifyPlaybackInfoChanged();
} catch (IOException e) {
Log.e(TAG, "Source error.", e);
- stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget();
+ stopInternal(
+ /* forceResetRenderers= */ false,
+ /* resetPositionAndState= */ false,
+ /* acknowledgeStop= */ false);
maybeNotifyPlaybackInfoChanged();
- } catch (RuntimeException e) {
+ } catch (RuntimeException | OutOfMemoryError e) {
Log.e(TAG, "Internal runtime error.", e);
- stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
- eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e))
- .sendToTarget();
+ ExoPlaybackException error =
+ e instanceof OutOfMemoryError
+ ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e)
+ : ExoPlaybackException.createForUnexpected((RuntimeException) e);
+ eventHandler.obtainMessage(MSG_ERROR, error).sendToTarget();
+ stopInternal(
+ /* forceResetRenderers= */ true,
+ /* resetPositionAndState= */ false,
+ /* acknowledgeStop= */ false);
maybeNotifyPlaybackInfoChanged();
}
return true;
@@ -391,7 +436,8 @@ import java.util.Collections;
private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
pendingPrepareCount++;
- resetInternal(/* releaseMediaSource= */ true, resetPosition, resetState);
+ resetInternal(
+ /* resetRenderers= */ false, /* releaseMediaSource= */ true, resetPosition, resetState);
loadControl.onPrepared();
this.mediaSource = mediaSource;
setState(Player.STATE_BUFFERING);
@@ -624,7 +670,10 @@ import java.util.Collections;
// End playback, as we didn't manage to find a valid seek position.
setState(Player.STATE_ENDED);
resetInternal(
- /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false);
+ /* resetRenderers= */ false,
+ /* releaseMediaSource= */ false,
+ /* resetPosition= */ true,
+ /* resetState= */ false);
} else {
// Execute the seek in the current media periods.
long newPeriodPositionUs = periodPositionUs;
@@ -721,6 +770,7 @@ import java.util.Collections;
for (Renderer renderer : enabledRenderers) {
renderer.resetPosition(rendererPositionUs);
}
+ notifyTrackSelectionDiscontinuity();
}
private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
@@ -731,9 +781,33 @@ import java.util.Collections;
this.seekParameters = seekParameters;
}
- private void stopInternal(boolean reset, boolean acknowledgeStop) {
+ private void setForegroundModeInternal(
+ boolean foregroundMode, @Nullable AtomicBoolean processedFlag) {
+ if (this.foregroundMode != foregroundMode) {
+ this.foregroundMode = foregroundMode;
+ if (!foregroundMode) {
+ for (Renderer renderer : renderers) {
+ if (renderer.getState() == Renderer.STATE_DISABLED) {
+ renderer.reset();
+ }
+ }
+ }
+ }
+ if (processedFlag != null) {
+ synchronized (this) {
+ processedFlag.set(true);
+ notifyAll();
+ }
+ }
+ }
+
+ private void stopInternal(
+ boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) {
resetInternal(
- /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset);
+ /* resetRenderers= */ forceResetRenderers || !foregroundMode,
+ /* releaseMediaSource= */ true,
+ /* resetPosition= */ resetPositionAndState,
+ /* resetState= */ resetPositionAndState);
playbackInfoUpdate.incrementPendingOperationAcks(
pendingPrepareCount + (acknowledgeStop ? 1 : 0));
pendingPrepareCount = 0;
@@ -743,7 +817,10 @@ import java.util.Collections;
private void releaseInternal() {
resetInternal(
- /* releaseMediaSource= */ true, /* resetPosition= */ true, /* resetState= */ true);
+ /* resetRenderers= */ true,
+ /* releaseMediaSource= */ true,
+ /* resetPosition= */ true,
+ /* resetState= */ true);
loadControl.onReleased();
setState(Player.STATE_IDLE);
internalPlaybackThread.quit();
@@ -754,7 +831,10 @@ import java.util.Collections;
}
private void resetInternal(
- boolean releaseMediaSource, boolean resetPosition, boolean resetState) {
+ boolean resetRenderers,
+ boolean releaseMediaSource,
+ boolean resetPosition,
+ boolean resetState) {
handler.removeMessages(MSG_DO_SOME_WORK);
rebuffering = false;
mediaClock.stop();
@@ -764,15 +844,37 @@ import java.util.Collections;
disableRenderer(renderer);
} catch (ExoPlaybackException | RuntimeException e) {
// There's nothing we can do.
- Log.e(TAG, "Stop failed.", e);
+ Log.e(TAG, "Disable failed.", e);
+ }
+ }
+ if (resetRenderers) {
+ for (Renderer renderer : renderers) {
+ try {
+ renderer.reset();
+ } catch (RuntimeException e) {
+ // There's nothing we can do.
+ Log.e(TAG, "Reset failed.", e);
+ }
}
}
enabledRenderers = new Renderer[0];
- queue.clear(/* keepFrontPeriodUid= */ !resetPosition);
- setIsLoading(false);
+
if (resetPosition) {
pendingInitialSeekPosition = null;
+ } else if (resetState) {
+ // When resetting the state, also reset the period-based PlaybackInfo position and convert
+ // existing position to initial seek instead.
+ resetPosition = true;
+ if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) {
+ playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
+ long windowPositionUs = playbackInfo.positionUs + period.getPositionInWindowUs();
+ pendingInitialSeekPosition =
+ new SeekPosition(Timeline.EMPTY, period.windowIndex, windowPositionUs);
+ }
}
+
+ queue.clear(/* keepFrontPeriodUid= */ !resetPosition);
+ setIsLoading(false);
if (resetState) {
queue.setTimeline(Timeline.EMPTY);
for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
@@ -986,12 +1088,14 @@ import java.util.Collections;
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
boolean selectionsChangedForReadPeriod = true;
+ TrackSelectorResult newTrackSelectorResult;
while (true) {
if (periodHolder == null || !periodHolder.prepared) {
// The reselection did not change any prepared periods.
return;
}
- if (periodHolder.selectTracks(playbackSpeed)) {
+ newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline);
+ if (newTrackSelectorResult != null) {
// Selected tracks have changed for this period.
break;
}
@@ -999,7 +1103,7 @@ import java.util.Collections;
// The track reselection didn't affect any period that has been read.
selectionsChangedForReadPeriod = false;
}
- periodHolder = periodHolder.next;
+ periodHolder = periodHolder.getNext();
}
if (selectionsChangedForReadPeriod) {
@@ -1010,7 +1114,7 @@ import java.util.Collections;
boolean[] streamResetFlags = new boolean[renderers.length];
long periodPositionUs =
playingPeriodHolder.applyTrackSelection(
- playbackInfo.positionUs, recreateStreams, streamResetFlags);
+ newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags);
if (playbackInfo.playbackState != Player.STATE_ENDED
&& periodPositionUs != playbackInfo.positionUs) {
playbackInfo =
@@ -1044,7 +1148,7 @@ import java.util.Collections;
}
playbackInfo =
playbackInfo.copyWithTrackInfo(
- playingPeriodHolder.trackGroups, playingPeriodHolder.trackSelectorResult);
+ playingPeriodHolder.getTrackGroups(), playingPeriodHolder.getTrackSelectorResult());
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
} else {
// Release and re-prepare/buffer periods after the one whose selection changed.
@@ -1053,7 +1157,7 @@ import java.util.Collections;
long loadingPeriodPositionUs =
Math.max(
periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs));
- periodHolder.applyTrackSelection(loadingPeriodPositionUs, false);
+ periodHolder.applyTrackSelection(newTrackSelectorResult, loadingPeriodPositionUs, false);
}
}
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ true);
@@ -1065,17 +1169,31 @@ import java.util.Collections;
}
private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) {
+ MediaPeriodHolder periodHolder = queue.getFrontPeriod();
+ while (periodHolder != null && periodHolder.prepared) {
+ TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
+ for (TrackSelection trackSelection : trackSelections) {
+ if (trackSelection != null) {
+ trackSelection.onPlaybackSpeed(playbackSpeed);
+ }
+ }
+ periodHolder = periodHolder.getNext();
+ }
+ }
+
+ private void notifyTrackSelectionDiscontinuity() {
MediaPeriodHolder periodHolder = queue.getFrontPeriod();
while (periodHolder != null) {
- if (periodHolder.trackSelectorResult != null) {
- TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll();
+ TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult();
+ if (trackSelectorResult != null) {
+ TrackSelection[] trackSelections = trackSelectorResult.selections.getAll();
for (TrackSelection trackSelection : trackSelections) {
if (trackSelection != null) {
- trackSelection.onPlaybackSpeed(playbackSpeed);
+ trackSelection.onDiscontinuity();
}
}
}
- periodHolder = periodHolder.next;
+ periodHolder = periodHolder.getNext();
}
}
@@ -1102,11 +1220,12 @@ import java.util.Collections;
private boolean isTimelineReady() {
MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ MediaPeriodHolder nextPeriodHolder = playingPeriodHolder.getNext();
long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
return playingPeriodDurationUs == C.TIME_UNSET
|| playbackInfo.positionUs < playingPeriodDurationUs
- || (playingPeriodHolder.next != null
- && (playingPeriodHolder.next.prepared || playingPeriodHolder.next.info.id.isAd()));
+ || (nextPeriodHolder != null
+ && (nextPeriodHolder.prepared || nextPeriodHolder.info.id.isAd()));
}
private void maybeThrowSourceInfoRefreshError() throws IOException {
@@ -1125,8 +1244,9 @@ import java.util.Collections;
private void maybeThrowPeriodPrepareError() throws IOException {
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
- if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared
- && (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) {
+ if (loadingPeriodHolder != null
+ && !loadingPeriodHolder.prepared
+ && (readingPeriodHolder == null || readingPeriodHolder.getNext() == loadingPeriodHolder)) {
for (Renderer renderer : enabledRenderers) {
if (!renderer.hasReadStreamToEnd()) {
return;
@@ -1142,6 +1262,8 @@ import java.util.Collections;
// Stale event.
return;
}
+ playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
+ pendingPrepareCount = 0;
Timeline oldTimeline = playbackInfo.timeline;
Timeline timeline = sourceRefreshInfo.timeline;
@@ -1150,138 +1272,107 @@ import java.util.Collections;
playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest);
resolvePendingMessagePositions();
- if (pendingPrepareCount > 0) {
- playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
- pendingPrepareCount = 0;
- if (pendingInitialSeekPosition != null) {
- Pair