(jContext);
+ if (context->libgav1_status_code != kLibgav1StatusOk ||
+ context->jni_status_code != kJniStatusOk) {
+ return kStatusError;
+ }
+ return kStatusOk;
+}
+
+// TODO(b/139902005): Add functions for getting libgav1 version and build
+// configuration once libgav1 ABI provides this information.
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..0d7d96db4c 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,22 @@ 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
+ api 'com.google.android.gms:play-services-cast-framework:17.0.0'
+ implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils')
- testImplementation 'junit:junit:' + junitVersion
- testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
- 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 871c28b785..5b91410ff9 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,11 +16,11 @@
package com.google.android.exoplayer2.ext.cast;
import android.os.Looper;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.BasePlayer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
@@ -45,16 +45,19 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
-import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* {@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.
+ * injected {@link CastContext}. 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
* alter it will be ignored. Querying the player state is possible even when no session is
@@ -64,6 +67,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
*/
public final class CastPlayer extends BasePlayer {
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.cast");
+ }
+
private static final String TAG = "CastPlayer";
private static final int RENDERER_COUNT = 3;
@@ -80,29 +87,29 @@ public final class CastPlayer extends BasePlayer {
private final CastTimelineTracker timelineTracker;
private final Timeline.Period period;
- private RemoteMediaClient remoteMediaClient;
-
// Result callbacks.
private final StatusListener statusListener;
private final SeekResultCallback seekResultCallback;
- // Listeners.
- private final CopyOnWriteArraySet listeners;
- private SessionAvailabilityListener sessionAvailabilityListener;
+ // Listeners and notification.
+ private final CopyOnWriteArrayList listeners;
+ private final ArrayList notificationsBatch;
+ private final ArrayDeque ongoingNotificationsTasks;
+ @Nullable private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state.
+ private final StateHolder playWhenReady;
+ private final StateHolder repeatMode;
+ @Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection;
- private int playbackState;
- private int repeatMode;
+ @Player.State private int playbackState;
private int currentWindowIndex;
- private boolean playWhenReady;
private long lastReportedPositionMs;
private int pendingSeekCount;
private int pendingSeekWindowIndex;
private long pendingSeekPositionMs;
- private boolean waitingForInitialTimeline;
/**
* @param castContext The context from which the cast session is obtained.
@@ -113,21 +120,24 @@ public final class CastPlayer extends BasePlayer {
period = new Timeline.Period();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
- listeners = new CopyOnWriteArraySet<>();
-
- SessionManager sessionManager = castContext.getSessionManager();
- sessionManager.addSessionManagerListener(statusListener, CastSession.class);
- CastSession session = sessionManager.getCurrentCastSession();
- remoteMediaClient = session != null ? session.getRemoteMediaClient() : null;
+ listeners = new CopyOnWriteArrayList<>();
+ notificationsBatch = new ArrayList<>();
+ ongoingNotificationsTasks = new ArrayDeque<>();
+ playWhenReady = new StateHolder<>(false);
+ repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
playbackState = STATE_IDLE;
- repeatMode = REPEAT_MODE_OFF;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
currentTrackGroups = TrackGroupArray.EMPTY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
- updateInternalState();
+
+ SessionManager sessionManager = castContext.getSessionManager();
+ sessionManager.addSessionManagerListener(statusListener, CastSession.class);
+ CastSession session = sessionManager.getCurrentCastSession();
+ setRemoteMediaClient(session != null ? session.getRemoteMediaClient() : null);
+ updateInternalStateAndNotifyIfChanged();
}
// Media Queue manipulation methods.
@@ -141,6 +151,7 @@ public final class CastPlayer extends BasePlayer {
* starts at position 0.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
+ @Nullable
public PendingResult loadItem(MediaQueueItem item, long positionMs) {
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
}
@@ -156,11 +167,11 @@ public final class CastPlayer extends BasePlayer {
* @param repeatMode The repeat mode for the created media queue.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
- public PendingResult loadItems(MediaQueueItem[] items, int startIndex,
- long positionMs, @RepeatMode int repeatMode) {
+ @Nullable
+ public PendingResult loadItems(
+ MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
if (remoteMediaClient != null) {
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
- waitingForInitialTimeline = true;
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
positionMs, null);
}
@@ -173,6 +184,7 @@ public final class CastPlayer extends BasePlayer {
* @param items The items to append.
* @return The Cast {@code PendingResult}, or null if no media queue exists.
*/
+ @Nullable
public PendingResult addItems(MediaQueueItem... items) {
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
}
@@ -187,6 +199,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
+ @Nullable
public PendingResult addItems(int periodId, MediaQueueItem... items) {
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
@@ -204,6 +217,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
+ @Nullable
public PendingResult removeItem(int periodId) {
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
return remoteMediaClient.queueRemoveItem(periodId, null);
@@ -222,6 +236,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
+ @Nullable
public PendingResult moveItem(int periodId, int newIndex) {
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
@@ -239,6 +254,7 @@ public final class CastPlayer extends BasePlayer {
* @return The item that corresponds to the period with the given id, or null if no media queue or
* period with id {@code periodId} exist.
*/
+ @Nullable
public MediaQueueItem getItem(int periodId) {
MediaStatus mediaStatus = getMediaStatus();
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
@@ -257,9 +273,9 @@ public final class CastPlayer extends BasePlayer {
/**
* Sets a listener for updates on the cast session availability.
*
- * @param listener The {@link SessionAvailabilityListener}.
+ * @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
*/
- public void setSessionAvailabilityListener(SessionAvailabilityListener listener) {
+ public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
sessionAvailabilityListener = listener;
}
@@ -296,20 +312,33 @@ public final class CastPlayer extends BasePlayer {
@Override
public void addListener(EventListener listener) {
- listeners.add(listener);
+ listeners.addIfAbsent(new ListenerHolder(listener));
}
@Override
public void removeListener(EventListener listener) {
- listeners.remove(listener);
+ for (ListenerHolder listenerHolder : listeners) {
+ if (listenerHolder.listener.equals(listener)) {
+ listenerHolder.release();
+ listeners.remove(listenerHolder);
+ }
+ }
}
@Override
+ @Player.State
public int getPlaybackState() {
return playbackState;
}
@Override
+ @PlaybackSuppressionReason
+ public int getPlaybackSuppressionReason() {
+ return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
+ }
+
+ @Override
+ @Nullable
public ExoPlaybackException getPlaybackError() {
return null;
}
@@ -319,16 +348,29 @@ public final class CastPlayer extends BasePlayer {
if (remoteMediaClient == null) {
return;
}
- if (playWhenReady) {
- remoteMediaClient.play();
- } else {
- remoteMediaClient.pause();
- }
+ // We update the local state and send the message to the receiver app, which will cause the
+ // operation to be perceived as synchronous by the user. When the operation reports a result,
+ // the local state will be updated to reflect the state reported by the Cast SDK.
+ setPlayerStateAndNotifyIfChanged(playWhenReady, playbackState);
+ flushNotifications();
+ PendingResult pendingResult =
+ playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause();
+ this.playWhenReady.pendingResultCallback =
+ new ResultCallback() {
+ @Override
+ public void onResult(MediaChannelResult mediaChannelResult) {
+ if (remoteMediaClient != null) {
+ updatePlayerStateAndNotifyIfChanged(this);
+ flushNotifications();
+ }
+ }
+ };
+ pendingResult.setResultCallback(this.playWhenReady.pendingResultCallback);
}
@Override
public boolean getPlayWhenReady() {
- return playWhenReady;
+ return playWhenReady.value;
}
@Override
@@ -347,14 +389,13 @@ public final class CastPlayer extends BasePlayer {
pendingSeekCount++;
pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs;
- for (EventListener listener : listeners) {
- listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
- }
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
} else if (pendingSeekCount == 0) {
- for (EventListener listener : listeners) {
- listener.onSeekProcessed();
- }
+ notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
}
+ flushNotifications();
}
@Override
@@ -405,14 +446,32 @@ public final class CastPlayer extends BasePlayer {
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
- if (remoteMediaClient != null) {
- remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null);
+ if (remoteMediaClient == null) {
+ return;
}
+ // We update the local state and send the message to the receiver app, which will cause the
+ // operation to be perceived as synchronous by the user. When the operation reports a result,
+ // the local state will be updated to reflect the state reported by the Cast SDK.
+ setRepeatModeAndNotifyIfChanged(repeatMode);
+ flushNotifications();
+ PendingResult pendingResult =
+ remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), /* jsonObject= */ null);
+ this.repeatMode.pendingResultCallback =
+ new ResultCallback() {
+ @Override
+ public void onResult(MediaChannelResult mediaChannelResult) {
+ if (remoteMediaClient != null) {
+ updateRepeatModeAndNotifyIfChanged(this);
+ flushNotifications();
+ }
+ }
+ };
+ pendingResult.setResultCallback(this.repeatMode.pendingResultCallback);
}
@Override
@RepeatMode public int getRepeatMode() {
- return repeatMode;
+ return repeatMode.value;
}
@Override
@@ -441,11 +500,6 @@ public final class CastPlayer extends BasePlayer {
return currentTimeline;
}
- @Override
- @Nullable public Object getCurrentManifest() {
- return null;
- }
-
@Override
public int getCurrentPeriodIndex() {
return getCurrentWindowIndex();
@@ -518,52 +572,80 @@ public final class CastPlayer extends BasePlayer {
// Internal methods.
- public void updateInternalState() {
+ private void updateInternalStateAndNotifyIfChanged() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return;
}
+ boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
+ updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
+ boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
+ if (wasPlaying != isPlaying) {
+ notificationsBatch.add(
+ new ListenerNotificationTask(listener -> listener.onIsPlayingChanged(isPlaying)));
+ }
+ updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
+ updateTimelineAndNotifyIfChanged();
- int playbackState = fetchPlaybackState(remoteMediaClient);
- boolean playWhenReady = !remoteMediaClient.isPaused();
- if (this.playbackState != playbackState
- || this.playWhenReady != playWhenReady) {
- this.playbackState = playbackState;
- this.playWhenReady = playWhenReady;
- for (EventListener listener : listeners) {
- listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
- }
+ int currentWindowIndex = C.INDEX_UNSET;
+ MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
+ if (currentItem != null) {
+ currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());
}
- @RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
- if (this.repeatMode != repeatMode) {
- this.repeatMode = repeatMode;
- for (EventListener listener : listeners) {
- listener.onRepeatModeChanged(repeatMode);
- }
+ if (currentWindowIndex == C.INDEX_UNSET) {
+ // The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
+ currentWindowIndex = 0;
}
- int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
this.currentWindowIndex = currentWindowIndex;
- for (EventListener listener : listeners) {
- listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION);
- }
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener ->
+ listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
}
- if (updateTracksAndSelections()) {
- for (EventListener listener : listeners) {
- listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
- }
+ if (updateTracksAndSelectionsAndNotifyIfChanged()) {
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
}
- maybeUpdateTimelineAndNotify();
+ flushNotifications();
}
- private void maybeUpdateTimelineAndNotify() {
+ /**
+ * Updates {@link #playWhenReady} and {@link #playbackState} to match the Cast {@code
+ * remoteMediaClient} state, and notifies listeners of any state changes.
+ *
+ * This method will only update values whose {@link StateHolder#pendingResultCallback} matches
+ * the given {@code resultCallback}.
+ */
+ @RequiresNonNull("remoteMediaClient")
+ private void updatePlayerStateAndNotifyIfChanged(@Nullable ResultCallback> resultCallback) {
+ boolean newPlayWhenReadyValue = playWhenReady.value;
+ if (playWhenReady.acceptsUpdate(resultCallback)) {
+ newPlayWhenReadyValue = !remoteMediaClient.isPaused();
+ playWhenReady.clearPendingResultCallback();
+ }
+ // We do not mask the playback state, so try setting it regardless of the playWhenReady masking.
+ setPlayerStateAndNotifyIfChanged(newPlayWhenReadyValue, fetchPlaybackState(remoteMediaClient));
+ }
+
+ @RequiresNonNull("remoteMediaClient")
+ private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback> resultCallback) {
+ if (repeatMode.acceptsUpdate(resultCallback)) {
+ setRepeatModeAndNotifyIfChanged(fetchRepeatMode(remoteMediaClient));
+ repeatMode.clearPendingResultCallback();
+ }
+ }
+
+ private void updateTimelineAndNotifyIfChanged() {
if (updateTimeline()) {
- @Player.TimelineChangeReason int reason = waitingForInitialTimeline
- ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
- waitingForInitialTimeline = false;
- for (EventListener listener : listeners) {
- listener.onTimelineChanged(currentTimeline, null, reason);
- }
+ // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
+ // TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener ->
+ listener.onTimelineChanged(
+ currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
}
}
@@ -574,14 +656,14 @@ 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);
}
- /**
- * Updates the internal tracks and selection and returns whether they have changed.
- */
- private boolean updateTracksAndSelections() {
+ /** Updates the internal tracks and selection and returns whether they have changed. */
+ private boolean updateTracksAndSelectionsAndNotifyIfChanged() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return false;
@@ -627,6 +709,25 @@ public final class CastPlayer extends BasePlayer {
return false;
}
+ private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
+ if (this.repeatMode.value != repeatMode) {
+ this.repeatMode.value = repeatMode;
+ notificationsBatch.add(
+ new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode)));
+ }
+ }
+
+ private void setPlayerStateAndNotifyIfChanged(
+ boolean playWhenReady, @Player.State int playbackState) {
+ if (this.playWhenReady.value != playWhenReady || this.playbackState != playbackState) {
+ this.playWhenReady.value = playWhenReady;
+ this.playbackState = playbackState;
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener -> listener.onPlayerStateChanged(playWhenReady, playbackState)));
+ }
+ }
+
private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
if (this.remoteMediaClient == remoteMediaClient) {
// Do nothing.
@@ -643,7 +744,7 @@ public final class CastPlayer extends BasePlayer {
}
remoteMediaClient.addListener(statusListener);
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
- updateInternalState();
+ updateInternalStateAndNotifyIfChanged();
} else {
if (sessionAvailabilityListener != null) {
sessionAvailabilityListener.onCastSessionUnavailable();
@@ -651,7 +752,8 @@ public final class CastPlayer extends BasePlayer {
}
}
- private @Nullable MediaStatus getMediaStatus() {
+ @Nullable
+ private MediaStatus getMediaStatus() {
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
}
@@ -699,16 +801,6 @@ public final class CastPlayer extends BasePlayer {
}
}
- /**
- * Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
- * there is no media session, returns 0.
- */
- private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
- Integer currentItemId = mediaStatus != null
- ? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
- return currentItemId != null ? currentItemId : 0;
- }
-
private static boolean isTrackActive(long id, long[] activeTrackIds) {
for (long activeTrackId : activeTrackIds) {
if (activeTrackId == id) {
@@ -739,8 +831,26 @@ public final class CastPlayer extends BasePlayer {
}
}
- private final class StatusListener implements RemoteMediaClient.Listener,
- SessionManagerListener, RemoteMediaClient.ProgressListener {
+ private void flushNotifications() {
+ boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
+ ongoingNotificationsTasks.addAll(notificationsBatch);
+ notificationsBatch.clear();
+ if (recursiveNotification) {
+ // This will be handled once the current notification task is finished.
+ return;
+ }
+ while (!ongoingNotificationsTasks.isEmpty()) {
+ ongoingNotificationsTasks.peekFirst().execute();
+ ongoingNotificationsTasks.removeFirst();
+ }
+ }
+
+ // Internal classes.
+
+ private final class StatusListener
+ implements RemoteMediaClient.Listener,
+ SessionManagerListener,
+ RemoteMediaClient.ProgressListener {
// RemoteMediaClient.ProgressListener implementation.
@@ -753,7 +863,7 @@ public final class CastPlayer extends BasePlayer {
@Override
public void onStatusUpdated() {
- updateInternalState();
+ updateInternalStateAndNotifyIfChanged();
}
@Override
@@ -761,7 +871,7 @@ public final class CastPlayer extends BasePlayer {
@Override
public void onQueueStatusUpdated() {
- maybeUpdateTimelineAndNotify();
+ updateTimelineAndNotifyIfChanged();
}
@Override
@@ -824,12 +934,10 @@ public final class CastPlayer extends BasePlayer {
}
- // Result callbacks hooks.
-
private final class SeekResultCallback implements ResultCallback {
@Override
- public void onResult(@NonNull MediaChannelResult result) {
+ public void onResult(MediaChannelResult result) {
int statusCode = result.getStatus().getStatusCode();
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
@@ -838,11 +946,62 @@ public final class CastPlayer extends BasePlayer {
if (--pendingSeekCount == 0) {
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
- for (EventListener listener : listeners) {
- listener.onSeekProcessed();
- }
+ notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
+ flushNotifications();
}
}
}
+ /** Holds the value and the masking status of a specific part of the {@link CastPlayer} state. */
+ private static final class StateHolder {
+
+ /** The user-facing value of a specific part of the {@link CastPlayer} state. */
+ public T value;
+
+ /**
+ * If {@link #value} is being masked, holds the result callback for the operation that triggered
+ * the masking. Or null if {@link #value} is not being masked.
+ */
+ @Nullable public ResultCallback pendingResultCallback;
+
+ public StateHolder(T initialValue) {
+ value = initialValue;
+ }
+
+ public void clearPendingResultCallback() {
+ pendingResultCallback = null;
+ }
+
+ /**
+ * Returns whether this state holder accepts updates coming from the given result callback.
+ *
+ * A null {@code resultCallback} means that the update is a regular receiver state update, in
+ * which case the update will only be accepted if {@link #value} is not being masked. If {@link
+ * #value} is being masked, the update will only be accepted if {@code resultCallback} is the
+ * same as the {@link #pendingResultCallback}.
+ *
+ * @param resultCallback A result callback. May be null if the update comes from a regular
+ * receiver status update.
+ */
+ public boolean acceptsUpdate(@Nullable ResultCallback> resultCallback) {
+ return pendingResultCallback == resultCallback;
+ }
+ }
+
+ private final class ListenerNotificationTask {
+
+ private final Iterator listenersSnapshot;
+ private final ListenerInvocation listenerInvocation;
+
+ private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
+ this.listenersSnapshot = listeners.iterator();
+ this.listenerInvocation = listenerInvocation;
+ }
+
+ public void execute() {
+ while (listenersSnapshot.hasNext()) {
+ listenersSnapshot.next().invoke(listenerInvocation);
+ }
+ }
+ }
}
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 d86c4b3ebf..38a7a692b2 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,53 +15,101 @@
*/
package com.google.android.exoplayer2.ext.cast;
-import android.support.annotation.Nullable;
+import android.util.SparseArray;
import android.util.SparseIntArray;
+import androidx.annotation.Nullable;
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;
+ /** Whether the item is live content, or {@code false} if unknown. */
+ public final boolean isLive;
+
+ private ItemData() {
+ this(
+ /* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */
+ C.TIME_UNSET,
+ /* isLive= */ false);
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param durationUs See {@link #durationsUs}.
+ * @param defaultPositionUs See {@link #defaultPositionUs}.
+ * @param isLive See {@link #isLive}.
+ */
+ public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
+ this.durationUs = durationUs;
+ this.defaultPositionUs = defaultPositionUs;
+ this.isLive = isLive;
+ }
+
+ /**
+ * Returns a copy of this instance with the given values.
+ *
+ * @param durationUs The duration in microseconds, or {@link C#TIME_UNSET} if unknown.
+ * @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
+ * if unknown.
+ * @param isLive Whether the item is live, or {@code false} if unknown.
+ */
+ public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) {
+ if (durationUs == this.durationUs
+ && defaultPositionUs == this.defaultPositionUs
+ && isLive == this.isLive) {
+ return this;
+ }
+ return new ItemData(durationUs, defaultPositionUs, isLive);
+ }
+ }
+
+ /** {@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;
private final long[] durationsUs;
private final long[] defaultPositionsUs;
+ private final boolean[] isLive;
/**
- * @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++;
+ isLive = new boolean[itemCount];
+ 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 == C.TIME_UNSET ? 0 : data.defaultPositionUs;
+ isLive[i] = data.isLive;
}
}
@@ -73,17 +121,19 @@ import java.util.Map;
}
@Override
- public Window getWindow(
- int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
+ public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET;
- Object tag = setTag ? ids[windowIndex] : null;
return window.set(
- tag,
+ /* uid= */ ids[windowIndex],
+ /* tag= */ ids[windowIndex],
+ /* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
+ /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic,
isDynamic,
+ isLive[windowIndex],
defaultPositionsUs[windowIndex],
durationUs,
/* firstPeriodIndex= */ windowIndex,
@@ -124,7 +174,8 @@ import java.util.Map;
CastTimeline that = (CastTimeline) other;
return Arrays.equals(ids, that.ids)
&& Arrays.equals(durationsUs, that.durationsUs)
- && Arrays.equals(defaultPositionsUs, that.defaultPositionsUs);
+ && Arrays.equals(defaultPositionsUs, that.defaultPositionsUs)
+ && Arrays.equals(isLive, that.isLive);
}
@Override
@@ -132,6 +183,7 @@ import java.util.Map;
int result = Arrays.hashCode(ids);
result = 31 * result + Arrays.hashCode(durationsUs);
result = 31 * result + Arrays.hashCode(defaultPositionsUs);
+ result = 31 * result + Arrays.hashCode(isLive);
return result;
}
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..3ebd89c8fc 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,94 @@
*/
package com.google.android.exoplayer2.ext.cast;
+import android.util.SparseArray;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
import com.google.android.gms.cast.MediaInfo;
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();
+ updateItemData(
+ currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET);
+
+ for (MediaQueueItem item : mediaStatus.getQueueItems()) {
+ long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
+ updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs);
+ }
+
+ return new CastTimeline(itemIds, itemIdToData);
}
- private void removeUnusedDurationEntries(List items) {
- scratchContentIdSet.clear();
- for (MediaQueueItem item : items) {
- scratchContentIdSet.add(item.getMedia().getContentId());
+ private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) {
+ CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY);
+ long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
+ if (durationUs == C.TIME_UNSET) {
+ durationUs = previousData.durationUs;
+ }
+ boolean isLive =
+ mediaInfo == null
+ ? previousData.isLive
+ : mediaInfo.getStreamType() == MediaInfo.STREAM_TYPE_LIVE;
+ if (defaultPositionUs == C.TIME_UNSET) {
+ defaultPositionUs = previousData.defaultPositionUs;
+ }
+ itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive));
+ }
+
+ 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..1dc25576a0 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
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.cast;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.gms.cast.CastStatusCodes;
@@ -31,11 +32,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;
+ public static long getStreamDurationUs(@Nullable MediaInfo mediaInfo) {
+ if (mediaInfo == null) {
+ return C.TIME_UNSET;
+ }
+ long durationMs = mediaInfo.getStreamDuration();
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
}
@@ -109,6 +112,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/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
index 06f0bec971..ebadb0a08a 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
@@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider;
+import java.util.Collections;
import java.util.List;
/**
@@ -27,16 +28,38 @@ import java.util.List;
*/
public final class DefaultCastOptionsProvider implements OptionsProvider {
+ /**
+ * App id of the Default Media Receiver app. Apps that do not require DRM support may use this
+ * receiver receiver app ID.
+ *
+ * See https://developers.google.com/cast/docs/caf_receiver/#default_media_receiver.
+ */
+ public static final String APP_ID_DEFAULT_RECEIVER =
+ CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
+
+ /**
+ * App id for receiver app with rudimentary support for DRM.
+ *
+ *
This app id is only suitable for ExoPlayer's Cast Demo app, and it is not intended for
+ * production use. In order to use DRM, custom receiver apps should be used. For environments that
+ * do not require DRM, the default receiver app should be used (see {@link
+ * #APP_ID_DEFAULT_RECEIVER}).
+ */
+ // TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref:
+ // b/128603245].
+ public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273";
+
@Override
public CastOptions getCastOptions(Context context) {
return new CastOptions.Builder()
- .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
- .setStopReceiverApplicationWhenEndingSession(true).build();
+ .setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM)
+ .setStopReceiverApplicationWhenEndingSession(true)
+ .build();
}
@Override
public List getAdditionalSessionProviders(Context context) {
- return null;
+ return Collections.emptyList();
}
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java
new file mode 100644
index 0000000000..098803a512
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java
@@ -0,0 +1,167 @@
+/*
+ * 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.cast;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaMetadata;
+import com.google.android.gms.cast.MediaQueueItem;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.UUID;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Default {@link MediaItemConverter} implementation. */
+public final class DefaultMediaItemConverter implements MediaItemConverter {
+
+ private static final String KEY_MEDIA_ITEM = "mediaItem";
+ private static final String KEY_PLAYER_CONFIG = "exoPlayerConfig";
+ private static final String KEY_URI = "uri";
+ private static final String KEY_TITLE = "title";
+ private static final String KEY_MIME_TYPE = "mimeType";
+ private static final String KEY_DRM_CONFIGURATION = "drmConfiguration";
+ private static final String KEY_UUID = "uuid";
+ private static final String KEY_LICENSE_URI = "licenseUri";
+ private static final String KEY_REQUEST_HEADERS = "requestHeaders";
+
+ @Override
+ public MediaItem toMediaItem(MediaQueueItem item) {
+ return getMediaItem(item.getMedia().getCustomData());
+ }
+
+ @Override
+ public MediaQueueItem toMediaQueueItem(MediaItem item) {
+ if (item.mimeType == null) {
+ throw new IllegalArgumentException("The item must specify its mimeType");
+ }
+ MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
+ if (item.title != null) {
+ metadata.putString(MediaMetadata.KEY_TITLE, item.title);
+ }
+ MediaInfo mediaInfo =
+ new MediaInfo.Builder(item.uri.toString())
+ .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
+ .setContentType(item.mimeType)
+ .setMetadata(metadata)
+ .setCustomData(getCustomData(item))
+ .build();
+ return new MediaQueueItem.Builder(mediaInfo).build();
+ }
+
+ // Deserialization.
+
+ private static MediaItem getMediaItem(JSONObject customData) {
+ try {
+ JSONObject mediaItemJson = customData.getJSONObject(KEY_MEDIA_ITEM);
+ MediaItem.Builder builder = new MediaItem.Builder();
+ builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
+ if (mediaItemJson.has(KEY_TITLE)) {
+ builder.setTitle(mediaItemJson.getString(KEY_TITLE));
+ }
+ if (mediaItemJson.has(KEY_MIME_TYPE)) {
+ builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE));
+ }
+ if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) {
+ builder.setDrmConfiguration(
+ getDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION)));
+ }
+ return builder.build();
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static DrmConfiguration getDrmConfiguration(JSONObject json) throws JSONException {
+ UUID uuid = UUID.fromString(json.getString(KEY_UUID));
+ Uri licenseUri = Uri.parse(json.getString(KEY_LICENSE_URI));
+ JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS);
+ HashMap requestHeaders = new HashMap<>();
+ for (Iterator iterator = requestHeadersJson.keys(); iterator.hasNext(); ) {
+ String key = iterator.next();
+ requestHeaders.put(key, requestHeadersJson.getString(key));
+ }
+ return new DrmConfiguration(uuid, licenseUri, requestHeaders);
+ }
+
+ // Serialization.
+
+ private static JSONObject getCustomData(MediaItem item) {
+ JSONObject json = new JSONObject();
+ try {
+ json.put(KEY_MEDIA_ITEM, getMediaItemJson(item));
+ JSONObject playerConfigJson = getPlayerConfigJson(item);
+ if (playerConfigJson != null) {
+ json.put(KEY_PLAYER_CONFIG, playerConfigJson);
+ }
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ return json;
+ }
+
+ private static JSONObject getMediaItemJson(MediaItem item) throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put(KEY_URI, item.uri.toString());
+ json.put(KEY_TITLE, item.title);
+ json.put(KEY_MIME_TYPE, item.mimeType);
+ if (item.drmConfiguration != null) {
+ json.put(KEY_DRM_CONFIGURATION, getDrmConfigurationJson(item.drmConfiguration));
+ }
+ return json;
+ }
+
+ private static JSONObject getDrmConfigurationJson(DrmConfiguration drmConfiguration)
+ throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put(KEY_UUID, drmConfiguration.uuid);
+ json.put(KEY_LICENSE_URI, drmConfiguration.licenseUri);
+ json.put(KEY_REQUEST_HEADERS, new JSONObject(drmConfiguration.requestHeaders));
+ return json;
+ }
+
+ @Nullable
+ private static JSONObject getPlayerConfigJson(MediaItem item) throws JSONException {
+ DrmConfiguration drmConfiguration = item.drmConfiguration;
+ if (drmConfiguration == null) {
+ return null;
+ }
+
+ String drmScheme;
+ if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) {
+ drmScheme = "widevine";
+ } else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) {
+ drmScheme = "playready";
+ } else {
+ return null;
+ }
+
+ JSONObject exoPlayerConfigJson = new JSONObject();
+ exoPlayerConfigJson.put("withCredentials", false);
+ exoPlayerConfigJson.put("protectionSystem", drmScheme);
+ if (drmConfiguration.licenseUri != null) {
+ exoPlayerConfigJson.put("licenseUrl", drmConfiguration.licenseUri);
+ }
+ if (!drmConfiguration.requestHeaders.isEmpty()) {
+ exoPlayerConfigJson.put("headers", new JSONObject(drmConfiguration.requestHeaders));
+ }
+
+ return exoPlayerConfigJson;
+ }
+}
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
index 8ab10e165d..7ac0da7078 100644
--- 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
@@ -16,43 +16,32 @@
package com.google.android.exoplayer2.ext.cast;
import android.net.Uri;
-import android.support.annotation.Nullable;
-import com.google.android.exoplayer2.C;
+import androidx.annotation.Nullable;
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. */
+/** Representation of a media item. */
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;
+ @Nullable private Uri uri;
+ @Nullable private String title;
+ @Nullable private String mimeType;
+ @Nullable private DrmConfiguration drmConfiguration;
- /** Creates an builder with default field values. */
- public Builder() {
- clearInternal();
+ /** See {@link MediaItem#uri}. */
+ public Builder setUri(String uri) {
+ return setUri(Uri.parse(uri));
}
- /** See {@link MediaItem#uuid}. */
- public Builder setUuid(UUID uuid) {
- this.uuid = uuid;
+ /** See {@link MediaItem#uri}. */
+ public Builder setUri(Uri uri) {
+ this.uri = uri;
return this;
}
@@ -62,307 +51,125 @@ public final class MediaItem {
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();
+ /** See {@link MediaItem#drmConfiguration}. */
+ public Builder setDrmConfiguration(DrmConfiguration drmConfiguration) {
+ this.drmConfiguration = drmConfiguration;
return this;
}
- /**
- * Returns a new {@link MediaItem} instance with the current builder values. This method also
- * clears any values passed to {@link #setUuid(UUID)}.
- */
+ /** Returns a new {@link MediaItem} instance with the current builder values. */
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 = "";
+ Assertions.checkNotNull(uri);
+ return new MediaItem(uri, title, mimeType, drmConfiguration);
}
}
- /** 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 {
+ /** DRM configuration for a media item. */
+ public static final class DrmConfiguration {
/** 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.
+ * Optional license server {@link Uri}. If {@code null} then the license server must be
+ * specified by the media.
*/
- @Nullable public final UriBundle licenseServer;
+ @Nullable public final Uri licenseUri;
+
+ /** Headers that should be attached to any license requests. */
+ public final Map requestHeaders;
/**
* Creates an instance.
*
* @param uuid See {@link #uuid}.
- * @param licenseServer See {@link #licenseServer}.
+ * @param licenseUri See {@link #licenseUri}.
+ * @param requestHeaders See {@link #requestHeaders}.
*/
- public DrmScheme(UUID uuid, @Nullable UriBundle licenseServer) {
+ public DrmConfiguration(
+ UUID uuid, @Nullable Uri licenseUri, @Nullable Map requestHeaders) {
this.uuid = uuid;
- this.licenseServer = licenseServer;
+ this.licenseUri = licenseUri;
+ this.requestHeaders =
+ requestHeaders == null
+ ? Collections.emptyMap()
+ : Collections.unmodifiableMap(requestHeaders);
}
@Override
- public boolean equals(@Nullable Object other) {
- if (this == other) {
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
return true;
}
- if (other == null || getClass() != other.getClass()) {
+ if (obj == null || getClass() != obj.getClass()) {
return false;
}
- DrmScheme drmScheme = (DrmScheme) other;
- return uuid.equals(drmScheme.uuid) && Util.areEqual(licenseServer, drmScheme.licenseServer);
+ DrmConfiguration other = (DrmConfiguration) obj;
+ return uuid.equals(other.uuid)
+ && Util.areEqual(licenseUri, other.licenseUri)
+ && requestHeaders.equals(other.requestHeaders);
}
@Override
public int hashCode() {
int result = uuid.hashCode();
- result = 31 * result + (licenseServer != null ? licenseServer.hashCode() : 0);
+ result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0);
+ result = 31 * result + requestHeaders.hashCode();
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 media {@link Uri}. */
+ public final Uri uri;
- /** The title of the item. The default value is an empty string. */
- public final String title;
+ /** The title of the item, or {@code null} if unspecified. */
+ @Nullable public final String title;
- /** A description for the item. The default value is an empty string. */
- public final String description;
+ /** The mime type for the media, or {@code null} if unspecified. */
+ @Nullable public final String mimeType;
- /**
- * A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}.
- */
- public final UriBundle media;
+ /** Optional {@link DrmConfiguration} for the media. */
+ @Nullable public final DrmConfiguration drmConfiguration;
- /**
- * An optional opaque object to attach to the media item. Handling of this attachment is
- * implementation specific. The default value is null.
- */
- @Nullable public final Object attachment;
-
- /**
- * Immutable list of {@link DrmScheme} instances sorted in decreasing order of preference. The
- * default value is an empty list.
- */
- public final List drmSchemes;
-
- /**
- * The position in microseconds at which playback of this media item should start. {@link
- * C#TIME_UNSET} if playback should start at the default position. The default value is {@link
- * C#TIME_UNSET}.
- */
- public final long startPositionUs;
-
- /**
- * The position in microseconds at which playback of this media item should end. {@link
- * C#TIME_UNSET} if playback should end at the end of the media. The default value is {@link
- * C#TIME_UNSET}.
- */
- public final long endPositionUs;
-
- /**
- * The mime type of this media item. The default value is an empty string.
- *
- * The usage of this mime type is optional and player implementation specific.
- */
- public final String mimeType;
-
- // TODO: Add support for sideloaded tracks, artwork, icon, and subtitle.
+ private MediaItem(
+ Uri uri,
+ @Nullable String title,
+ @Nullable String mimeType,
+ @Nullable DrmConfiguration drmConfiguration) {
+ this.uri = uri;
+ this.title = title;
+ this.mimeType = mimeType;
+ this.drmConfiguration = drmConfiguration;
+ }
@Override
- public boolean equals(@Nullable Object other) {
- if (this == other) {
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
return true;
}
- if (other == null || getClass() != other.getClass()) {
+ if (obj == null || getClass() != obj.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);
+ MediaItem other = (MediaItem) obj;
+ return uri.equals(other.uri)
+ && Util.areEqual(title, other.title)
+ && Util.areEqual(mimeType, other.mimeType)
+ && Util.areEqual(drmConfiguration, other.drmConfiguration);
}
@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();
+ int result = uri.hashCode();
+ result = 31 * result + (title == null ? 0 : title.hashCode());
+ result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
+ result = 31 * result + (mimeType == null ? 0 : 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/MediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java
new file mode 100644
index 0000000000..23633aa4d2
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java
@@ -0,0 +1,38 @@
+/*
+ * 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.cast;
+
+import com.google.android.gms.cast.MediaQueueItem;
+
+/** Converts between {@link MediaItem} and the Cast SDK's {@link MediaQueueItem}. */
+public interface MediaItemConverter {
+
+ /**
+ * Converts a {@link MediaItem} to a {@link MediaQueueItem}.
+ *
+ * @param mediaItem The {@link MediaItem}.
+ * @return An equivalent {@link MediaQueueItem}.
+ */
+ MediaQueueItem toMediaQueueItem(MediaItem mediaItem);
+
+ /**
+ * Converts a {@link MediaQueueItem} to a {@link MediaItem}.
+ *
+ * @param mediaQueueItem The {@link MediaQueueItem}.
+ * @return The equivalent {@link MediaItem}.
+ */
+ MediaItem toMediaItem(MediaQueueItem mediaQueueItem);
+}
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
deleted file mode 100644
index 184e347e1c..0000000000
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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/package-info.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java
new file mode 100644
index 0000000000..07055905a6
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.cast;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/cast/src/test/AndroidManifest.xml b/extensions/cast/src/test/AndroidManifest.xml
index aea8bda663..35a5150a47 100644
--- a/extensions/cast/src/test/AndroidManifest.xml
+++ b/extensions/cast/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java
new file mode 100644
index 0000000000..1346c1f842
--- /dev/null
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.cast;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.Player;
+import com.google.android.gms.cast.MediaStatus;
+import com.google.android.gms.cast.framework.CastContext;
+import com.google.android.gms.cast.framework.CastSession;
+import com.google.android.gms.cast.framework.SessionManager;
+import com.google.android.gms.cast.framework.media.MediaQueue;
+import com.google.android.gms.cast.framework.media.RemoteMediaClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.common.api.ResultCallback;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+/** Tests for {@link CastPlayer}. */
+@RunWith(AndroidJUnit4.class)
+public class CastPlayerTest {
+
+ private CastPlayer castPlayer;
+ private RemoteMediaClient.Listener remoteMediaClientListener;
+ @Mock private RemoteMediaClient mockRemoteMediaClient;
+ @Mock private MediaStatus mockMediaStatus;
+ @Mock private MediaQueue mockMediaQueue;
+ @Mock private CastContext mockCastContext;
+ @Mock private SessionManager mockSessionManager;
+ @Mock private CastSession mockCastSession;
+ @Mock private Player.EventListener mockListener;
+ @Mock private PendingResult mockPendingResult;
+
+ @Captor
+ private ArgumentCaptor>
+ setResultCallbackArgumentCaptor;
+
+ @Captor private ArgumentCaptor listenerArgumentCaptor;
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ when(mockCastContext.getSessionManager()).thenReturn(mockSessionManager);
+ when(mockSessionManager.getCurrentCastSession()).thenReturn(mockCastSession);
+ when(mockCastSession.getRemoteMediaClient()).thenReturn(mockRemoteMediaClient);
+ when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus);
+ when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue);
+ when(mockMediaQueue.getItemIds()).thenReturn(new int[0]);
+ // Make the remote media client present the same default values as ExoPlayer:
+ when(mockRemoteMediaClient.isPaused()).thenReturn(true);
+ when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
+ castPlayer = new CastPlayer(mockCastContext);
+ castPlayer.addListener(mockListener);
+ verify(mockRemoteMediaClient).addListener(listenerArgumentCaptor.capture());
+ remoteMediaClientListener = listenerArgumentCaptor.getValue();
+ }
+
+ @Test
+ public void testSetPlayWhenReady_masksRemoteState() {
+ when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
+ assertThat(castPlayer.getPlayWhenReady()).isFalse();
+
+ castPlayer.play();
+ verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
+ assertThat(castPlayer.getPlayWhenReady()).isTrue();
+ verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
+
+ // There is a status update in the middle, which should be hidden by masking.
+ remoteMediaClientListener.onStatusUpdated();
+ verifyNoMoreInteractions(mockListener);
+
+ // Upon result, the remoteMediaClient has updated its state according to the play() call.
+ when(mockRemoteMediaClient.isPaused()).thenReturn(false);
+ setResultCallbackArgumentCaptor
+ .getValue()
+ .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void testSetPlayWhenReadyMasking_updatesUponResultChange() {
+ when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
+ assertThat(castPlayer.getPlayWhenReady()).isFalse();
+
+ castPlayer.play();
+ verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
+ assertThat(castPlayer.getPlayWhenReady()).isTrue();
+ verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
+
+ // Upon result, the remote media client is still paused. The state should reflect that.
+ setResultCallbackArgumentCaptor
+ .getValue()
+ .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
+ verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
+ assertThat(castPlayer.getPlayWhenReady()).isFalse();
+ }
+
+ @Test
+ public void testPlayWhenReady_changesOnStatusUpdates() {
+ assertThat(castPlayer.getPlayWhenReady()).isFalse();
+ when(mockRemoteMediaClient.isPaused()).thenReturn(false);
+ remoteMediaClientListener.onStatusUpdated();
+ verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
+ assertThat(castPlayer.getPlayWhenReady()).isTrue();
+ }
+
+ @Test
+ public void testSetRepeatMode_masksRemoteState() {
+ when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
+ assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
+
+ castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
+ verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
+ assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
+ verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
+
+ // There is a status update in the middle, which should be hidden by masking.
+ when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
+ remoteMediaClientListener.onStatusUpdated();
+ verifyNoMoreInteractions(mockListener);
+
+ // Upon result, the mediaStatus now exposes the new repeat mode.
+ when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
+ setResultCallbackArgumentCaptor
+ .getValue()
+ .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void testSetRepeatMode_updatesUponResultChange() {
+ when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
+
+ castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
+ verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
+ assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
+ verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
+
+ // There is a status update in the middle, which should be hidden by masking.
+ when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
+ remoteMediaClientListener.onStatusUpdated();
+ verifyNoMoreInteractions(mockListener);
+
+ // Upon result, the repeat mode is ALL. The state should reflect that.
+ setResultCallbackArgumentCaptor
+ .getValue()
+ .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
+ verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL);
+ assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL);
+ }
+
+ @Test
+ public void testRepeatMode_changesOnStatusUpdates() {
+ assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
+ when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
+ remoteMediaClientListener.onStatusUpdated();
+ verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
+ assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
+ }
+}
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/DefaultMediaItemConverterTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java
new file mode 100644
index 0000000000..cf9b9d3496
--- /dev/null
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.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.ext.cast.MediaItem.DrmConfiguration;
+import com.google.android.gms.cast.MediaQueueItem;
+import java.util.Collections;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test for {@link DefaultMediaItemConverter}. */
+@RunWith(AndroidJUnit4.class)
+public class DefaultMediaItemConverterTest {
+
+ @Test
+ public void serialize_deserialize_minimal() {
+ MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem item = builder.setUri(Uri.parse("http://example.com")).setMimeType("mime").build();
+
+ DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
+ MediaQueueItem queueItem = converter.toMediaQueueItem(item);
+ MediaItem reconstructedItem = converter.toMediaItem(queueItem);
+
+ assertThat(reconstructedItem).isEqualTo(item);
+ }
+
+ @Test
+ public void serialize_deserialize_complete() {
+ MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem item =
+ builder
+ .setUri(Uri.parse("http://example.com"))
+ .setTitle("title")
+ .setMimeType("mime")
+ .setDrmConfiguration(
+ new DrmConfiguration(
+ C.WIDEVINE_UUID,
+ Uri.parse("http://license.com"),
+ Collections.singletonMap("key", "value")))
+ .build();
+
+ DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
+ MediaQueueItem queueItem = converter.toMediaQueueItem(item);
+ MediaItem reconstructedItem = converter.toMediaItem(queueItem);
+
+ assertThat(reconstructedItem).isEqualTo(item);
+ }
+}
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
index 98df0d5690..7b410a8fbc 100644
--- 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
@@ -18,127 +18,69 @@ 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;
-import org.robolectric.RobolectricTestRunner;
/** Test for {@link MediaItem}. */
-@RunWith(RobolectricTestRunner.class)
+@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")
+ .setUri(Uri.parse("http://example.com"))
.setTitle("title")
.setMimeType(MimeTypes.AUDIO_MP4)
- .setStartPositionUs(3)
- .setEndPositionUs(4)
.build();
- MediaItem item2 = builder.setUuid(new UUID(0, 1)).build();
+ MediaItem item2 = builder.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.Builder builder1 = new MediaItem.Builder();
MediaItem mediaItem1 =
- builder
- .setUuid(new UUID(0, 1))
- .setMedia("www.google.com")
- .setDrmSchemes(createDummyDrmSchemes(1))
- .buildAndClear();
+ builder1
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(1))
+ .build();
+ MediaItem.Builder builder2 = new MediaItem.Builder();
MediaItem mediaItem2 =
- builder
- .setUuid(new UUID(0, 1))
- .setMedia("www.google.com")
- .setDrmSchemes(createDummyDrmSchemes(1))
- .buildAndClear();
+ builder2
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(1))
+ .build();
assertThat(mediaItem1).isEqualTo(mediaItem2);
}
@Test
public void equals_withDifferentDrmRequestHeaders_returnsFalse() {
- MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem.Builder builder1 = new MediaItem.Builder();
MediaItem mediaItem1 =
- builder
- .setUuid(new UUID(0, 1))
- .setMedia("www.google.com")
- .setDrmSchemes(createDummyDrmSchemes(1))
- .buildAndClear();
+ builder1
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(1))
+ .build();
+ MediaItem.Builder builder2 = new MediaItem.Builder();
MediaItem mediaItem2 =
- builder
- .setUuid(new UUID(0, 1))
- .setMedia("www.google.com")
- .setDrmSchemes(createDummyDrmSchemes(2))
- .buildAndClear();
+ builder2
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(2))
+ .build();
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);
+ private static MediaItem.DrmConfiguration buildDrmConfiguration(int seed) {
+ HashMap requestHeaders = new HashMap<>();
+ requestHeaders.put("key1", "value1");
+ requestHeaders.put("key2", "value2" + seed);
+ return new MediaItem.DrmConfiguration(
+ C.WIDEVINE_UUID, Uri.parse("www.uri1.com"), requestHeaders);
}
}
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 7d8c217b58..d5b7a99f96 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,14 +26,18 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
- api 'org.chromium.net:cronet-embedded:66.3359.158'
+ api 'org.chromium.net:cronet-embedded:76.3809.111'
implementation project(modulePrefix + 'library-core')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'library')
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
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 88276c17fe..1903e33995 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
@@ -15,12 +15,13 @@
*/
package com.google.android.exoplayer2.ext.cronet;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.net.Uri;
-import android.support.annotation.Nullable;
import android.text.TextUtils;
+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;
@@ -35,12 +36,14 @@ import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.chromium.net.CronetEngine;
import org.chromium.net.CronetException;
import org.chromium.net.NetworkException;
@@ -51,7 +54,9 @@ import org.chromium.net.UrlResponseInfo;
/**
* DataSource without intermediate buffer based on Cronet API set using UrlRequest.
*
- * This class's methods are organized in the sequence of expected calls.
+ *
Note: HTTP request headers will be set using all parameters passed via (in order of decreasing
+ * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to
+ * construct the instance.
*/
public class CronetDataSource extends BaseDataSource implements HttpDataSource {
@@ -113,16 +118,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
private final CronetEngine cronetEngine;
private final Executor executor;
- private final Predicate contentTypePredicate;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
private final boolean handleSetCookieRequests;
- private final RequestProperties defaultRequestProperties;
+ @Nullable private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private final ConditionVariable operation;
private final Clock clock;
+ @Nullable private Predicate contentTypePredicate;
+
// Accessed by the calling thread only.
private boolean opened;
private long bytesToSkip;
@@ -130,18 +136,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
// to reads made by the Cronet thread.
- private UrlRequest currentUrlRequest;
- private DataSpec currentDataSpec;
+ @Nullable private UrlRequest currentUrlRequest;
+ @Nullable private DataSpec currentDataSpec;
// Reference written and read by calling thread only. Passed to Cronet thread as a local variable.
// operation.open() calls ensure writes into the buffer are visible to reads made by the calling
// thread.
- private ByteBuffer readBuffer;
+ @Nullable private ByteBuffer readBuffer;
// Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads
// made by the calling thread.
- private UrlResponseInfo responseInfo;
- private IOException exception;
+ @Nullable private UrlResponseInfo responseInfo;
+ @Nullable private IOException exception;
private boolean finished;
private volatile long currentConnectTimeoutMs;
@@ -153,21 +159,15 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* hop from Cronet's internal network thread to the response handling thread. However, to
* avoid slowing down overall network performance, care must be taken to make sure response
* handling is a fast operation when using a direct executor.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * #open(DataSpec)}.
*/
- public CronetDataSource(
- CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate) {
+ public CronetDataSource(CronetEngine cronetEngine, Executor executor) {
this(
cronetEngine,
executor,
- contentTypePredicate,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
- false,
- null,
- false);
+ /* resetTimeoutOnRedirects= */ false,
+ /* defaultRequestProperties= */ null);
}
/**
@@ -177,32 +177,28 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* hop from Cronet's internal network thread to the response handling thread. However, to
* avoid slowing down overall network performance, care must be taken to make sure response
* handling is a fast operation when using a direct executor.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * #open(DataSpec)}.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param defaultRequestProperties The default request properties to be used.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
*/
public CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
- RequestProperties defaultRequestProperties) {
+ @Nullable RequestProperties defaultRequestProperties) {
this(
cronetEngine,
executor,
- contentTypePredicate,
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
Clock.DEFAULT,
defaultRequestProperties,
- false);
+ /* handleSetCookieRequests= */ false);
}
/**
@@ -212,29 +208,25 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* hop from Cronet's internal network thread to the response handling thread. However, to
* avoid slowing down overall network performance, care must be taken to make sure response
* handling is a fast operation when using a direct executor.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * #open(DataSpec)}.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param defaultRequestProperties The default request properties to be used.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
* the redirect url in the "Cookie" header.
*/
public CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
- RequestProperties defaultRequestProperties,
+ @Nullable RequestProperties defaultRequestProperties,
boolean handleSetCookieRequests) {
this(
cronetEngine,
executor,
- contentTypePredicate,
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
@@ -243,21 +235,127 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
handleSetCookieRequests);
}
+ /**
+ * @param cronetEngine A CronetEngine.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
+ * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
+ * hop from Cronet's internal network thread to the response handling thread. However, to
+ * avoid slowing down overall network performance, care must be taken to make sure response
+ * handling is a fast operation when using a direct executor.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
+ * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link
+ * #setContentTypePredicate(Predicate)}.
+ */
+ @Deprecated
+ public CronetDataSource(
+ CronetEngine cronetEngine,
+ Executor executor,
+ @Nullable Predicate contentTypePredicate) {
+ this(
+ cronetEngine,
+ executor,
+ contentTypePredicate,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ /* resetTimeoutOnRedirects= */ false,
+ /* defaultRequestProperties= */ null);
+ }
+
+ /**
+ * @param cronetEngine A CronetEngine.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
+ * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
+ * hop from Cronet's internal network thread to the response handling thread. However, to
+ * avoid slowing down overall network performance, care must be taken to make sure response
+ * handling is a fast operation when using a direct executor.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
+ * @param connectTimeoutMs The connection timeout, in milliseconds.
+ * @param readTimeoutMs The read timeout, in milliseconds.
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
+ * RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
+ */
+ @Deprecated
+ public CronetDataSource(
+ CronetEngine cronetEngine,
+ Executor executor,
+ @Nullable Predicate contentTypePredicate,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
+ @Nullable RequestProperties defaultRequestProperties) {
+ this(
+ cronetEngine,
+ executor,
+ contentTypePredicate,
+ connectTimeoutMs,
+ readTimeoutMs,
+ resetTimeoutOnRedirects,
+ defaultRequestProperties,
+ /* handleSetCookieRequests= */ false);
+ }
+
+ /**
+ * @param cronetEngine A CronetEngine.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
+ * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
+ * hop from Cronet's internal network thread to the response handling thread. However, to
+ * avoid slowing down overall network performance, care must be taken to make sure response
+ * handling is a fast operation when using a direct executor.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from {@link
+ * #open(DataSpec)}.
+ * @param connectTimeoutMs The connection timeout, in milliseconds.
+ * @param readTimeoutMs The read timeout, in milliseconds.
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
+ * the redirect url in the "Cookie" header.
+ * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
+ * RequestProperties, boolean)} and {@link #setContentTypePredicate(Predicate)}.
+ */
+ @Deprecated
+ public CronetDataSource(
+ CronetEngine cronetEngine,
+ Executor executor,
+ @Nullable Predicate contentTypePredicate,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
+ @Nullable RequestProperties defaultRequestProperties,
+ boolean handleSetCookieRequests) {
+ this(
+ cronetEngine,
+ executor,
+ connectTimeoutMs,
+ readTimeoutMs,
+ resetTimeoutOnRedirects,
+ Clock.DEFAULT,
+ defaultRequestProperties,
+ handleSetCookieRequests);
+ this.contentTypePredicate = contentTypePredicate;
+ }
+
/* package */ CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
Clock clock,
- RequestProperties defaultRequestProperties,
+ @Nullable RequestProperties defaultRequestProperties,
boolean handleSetCookieRequests) {
super(/* isNetwork= */ true);
this.urlRequestCallback = new UrlRequestCallback();
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
this.executor = Assertions.checkNotNull(executor);
- this.contentTypePredicate = contentTypePredicate;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
@@ -268,6 +366,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
operation = new ConditionVariable();
}
+ /**
+ * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
+ * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
+ *
+ * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
+ * predicate that was previously set.
+ */
+ public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) {
+ this.contentTypePredicate = contentTypePredicate;
+ }
+
// HttpDataSource implementation.
@Override
@@ -285,12 +394,20 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
requestProperties.clear();
}
+ @Override
+ public int getResponseCode() {
+ return responseInfo == null || responseInfo.getHttpStatusCode() <= 0
+ ? -1
+ : responseInfo.getHttpStatusCode();
+ }
+
@Override
public Map> getResponseHeaders() {
return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders();
}
@Override
+ @Nullable
public Uri getUri() {
return responseInfo == null ? null : Uri.parse(responseInfo.getUrl());
}
@@ -303,22 +420,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
operation.close();
resetConnectTimeout();
currentDataSpec = dataSpec;
+ UrlRequest urlRequest;
try {
- currentUrlRequest = buildRequestBuilder(dataSpec).build();
+ urlRequest = buildRequestBuilder(dataSpec).build();
+ currentUrlRequest = urlRequest;
} catch (IOException e) {
- throw new OpenException(e, currentDataSpec, Status.IDLE);
+ throw new OpenException(e, dataSpec, Status.IDLE);
}
- currentUrlRequest.start();
+ urlRequest.start();
transferInitializing(dataSpec);
try {
boolean connectionOpened = blockUntilConnectTimeout();
if (exception != null) {
- throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));
+ throw new OpenException(exception, dataSpec, getStatus(urlRequest));
} else if (!connectionOpened) {
// The timeout was reached before the connection was opened.
- throw new OpenException(
- new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
+ throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(urlRequest));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
@@ -326,6 +444,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
// Check for a valid response code.
+ UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
int responseCode = responseInfo.getHttpStatusCode();
if (responseCode < 200 || responseCode > 299) {
InvalidResponseCodeException exception =
@@ -333,7 +452,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
responseCode,
responseInfo.getHttpStatusText(),
responseInfo.getAllHeaders(),
- currentDataSpec);
+ dataSpec);
if (responseCode == 416) {
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
}
@@ -341,11 +460,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
// Check for a valid content type.
+ Predicate contentTypePredicate = this.contentTypePredicate;
if (contentTypePredicate != null) {
List contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
- if (!contentTypePredicate.evaluate(contentType)) {
- throw new InvalidContentTypeException(contentType, currentDataSpec);
+ if (contentType != null && !contentTypePredicate.evaluate(contentType)) {
+ throw new InvalidContentTypeException(contentType, dataSpec);
}
}
@@ -355,7 +475,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Calculate the content length.
- if (!getIsCompressed(responseInfo)) {
+ if (!isCompressed(responseInfo)) {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;
} else {
@@ -364,7 +484,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
} else {
// If the response is compressed then the content length will be that of the compressed data
// which isn't what we want. Always use the dataSpec length in this case.
- bytesRemaining = currentDataSpec.length;
+ bytesRemaining = dataSpec.length;
}
opened = true;
@@ -383,37 +503,19 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return C.RESULT_END_OF_INPUT;
}
+ ByteBuffer readBuffer = this.readBuffer;
if (readBuffer == null) {
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
readBuffer.limit(0);
+ this.readBuffer = readBuffer;
}
while (!readBuffer.hasRemaining()) {
// Fill readBuffer with more data from Cronet.
operation.close();
readBuffer.clear();
- currentUrlRequest.read(readBuffer);
- try {
- if (!operation.block(readTimeoutMs)) {
- throw new SocketTimeoutException();
- }
- } catch (InterruptedException e) {
- // The operation is ongoing so replace readBuffer to avoid it being written to by this
- // operation during a subsequent request.
- readBuffer = null;
- Thread.currentThread().interrupt();
- throw new HttpDataSourceException(
- new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ);
- } catch (SocketTimeoutException e) {
- // The operation is ongoing so replace readBuffer to avoid it being written to by this
- // operation during a subsequent request.
- readBuffer = null;
- throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ);
- }
+ readInternal(castNonNull(readBuffer));
- if (exception != null) {
- throw new HttpDataSourceException(exception, currentDataSpec,
- HttpDataSourceException.TYPE_READ);
- } else if (finished) {
+ if (finished) {
bytesRemaining = 0;
return C.RESULT_END_OF_INPUT;
} else {
@@ -438,6 +540,115 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return bytesRead;
}
+ /**
+ * Reads up to {@code buffer.remaining()} bytes of data and stores them into {@code buffer},
+ * starting at {@code buffer.position()}. Advances the position of the buffer by the number of
+ * bytes read and returns this length.
+ *
+ * If there is an error, a {@link HttpDataSourceException} is thrown and the contents of {@code
+ * buffer} should be ignored. If the exception has error code {@code
+ * HttpDataSourceException.TYPE_READ}, note that Cronet may continue writing into {@code buffer}
+ * after the method has returned. Thus the caller should not attempt to reuse the buffer.
+ *
+ *
If {@code buffer.remaining()} is zero then 0 is returned. Otherwise, if no data is available
+ * because the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is
+ * returned. Otherwise, the call will block until at least one byte of data has been read and the
+ * number of bytes read is returned.
+ *
+ *
Passed buffer must be direct ByteBuffer. If you have a non-direct ByteBuffer, consider the
+ * alternative read method with its backed array.
+ *
+ * @param buffer The ByteBuffer into which the read data should be stored. Must be a direct
+ * ByteBuffer.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available
+ * because the end of the opened range has been reached.
+ * @throws HttpDataSourceException If an error occurs reading from the source.
+ * @throws IllegalArgumentException If {@code buffer} is not a direct ByteBuffer.
+ */
+ public int read(ByteBuffer buffer) throws HttpDataSourceException {
+ Assertions.checkState(opened);
+
+ if (!buffer.isDirect()) {
+ throw new IllegalArgumentException("Passed buffer is not a direct ByteBuffer");
+ }
+ if (!buffer.hasRemaining()) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ int readLength = buffer.remaining();
+
+ if (readBuffer != null) {
+ // Skip all the bytes we can from readBuffer if there are still bytes to skip.
+ if (bytesToSkip != 0) {
+ if (bytesToSkip >= readBuffer.remaining()) {
+ bytesToSkip -= readBuffer.remaining();
+ readBuffer.position(readBuffer.limit());
+ } else {
+ readBuffer.position(readBuffer.position() + (int) bytesToSkip);
+ bytesToSkip = 0;
+ }
+ }
+
+ // If there is existing data in the readBuffer, read as much as possible. Return if any read.
+ int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer);
+ if (copyBytes != 0) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= copyBytes;
+ }
+ bytesTransferred(copyBytes);
+ return copyBytes;
+ }
+ }
+
+ boolean readMore = true;
+ while (readMore) {
+ // If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's
+ // buffer. If we do not need to skip bytes, we may write to buffer directly.
+ final boolean useCallerBuffer = bytesToSkip == 0;
+
+ operation.close();
+
+ if (!useCallerBuffer) {
+ if (readBuffer == null) {
+ readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
+ } else {
+ readBuffer.clear();
+ }
+ if (bytesToSkip < READ_BUFFER_SIZE_BYTES) {
+ readBuffer.limit((int) bytesToSkip);
+ }
+ }
+
+ // Fill buffer with more data from Cronet.
+ readInternal(useCallerBuffer ? buffer : castNonNull(readBuffer));
+
+ if (finished) {
+ bytesRemaining = 0;
+ return C.RESULT_END_OF_INPUT;
+ } else {
+ // The operation didn't time out, fail or finish, and therefore data must have been read.
+ Assertions.checkState(
+ useCallerBuffer
+ ? readLength > buffer.remaining()
+ : castNonNull(readBuffer).position() > 0);
+ // If we meant to skip bytes, subtract what was left and repeat, otherwise, continue.
+ if (useCallerBuffer) {
+ readMore = false;
+ } else {
+ bytesToSkip -= castNonNull(readBuffer).position();
+ }
+ }
+ }
+
+ final int bytesRead = readLength - buffer.remaining();
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ bytesTransferred(bytesRead);
+ return bytesRead;
+ }
+
@Override
public synchronized void close() {
if (currentUrlRequest != null) {
@@ -476,29 +687,25 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
cronetEngine
.newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor)
.allowDirectExecutor();
+
// Set the headers.
- boolean isContentTypeHeaderSet = false;
+ Map requestHeaders = new HashMap<>();
if (defaultRequestProperties != null) {
- for (Entry headerEntry : defaultRequestProperties.getSnapshot().entrySet()) {
- String key = headerEntry.getKey();
- isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
- requestBuilder.addHeader(key, headerEntry.getValue());
- }
+ requestHeaders.putAll(defaultRequestProperties.getSnapshot());
}
- Map requestPropertiesSnapshot = requestProperties.getSnapshot();
- for (Entry headerEntry : requestPropertiesSnapshot.entrySet()) {
+ requestHeaders.putAll(requestProperties.getSnapshot());
+ requestHeaders.putAll(dataSpec.httpRequestHeaders);
+
+ for (Entry headerEntry : requestHeaders.entrySet()) {
String key = headerEntry.getKey();
- isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
- requestBuilder.addHeader(key, headerEntry.getValue());
+ String value = headerEntry.getValue();
+ requestBuilder.addHeader(key, value);
}
- if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
+
+ if (dataSpec.httpBody != null && !requestHeaders.containsKey(CONTENT_TYPE)) {
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();
@@ -510,7 +717,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
requestBuilder.addHeader("Range", rangeValue.toString());
}
- // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=767025 is fixed
+ // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed
// (adjusting the code as necessary).
// Force identity encoding unless gzip is allowed.
// if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
@@ -539,7 +746,49 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
}
- private static boolean getIsCompressed(UrlResponseInfo info) {
+ /**
+ * Reads up to {@code buffer.remaining()} bytes of data from {@code currentUrlRequest} and stores
+ * them into {@code buffer}. If there is an error and {@code buffer == readBuffer}, then it resets
+ * the current {@code readBuffer} object so that it is not reused in the future.
+ *
+ * @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer.
+ * @throws HttpDataSourceException If an error occurs reading from the source.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private void readInternal(ByteBuffer buffer) throws HttpDataSourceException {
+ castNonNull(currentUrlRequest).read(buffer);
+ try {
+ if (!operation.block(readTimeoutMs)) {
+ throw new SocketTimeoutException();
+ }
+ } catch (InterruptedException e) {
+ // The operation is ongoing so replace buffer to avoid it being written to by this
+ // operation during a subsequent request.
+ if (buffer == readBuffer) {
+ readBuffer = null;
+ }
+ Thread.currentThread().interrupt();
+ throw new HttpDataSourceException(
+ new InterruptedIOException(e),
+ castNonNull(currentDataSpec),
+ HttpDataSourceException.TYPE_READ);
+ } catch (SocketTimeoutException e) {
+ // The operation is ongoing so replace buffer to avoid it being written to by this
+ // operation during a subsequent request.
+ if (buffer == readBuffer) {
+ readBuffer = null;
+ }
+ throw new HttpDataSourceException(
+ e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
+ }
+
+ if (exception != null) {
+ throw new HttpDataSourceException(
+ exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
+ }
+ }
+
+ private static boolean isCompressed(UrlResponseInfo info) {
for (Map.Entry entry : info.getAllHeadersAsList()) {
if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
return !entry.getValue().equalsIgnoreCase("identity");
@@ -617,10 +866,22 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return statusHolder[0];
}
- private static boolean isEmpty(List> list) {
+ @EnsuresNonNullIf(result = false, expression = "#1")
+ private static boolean isEmpty(@Nullable List> list) {
return list == null || list.isEmpty();
}
+ // Copy as much as possible from the src buffer into dst buffer.
+ // Returns the number of bytes copied.
+ private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) {
+ int remaining = Math.min(src.remaining(), dst.remaining());
+ int limit = src.limit();
+ src.limit(src.position() + remaining);
+ dst.put(src);
+ src.limit(limit);
+ return remaining;
+ }
+
private final class UrlRequestCallback extends UrlRequest.Callback {
@Override
@@ -629,13 +890,15 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (request != currentUrlRequest) {
return;
}
- if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
+ UrlRequest urlRequest = Assertions.checkNotNull(currentUrlRequest);
+ DataSpec dataSpec = Assertions.checkNotNull(currentDataSpec);
+ if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
int responseCode = info.getHttpStatusCode();
// The industry standard is to disregard POST redirects when the status code is 307 or 308.
if (responseCode == 307 || responseCode == 308) {
exception =
new InvalidResponseCodeException(
- responseCode, info.getHttpStatusText(), info.getAllHeaders(), currentDataSpec);
+ responseCode, info.getHttpStatusText(), info.getAllHeaders(), dataSpec);
operation.open();
return;
}
@@ -644,40 +907,47 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
resetConnectTimeout();
}
- Map> headers = info.getAllHeaders();
- if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
+ if (!handleSetCookieRequests) {
request.followRedirect();
- } else {
- currentUrlRequest.cancel();
- DataSpec redirectUrlDataSpec;
- if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
- // For POST redirects that aren't 307 or 308, the redirect is followed but request is
- // transformed into a GET.
- redirectUrlDataSpec =
- new DataSpec(
- Uri.parse(newLocationUrl),
- DataSpec.HTTP_METHOD_GET,
- /* httpBody= */ null,
- currentDataSpec.absoluteStreamPosition,
- currentDataSpec.position,
- currentDataSpec.length,
- currentDataSpec.key,
- currentDataSpec.flags);
- } else {
- redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl));
- }
- UrlRequest.Builder requestBuilder;
- try {
- requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
- } catch (IOException e) {
- exception = e;
- return;
- }
- String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
- attachCookies(requestBuilder, cookieHeadersValue);
- currentUrlRequest = requestBuilder.build();
- currentUrlRequest.start();
+ return;
}
+
+ List setCookieHeaders = info.getAllHeaders().get(SET_COOKIE);
+ if (isEmpty(setCookieHeaders)) {
+ request.followRedirect();
+ return;
+ }
+
+ urlRequest.cancel();
+ DataSpec redirectUrlDataSpec;
+ if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
+ // For POST redirects that aren't 307 or 308, the redirect is followed but request is
+ // transformed into a GET.
+ redirectUrlDataSpec =
+ new DataSpec(
+ Uri.parse(newLocationUrl),
+ DataSpec.HTTP_METHOD_GET,
+ /* httpBody= */ null,
+ dataSpec.absoluteStreamPosition,
+ dataSpec.position,
+ dataSpec.length,
+ dataSpec.key,
+ dataSpec.flags,
+ dataSpec.httpRequestHeaders);
+ } else {
+ redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
+ }
+ UrlRequest.Builder requestBuilder;
+ try {
+ requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
+ } catch (IOException e) {
+ exception = e;
+ return;
+ }
+ String cookieHeadersValue = parseCookies(setCookieHeaders);
+ attachCookies(requestBuilder, cookieHeadersValue);
+ currentUrlRequest = requestBuilder.build();
+ currentUrlRequest.start();
}
@Override
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..4086011b4f 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,14 +15,12 @@
*/
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;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
-import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException;
import com.google.android.exoplayer2.upstream.TransferListener;
-import com.google.android.exoplayer2.util.Predicate;
import java.util.concurrent.Executor;
import org.chromium.net.CronetEngine;
@@ -45,8 +43,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
private final CronetEngineWrapper cronetEngineWrapper;
private final Executor executor;
- private final Predicate contentTypePredicate;
- private final @Nullable TransferListener transferListener;
+ @Nullable private final TransferListener transferListener;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
@@ -64,21 +61,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
* suitable CronetEngine can be build.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
HttpDataSource.Factory fallbackFactory) {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
@@ -98,20 +90,15 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
String userAgent) {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
@@ -132,9 +119,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
@@ -143,7 +127,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
@@ -151,7 +134,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
@@ -172,9 +154,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
@@ -184,7 +163,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
@@ -192,7 +170,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
connectTimeoutMs,
readTimeoutMs,
@@ -212,9 +189,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
* suitable CronetEngine can be build.
@@ -222,11 +196,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
HttpDataSource.Factory fallbackFactory) {
- this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
+ this(
+ cronetEngineWrapper,
+ executor,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false,
+ fallbackFactory);
}
/**
@@ -241,22 +220,27 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
String userAgent) {
- this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
- new DefaultHttpDataSourceFactory(userAgent, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));
+ this(
+ cronetEngineWrapper,
+ executor,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false,
+ new DefaultHttpDataSourceFactory(
+ userAgent,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false));
}
/**
@@ -267,9 +251,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
@@ -279,16 +260,20 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
String userAgent) {
- this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
- new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
- readTimeoutMs, resetTimeoutOnRedirects));
+ this(
+ cronetEngineWrapper,
+ executor,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ resetTimeoutOnRedirects,
+ new DefaultHttpDataSourceFactory(
+ userAgent, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects));
}
/**
@@ -299,9 +284,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
@@ -312,7 +294,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
int connectTimeoutMs,
int readTimeoutMs,
@@ -320,7 +301,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
HttpDataSource.Factory fallbackFactory) {
this.cronetEngineWrapper = cronetEngineWrapper;
this.executor = executor;
- this.contentTypePredicate = contentTypePredicate;
this.transferListener = transferListener;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
@@ -339,7 +319,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
new CronetDataSource(
cronetEngine,
executor,
- contentTypePredicate,
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
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..7d549be7cb 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,8 @@
package com.google.android.exoplayer2.ext.cronet;
import android.content.Context;
-import android.support.annotation.IntDef;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
@@ -37,8 +38,8 @@ public final class CronetEngineWrapper {
private static final String TAG = "CronetEngineWrapper";
- private final CronetEngine cronetEngine;
- private final @CronetEngineSource int cronetEngineSource;
+ @Nullable private final CronetEngine cronetEngine;
+ @CronetEngineSource private final int cronetEngineSource;
/**
* Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link
@@ -144,7 +145,8 @@ public final class CronetEngineWrapper {
*
* @return A {@link CronetEngineSource} value.
*/
- public @CronetEngineSource int getCronetEngineSource() {
+ @CronetEngineSource
+ public int getCronetEngineSource() {
return cronetEngineSource;
}
@@ -153,13 +155,14 @@ public final class CronetEngineWrapper {
*
* @return The CronetEngine, or null if no CronetEngine is available.
*/
+ @Nullable
/* package */ CronetEngine getCronetEngine() {
return cronetEngine;
}
private static class CronetProviderComparator implements Comparator {
- private final String gmsCoreCronetName;
+ @Nullable private final String gmsCoreCronetName;
private final boolean preferGMSCoreCronet;
// Multi-catch can only be used for API 19+ in this case.
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java
new file mode 100644
index 0000000000..ec0cf8df05
--- /dev/null
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.cronet;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/cronet/src/test/AndroidManifest.xml b/extensions/cronet/src/test/AndroidManifest.xml
index 82cffe17c2..d6e09107a7 100644
--- a/extensions/cronet/src/test/AndroidManifest.xml
+++ b/extensions/cronet/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
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..47f6fa7d2f 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
@@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.cronet;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
@@ -31,13 +31,13 @@ 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;
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Clock;
-import com.google.android.exoplayer2.util.Predicate;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.net.SocketTimeoutException;
@@ -60,12 +60,12 @@ import org.chromium.net.impl.UrlResponseInfoImpl;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
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;
@@ -85,7 +85,6 @@ public final class CronetDataSourceTest {
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
@Mock private UrlRequest mockUrlRequest;
- @Mock private Predicate mockContentTypePredicate;
@Mock private TransferListener mockTransferListener;
@Mock private Executor mockExecutor;
@Mock private NetworkException mockNetworkException;
@@ -95,21 +94,25 @@ public final class CronetDataSourceTest {
private boolean redirectCalled;
@Before
- public void setUp() throws Exception {
+ public void setUp() {
MockitoAnnotations.initMocks(this);
+
+ HttpDataSource.RequestProperties defaultRequestProperties =
+ new HttpDataSource.RequestProperties();
+ defaultRequestProperties.set("defaultHeader1", "defaultValue1");
+ defaultRequestProperties.set("defaultHeader2", "defaultValue2");
+
dataSourceUnderTest =
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
+ /* resetTimeoutOnRedirects= */ true,
Clock.DEFAULT,
- null,
- false);
+ defaultRequestProperties,
+ /* handleSetCookieRequests= */ false);
dataSourceUnderTest.addTransferListener(mockTransferListener);
- when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
.thenReturn(mockUrlRequestBuilder);
@@ -193,18 +196,59 @@ public final class CronetDataSourceTest {
}
@Test
- public void testRequestHeadersSet() throws HttpDataSourceException {
+ public void testRequestSetsRangeHeader() throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
mockResponseStartSuccess();
- dataSourceUnderTest.setRequestProperty("firstHeader", "firstValue");
- dataSourceUnderTest.setRequestProperty("secondHeader", "secondValue");
-
dataSourceUnderTest.open(testDataSpec);
// The header value to add is current position to current position + length - 1.
verify(mockUrlRequestBuilder).addHeader("Range", "bytes=1000-5999");
- verify(mockUrlRequestBuilder).addHeader("firstHeader", "firstValue");
- verify(mockUrlRequestBuilder).addHeader("secondHeader", "secondValue");
+ }
+
+ @Test
+ public void testRequestHeadersSet() throws HttpDataSourceException {
+
+ Map headersSet = new HashMap<>();
+ doAnswer(
+ (invocation) -> {
+ String key = invocation.getArgument(0);
+ String value = invocation.getArgument(1);
+ headersSet.put(key, value);
+ return null;
+ })
+ .when(mockUrlRequestBuilder)
+ .addHeader(ArgumentMatchers.anyString(), ArgumentMatchers.anyString());
+
+ dataSourceUnderTest.setRequestProperty("defaultHeader2", "dataSourceOverridesDefault");
+ dataSourceUnderTest.setRequestProperty("dataSourceHeader1", "dataSourceValue1");
+ dataSourceUnderTest.setRequestProperty("dataSourceHeader2", "dataSourceValue2");
+
+ Map dataSpecRequestProperties = new HashMap<>();
+ dataSpecRequestProperties.put("defaultHeader3", "dataSpecOverridesAll");
+ dataSpecRequestProperties.put("dataSourceHeader2", "dataSpecOverridesDataSource");
+ dataSpecRequestProperties.put("dataSpecHeader1", "dataSpecValue1");
+ testDataSpec =
+ new DataSpec(
+ /* uri= */ Uri.parse(TEST_URL),
+ /* httpMethod= */ DataSpec.HTTP_METHOD_GET,
+ /* httpBody= */ null,
+ /* absoluteStreamPosition= */ 1000,
+ /* position= */ 1000,
+ /* length= */ 5000,
+ /* key= */ null,
+ /* flags= */ 0,
+ dataSpecRequestProperties);
+ mockResponseStartSuccess();
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ assertThat(headersSet.get("defaultHeader1")).isEqualTo("defaultValue1");
+ assertThat(headersSet.get("defaultHeader2")).isEqualTo("dataSourceOverridesDefault");
+ assertThat(headersSet.get("defaultHeader3")).isEqualTo("dataSpecOverridesAll");
+ assertThat(headersSet.get("dataSourceHeader1")).isEqualTo("dataSourceValue1");
+ assertThat(headersSet.get("dataSourceHeader2")).isEqualTo("dataSpecOverridesDataSource");
+ assertThat(headersSet.get("dataSpecHeader1")).isEqualTo("dataSpecValue1");
+
verify(mockUrlRequest).start();
}
@@ -245,6 +289,26 @@ public final class CronetDataSourceTest {
}
}
+ @Test
+ public void open_ifBodyIsSetWithoutContentTypeHeader_fails() {
+ testDataSpec =
+ new DataSpec(
+ /* uri= */ Uri.parse(TEST_URL),
+ /* postBody= */ new byte[1024],
+ /* absoluteStreamPosition= */ 200,
+ /* position= */ 200,
+ /* length= */ 1024,
+ /* key= */ "key",
+ /* flags= */ 0);
+
+ try {
+ dataSourceUnderTest.open(testDataSpec);
+ fail();
+ } catch (IOException expected) {
+ // Expected
+ }
+ }
+
@Test
public void testRequestOpenFailDueToDnsFailure() {
mockResponseStartFailure();
@@ -283,7 +347,13 @@ public final class CronetDataSourceTest {
@Test
public void testRequestOpenValidatesContentTypePredicate() {
mockResponseStartSuccess();
- when(mockContentTypePredicate.evaluate(anyString())).thenReturn(false);
+
+ ArrayList testedContentTypes = new ArrayList<>();
+ dataSourceUnderTest.setContentTypePredicate(
+ (String input) -> {
+ testedContentTypes.add(input);
+ return false;
+ });
try {
dataSourceUnderTest.open(testDataSpec);
@@ -292,7 +362,8 @@ public final class CronetDataSourceTest {
assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
- verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE);
+ assertThat(testedContentTypes).hasSize(1);
+ assertThat(testedContentTypes.get(0)).isEqualTo(TEST_CONTENT_TYPE);
}
}
@@ -551,6 +622,260 @@ public final class CronetDataSourceTest {
assertThat(bytesRead).isEqualTo(16);
}
+ @Test
+ public void testRequestReadByteBufferTwice() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(8);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+
+ // Use a wrapped ByteBuffer instead of direct for coverage.
+ returnedBuffer.rewind();
+ bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 8));
+ assertThat(bytesRead).isEqualTo(8);
+
+ // Separate cronet calls for each read.
+ verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
+ verify(mockTransferListener, times(2))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ }
+
+ @Test
+ public void testRequestIntermixRead() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ // Chunking reads into parts 6, 7, 8, 9.
+ mockReadSuccess(0, 30);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(6);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 6));
+ assertThat(bytesRead).isEqualTo(6);
+
+ byte[] returnedBytes = new byte[7];
+ bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 7);
+ assertThat(returnedBytes).isEqualTo(buildTestDataArray(6, 7));
+ assertThat(bytesRead).isEqualTo(6 + 7);
+
+ returnedBuffer = ByteBuffer.allocateDirect(8);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(13, 8));
+ assertThat(bytesRead).isEqualTo(6 + 7 + 8);
+
+ returnedBytes = new byte[9];
+ bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 9);
+ assertThat(returnedBytes).isEqualTo(buildTestDataArray(21, 9));
+ assertThat(bytesRead).isEqualTo(6 + 7 + 8 + 9);
+
+ // First ByteBuffer call. The first byte[] call populates enough bytes for the rest.
+ verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 7);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 9);
+ }
+
+ @Test
+ public void testSecondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ testResponseHeader.put("Content-Length", Long.toString(1L));
+ mockReadSuccess(0, 16);
+
+ // First request.
+ dataSourceUnderTest.open(testDataSpec);
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ dataSourceUnderTest.read(returnedBuffer);
+ dataSourceUnderTest.close();
+
+ testResponseHeader.remove("Content-Length");
+ mockReadSuccess(0, 16);
+
+ // Second request.
+ dataSourceUnderTest.open(testDataSpec);
+ returnedBuffer = ByteBuffer.allocateDirect(16);
+ returnedBuffer.limit(10);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(10);
+ returnedBuffer.limit(returnedBuffer.capacity());
+ bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(6);
+ returnedBuffer.rewind();
+ bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
+ }
+
+ @Test
+ public void testRangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(1000, 5000);
+ testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
+ testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
+ }
+
+ @Test
+ public void testRangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException {
+ // Tests for skipping bytes.
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 7000);
+ testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
+ testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
+ }
+
+ @Test
+ public void testReadByteBufferWithUnsetLength() throws HttpDataSourceException {
+ testResponseHeader.remove("Content-Length");
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ returnedBuffer.limit(8);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+ assertThat(bytesRead).isEqualTo(8);
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ }
+
+ @Test
+ public void testReadByteBufferReturnsWhatItCan() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(24);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 16));
+ assertThat(bytesRead).isEqualTo(16);
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
+ }
+
+ @Test
+ public void testOverreadByteBuffer() throws HttpDataSourceException {
+ testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);
+ testResponseHeader.put("Content-Length", Long.toString(16L));
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(8);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+
+ // The current buffer is kept if not completely consumed by DataSource reader.
+ returnedBuffer = ByteBuffer.allocateDirect(6);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(14);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 6));
+
+ // 2 bytes left at this point.
+ returnedBuffer = ByteBuffer.allocateDirect(8);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(14, 2));
+
+ // Called on each.
+ verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);
+
+ // Now we already returned the 16 bytes initially asked.
+ // Try to read again even though all requested 16 bytes are already returned.
+ // Return C.RESULT_END_OF_INPUT
+ returnedBuffer = ByteBuffer.allocateDirect(16);
+ int bytesOverRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
+ assertThat(returnedBuffer.position()).isEqualTo(0);
+ // C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
+ verify(mockTransferListener, never())
+ .onBytesTransferred(
+ dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);
+ // Number of calls to cronet should not have increased.
+ verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
+ // Check for connection not automatically closed.
+ verify(mockUrlRequest, never()).cancel();
+ assertThat(bytesRead).isEqualTo(16);
+ }
+
+ @Test
+ public void testClosedMeansClosedReadByteBuffer() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ int bytesRead = 0;
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ returnedBuffer.limit(8);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+ assertThat(bytesRead).isEqualTo(8);
+
+ dataSourceUnderTest.close();
+ verify(mockTransferListener)
+ .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
+
+ try {
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected.
+ }
+
+ // 16 bytes were attempted but only 8 should have been successfully read.
+ assertThat(bytesRead).isEqualTo(8);
+ }
+
@Test
public void testConnectTimeout() throws InterruptedException {
long startTimeMs = SystemClock.elapsedRealtime();
@@ -734,7 +1059,6 @@ public final class CronetDataSourceTest {
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects
@@ -765,13 +1089,12 @@ public final class CronetDataSourceTest {
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
+ /* resetTimeoutOnRedirects= */ true,
Clock.DEFAULT,
- null,
- true);
+ /* defaultRequestProperties= */ null,
+ /* handleSetCookieRequests= */ true);
dataSourceUnderTest.addTransferListener(mockTransferListener);
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
@@ -804,13 +1127,12 @@ public final class CronetDataSourceTest {
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
+ /* resetTimeoutOnRedirects= */ true,
Clock.DEFAULT,
- null,
- true);
+ /* defaultRequestProperties= */ null,
+ /* handleSetCookieRequests= */ true);
dataSourceUnderTest.addTransferListener(mockTransferListener);
mockSingleRedirectSuccess();
mockFollowRedirectSuccess();
@@ -855,6 +1177,36 @@ public final class CronetDataSourceTest {
}
}
+ @Test
+ public void testReadByteBufferFailure() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadFailure();
+
+ dataSourceUnderTest.open(testDataSpec);
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ try {
+ dataSourceUnderTest.read(returnedBuffer);
+ fail("dataSourceUnderTest.read() returned, but IOException expected");
+ } catch (IOException e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void testReadNonDirectedByteBufferFailure() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadFailure();
+
+ dataSourceUnderTest.open(testDataSpec);
+ byte[] returnedBuffer = new byte[8];
+ try {
+ dataSourceUnderTest.read(ByteBuffer.wrap(returnedBuffer));
+ fail("dataSourceUnderTest.read() returned, but IllegalArgumentException expected");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
@Test
public void testReadInterrupted() throws HttpDataSourceException, InterruptedException {
mockResponseStartSuccess();
@@ -886,6 +1238,37 @@ public final class CronetDataSourceTest {
timedOutLatch.await();
}
+ @Test
+ public void testReadByteBufferInterrupted() throws HttpDataSourceException, InterruptedException {
+ mockResponseStartSuccess();
+ dataSourceUnderTest.open(testDataSpec);
+
+ final ConditionVariable startCondition = buildReadStartedCondition();
+ final CountDownLatch timedOutLatch = new CountDownLatch(1);
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ Thread thread =
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ dataSourceUnderTest.read(returnedBuffer);
+ fail();
+ } catch (HttpDataSourceException e) {
+ // Expected.
+ assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
+ timedOutLatch.countDown();
+ }
+ }
+ };
+ thread.start();
+ startCondition.block();
+
+ assertNotCountedDown(timedOutLatch);
+ // Now we interrupt.
+ thread.interrupt();
+ timedOutLatch.await();
+ }
+
@Test
public void testAllowDirectExecutor() throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
@@ -1064,4 +1447,17 @@ public final class CronetDataSourceTest {
testBuffer.flip();
return testBuffer;
}
+
+ // Returns a copy of what is remaining in the src buffer from the current position to capacity.
+ private static byte[] copyByteBufferToArray(ByteBuffer src) {
+ if (src == null) {
+ return null;
+ }
+ byte[] copy = new byte[src.remaining()];
+ int index = 0;
+ while (src.hasRemaining()) {
+ copy[index++] = src.get();
+ }
+ return copy;
+ }
}
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..1b2db8f0f4 100644
--- a/extensions/ffmpeg/README.md
+++ b/extensions/ffmpeg/README.md
@@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately.
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
-## Build instructions ##
+## Build instructions (Linux, macOS) ##
To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's
@@ -21,16 +21,15 @@ for more information).
In addition, it's necessary to build the extension's native components as
follows:
-* Set the following environment variables:
+* Set the following shell variable:
```
cd ""
-EXOPLAYER_ROOT="$(pwd)"
-FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
+FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni"
```
-* Download the [Android NDK][] and set its location in an environment variable.
- Only versions up to NDK 15c are supported currently.
+* Download the [Android NDK][] and set its location in a shell variable.
+ This build configuration has been tested on NDK r20.
```
NDK_PATH=""
@@ -42,102 +41,60 @@ NDK_PATH=""
HOST_PLATFORM="linux-x86_64"
```
-* Fetch and build FFmpeg. The configuration flags determine which formats will
- be supported. See the [Supported formats][] page for more details of the
- available flags.
-
-For example, to fetch and build FFmpeg release 4.0 for armeabi-v7a,
- arm64-v8a and x86 on Linux x86_64:
+* Configure the formats supported by adapting the following variable if needed
+ and by setting it. See the [Supported formats][] page for more details of the
+ formats.
```
-COMMON_OPTIONS="\
- --target-os=android \
- --disable-static \
- --enable-shared \
- --disable-doc \
- --disable-programs \
- --disable-everything \
- --disable-avdevice \
- --disable-avformat \
- --disable-swscale \
- --disable-postproc \
- --disable-avfilter \
- --disable-symver \
- --disable-swresample \
- --enable-avresample \
- --enable-decoder=vorbis \
- --enable-decoder=opus \
- --enable-decoder=flac \
- " && \
-cd "${FFMPEG_EXT_PATH}/jni" && \
-(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
-cd ffmpeg && git checkout release/4.0 && \
-./configure \
- --libdir=android-libs/armeabi-v7a \
- --arch=arm \
- --cpu=armv7-a \
- --cross-prefix="${NDK_PATH}/toolchains/arm-linux-androideabi-4.9/prebuilt/${HOST_PLATFORM}/bin/arm-linux-androideabi-" \
- --sysroot="${NDK_PATH}/platforms/android-9/arch-arm/" \
- --extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
- --extra-ldflags="-Wl,--fix-cortex-a8" \
- --extra-ldexeflags=-pie \
- ${COMMON_OPTIONS} \
- && \
-make -j4 && make install-libs && \
-make clean && ./configure \
- --libdir=android-libs/arm64-v8a \
- --arch=aarch64 \
- --cpu=armv8-a \
- --cross-prefix="${NDK_PATH}/toolchains/aarch64-linux-android-4.9/prebuilt/${HOST_PLATFORM}/bin/aarch64-linux-android-" \
- --sysroot="${NDK_PATH}/platforms/android-21/arch-arm64/" \
- --extra-ldexeflags=-pie \
- ${COMMON_OPTIONS} \
- && \
-make -j4 && make install-libs && \
-make clean && ./configure \
- --libdir=android-libs/x86 \
- --arch=x86 \
- --cpu=i686 \
- --cross-prefix="${NDK_PATH}/toolchains/x86-4.9/prebuilt/${HOST_PLATFORM}/bin/i686-linux-android-" \
- --sysroot="${NDK_PATH}/platforms/android-9/arch-x86/" \
- --extra-ldexeflags=-pie \
- --disable-asm \
- ${COMMON_OPTIONS} \
- && \
-make -j4 && make install-libs && \
-make clean
+ENABLED_DECODERS=(vorbis opus flac)
+```
+
+* Fetch and build FFmpeg. For example, executing script `build_ffmpeg.sh` will
+ fetch and build FFmpeg release 4.2 for armeabi-v7a, arm64-v8a and x86:
+
+```
+cd "${FFMPEG_EXT_PATH}" && \
+./build_ffmpeg.sh \
+ "${FFMPEG_EXT_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}" "${ENABLED_DECODERS[@]}"
```
* Build the JNI native libraries, setting `APP_ABI` to include the architectures
built in the previous step. For example:
```
-cd "${FFMPEG_EXT_PATH}"/jni && \
+cd "${FFMPEG_EXT_PATH}" && \
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
```
+## Build instructions (Windows) ##
+
+We do not provide support for building this extension on Windows, however it
+should be possible to follow the Linux instructions in [Windows PowerShell][].
+
+[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
+
## Using the extension ##
Once you've followed the instructions above to check out, build and depend on
the extension, the next step is to tell ExoPlayer to use `FfmpegAudioRenderer`.
How you do this depends on which player API you're using:
-* If you're passing a `DefaultRenderersFactory` to
- `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by
- setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`
- constructor to `EXTENSION_RENDERER_MODE_ON`. This will use
- `FfmpegAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't
- support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give
- `FfmpegAudioRenderer` priority over `MediaCodecAudioRenderer`.
+* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
+ you can enable using the extension by setting the `extensionRendererMode`
+ parameter of the `DefaultRenderersFactory` constructor to
+ `EXTENSION_RENDERER_MODE_ON`. This will use `FfmpegAudioRenderer` for playback
+ if `MediaCodecAudioRenderer` doesn't support the input format. Pass
+ `EXTENSION_RENDERER_MODE_PREFER` to give `FfmpegAudioRenderer` priority over
+ `MediaCodecAudioRenderer`.
* If you've subclassed `DefaultRenderersFactory`, add an `FfmpegAudioRenderer`
to the output list in `buildAudioRenderers`. ExoPlayer will use the first
`Renderer` in the list that supports the input media format.
* If you've implemented your own `RenderersFactory`, return an
`FfmpegAudioRenderer` instance from `createRenderers`. ExoPlayer will use the
first `Renderer` in the returned array that supports the input media format.
-* If you're using `ExoPlayerFactory.newInstance`, pass an `FfmpegAudioRenderer`
- in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the
- list that supports the input media format.
+* If you're using `ExoPlayer.Builder`, pass an `FfmpegAudioRenderer` in the
+ array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
+ supports the input media format.
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation,
@@ -147,11 +104,21 @@ 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
+
+## Using the extension in the demo application ##
+
+To try out playback using the extension in the [demo application][], see
+[enabling extension decoders][].
+
+[demo application]: https://exoplayer.dev/demo-application.html
+[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
## Links ##
+* [Troubleshooting using extensions][]
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
belong to this module.
-[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
+[Troubleshooting using extensions]: https://exoplayer.dev/troubleshooting.html#how-can-i-get-a-decoding-extension-to-load-and-be-used-for-playback
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle
index 1630b6f775..657fa75c24 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,16 @@ 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:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
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..17292cec34 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;
@@ -92,8 +92,9 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
- protected int supportsFormatInternal(DrmSessionManager drmSessionManager,
- Format format) {
+ @FormatSupport
+ protected int supportsFormatInternal(
+ @Nullable DrmSessionManager drmSessionManager, Format format) {
Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE;
@@ -108,12 +109,13 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
+ @AdaptiveSupport
public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
return ADAPTIVE_NOT_SEAMLESS;
}
@Override
- protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
+ protected FfmpegDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException {
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
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..5314835d1e 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;
@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.util.List;
@@ -42,7 +43,7 @@ import java.util.List;
private static final int DECODER_ERROR_OTHER = -2;
private final String codecName;
- private final @Nullable byte[] extraData;
+ @Nullable private final byte[] extraData;
private final @C.Encoding int encoding;
private final int outputBufferSize;
@@ -106,7 +107,7 @@ import java.util.List;
return new FfmpegDecoderException("Error resetting (see logcat).");
}
}
- ByteBuffer inputData = inputBuffer.data;
+ ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
int inputSize = inputData.limit();
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
@@ -132,8 +133,8 @@ import java.util.List;
}
hasOutputFormat = true;
}
- outputBuffer.data.position(0);
- outputBuffer.data.limit(result);
+ outputData.position(0);
+ outputData.limit(result);
return null;
}
@@ -172,28 +173,49 @@ import java.util.List;
private static @Nullable byte[] getExtraData(String mimeType, List initializationData) {
switch (mimeType) {
case MimeTypes.AUDIO_AAC:
- case MimeTypes.AUDIO_ALAC:
case MimeTypes.AUDIO_OPUS:
return initializationData.get(0);
+ case MimeTypes.AUDIO_ALAC:
+ return getAlacExtraData(initializationData);
case MimeTypes.AUDIO_VORBIS:
- byte[] header0 = initializationData.get(0);
- byte[] header1 = initializationData.get(1);
- byte[] extraData = new byte[header0.length + header1.length + 6];
- extraData[0] = (byte) (header0.length >> 8);
- extraData[1] = (byte) (header0.length & 0xFF);
- System.arraycopy(header0, 0, extraData, 2, header0.length);
- extraData[header0.length + 2] = 0;
- extraData[header0.length + 3] = 0;
- extraData[header0.length + 4] = (byte) (header1.length >> 8);
- extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
- System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
- return extraData;
+ return getVorbisExtraData(initializationData);
default:
// Other codecs do not require extra data.
return null;
}
}
+ private static byte[] getAlacExtraData(List initializationData) {
+ // FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC "magic cookie", as extra
+ // data. initializationData[0] contains only the magic cookie, and so we need to package it into
+ // an ALAC atom. See:
+ // https://ffmpeg.org/doxygen/0.6/alac_8c.html
+ // https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt
+ byte[] magicCookie = initializationData.get(0);
+ int alacAtomLength = 12 + magicCookie.length;
+ ByteBuffer alacAtom = ByteBuffer.allocate(alacAtomLength);
+ alacAtom.putInt(alacAtomLength);
+ alacAtom.putInt(0x616c6163); // type=alac
+ alacAtom.putInt(0); // version=0, flags=0
+ alacAtom.put(magicCookie, /* offset= */ 0, magicCookie.length);
+ return alacAtom.array();
+ }
+
+ private static byte[] getVorbisExtraData(List initializationData) {
+ byte[] header0 = initializationData.get(0);
+ byte[] header1 = initializationData.get(1);
+ byte[] extraData = new byte[header0.length + header1.length + 6];
+ extraData[0] = (byte) (header0.length >> 8);
+ extraData[1] = (byte) (header0.length & 0xFF);
+ System.arraycopy(header0, 0, extraData, 2, header0.length);
+ extraData[header0.length + 2] = 0;
+ extraData[header0.length + 3] = 0;
+ extraData[header0.length + 4] = (byte) (header1.length >> 8);
+ extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
+ System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
+ return extraData;
+ }
+
private native long ffmpegInitialize(
String codecName,
@Nullable byte[] extraData,
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..5b816b8c20 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,8 +31,10 @@ 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");
+ new LibraryLoader("avutil", "avresample", "swresample", "avcodec", "ffmpeg");
private FfmpegLibrary() {}
@@ -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/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java
new file mode 100644
index 0000000000..a9fedb19cb
--- /dev/null
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.ffmpeg;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/ffmpeg/src/main/jni/Android.mk b/extensions/ffmpeg/src/main/jni/Android.mk
index 046f90a5b2..22a4edcdae 100644
--- a/extensions/ffmpeg/src/main/jni/Android.mk
+++ b/extensions/ffmpeg/src/main/jni/Android.mk
@@ -22,12 +22,17 @@ LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
-LOCAL_MODULE := libavutil
+LOCAL_MODULE := libavresample
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
-LOCAL_MODULE := libavresample
+LOCAL_MODULE := libswresample
+LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
+include $(PREBUILT_SHARED_LIBRARY)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := libavutil
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
@@ -35,6 +40,6 @@ include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg
LOCAL_SRC_FILES := ffmpeg_jni.cc
LOCAL_C_INCLUDES := ffmpeg
-LOCAL_SHARED_LIBRARIES := libavcodec libavresample libavutil
+LOCAL_SHARED_LIBRARIES := libavcodec libavresample libswresample libavutil
LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog
include $(BUILD_SHARED_LIBRARY)
diff --git a/extensions/ffmpeg/src/main/jni/Application.mk b/extensions/ffmpeg/src/main/jni/Application.mk
index 59bf5f8f87..7d6f732548 100644
--- a/extensions/ffmpeg/src/main/jni/Application.mk
+++ b/extensions/ffmpeg/src/main/jni/Application.mk
@@ -15,6 +15,6 @@
#
APP_OPTIM := release
-APP_STL := gnustl_static
+APP_STL := c++_static
APP_CPPFLAGS := -frtti
APP_PLATFORM := android-9
diff --git a/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh b/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh
new file mode 100755
index 0000000000..a76fa0e589
--- /dev/null
+++ b/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+#
+# 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.
+#
+
+FFMPEG_EXT_PATH=$1
+NDK_PATH=$2
+HOST_PLATFORM=$3
+ENABLED_DECODERS=("${@:4}")
+COMMON_OPTIONS="
+ --target-os=android
+ --disable-static
+ --enable-shared
+ --disable-doc
+ --disable-programs
+ --disable-everything
+ --disable-avdevice
+ --disable-avformat
+ --disable-swscale
+ --disable-postproc
+ --disable-avfilter
+ --disable-symver
+ --enable-avresample
+ --enable-swresample
+ "
+TOOLCHAIN_PREFIX="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin"
+for decoder in "${ENABLED_DECODERS[@]}"
+do
+ COMMON_OPTIONS="${COMMON_OPTIONS} --enable-decoder=${decoder}"
+done
+cd "${FFMPEG_EXT_PATH}"
+(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg)
+cd ffmpeg
+git checkout release/4.2
+./configure \
+ --libdir=android-libs/armeabi-v7a \
+ --arch=arm \
+ --cpu=armv7-a \
+ --cross-prefix="${TOOLCHAIN_PREFIX}/armv7a-linux-androideabi16-" \
+ --nm="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-nm" \
+ --strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \
+ --extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
+ --extra-ldflags="-Wl,--fix-cortex-a8" \
+ --extra-ldexeflags=-pie \
+ ${COMMON_OPTIONS}
+make -j4
+make install-libs
+make clean
+./configure \
+ --libdir=android-libs/arm64-v8a \
+ --arch=aarch64 \
+ --cpu=armv8-a \
+ --cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \
+ --nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \
+ --strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \
+ --extra-ldexeflags=-pie \
+ ${COMMON_OPTIONS}
+make -j4
+make install-libs
+make clean
+./configure \
+ --libdir=android-libs/x86 \
+ --arch=x86 \
+ --cpu=i686 \
+ --cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \
+ --nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \
+ --strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \
+ --extra-ldexeflags=-pie \
+ --disable-asm \
+ ${COMMON_OPTIONS}
+make -j4
+make install-libs
+make clean
diff --git a/demos/ima/src/main/res/layout/main_activity.xml b/extensions/ffmpeg/src/test/AndroidManifest.xml
similarity index 66%
rename from demos/ima/src/main/res/layout/main_activity.xml
rename to extensions/ffmpeg/src/test/AndroidManifest.xml
index f7ea5c9b88..6ec1cea289 100644
--- a/demos/ima/src/main/res/layout/main_activity.xml
+++ b/extensions/ffmpeg/src/test/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
-
+
+
+
+
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..a9d4c3094e 100644
--- a/extensions/flac/README.md
+++ b/extensions/flac/README.md
@@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately.
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
-## Build instructions ##
+## Build instructions (Linux, macOS) ##
To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's
@@ -28,8 +28,8 @@ EXOPLAYER_ROOT="$(pwd)"
FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
```
-* Download the [Android NDK][] (version <= 17c) and set its location in an
- environment variable:
+* Download the [Android NDK][] and set its location in an environment variable.
+ This build configuration has been tested on NDK r20.
```
NDK_PATH=""
@@ -53,6 +53,13 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
+## Build instructions (Windows) ##
+
+We do not provide support for building this extension on Windows, however it
+should be possible to follow the Linux instructions in [Windows PowerShell][].
+
+[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
+
## Using the extension ##
Once you've followed the instructions above to check out, build and depend on
@@ -68,31 +75,39 @@ renderer.
### Using `LibflacAudioRenderer` ###
-* If you're passing a `DefaultRenderersFactory` to
- `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by
- setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`
- constructor to `EXTENSION_RENDERER_MODE_ON`. This will use
- `LibflacAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't
- support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give
- `LibflacAudioRenderer` priority over `MediaCodecAudioRenderer`.
+* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
+ you can enable using the extension by setting the `extensionRendererMode`
+ parameter of the `DefaultRenderersFactory` constructor to
+ `EXTENSION_RENDERER_MODE_ON`. This will use `LibflacAudioRenderer` for
+ playback if `MediaCodecAudioRenderer` doesn't support the input format. Pass
+ `EXTENSION_RENDERER_MODE_PREFER` to give `LibflacAudioRenderer` priority over
+ `MediaCodecAudioRenderer`.
* If you've subclassed `DefaultRenderersFactory`, add a `LibflacAudioRenderer`
to the output list in `buildAudioRenderers`. ExoPlayer will use the first
`Renderer` in the list that supports the input media format.
* If you've implemented your own `RenderersFactory`, return a
`LibflacAudioRenderer` instance from `createRenderers`. ExoPlayer will use the
first `Renderer` in the returned array that supports the input media format.
-* If you're using `ExoPlayerFactory.newInstance`, pass a `LibflacAudioRenderer`
- in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the
- list that supports the input media format.
+* If you're using `ExoPlayer.Builder`, pass a `LibflacAudioRenderer` in the
+ array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
+ supports the input media format.
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation,
so you need to make sure you are passing an `LibflacAudioRenderer` to the
player, then implement your own logic to use the renderer for a given track.
+## Using the extension in the demo application ##
+
+To try out playback using the extension in the [demo application][], see
+[enabling extension decoders][].
+
+[demo application]: https://exoplayer.dev/demo-application.html
+[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
+
## Links ##
* [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..4a326ac646 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,14 +33,20 @@ 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:' + androidxAnnotationVersion
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
androidTestImplementation project(modulePrefix + 'testutils')
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
+ testImplementation 'androidx.test:core:' + androidxTestCoreVersion
+ testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt
index ee0a9fa5b5..3e52f643e7 100644
--- a/extensions/flac/proguard-rules.txt
+++ b/extensions/flac/proguard-rules.txt
@@ -9,6 +9,9 @@
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
*;
}
--keep class com.google.android.exoplayer2.util.FlacStreamInfo {
+-keep class com.google.android.exoplayer2.util.FlacStreamMetadata {
+ *;
+}
+-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame {
*;
}
diff --git a/extensions/flac/src/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml
index 68ab6fe0c3..6736ab4b16 100644
--- a/extensions/flac/src/androidTest/AndroidManifest.xml
+++ b/extensions/flac/src/androidTest/AndroidManifest.xml
@@ -19,8 +19,9 @@
package="com.google.android.exoplayer2.ext.flac.test">
+
-
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.0.dump b/extensions/flac/src/androidTest/assets/bear.flac.0.dump
index 71359322b0..d562052a4f 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.0.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.0.dump
@@ -5,7 +5,7 @@ seekMap:
numberOfTracks = 1
track 0:
format:
- bitrate = 768000
+ bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/raw
@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
drmInitData = -
+ metadata = null
initializationData:
total output bytes = 526272
sample count = 33
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.1.dump b/extensions/flac/src/androidTest/assets/bear.flac.1.dump
index 820b9eed10..93f38227b8 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.1.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.1.dump
@@ -5,7 +5,7 @@ seekMap:
numberOfTracks = 1
track 0:
format:
- bitrate = 768000
+ bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/raw
@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
drmInitData = -
+ metadata = null
initializationData:
total output bytes = 362432
sample count = 23
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.2.dump b/extensions/flac/src/androidTest/assets/bear.flac.2.dump
index c2d58347eb..9c53a95b06 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.2.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.2.dump
@@ -5,7 +5,7 @@ seekMap:
numberOfTracks = 1
track 0:
format:
- bitrate = 768000
+ bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/raw
@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
drmInitData = -
+ metadata = null
initializationData:
total output bytes = 182208
sample count = 12
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.3.dump b/extensions/flac/src/androidTest/assets/bear.flac.3.dump
index 8c1115f1ec..82e23a21c1 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.3.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.3.dump
@@ -5,7 +5,7 @@ seekMap:
numberOfTracks = 1
track 0:
format:
- bitrate = 768000
+ bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/raw
@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
drmInitData = -
+ metadata = null
initializationData:
total output bytes = 18368
sample count = 2
diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump
index d8903fcade..59a9f37443 100644
--- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump
+++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump
@@ -5,7 +5,7 @@ seekMap:
numberOfTracks = 1
track 0:
format:
- bitrate = 768000
+ bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/raw
@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
drmInitData = -
+ metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=]
initializationData:
total output bytes = 526272
sample count = 33
diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump
index 100fdd1eaf..a2ad67c9e4 100644
--- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump
+++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump
@@ -5,7 +5,7 @@ seekMap:
numberOfTracks = 1
track 0:
format:
- bitrate = 768000
+ bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/raw
@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
drmInitData = -
+ metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=]
initializationData:
total output bytes = 362432
sample count = 23
diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump
index 6c3cd731b3..067d67f9b8 100644
--- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump
+++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump
@@ -5,7 +5,7 @@ seekMap:
numberOfTracks = 1
track 0:
format:
- bitrate = 768000
+ bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/raw
@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
drmInitData = -
+ metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=]
initializationData:
total output bytes = 182208
sample count = 12
diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump
index decf9c6af3..6edec0017d 100644
--- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump
+++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump
@@ -5,7 +5,7 @@ seekMap:
numberOfTracks = 1
track 0:
format:
- bitrate = 768000
+ bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/raw
@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
drmInitData = -
+ metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=]
initializationData:
total output bytes = 18368
sample count = 2
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..a18202f4e2 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,57 +16,77 @@
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.ext.flac.FlacBinarySearchSeeker.OutputFrameHolder;
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 java.nio.ByteBuffer;
+import org.junit.Before;
+import org.junit.Test;
+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.");
}
}
+ @Test
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();
decoderJni.setData(input);
+ OutputFrameHolder outputFrameHolder = new OutputFrameHolder(ByteBuffer.allocateDirect(0));
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
- decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
-
+ decoderJni.decodeStreamMetadata(),
+ /* firstFramePosition= */ 0,
+ data.length,
+ decoderJni,
+ outputFrameHolder);
SeekMap seekMap = seeker.getSeekMap();
+
assertThat(seekMap).isNotNull();
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
assertThat(seekMap.isSeekable()).isTrue();
}
+ @Test
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();
decoderJni.setData(input);
+ OutputFrameHolder outputFrameHolder = new OutputFrameHolder(ByteBuffer.allocateDirect(0));
+
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
- decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
-
+ decoderJni.decodeStreamMetadata(),
+ /* firstFramePosition= */ 0,
+ data.length,
+ decoderJni,
+ outputFrameHolder);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
+
assertThat(seeker.isSeeking()).isTrue();
}
}
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..a64a52b411 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,13 @@ 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.Test;
+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,23 +60,24 @@ 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();
}
+ @Test
public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
@@ -82,6 +89,7 @@ public final class FlacExtractorSeekTest extends InstrumentationTestCase {
assertThat(seekMap.isSeekable()).isTrue();
}
+ @Test
public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
@@ -98,6 +106,7 @@ public final class FlacExtractorSeekTest extends InstrumentationTestCase {
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
+ @Test
public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
@@ -115,6 +124,7 @@ public final class FlacExtractorSeekTest extends InstrumentationTestCase {
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
+ @Test
public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
@@ -134,6 +144,7 @@ public final class FlacExtractorSeekTest extends InstrumentationTestCase {
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
+ @Test
public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
@@ -153,6 +164,7 @@ public final class FlacExtractorSeekTest extends InstrumentationTestCase {
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
+ @Test
public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
@@ -223,7 +235,8 @@ public final class FlacExtractorSeekTest extends InstrumentationTestCase {
}
}
- private @Nullable SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
+ @Nullable
+ private SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
throws IOException, InterruptedException {
try {
ExtractorInput input = getExtractorInputFromPosition(0);
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..c8033e04d3 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,29 +15,35 @@
*/
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.Test;
+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() {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
}
+ @Test
public void testExtractFlacSample() throws Exception {
ExtractorAsserts.assertBehavior(
- FlacExtractor::new, "bear.flac", getInstrumentation().getContext());
+ FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext());
}
+ @Test
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 2efdde4e58..d3f4f6ac51 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,22 +15,19 @@
*/
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.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
import org.junit.Test;
@@ -56,7 +53,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();
@@ -82,16 +79,15 @@ public class FlacPlaybackTest {
public void run() {
Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
- DefaultTrackSelector trackSelector = new DefaultTrackSelector();
- player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
+ player = new ExoPlayer.Builder(context, audioRenderer).build();
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);
+ player.play();
Looper.loop();
}
@@ -101,7 +97,7 @@ public class FlacPlaybackTest {
}
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playbackState == Player.STATE_ENDED
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
player.release();
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
index b9c6ea06dd..cad5219883 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
@@ -19,7 +19,8 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.FlacConstants;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -31,23 +32,50 @@ import java.nio.ByteBuffer;
*/
/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker {
+ /**
+ * Holds a frame extracted from a stream, together with the time stamp of the frame in
+ * microseconds.
+ */
+ public static final class OutputFrameHolder {
+
+ public final ByteBuffer byteBuffer;
+ public long timeUs;
+
+ /** Constructs an instance, wrapping the given byte buffer. */
+ public OutputFrameHolder(ByteBuffer outputByteBuffer) {
+ this.timeUs = 0;
+ this.byteBuffer = outputByteBuffer;
+ }
+ }
+
private final FlacDecoderJni decoderJni;
+ /**
+ * Creates a {@link FlacBinarySearchSeeker}.
+ *
+ * @param streamMetadata The stream metadata.
+ * @param firstFramePosition The byte offset of the first frame in the stream.
+ * @param inputLength The length of the stream in bytes.
+ * @param decoderJni The FLAC JNI decoder.
+ * @param outputFrameHolder A holder used to retrieve the frame found by a seeking operation.
+ */
public FlacBinarySearchSeeker(
- FlacStreamInfo streamInfo,
+ FlacStreamMetadata streamMetadata,
long firstFramePosition,
long inputLength,
- FlacDecoderJni decoderJni) {
+ FlacDecoderJni decoderJni,
+ OutputFrameHolder outputFrameHolder) {
super(
- new FlacSeekTimestampConverter(streamInfo),
- new FlacTimestampSeeker(decoderJni),
- streamInfo.durationUs(),
+ /* seekTimestampConverter= */ streamMetadata::getSampleNumber,
+ new FlacTimestampSeeker(decoderJni, outputFrameHolder),
+ streamMetadata.getDurationUs(),
/* floorTimePosition= */ 0,
- /* ceilingTimePosition= */ streamInfo.totalSamples,
+ /* ceilingTimePosition= */ streamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength,
- /* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(),
- /* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize));
+ /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
+ /* minimumSearchRange= */ Math.max(
+ FlacConstants.MIN_FRAME_HEADER_SIZE, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni);
}
@@ -63,14 +91,15 @@ import java.nio.ByteBuffer;
private static final class FlacTimestampSeeker implements TimestampSeeker {
private final FlacDecoderJni decoderJni;
+ private final OutputFrameHolder outputFrameHolder;
- private FlacTimestampSeeker(FlacDecoderJni decoderJni) {
+ private FlacTimestampSeeker(FlacDecoderJni decoderJni, OutputFrameHolder outputFrameHolder) {
this.decoderJni = decoderJni;
+ this.outputFrameHolder = outputFrameHolder;
}
@Override
- public TimestampSearchResult searchForTimestamp(
- ExtractorInput input, long targetSampleIndex, OutputFrameHolder outputFrameHolder)
+ public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleIndex)
throws IOException, InterruptedException {
ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
long searchPosition = input.getPosition();
@@ -106,21 +135,4 @@ import java.nio.ByteBuffer;
}
}
}
-
- /**
- * A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as
- * the timestamp for a stream seek time position.
- */
- private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
- private final FlacStreamInfo streamInfo;
-
- public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
- this.streamInfo = streamInfo;
- }
-
- @Override
- public long timeUsToTargetTime(long timeUs) {
- return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
- }
- }
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
index 2d74bce5f1..e1f6112319 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
@@ -15,11 +15,14 @@
*/
package com.google.android.exoplayer2.ext.flac;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
@@ -56,21 +59,20 @@ import java.util.List;
}
decoderJni = new FlacDecoderJni();
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
- FlacStreamInfo streamInfo;
+ FlacStreamMetadata streamMetadata;
try {
- streamInfo = decoderJni.decodeMetadata();
+ streamMetadata = decoderJni.decodeStreamMetadata();
+ } catch (ParserException e) {
+ throw new FlacDecoderException("Failed to decode StreamInfo", e);
} catch (IOException | InterruptedException e) {
// Never happens.
throw new IllegalStateException(e);
}
- if (streamInfo == null) {
- throw new FlacDecoderException("Metadata decoding failed");
- }
int initialInputBufferSize =
- maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;
+ maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize);
- maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
+ maxOutputBufferSize = streamMetadata.getMaxDecodedFrameSize();
}
@Override
@@ -94,12 +96,13 @@ import java.util.List;
}
@Override
+ @Nullable
protected FlacDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
decoderJni.flush();
}
- decoderJni.setData(inputBuffer.data);
+ decoderJni.setData(Util.castNonNull(inputBuffer.data));
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);
try {
decoderJni.decodeSample(outputData);
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
index de038921aa..5e020175e7 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
@@ -15,9 +15,14 @@
*/
package com.google.android.exoplayer2.ext.flac;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.SeekPoint;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -37,14 +42,14 @@ import java.nio.ByteBuffer;
}
}
- private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
+ private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac.
private final long nativeDecoderContext;
- private ByteBuffer byteBufferData;
- private ExtractorInput extractorInput;
+ @Nullable private ByteBuffer byteBufferData;
+ @Nullable private ExtractorInput extractorInput;
+ @Nullable private byte[] tempBuffer;
private boolean endOfExtractorInput;
- private byte[] tempBuffer;
public FlacDecoderJni() throws FlacDecoderException {
if (!FlacLibrary.isAvailable()) {
@@ -57,67 +62,79 @@ import java.nio.ByteBuffer;
}
/**
- * Sets data to be parsed by libflac.
- * @param byteBufferData Source {@link ByteBuffer}
+ * Sets the data to be parsed.
+ *
+ * @param byteBufferData Source {@link ByteBuffer}.
*/
public void setData(ByteBuffer byteBufferData) {
this.byteBufferData = byteBufferData;
this.extractorInput = null;
- this.tempBuffer = null;
}
/**
- * Sets data to be parsed by libflac.
- * @param extractorInput Source {@link ExtractorInput}
+ * Sets the data to be parsed.
+ *
+ * @param extractorInput Source {@link ExtractorInput}.
*/
public void setData(ExtractorInput extractorInput) {
this.byteBufferData = null;
this.extractorInput = extractorInput;
- if (tempBuffer == null) {
- this.tempBuffer = new byte[TEMP_BUFFER_SIZE];
- }
endOfExtractorInput = false;
+ if (tempBuffer == null) {
+ tempBuffer = new byte[TEMP_BUFFER_SIZE];
+ }
}
+ /**
+ * Returns whether the end of the data to be parsed has been reached, or true if no data was set.
+ */
public boolean isEndOfData() {
if (byteBufferData != null) {
return byteBufferData.remaining() == 0;
} else if (extractorInput != null) {
return endOfExtractorInput;
+ } else {
+ return true;
}
- return true;
+ }
+
+ /** Clears the data to be parsed. */
+ public void clearData() {
+ byteBufferData = null;
+ extractorInput = null;
}
/**
* Reads up to {@code length} bytes from the data source.
- *
- * This method blocks until at least one byte of data can be read, the end of the input is
+ *
+ *
This method blocks until at least one byte of data can be read, the end of the input is
* detected or an exception is thrown.
- *
- * This method is called from the native code.
*
* @param target A target {@link ByteBuffer} into which data should be written.
- * @return Returns the number of bytes read, or -1 on failure. It's not an error if this returns
- * zero; it just means all the data read from the source.
+ * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been
+ * read from the source, then 0 is returned.
*/
+ @SuppressWarnings("unused") // Called from native code.
public int read(ByteBuffer target) throws IOException, InterruptedException {
int byteCount = target.remaining();
if (byteBufferData != null) {
byteCount = Math.min(byteCount, byteBufferData.remaining());
int originalLimit = byteBufferData.limit();
byteBufferData.limit(byteBufferData.position() + byteCount);
-
target.put(byteBufferData);
-
byteBufferData.limit(originalLimit);
} else if (extractorInput != null) {
+ ExtractorInput extractorInput = this.extractorInput;
+ byte[] tempBuffer = Util.castNonNull(this.tempBuffer);
byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE);
- int read = readFromExtractorInput(0, byteCount);
+ int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount);
if (read < 4) {
// Reading less than 4 bytes, most of the time, happens because of getting the bytes left in
// the buffer of the input. Do another read to reduce the number of calls to this method
// from the native code.
- read += readFromExtractorInput(read, byteCount - read);
+ read +=
+ readFromExtractorInput(
+ extractorInput, tempBuffer, read, /* length= */ byteCount - read);
}
byteCount = read;
target.put(tempBuffer, 0, byteCount);
@@ -127,9 +144,13 @@ import java.nio.ByteBuffer;
return byteCount;
}
- /** Decodes and consumes the StreamInfo section from the FLAC stream. */
- public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException {
- return flacDecodeMetadata(nativeDecoderContext);
+ /** Decodes and consumes the metadata from the FLAC stream. */
+ public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {
+ FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);
+ if (streamMetadata == null) {
+ throw new ParserException("Failed to decode stream metadata");
+ }
+ return streamMetadata;
}
/**
@@ -197,15 +218,25 @@ import java.nio.ByteBuffer;
}
/**
- * Maps a seek position in microseconds to a corresponding position (byte offset) in the flac
+ * Maps a seek position in microseconds to the corresponding {@link SeekMap.SeekPoints} in the
* stream.
*
* @param timeUs A seek position in microseconds.
- * @return The corresponding position (byte offset) in the flac stream or -1 if the stream doesn't
- * have a seek table.
+ * @return The corresponding {@link SeekMap.SeekPoints} obtained from the seek table, or {@code
+ * null} if the stream doesn't have a seek table.
*/
- public long getSeekPosition(long timeUs) {
- return flacGetSeekPosition(nativeDecoderContext, timeUs);
+ @Nullable
+ public SeekMap.SeekPoints getSeekPoints(long timeUs) {
+ long[] seekPoints = new long[4];
+ if (!flacGetSeekPoints(nativeDecoderContext, timeUs, seekPoints)) {
+ return null;
+ }
+ SeekPoint firstSeekPoint = new SeekPoint(seekPoints[0], seekPoints[1]);
+ SeekPoint secondSeekPoint =
+ seekPoints[2] == seekPoints[0]
+ ? firstSeekPoint
+ : new SeekPoint(seekPoints[2], seekPoints[3]);
+ return new SeekMap.SeekPoints(firstSeekPoint, secondSeekPoint);
}
public String getStateString() {
@@ -234,7 +265,8 @@ import java.nio.ByteBuffer;
flacRelease(nativeDecoderContext);
}
- private int readFromExtractorInput(int offset, int length)
+ private int readFromExtractorInput(
+ ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length)
throws IOException, InterruptedException {
int read = extractorInput.read(tempBuffer, offset, length);
if (read == C.RESULT_END_OF_INPUT) {
@@ -246,7 +278,7 @@ import java.nio.ByteBuffer;
private native long flacInit();
- private native FlacStreamInfo flacDecodeMetadata(long context)
+ private native FlacStreamMetadata flacDecodeMetadata(long context)
throws IOException, InterruptedException;
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
@@ -263,7 +295,7 @@ import java.nio.ByteBuffer;
private native long flacGetNextFrameFirstSampleIndex(long context);
- private native long flacGetSeekPosition(long context, long timeUs);
+ private native boolean flacGetSeekPoints(long context, long timeUs, long[] outSeekPoints);
private native String flacGetStateString(long context);
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..2c6f51da02 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,23 +17,23 @@ 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;
+import com.google.android.exoplayer2.ext.flac.FlacBinarySearchSeeker.OutputFrameHolder;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
-import com.google.android.exoplayer2.extractor.Id3Peeker;
+import com.google.android.exoplayer2.extractor.FlacMetadataReader;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
-import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
@@ -41,7 +41,9 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
-import java.util.Arrays;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Facilitates the extraction of data from the FLAC container format.
@@ -68,43 +70,34 @@ public final class FlacExtractor implements Extractor {
*/
public static final int FLAG_DISABLE_ID3_METADATA = 1;
- /**
- * FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the
- * mandatory STREAMINFO.
- */
- private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
+ private final ParsableByteArray outputBuffer;
+ private final boolean id3MetadataDisabled;
- private final Id3Peeker id3Peeker;
- private final boolean isId3MetadataDisabled;
+ @Nullable private FlacDecoderJni decoderJni;
+ private @MonotonicNonNull ExtractorOutput extractorOutput;
+ private @MonotonicNonNull TrackOutput trackOutput;
- private FlacDecoderJni decoderJni;
+ private boolean streamMetadataDecoded;
+ private @MonotonicNonNull FlacStreamMetadata streamMetadata;
+ private @MonotonicNonNull OutputFrameHolder outputFrameHolder;
- private ExtractorOutput extractorOutput;
- private TrackOutput trackOutput;
+ @Nullable private Metadata id3Metadata;
+ @Nullable private FlacBinarySearchSeeker binarySearchSeeker;
- private ParsableByteArray outputBuffer;
- private ByteBuffer outputByteBuffer;
- private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
- private FlacStreamInfo streamInfo;
-
- private Metadata id3Metadata;
- private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
-
- private boolean readPastStreamInfo;
-
- /** Constructs an instance with flags = 0. */
+ /** Constructs an instance with {@code flags = 0}. */
public FlacExtractor() {
- this(0);
+ this(/* flags= */ 0);
}
/**
* Constructs an instance.
*
- * @param flags Flags that control the extractor's behavior.
+ * @param flags Flags that control the extractor's behavior. Possible flags are described by
+ * {@link Flags}.
*/
public FlacExtractor(int flags) {
- id3Peeker = new Id3Peeker();
- isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
+ outputBuffer = new ParsableByteArray();
+ id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
}
@Override
@@ -121,181 +114,183 @@ public final class FlacExtractor implements Extractor {
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
- if (input.getPosition() == 0) {
- id3Metadata = peekId3Data(input);
- }
- return peekFlacSignature(input);
+ id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
+ return FlacMetadataReader.checkAndPeekStreamMarker(input);
}
@Override
public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
- if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
- id3Metadata = peekId3Data(input);
+ if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
+ id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true);
}
- decoderJni.setData(input);
- readPastStreamInfo(input);
-
- if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
- return handlePendingSeek(input, seekPosition);
- }
-
- long lastDecodePosition = decoderJni.getDecodePosition();
+ FlacDecoderJni decoderJni = initDecoderJni(input);
try {
- decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
- } catch (FlacDecoderJni.FlacFrameDecodeException e) {
- throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
- }
- int outputSize = outputByteBuffer.limit();
- if (outputSize == 0) {
- return RESULT_END_OF_INPUT;
- }
+ decodeStreamMetadata(input);
- writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp());
- return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
+ if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
+ return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);
+ }
+
+ ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
+ long lastDecodePosition = decoderJni.getDecodePosition();
+ try {
+ decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
+ } catch (FlacDecoderJni.FlacFrameDecodeException e) {
+ throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
+ }
+ int outputSize = outputByteBuffer.limit();
+ if (outputSize == 0) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput);
+ return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
+ } finally {
+ decoderJni.clearData();
+ }
}
@Override
public void seek(long position, long timeUs) {
if (position == 0) {
- readPastStreamInfo = false;
+ streamMetadataDecoded = false;
}
if (decoderJni != null) {
decoderJni.reset(position);
}
- if (flacBinarySearchSeeker != null) {
- flacBinarySearchSeeker.setSeekTargetUs(timeUs);
+ if (binarySearchSeeker != null) {
+ binarySearchSeeker.setSeekTargetUs(timeUs);
}
}
@Override
public void release() {
- flacBinarySearchSeeker = null;
+ binarySearchSeeker = null;
if (decoderJni != null) {
decoderJni.release();
decoderJni = null;
}
}
- /**
- * Peeks ID3 tag data (if present) at the beginning of the input.
- *
- * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
- * present in the input.
- */
- @Nullable
- private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
- input.resetPeekPosition();
- Id3Decoder.FramePredicate id3FramePredicate =
- isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
- return id3Peeker.peekId3Data(input, id3FramePredicate);
+ @EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized.
+ @SuppressWarnings({"contracts.postcondition.not.satisfied"})
+ private FlacDecoderJni initDecoderJni(ExtractorInput input) {
+ FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni);
+ decoderJni.setData(input);
+ return decoderJni;
}
- /**
- * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.
- *
- * @return Whether the input begins with {@link #FLAC_SIGNATURE}.
- */
- private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException {
- byte[] header = new byte[FLAC_SIGNATURE.length];
- input.peekFully(header, 0, FLAC_SIGNATURE.length);
- return Arrays.equals(header, FLAC_SIGNATURE);
- }
-
- private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
- if (readPastStreamInfo) {
+ @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
+ @EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
+ @SuppressWarnings({"contracts.postcondition.not.satisfied"})
+ private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {
+ if (streamMetadataDecoded) {
return;
}
- FlacStreamInfo streamInfo = decodeStreamInfo(input);
- readPastStreamInfo = true;
- if (this.streamInfo == null) {
- updateFlacStreamInfo(input, streamInfo);
- }
- }
-
- private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) {
- this.streamInfo = streamInfo;
- outputSeekMap(input, streamInfo);
- outputFormat(streamInfo);
- outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
- outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
- outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
- }
-
- private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
- throws InterruptedException, IOException {
+ FlacDecoderJni flacDecoderJni = decoderJni;
+ FlacStreamMetadata streamMetadata;
try {
- FlacStreamInfo streamInfo = decoderJni.decodeMetadata();
- if (streamInfo == null) {
- throw new IOException("Metadata decoding failed");
- }
- return streamInfo;
+ streamMetadata = flacDecoderJni.decodeStreamMetadata();
} catch (IOException e) {
- decoderJni.reset(0);
- input.setRetryPosition(0, e);
+ flacDecoderJni.reset(/* newPosition= */ 0);
+ input.setRetryPosition(/* position= */ 0, e);
throw e;
}
- }
- private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) {
- boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1;
- SeekMap seekMap =
- hasSeekTable
- ? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
- : getSeekMapForNonSeekTableFlac(input, streamInfo);
- extractorOutput.seekMap(seekMap);
- }
-
- private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) {
- long inputLength = input.getLength();
- if (inputLength != C.LENGTH_UNSET) {
- long firstFramePosition = decoderJni.getDecodePosition();
- flacBinarySearchSeeker =
- new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
- return flacBinarySearchSeeker.getSeekMap();
- } else { // can't seek at all, because there's no SeekTable and the input length is unknown.
- return new SeekMap.Unseekable(streamInfo.durationUs());
+ streamMetadataDecoded = true;
+ if (this.streamMetadata == null) {
+ this.streamMetadata = streamMetadata;
+ outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize());
+ outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
+ binarySearchSeeker =
+ outputSeekMap(
+ flacDecoderJni,
+ streamMetadata,
+ input.getLength(),
+ extractorOutput,
+ outputFrameHolder);
+ @Nullable
+ Metadata metadata = streamMetadata.getMetadataCopyWithAppendedEntriesFrom(id3Metadata);
+ outputFormat(streamMetadata, metadata, trackOutput);
}
}
- private void outputFormat(FlacStreamInfo streamInfo) {
+ @RequiresNonNull("binarySearchSeeker")
+ private int handlePendingSeek(
+ ExtractorInput input,
+ PositionHolder seekPosition,
+ ParsableByteArray outputBuffer,
+ OutputFrameHolder outputFrameHolder,
+ TrackOutput trackOutput)
+ throws InterruptedException, IOException {
+ int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition);
+ ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
+ if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
+ outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput);
+ }
+ return seekResult;
+ }
+
+ /**
+ * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
+ * handle seeks.
+ */
+ @Nullable
+ private static FlacBinarySearchSeeker outputSeekMap(
+ FlacDecoderJni decoderJni,
+ FlacStreamMetadata streamMetadata,
+ long streamLength,
+ ExtractorOutput output,
+ OutputFrameHolder outputFrameHolder) {
+ boolean haveSeekTable = decoderJni.getSeekPoints(/* timeUs= */ 0) != null;
+ FlacBinarySearchSeeker binarySearchSeeker = null;
+ SeekMap seekMap;
+ if (haveSeekTable) {
+ seekMap = new FlacSeekMap(streamMetadata.getDurationUs(), decoderJni);
+ } else if (streamLength != C.LENGTH_UNSET) {
+ long firstFramePosition = decoderJni.getDecodePosition();
+ binarySearchSeeker =
+ new FlacBinarySearchSeeker(
+ streamMetadata, firstFramePosition, streamLength, decoderJni, outputFrameHolder);
+ seekMap = binarySearchSeeker.getSeekMap();
+ } else {
+ seekMap = new SeekMap.Unseekable(streamMetadata.getDurationUs());
+ }
+ output.seekMap(seekMap);
+ return binarySearchSeeker;
+ }
+
+ private static void outputFormat(
+ FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
Format mediaFormat =
Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
- streamInfo.bitRate(),
- streamInfo.maxDecodedFrameSize(),
- streamInfo.channels,
- streamInfo.sampleRate,
- getPcmEncoding(streamInfo.bitsPerSample),
+ streamMetadata.getBitRate(),
+ streamMetadata.getMaxDecodedFrameSize(),
+ streamMetadata.channels,
+ streamMetadata.sampleRate,
+ getPcmEncoding(streamMetadata.bitsPerSample),
/* encoderDelay= */ 0,
/* encoderPadding= */ 0,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null,
- isId3MetadataDisabled ? null : id3Metadata);
- trackOutput.format(mediaFormat);
+ metadata);
+ output.format(mediaFormat);
}
- private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
- throws InterruptedException, IOException {
- int seekResult =
- flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
- ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
- if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
- writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
- }
- return seekResult;
- }
-
- private void writeLastSampleToOutput(int size, long lastSampleTimestamp) {
- outputBuffer.setPosition(0);
- trackOutput.sampleData(outputBuffer, size);
- trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null);
+ private static void outputSample(
+ ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) {
+ sampleData.setPosition(0);
+ output.sampleData(sampleData, size);
+ output.sampleMetadata(
+ timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);
}
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */
@@ -316,8 +311,8 @@ public final class FlacExtractor implements Extractor {
@Override
public SeekPoints getSeekPoints(long timeUs) {
- // TODO: Access the seek table via JNI to return two seek points when appropriate.
- return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs)));
+ @Nullable SeekPoints seekPoints = decoderJni.getSeekPoints(timeUs);
+ return seekPoints == null ? new SeekPoints(SeekPoint.START) : seekPoints;
}
@Override
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..3e8d727476 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
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.flac;
import android.os.Handler;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
@@ -33,7 +34,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
private static final int NUM_BUFFERS = 16;
public LibflacAudioRenderer() {
- this(null, null);
+ this(/* eventHandler= */ null, /* eventListener= */ null);
}
/**
@@ -42,14 +43,17 @@ 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(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
@Override
- protected int supportsFormatInternal(DrmSessionManager drmSessionManager,
- Format format) {
+ @FormatSupport
+ protected int supportsFormatInternal(
+ @Nullable DrmSessionManager drmSessionManager, Format format) {
if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
@@ -63,7 +67,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
- protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
+ protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FlacDecoderException {
return new FlacDecoder(
NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java
new file mode 100644
index 0000000000..ef6da7e3c6
--- /dev/null
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.flac;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/flac/src/main/jni/Application.mk b/extensions/flac/src/main/jni/Application.mk
index eba20352f4..e33070e121 100644
--- a/extensions/flac/src/main/jni/Application.mk
+++ b/extensions/flac/src/main/jni/Application.mk
@@ -15,6 +15,6 @@
#
APP_OPTIM := release
-APP_STL := gnustl_static
+APP_STL := c++_static
APP_CPPFLAGS := -frtti
APP_PLATFORM := android-14
diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc
index 298719d48d..4fc28ce887 100644
--- a/extensions/flac/src/main/jni/flac_jni.cc
+++ b/extensions/flac/src/main/jni/flac_jni.cc
@@ -14,9 +14,13 @@
* limitations under the License.
*/
-#include
#include
+#include
+
+#include
#include
+#include
+
#include "include/flac_parser.h"
#define LOG_TAG "flac_jni"
@@ -43,7 +47,6 @@ class JavaDataSource : public DataSource {
if (mid == NULL) {
jclass cls = env->GetObjectClass(flacDecoderJni);
mid = env->GetMethodID(cls, "read", "(Ljava/nio/ByteBuffer;)I");
- env->DeleteLocalRef(cls);
}
}
@@ -54,7 +57,6 @@ class JavaDataSource : public DataSource {
// Exception is thrown in Java when returning from the native call.
result = -1;
}
- env->DeleteLocalRef(byteBuffer);
return result;
}
@@ -95,19 +97,68 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
return NULL;
}
+ jclass arrayListClass = env->FindClass("java/util/ArrayList");
+ jmethodID arrayListConstructor =
+ env->GetMethodID(arrayListClass, "", "()V");
+ jobject commentList = env->NewObject(arrayListClass, arrayListConstructor);
+ jmethodID arrayListAddMethod =
+ env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
+
+ if (context->parser->areVorbisCommentsValid()) {
+ std::vector vorbisComments =
+ context->parser->getVorbisComments();
+ for (std::vector::const_iterator vorbisComment =
+ vorbisComments.begin();
+ vorbisComment != vorbisComments.end(); ++vorbisComment) {
+ jstring commentString = env->NewStringUTF((*vorbisComment).c_str());
+ env->CallBooleanMethod(commentList, arrayListAddMethod, commentString);
+ env->DeleteLocalRef(commentString);
+ }
+ }
+
+ jobject pictureFrames = env->NewObject(arrayListClass, arrayListConstructor);
+ bool picturesValid = context->parser->arePicturesValid();
+ if (picturesValid) {
+ std::vector pictures = context->parser->getPictures();
+ jclass pictureFrameClass = env->FindClass(
+ "com/google/android/exoplayer2/metadata/flac/PictureFrame");
+ jmethodID pictureFrameConstructor =
+ env->GetMethodID(pictureFrameClass, "",
+ "(ILjava/lang/String;Ljava/lang/String;IIII[B)V");
+ for (std::vector::const_iterator picture = pictures.begin();
+ picture != pictures.end(); ++picture) {
+ jstring mimeType = env->NewStringUTF(picture->mimeType.c_str());
+ jstring description = env->NewStringUTF(picture->description.c_str());
+ jbyteArray pictureData = env->NewByteArray(picture->data.size());
+ env->SetByteArrayRegion(pictureData, 0, picture->data.size(),
+ (signed char *)&picture->data[0]);
+ jobject pictureFrame = env->NewObject(
+ pictureFrameClass, pictureFrameConstructor, picture->type, mimeType,
+ description, picture->width, picture->height, picture->depth,
+ picture->colors, pictureData);
+ env->CallBooleanMethod(pictureFrames, arrayListAddMethod, pictureFrame);
+ env->DeleteLocalRef(mimeType);
+ env->DeleteLocalRef(description);
+ env->DeleteLocalRef(pictureData);
+ }
+ }
+
const FLAC__StreamMetadata_StreamInfo &streamInfo =
context->parser->getStreamInfo();
- jclass cls = env->FindClass(
+ jclass flacStreamMetadataClass = env->FindClass(
"com/google/android/exoplayer2/util/"
- "FlacStreamInfo");
- jmethodID constructor = env->GetMethodID(cls, "", "(IIIIIIIJ)V");
+ "FlacStreamMetadata");
+ jmethodID flacStreamMetadataConstructor =
+ env->GetMethodID(flacStreamMetadataClass, "",
+ "(IIIIIIIJLjava/util/ArrayList;Ljava/util/ArrayList;)V");
- return env->NewObject(cls, constructor, streamInfo.min_blocksize,
- streamInfo.max_blocksize, streamInfo.min_framesize,
- streamInfo.max_framesize, streamInfo.sample_rate,
- streamInfo.channels, streamInfo.bits_per_sample,
- streamInfo.total_samples);
+ return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
+ streamInfo.min_blocksize, streamInfo.max_blocksize,
+ streamInfo.min_framesize, streamInfo.max_framesize,
+ streamInfo.sample_rate, streamInfo.channels,
+ streamInfo.bits_per_sample, streamInfo.total_samples,
+ commentList, pictureFrames);
}
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {
@@ -148,9 +199,15 @@ DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) {
return context->parser->getNextFrameFirstSampleIndex();
}
-DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) {
+DECODER_FUNC(jboolean, flacGetSeekPoints, jlong jContext, jlong timeUs,
+ jlongArray outSeekPoints) {
Context *context = reinterpret_cast(jContext);
- return context->parser->getSeekPosition(timeUs);
+ std::array result;
+ bool success = context->parser->getSeekPositions(timeUs, result);
+ if (success) {
+ env->SetLongArrayRegion(outSeekPoints, 0, result.size(), result.data());
+ }
+ return success;
}
DECODER_FUNC(jstring, flacGetStateString, jlong jContext) {
diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc
index 83d3367415..f39e4bd1f7 100644
--- a/extensions/flac/src/main/jni/flac_parser.cc
+++ b/extensions/flac/src/main/jni/flac_parser.cc
@@ -172,6 +172,43 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {
case FLAC__METADATA_TYPE_SEEKTABLE:
mSeekTable = &metadata->data.seek_table;
break;
+ case FLAC__METADATA_TYPE_VORBIS_COMMENT:
+ if (!mVorbisCommentsValid) {
+ FLAC__StreamMetadata_VorbisComment vorbisComment =
+ metadata->data.vorbis_comment;
+ for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) {
+ FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry =
+ vorbisComment.comments[i];
+ if (vorbisCommentEntry.entry != NULL) {
+ std::string comment(
+ reinterpret_cast(vorbisCommentEntry.entry),
+ vorbisCommentEntry.length);
+ mVorbisComments.push_back(comment);
+ }
+ }
+ mVorbisCommentsValid = true;
+ } else {
+ ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT");
+ }
+ break;
+ case FLAC__METADATA_TYPE_PICTURE: {
+ const FLAC__StreamMetadata_Picture *parsedPicture =
+ &metadata->data.picture;
+ FlacPicture picture;
+ picture.mimeType.assign(std::string(parsedPicture->mime_type));
+ picture.description.assign(
+ std::string((char *)parsedPicture->description));
+ picture.data.assign(parsedPicture->data,
+ parsedPicture->data + parsedPicture->data_length);
+ picture.width = parsedPicture->width;
+ picture.height = parsedPicture->height;
+ picture.depth = parsedPicture->depth;
+ picture.colors = parsedPicture->colors;
+ picture.type = parsedPicture->type;
+ mPictures.push_back(picture);
+ mPicturesValid = true;
+ break;
+ }
default:
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
break;
@@ -228,11 +265,13 @@ FLACParser::FLACParser(DataSource *source)
: mDataSource(source),
mCopy(copyTrespass),
mDecoder(NULL),
- mSeekTable(NULL),
- firstFrameOffset(0LL),
mCurrentPos(0LL),
mEOF(false),
mStreamInfoValid(false),
+ mSeekTable(NULL),
+ firstFrameOffset(0LL),
+ mVorbisCommentsValid(false),
+ mPicturesValid(false),
mWriteRequested(false),
mWriteCompleted(false),
mWriteBuffer(NULL),
@@ -266,6 +305,10 @@ bool FLACParser::init() {
FLAC__METADATA_TYPE_STREAMINFO);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_SEEKTABLE);
+ FLAC__stream_decoder_set_metadata_respond(mDecoder,
+ FLAC__METADATA_TYPE_VORBIS_COMMENT);
+ FLAC__stream_decoder_set_metadata_respond(mDecoder,
+ FLAC__METADATA_TYPE_PICTURE);
FLAC__StreamDecoderInitStatus initStatus;
initStatus = FLAC__stream_decoder_init_stream(
mDecoder, read_callback, seek_callback, tell_callback, length_callback,
@@ -395,22 +438,45 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) {
return bufferSize;
}
-int64_t FLACParser::getSeekPosition(int64_t timeUs) {
+bool FLACParser::getSeekPositions(int64_t timeUs,
+ std::array &result) {
if (!mSeekTable) {
- return -1;
+ return false;
}
- int64_t sample = (timeUs * getSampleRate()) / 1000000LL;
- if (sample >= getTotalSamples()) {
- sample = getTotalSamples();
+ unsigned sampleRate = getSampleRate();
+ int64_t totalSamples = getTotalSamples();
+ int64_t targetSampleNumber = (timeUs * sampleRate) / 1000000LL;
+ if (targetSampleNumber >= totalSamples) {
+ targetSampleNumber = totalSamples - 1;
}
FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points;
- for (unsigned i = mSeekTable->num_points; i > 0; ) {
- i--;
- if (points[i].sample_number <= sample) {
- return firstFrameOffset + points[i].stream_offset;
+ unsigned length = mSeekTable->num_points;
+
+ for (unsigned i = length; i != 0; i--) {
+ int64_t sampleNumber = points[i - 1].sample_number;
+ if (sampleNumber == -1) { // placeholder
+ continue;
+ }
+ if (sampleNumber <= targetSampleNumber) {
+ result[0] = (sampleNumber * 1000000LL) / sampleRate;
+ result[1] = firstFrameOffset + points[i - 1].stream_offset;
+ if (sampleNumber == targetSampleNumber || i >= length ||
+ points[i].sample_number == -1) { // placeholder
+ // exact seek, or no following non-placeholder seek point
+ result[2] = result[0];
+ result[3] = result[1];
+ } else {
+ result[2] = (points[i].sample_number * 1000000LL) / sampleRate;
+ result[3] = firstFrameOffset + points[i].stream_offset;
+ }
+ return true;
}
}
- return firstFrameOffset;
+ result[0] = 0;
+ result[1] = firstFrameOffset;
+ result[2] = 0;
+ result[3] = firstFrameOffset;
+ return true;
}
diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h
index cea7fbe33b..44a0d08718 100644
--- a/extensions/flac/src/main/jni/include/flac_parser.h
+++ b/extensions/flac/src/main/jni/include/flac_parser.h
@@ -19,6 +19,11 @@
#include
+#include
+#include
+#include
+#include
+
// libFLAC parser
#include "FLAC/stream_decoder.h"
@@ -26,6 +31,17 @@
typedef int status_t;
+struct FlacPicture {
+ int type;
+ std::string mimeType;
+ std::string description;
+ FLAC__uint32 width;
+ FLAC__uint32 height;
+ FLAC__uint32 depth;
+ FLAC__uint32 colors;
+ std::vector data;
+};
+
class FLACParser {
public:
FLACParser(DataSource *source);
@@ -44,6 +60,16 @@ class FLACParser {
return mStreamInfo;
}
+ bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }
+
+ const std::vector& getVorbisComments() const {
+ return mVorbisComments;
+ }
+
+ bool arePicturesValid() const { return mPicturesValid; }
+
+ const std::vector &getPictures() const { return mPictures; }
+
int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
}
@@ -59,7 +85,7 @@ class FLACParser {
bool decodeMetadata();
size_t readBuffer(void *output, size_t output_size);
- int64_t getSeekPosition(int64_t timeUs);
+ bool getSeekPositions(int64_t timeUs, std::array &result);
void flush() {
reset(mCurrentPos);
@@ -71,6 +97,10 @@ class FLACParser {
mEOF = false;
if (newPosition == 0) {
mStreamInfoValid = false;
+ mVorbisCommentsValid = false;
+ mPicturesValid = false;
+ mVorbisComments.clear();
+ mPictures.clear();
FLAC__stream_decoder_reset(mDecoder);
} else {
FLAC__stream_decoder_flush(mDecoder);
@@ -116,6 +146,14 @@ class FLACParser {
const FLAC__StreamMetadata_SeekTable *mSeekTable;
uint64_t firstFrameOffset;
+ // cached when the VORBIS_COMMENT metadata is parsed by libFLAC
+ std::vector mVorbisComments;
+ bool mVorbisCommentsValid;
+
+ // cached when the PICTURE metadata is parsed by libFLAC
+ std::vector mPictures;
+ bool mPicturesValid;
+
// cached when a decoded PCM block is "written" by libFLAC parser
bool mWriteRequested;
bool mWriteCompleted;
diff --git a/extensions/flac/src/test/AndroidManifest.xml b/extensions/flac/src/test/AndroidManifest.xml
index 1d68b376ac..509151aa21 100644
--- a/extensions/flac/src/test/AndroidManifest.xml
+++ b/extensions/flac/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
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..43a9e2cb62 100644
--- a/extensions/gvr/README.md
+++ b/extensions/gvr/README.md
@@ -1,11 +1,15 @@
# ExoPlayer GVR extension #
+**DEPRECATED - If you still need this extension, please contact us by filing an
+issue on our [issue tracker][].**
+
The GVR extension wraps the [Google VR SDK for Android][]. It provides a
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
of surround sound and ambisonic soundfields.
[Google VR SDK for Android]: https://developers.google.com/vr/android/
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
+[issue tracker]: https://github.com/google/ExoPlayer/issues
## Getting the extension ##
@@ -37,4 +41,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 6c0ec05bfb..f8992616a2 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,12 +26,14 @@ android {
minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
- implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
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/BaseGvrPlayerActivity.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/BaseGvrPlayerActivity.java
deleted file mode 100644
index acddae49e9..0000000000
--- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/BaseGvrPlayerActivity.java
+++ /dev/null
@@ -1,356 +0,0 @@
-/*
- * 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 android.support.annotation.BinderThread;
-import android.support.annotation.CallSuper;
-import android.support.annotation.Nullable;
-import android.support.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;
-
-/** VR 360 video player base activity class. */
-public abstract class BaseGvrPlayerActivity extends GvrActivity {
- private static final String TAG = "GvrPlayerActivity";
-
- 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 BaseGvrPlayerActivity() {
- mainHandler = new Handler(Looper.getMainLooper());
- }
-
- @Override
- protected void onCreate(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(BaseGvrPlayerActivity.this::togglePlayerControlVisibility);
- }
- appButtonDown = controller.appButtonState;
- }
- }
-}
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..8ba33290ea 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,10 +15,9 @@
*/
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;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.util.Assertions;
import com.google.vr.sdk.audio.GvrAudioSurround;
@@ -28,7 +27,11 @@ import java.nio.ByteOrder;
/**
* An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of
* surround sound and ambisonic soundfields.
+ *
+ * @deprecated If you still need this component, please contact us by filing an issue on our issue tracker.
*/
+@Deprecated
public final class GvrAudioProcessor implements AudioProcessor {
static {
@@ -38,9 +41,10 @@ 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 AudioFormat pendingInputAudioFormat;
+ private int pendingGvrAudioSurroundFormat;
@Nullable private GvrAudioSurround gvrAudioSurround;
private ByteBuffer buffer;
private boolean inputEnded;
@@ -54,9 +58,9 @@ public final class GvrAudioProcessor implements AudioProcessor {
public GvrAudioProcessor() {
// Use the identity for the initial orientation.
w = 1f;
- sampleRateHz = Format.NO_VALUE;
- channelCount = Format.NO_VALUE;
+ pendingInputAudioFormat = AudioFormat.NOT_SET;
buffer = EMPTY_BUFFER;
+ pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
}
/**
@@ -80,70 +84,45 @@ public final class GvrAudioProcessor implements AudioProcessor {
@SuppressWarnings("ReferenceEquality")
@Override
- public synchronized boolean configure(
- int sampleRateHz, int channelCount, @C.Encoding int encoding)
- throws UnhandledFormatException {
- if (encoding != C.ENCODING_PCM_16BIT) {
+ public synchronized AudioFormat configure(AudioFormat inputAudioFormat)
+ throws UnhandledAudioFormatException {
+ if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
maybeReleaseGvrAudioSurround();
- throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
+ throw new UnhandledAudioFormatException(inputAudioFormat);
}
- if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) {
- return false;
- }
- this.sampleRateHz = sampleRateHz;
- this.channelCount = channelCount;
- maybeReleaseGvrAudioSurround();
- int surroundFormat;
- switch (channelCount) {
+ switch (inputAudioFormat.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);
+ throw new UnhandledAudioFormatException(inputAudioFormat);
}
- 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());
}
- return true;
+ pendingInputAudioFormat = inputAudioFormat;
+ return new AudioFormat(inputAudioFormat.sampleRate, OUTPUT_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);
}
@Override
public boolean isActive() {
- return gvrAudioSurround != null;
- }
-
- @Override
- public int getOutputChannelCount() {
- return OUTPUT_CHANNEL_COUNT;
- }
-
- @Override
- public int getOutputEncoding() {
- return C.ENCODING_PCM_16BIT;
- }
-
- @Override
- public int getOutputSampleRateHz() {
- return sampleRateHz;
+ return pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT || gvrAudioSurround != null;
}
@Override
@@ -156,14 +135,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 +153,23 @@ 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,
+ pendingInputAudioFormat.sampleRate,
+ pendingInputAudioFormat.channelCount,
+ FRAMES_PER_OUTPUT_BUFFER);
+ gvrAudioSurround.updateNativeOrientation(w, x, y, z);
+ pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
+ } else if (gvrAudioSurround != null) {
gvrAudioSurround.flush();
}
inputEnded = false;
@@ -188,16 +180,15 @@ public final class GvrAudioProcessor implements AudioProcessor {
maybeReleaseGvrAudioSurround();
updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f);
inputEnded = false;
- sampleRateHz = Format.NO_VALUE;
- channelCount = Format.NO_VALUE;
+ pendingInputAudioFormat = AudioFormat.NOT_SET;
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/demos/main/src/main/res/layout/start_download_dialog.xml b/extensions/gvr/src/main/res/layout/exo_vr_ui.xml
similarity index 83%
rename from demos/main/src/main/res/layout/start_download_dialog.xml
rename to extensions/gvr/src/main/res/layout/exo_vr_ui.xml
index c182047ff8..6863da9578 100644
--- a/demos/main/src/main/res/layout/start_download_dialog.xml
+++ b/extensions/gvr/src/main/res/layout/exo_vr_ui.xml
@@ -13,8 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
diff --git a/extensions/gvr/src/main/res/layout/vr_ui.xml b/extensions/gvr/src/main/res/layout/vr_ui.xml
deleted file mode 100644
index 84e7ac7c6f..0000000000
--- a/extensions/gvr/src/main/res/layout/vr_ui.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
diff --git a/extensions/gvr/src/main/res/values/styles.xml b/extensions/gvr/src/main/res/values/styles.xml
index c79e1dfa60..2affbb2f05 100644
--- a/extensions/gvr/src/main/res/values/styles.xml
+++ b/extensions/gvr/src/main/res/values/styles.xml
@@ -1,5 +1,5 @@
-
-
-
+
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 22196ff3ab..e2292aed8f 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,24 +27,17 @@ android {
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
+
+ testOptions.unitTests.includeAndroidResources = true
}
dependencies {
- api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.2'
+ api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3'
implementation project(modulePrefix + 'library-core')
- implementation 'com.google.android.gms:play-services-ads:17.1.1'
- // 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.1
- // |-- 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
+ implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
+ implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
+ testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
- testImplementation project(modulePrefix + 'testutils-robolectric')
}
ext {
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 311752c7ab..fd777a9e4d 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,10 +19,11 @@ 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 android.support.annotation.VisibleForTesting;
+import android.view.View;
import android.view.ViewGroup;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdError;
@@ -79,6 +80,10 @@ import java.util.Set;
* The player instance that will play the loaded ads must be set before playback using {@link
* #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
* {@link #release()}.
+ *
+ *
The IMA SDK can take into account video control overlay views when calculating ad viewability.
+ * For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} and {@link
+ * AdViewProvider#getAdOverlayViews()}.
*/
public final class ImaAdsLoader
implements Player.EventListener,
@@ -267,11 +272,6 @@ public final class ImaAdsLoader
private static final boolean DEBUG = false;
private static final String TAG = "ImaAdsLoader";
- /**
- * Whether to enable preloading of ads in {@link AdsRenderingSettings}.
- */
- private static final boolean ENABLE_PRELOADING = true;
-
private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima";
private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION;
@@ -308,20 +308,21 @@ public final class ImaAdsLoader
*/
private static final int IMA_AD_STATE_PAUSED = 2;
- private final @Nullable Uri adTagUri;
- private final @Nullable String adsResponse;
+ @Nullable private final Uri adTagUri;
+ @Nullable private final String adsResponse;
private final int vastLoadTimeoutMs;
private final int mediaLoadTimeoutMs;
private final boolean focusSkipButtonWhenAvailable;
private final int mediaBitrate;
- private final @Nullable Set adUiElements;
- private final @Nullable AdEventListener adEventListener;
+ @Nullable private final Set adUiElements;
+ @Nullable private final AdEventListener adEventListener;
private final ImaFactory imaFactory;
private final Timeline.Period period;
private final List adCallbacks;
private final AdDisplayContainer adDisplayContainer;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
+ private boolean wasSetPlayerCalled;
@Nullable private Player nextPlayer;
private Object pendingAdRequestContext;
private List supportedMimeTypes;
@@ -332,6 +333,7 @@ public final class ImaAdsLoader
private int lastVolumePercentage;
private AdsManager adsManager;
+ private boolean initializedAdsManager;
private AdLoadException pendingAdLoadError;
private Timeline timeline;
private long contentDurationMs;
@@ -421,7 +423,7 @@ public final class ImaAdsLoader
* @deprecated Use {@link ImaAdsLoader.Builder}.
*/
@Deprecated
- public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) {
+ public ImaAdsLoader(Context context, Uri adTagUri, @Nullable ImaSdkSettings imaSdkSettings) {
this(
context,
adTagUri,
@@ -466,11 +468,11 @@ public final class ImaAdsLoader
}
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
- adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings);
period = new Timeline.Period();
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
adDisplayContainer = imaFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
+ adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(/* adErrorListener= */ this);
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
@@ -478,6 +480,7 @@ public final class ImaAdsLoader
pendingContentPositionMs = C.TIME_UNSET;
adGroupIndex = C.INDEX_UNSET;
contentDurationMs = C.TIME_UNSET;
+ timeline = Timeline.EMPTY;
}
/**
@@ -488,13 +491,29 @@ public final class ImaAdsLoader
return adsLoader;
}
+ /**
+ * Returns the {@link AdDisplayContainer} used by this loader.
+ *
+ * Note: any video controls overlays registered via {@link
+ * AdDisplayContainer#registerVideoControlsOverlay(View)} will be unregistered automatically when
+ * the media source detaches from this instance. It is therefore necessary to re-register views
+ * each time the ads loader is reused. Alternatively, provide overlay views via the {@link
+ * AdsLoader.AdViewProvider} when creating the media source to benefit from automatic
+ * registration.
+ */
+ public AdDisplayContainer getAdDisplayContainer() {
+ return adDisplayContainer;
+ }
+
/**
* Sets the slots for displaying companion ads. Individual slots can be created using {@link
* ImaSdkFactory#createCompanionAdSlot()}.
*
* @param companionSlots Slots for displaying companion ads.
* @see AdDisplayContainer#setCompanionSlots(Collection)
+ * @deprecated Use {@code getAdDisplayContainer().setCompanionSlots(...)}.
*/
+ @Deprecated
public void setCompanionSlots(Collection companionSlots) {
adDisplayContainer.setCompanionSlots(companionSlots);
}
@@ -506,14 +525,14 @@ public final class ImaAdsLoader
* called, so it is only necessary to call this method if you want to request ads before preparing
* the player.
*
- * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
+ * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
*/
- public void requestAds(ViewGroup adUiViewGroup) {
+ public void requestAds(ViewGroup adViewGroup) {
if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) {
// Ads have already been requested.
return;
}
- adDisplayContainer.setAdContainer(adUiViewGroup);
+ adDisplayContainer.setAdContainer(adViewGroup);
pendingAdRequestContext = new Object();
AdsRequest request = imaFactory.createAdsRequest();
if (adTagUri != null) {
@@ -524,7 +543,6 @@ public final class ImaAdsLoader
if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
request.setVastLoadTimeout(vastLoadTimeoutMs);
}
- request.setAdDisplayContainer(adDisplayContainer);
request.setContentProgressProvider(this);
request.setUserRequestContext(pendingAdRequestContext);
adsLoader.requestAds(request);
@@ -538,6 +556,7 @@ public final class ImaAdsLoader
Assertions.checkState(
player == null || player.getApplicationLooper() == Looper.getMainLooper());
nextPlayer = player;
+ wasSetPlayerCalled = true;
}
@Override
@@ -564,15 +583,23 @@ public final class ImaAdsLoader
}
@Override
- public void start(EventListener eventListener, ViewGroup adUiViewGroup) {
- Assertions.checkNotNull(
- nextPlayer, "Set player using adsLoader.setPlayer before preparing the player.");
+ public void start(EventListener eventListener, AdViewProvider adViewProvider) {
+ Assertions.checkState(
+ wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player.");
player = nextPlayer;
+ if (player == null) {
+ return;
+ }
this.eventListener = eventListener;
lastVolumePercentage = 0;
lastAdProgress = null;
lastContentProgress = null;
- adDisplayContainer.setAdContainer(adUiViewGroup);
+ ViewGroup adViewGroup = adViewProvider.getAdViewGroup();
+ adDisplayContainer.setAdContainer(adViewGroup);
+ View[] adOverlayViews = adViewProvider.getAdOverlayViews();
+ for (View view : adOverlayViews) {
+ adDisplayContainer.registerVideoControlsOverlay(view);
+ }
player.addListener(this);
maybeNotifyPendingAdLoadError();
if (adPlaybackState != null) {
@@ -582,16 +609,19 @@ public final class ImaAdsLoader
adsManager.resume();
}
} else if (adsManager != null) {
- // Ads have loaded but the ads manager is not initialized.
- startAdPlayback();
+ adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints()));
+ updateAdPlaybackState();
} else {
// Ads haven't loaded yet, so request them.
- requestAds(adUiViewGroup);
+ requestAds(adViewGroup);
}
}
@Override
public void stop() {
+ if (player == null) {
+ return;
+ }
if (adsManager != null && imaPausedContent) {
adPlaybackState =
adPlaybackState.withAdResumePositionUs(
@@ -601,6 +631,7 @@ public final class ImaAdsLoader
lastVolumePercentage = getVolume();
lastAdProgress = getAdProgress();
lastContentProgress = getContentProgress();
+ adDisplayContainer.unregisterAllVideoControlsOverlays();
player.removeListener(this);
player = null;
eventListener = null;
@@ -610,6 +641,11 @@ public final class ImaAdsLoader
public void release() {
pendingAdRequestContext = null;
if (adsManager != null) {
+ adsManager.removeAdErrorListener(this);
+ adsManager.removeAdEventListener(this);
+ if (adEventListener != null) {
+ adsManager.removeAdEventListener(adEventListener);
+ }
adsManager.destroy();
adsManager = null;
}
@@ -653,7 +689,8 @@ public final class ImaAdsLoader
if (player != null) {
// If a player is attached already, start playback immediately.
try {
- startAdPlayback();
+ adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints()));
+ updateAdPlaybackState();
} catch (Exception e) {
maybeNotifyInternalError("onAdsManagerLoaded", e);
}
@@ -920,10 +957,9 @@ public final class ImaAdsLoader
// Player.EventListener implementation.
@Override
- public void onTimelineChanged(
- Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
- if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
- // The player is being reset and this source will be released.
+ public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
+ if (timeline.isEmpty()) {
+ // The player is being reset or contains no media.
return;
}
Assertions.checkArgument(timeline.getPeriodCount() == 1);
@@ -933,11 +969,15 @@ public final class ImaAdsLoader
if (contentDurationUs != C.TIME_UNSET) {
adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);
}
- updateImaStateForPlayerState();
+ if (!initializedAdsManager && adsManager != null) {
+ initializedAdsManager = true;
+ initializeAdsManager();
+ }
+ onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
}
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (adsManager == null) {
return;
}
@@ -988,7 +1028,7 @@ public final class ImaAdsLoader
}
}
updateAdPlaybackState();
- } else {
+ } else if (!timeline.isEmpty()) {
long positionMs = player.getCurrentPosition();
timeline.getPeriod(0, period);
int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs));
@@ -1000,16 +1040,15 @@ public final class ImaAdsLoader
}
}
}
- } else {
- updateImaStateForPlayerState();
}
+ updateImaStateForPlayerState();
}
// Internal methods.
- private void startAdPlayback() {
+ private void initializeAdsManager() {
AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
- adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
+ adsRenderingSettings.setEnablePreloading(true);
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs);
@@ -1022,19 +1061,13 @@ public final class ImaAdsLoader
adsRenderingSettings.setUiElements(adUiElements);
}
- // Set up the ad playback state, skipping ads based on the start position as required.
+ // Skip ads based on the start position as required.
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
- adPlaybackState = new AdPlaybackState(adGroupTimesUs);
- long contentPositionMs = player.getCurrentPosition();
+ long contentPositionMs = player.getContentPosition();
int adGroupIndexForPosition =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
- if (adGroupIndexForPosition == 0) {
- podIndexOffset = 0;
- } else if (adGroupIndexForPosition == C.INDEX_UNSET) {
- // There is no preroll and midroll pod indices start at 1.
- podIndexOffset = -1;
- } else /* adGroupIndexForPosition > 0 */ {
- // Skip ad groups before the one at or immediately before the playback position.
+ if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) {
+ // Skip any ad groups before the one at or immediately before the playback position.
for (int i = 0; i < adGroupIndexForPosition; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
@@ -1044,9 +1077,18 @@ public final class ImaAdsLoader
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
+ }
- // We're removing one or more ads, which means that the earliest ad (if any) will be a
- // midroll/postroll. Midroll pod indices start at 1.
+ // IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0.
+ // Store an index offset as we want to index all ads (including skipped ones) from 0.
+ if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) {
+ // We are playing a preroll.
+ podIndexOffset = 0;
+ } else if (adGroupIndexForPosition == C.INDEX_UNSET) {
+ // There's no ad to play which means there's no preroll.
+ podIndexOffset = -1;
+ } else {
+ // We are playing a midroll and any ads before it were skipped.
podIndexOffset = adGroupIndexForPosition - 1;
}
@@ -1055,7 +1097,6 @@ public final class ImaAdsLoader
pendingContentPositionMs = contentPositionMs;
}
- // Start ad playback.
adsManager.init(adsRenderingSettings);
updateAdPlaybackState();
if (DEBUG) {
@@ -1348,7 +1389,8 @@ public final class ImaAdsLoader
private static boolean isAdGroupLoadError(AdError adError) {
// TODO: Find out what other errors need to be handled (if any), and whether each one relates to
// a single ad, ad group or the whole timeline.
- return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH;
+ return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH
+ || adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR;
}
private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) {
@@ -1374,9 +1416,9 @@ public final class ImaAdsLoader
AdDisplayContainer createAdDisplayContainer();
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */
AdsRequest createAdsRequest();
- /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings) */
+ /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */
com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
- Context context, ImaSdkSettings imaSdkSettings);
+ Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
}
/** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */
@@ -1403,8 +1445,9 @@ public final class ImaAdsLoader
@Override
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
- Context context, ImaSdkSettings imaSdkSettings) {
- return ImaSdkFactory.getInstance().createAdsLoader(context, imaSdkSettings);
+ Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
+ return ImaSdkFactory.getInstance()
+ .createAdsLoader(context, imaSdkSettings, adDisplayContainer);
}
}
}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
deleted file mode 100644
index bcccd6cec7..0000000000
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
+++ /dev/null
@@ -1,115 +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.ima;
-
-import android.os.Handler;
-import android.support.annotation.Nullable;
-import android.view.ViewGroup;
-import com.google.android.exoplayer2.Timeline;
-import com.google.android.exoplayer2.source.BaseMediaSource;
-import com.google.android.exoplayer2.source.MediaPeriod;
-import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.MediaSource.SourceInfoRefreshListener;
-import com.google.android.exoplayer2.source.ads.AdsMediaSource;
-import com.google.android.exoplayer2.upstream.Allocator;
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.TransferListener;
-import java.io.IOException;
-
-/**
- * A {@link MediaSource} that inserts ads linearly with a provided content media source.
- *
- * @deprecated Use {@link com.google.android.exoplayer2.source.ads.AdsMediaSource} with
- * ImaAdsLoader.
- */
-@Deprecated
-public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener {
-
- private final AdsMediaSource adsMediaSource;
-
- /**
- * Constructs a new source that inserts ads linearly with the content specified by
- * {@code contentMediaSource}.
- *
- * @param contentMediaSource The {@link MediaSource} providing the content to play.
- * @param dataSourceFactory Factory for data sources used to load ad media.
- * @param imaAdsLoader The loader for ads.
- * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
- */
- public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
- ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup) {
- this(contentMediaSource, dataSourceFactory, imaAdsLoader, adUiViewGroup, null, null);
- }
-
- /**
- * Constructs a new source that inserts ads linearly with the content specified by {@code
- * contentMediaSource}.
- *
- * @param contentMediaSource The {@link MediaSource} providing the content to play.
- * @param dataSourceFactory Factory for data sources used to load ad media.
- * @param imaAdsLoader The loader for ads.
- * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
- * @param eventHandler A handler for events. 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.
- */
- public ImaAdsMediaSource(
- MediaSource contentMediaSource,
- DataSource.Factory dataSourceFactory,
- ImaAdsLoader imaAdsLoader,
- ViewGroup adUiViewGroup,
- @Nullable Handler eventHandler,
- @Nullable AdsMediaSource.EventListener eventListener) {
- adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader,
- adUiViewGroup, eventHandler, eventListener);
- }
-
- @Override
- @Nullable
- public Object getTag() {
- return adsMediaSource.getTag();
- }
-
- @Override
- public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
- adsMediaSource.prepareSource(/* listener= */ this, mediaTransferListener);
- }
-
- @Override
- public void maybeThrowSourceInfoRefreshError() throws IOException {
- adsMediaSource.maybeThrowSourceInfoRefreshError();
- }
-
- @Override
- public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
- return adsMediaSource.createPeriod(id, allocator, startPositionUs);
- }
-
- @Override
- public void releasePeriod(MediaPeriod mediaPeriod) {
- adsMediaSource.releasePeriod(mediaPeriod);
- }
-
- @Override
- public void releaseSourceInternal() {
- adsMediaSource.releaseSource(/* listener= */ this);
- }
-
- @Override
- public void onSourceInfoRefreshed(
- MediaSource source, Timeline timeline, @Nullable Object manifest) {
- refreshSourceInfo(timeline, manifest);
- }
-}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java
new file mode 100644
index 0000000000..9a382eb18f
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.ima;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/ima/src/test/AndroidManifest.xml b/extensions/ima/src/test/AndroidManifest.xml
index 9a4e33189e..564c5d94dd 100644
--- a/extensions/ima/src/test/AndroidManifest.xml
+++ b/extensions/ima/src/test/AndroidManifest.xml
@@ -13,4 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
index b8024d6534..e3af8dbb8f 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.testutil.StubExoPlayer;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import java.util.ArrayList;
/** A fake player for testing content/ad playback. */
@@ -29,8 +30,7 @@ import java.util.ArrayList;
private final Timeline.Period period;
private final Timeline timeline;
- private boolean prepared;
- private int state;
+ @Player.State private int state;
private boolean playWhenReady;
private long position;
private long contentPosition;
@@ -47,14 +47,10 @@ import java.util.ArrayList;
}
/** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */
- public void updateTimeline(Timeline timeline) {
+ public void updateTimeline(Timeline timeline, @TimelineChangeReason int reason) {
for (Player.EventListener listener : listeners) {
- listener.onTimelineChanged(
- timeline,
- null,
- prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
+ listener.onTimelineChanged(timeline, reason);
}
- prepared = true;
}
/**
@@ -95,8 +91,8 @@ import java.util.ArrayList;
}
}
- /** Sets the state of this player with the given {@code STATE} constant. */
- public void setState(int state, boolean playWhenReady) {
+ /** Sets the {@link Player.State} of this player. */
+ public void setState(@Player.State int state, boolean playWhenReady) {
boolean notify = this.state != state || this.playWhenReady != playWhenReady;
this.state = state;
this.playWhenReady = playWhenReady;
@@ -109,6 +105,11 @@ import java.util.ArrayList;
// ExoPlayer methods. Other methods are unsupported.
+ @Override
+ public AudioComponent getAudioComponent() {
+ return null;
+ }
+
@Override
public Looper getApplicationLooper() {
return Looper.getMainLooper();
@@ -125,6 +126,7 @@ import java.util.ArrayList;
}
@Override
+ @Player.State
public int getPlaybackState() {
return state;
}
@@ -134,6 +136,16 @@ import java.util.ArrayList;
return playWhenReady;
}
+ @Override
+ public int getRendererCount() {
+ return 0;
+ }
+
+ @Override
+ public TrackSelectionArray getCurrentTrackSelections() {
+ return new TrackSelectionArray();
+ }
+
@Override
public Timeline getCurrentTimeline() {
return timeline;
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 0b097f26f0..2452da474d 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
@@ -17,13 +17,17 @@ package com.google.android.exoplayer2.ext.ima;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.net.Uri;
-import android.support.annotation.Nullable;
+import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
+import androidx.annotation.Nullable;
+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;
@@ -49,18 +53,18 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
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;
private static final Timeline CONTENT_TIMELINE =
- new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false);
+ new SinglePeriodTimeline(
+ CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false);
private static final Uri TEST_URI = Uri.EMPTY;
private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;
private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}};
@@ -73,7 +77,9 @@ public class ImaAdsLoaderTest {
private @Mock AdDisplayContainer adDisplayContainer;
private @Mock AdsManager adsManager;
private SingletonImaFactory testImaFactory;
- private ViewGroup adUiViewGroup;
+ private ViewGroup adViewGroup;
+ private View adOverlayView;
+ private AdsLoader.AdViewProvider adViewProvider;
private TestAdsLoaderListener adsLoaderListener;
private FakePlayer fakeExoPlayer;
private ImaAdsLoader imaAdsLoader;
@@ -90,7 +96,20 @@ public class ImaAdsLoaderTest {
adDisplayContainer,
fakeAdsRequest,
fakeAdsLoader);
- adUiViewGroup = new FrameLayout(RuntimeEnvironment.application);
+ adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext());
+ adOverlayView = new View(ApplicationProvider.getApplicationContext());
+ adViewProvider =
+ new AdsLoader.AdViewProvider() {
+ @Override
+ public ViewGroup getAdViewGroup() {
+ return adViewGroup;
+ }
+
+ @Override
+ public View[] getAdOverlayViews() {
+ return new View[] {adOverlayView};
+ }
+ };
}
@After
@@ -109,36 +128,38 @@ public class ImaAdsLoaderTest {
}
@Test
- public void testAttachPlayer_setsAdUiViewGroup() {
+ public void testStart_setsAdUiViewGroup() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
- imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
- verify(adDisplayContainer, atLeastOnce()).setAdContainer(adUiViewGroup);
+ verify(adDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup);
+ verify(adDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView);
}
@Test
- public void testAttachPlayer_updatesAdPlaybackState() {
+ public void testStart_updatesAdPlaybackState() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
- imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
new AdPlaybackState(/* adGroupTimesUs= */ 0)
- .withAdDurationsUs(PREROLL_ADS_DURATIONS_US));
+ .withAdDurationsUs(PREROLL_ADS_DURATIONS_US)
+ .withContentDurationUs(CONTENT_DURATION_US));
}
@Test
- public void testAttachAfterRelease() {
+ public void testStartAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
- imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
}
@Test
- public void testAttachAndCallbacksAfterRelease() {
+ public void testStartAndCallbacksAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
- imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
fakeExoPlayer.setState(Player.STATE_READY, true);
@@ -146,7 +167,7 @@ public class ImaAdsLoaderTest {
// Note: we can't currently call getContentProgress/getAdProgress as a VerifyError is thrown
// when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA
// SDK being proguarded.
- imaAdsLoader.requestAds(adUiViewGroup);
+ imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
imaAdsLoader.loadAd(TEST_URI.toString());
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
@@ -166,7 +187,7 @@ public class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
// Load the preroll ad.
- imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
imaAdsLoader.loadAd(TEST_URI.toString());
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
@@ -201,12 +222,24 @@ public class ImaAdsLoaderTest {
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
}
+ @Test
+ public void testStop_unregistersAllVideoControlOverlays() {
+ setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
+ imaAdsLoader.start(adsLoaderListener, adViewProvider);
+ imaAdsLoader.requestAds(adViewGroup);
+ imaAdsLoader.stop();
+
+ InOrder inOrder = inOrder(adDisplayContainer);
+ inOrder.verify(adDisplayContainer).registerVideoControlsOverlay(adOverlayView);
+ inOrder.verify(adDisplayContainer).unregisterAllVideoControlsOverlays();
+ }
+
private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) {
fakeExoPlayer = new FakePlayer();
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);
@@ -221,7 +254,8 @@ public class ImaAdsLoaderTest {
}
@Override
- public @Nullable Ad getAd() {
+ @Nullable
+ public Ad getAd() {
return ad;
}
@@ -252,7 +286,9 @@ public class ImaAdsLoaderTest {
public void onAdPlaybackState(AdPlaybackState adPlaybackState) {
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
this.adPlaybackState = adPlaybackState;
- fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));
+ fakeExoPlayer.updateTimeline(
+ new SinglePeriodAdTimeline(contentTimeline, adPlaybackState),
+ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Override
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java
index dd46d8a68b..4efd8cf38c 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.ima;
import android.content.Context;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
+import com.google.ads.interactivemedia.v3.api.AdsLoader;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
@@ -64,8 +65,8 @@ final class SingletonImaFactory implements ImaAdsLoader.ImaFactory {
}
@Override
- public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
- Context context, ImaSdkSettings imaSdkSettings) {
+ public AdsLoader createAdsLoader(
+ Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
return adsLoader;
}
}
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/README.md b/extensions/jobdispatcher/README.md
index f70125ba38..613277bad2 100644
--- a/extensions/jobdispatcher/README.md
+++ b/extensions/jobdispatcher/README.md
@@ -1,7 +1,12 @@
# ExoPlayer Firebase JobDispatcher extension #
+**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][]
+instead.**
+
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
+[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
+[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ##
@@ -20,4 +25,3 @@ locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
-
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 677d3c2ebd..c8975275f1 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
@@ -54,9 +54,13 @@ import com.google.android.exoplayer2.util.Util;
*
* @see GoogleApiAvailability
+ * @deprecated Use com.google.android.exoplayer2.ext.workmanager.WorkManagerScheduler or {@link
+ * com.google.android.exoplayer2.scheduler.PlatformScheduler}.
*/
+@Deprecated
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 +82,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 +100,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()) {
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java
new file mode 100644
index 0000000000..a66904b505
--- /dev/null
+++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.jobdispatcher;
+
+import com.google.android.exoplayer2.util.NonNullApi;
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..f0be172c90 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:' + androidxAnnotationVersion
+ 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..7c2285c57e 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,14 +17,14 @@ 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 android.util.Pair;
import android.view.Surface;
import android.view.SurfaceHolder;
+import androidx.annotation.Nullable;
+import androidx.leanback.R;
+import androidx.leanback.media.PlaybackGlueHost;
+import androidx.leanback.media.PlayerAdapter;
+import androidx.leanback.media.SurfaceHolderGlueHost;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
@@ -36,6 +36,7 @@ import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
+import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoListener;
/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */
@@ -51,10 +52,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
private final ComponentListener componentListener;
private final int updatePeriodMs;
- private @Nullable PlaybackPreparer playbackPreparer;
+ @Nullable private PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher;
- private @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
- private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost;
+ @Nullable private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
+ @Nullable private SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean hasSurface;
private boolean lastNotifiedPreparedState;
@@ -71,7 +72,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
this.context = context;
this.player = player;
this.updatePeriodMs = updatePeriodMs;
- handler = new Handler();
+ handler = Util.createHandler();
componentListener = new ComponentListener();
controlDispatcher = new DefaultControlDispatcher();
}
@@ -271,7 +272,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
// Player.EventListener implementation.
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
notifyStateChanged();
}
@@ -288,8 +289,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
}
@Override
- public void onTimelineChanged(
- Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
+ public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
@@ -308,7 +308,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
@Override
public void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
- getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height);
+ // There's no way to pass pixelWidthHeightRatio to leanback, so we scale the width that we
+ // pass to take it into account. This is necessary to ensure that leanback uses the correct
+ // aspect ratio when playing content with non-square pixels.
+ int scaledWidth = Math.round(width * pixelWidthHeightRatio);
+ getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, scaledWidth, height);
}
@Override
diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java
new file mode 100644
index 0000000000..79c544fc0f
--- /dev/null
+++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.leanback;
+
+import com.google.android.exoplayer2.util.NonNullApi;
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..537c5ba534 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,14 @@ 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:' + androidxMediaVersion
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}
ext {
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 b4811f040a..6ae35d8c57 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,9 +23,6 @@ import android.os.Handler;
import android.os.Looper;
import android.os.ResultReceiver;
import android.os.SystemClock;
-import android.support.annotation.LongDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
@@ -32,6 +30,9 @@ import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Pair;
+import android.view.KeyEvent;
+import androidx.annotation.LongDef;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
@@ -51,6 +52,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
/**
* Connects a {@link MediaSessionCompat} to a {@link Player}.
@@ -76,9 +78,15 @@ import java.util.Map;
* 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 {
@@ -124,6 +132,15 @@ public final class MediaSessionConnector {
/** The default rewind increment, in milliseconds. */
public static final int DEFAULT_REWIND_MS = 5000;
+ /**
+ * The name of the {@link PlaybackStateCompat} float extra with the value of {@link
+ * PlaybackParameters#speed}.
+ */
+ public static final String EXTRAS_SPEED = "EXO_SPEED";
+ /**
+ * The name of the {@link PlaybackStateCompat} float extra with the value of {@link
+ * PlaybackParameters#pitch}.
+ */
public static final String EXTRAS_PITCH = "EXO_PITCH";
private static final long BASE_PLAYBACK_ACTIONS =
@@ -139,6 +156,9 @@ public final class MediaSessionConnector {
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 {
/**
@@ -162,7 +182,7 @@ public final class MediaSessionConnector {
ResultReceiver cb);
}
- /** Interface to which playback preparation actions are delegated. */
+ /** Interface to which playback preparation and play actions are delegated. */
public interface PlaybackPreparer extends CommandReceiver {
long ACTIONS =
@@ -187,14 +207,36 @@ public final class MediaSessionConnector {
* @return The bitmask of the supported media actions.
*/
long getSupportedPrepareActions();
- /** See {@link MediaSessionCompat.Callback#onPrepare()}. */
- void onPrepare();
- /** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */
- void onPrepareFromMediaId(String mediaId, Bundle extras);
- /** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */
- void onPrepareFromSearch(String query, Bundle extras);
- /** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */
- void onPrepareFromUri(Uri uri, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepare()}.
+ *
+ * @param playWhenReady Whether playback should be started after preparation.
+ */
+ void onPrepare(boolean playWhenReady);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
+ *
+ * @param mediaId The media id of the media item to be prepared.
+ * @param playWhenReady Whether playback should be started after preparation.
+ * @param extras A {@link Bundle} of extras passed by the media controller.
+ */
+ void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
+ *
+ * @param query The search query.
+ * @param playWhenReady Whether playback should be started after preparation.
+ * @param extras A {@link Bundle} of extras passed by the media controller.
+ */
+ void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
+ *
+ * @param uri The {@link Uri} of the media item to be prepared.
+ * @param playWhenReady Whether playback should be started after preparation.
+ * @param extras A {@link Bundle} of extras passed by the media controller.
+ */
+ void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras);
}
/**
@@ -297,6 +339,36 @@ public final class MediaSessionConnector {
void onSetRating(Player player, RatingCompat rating, Bundle extras);
}
+ /** Handles requests for enabling or disabling captions. */
+ public interface CaptionCallback extends CommandReceiver {
+
+ /** See {@link MediaSessionCompat.Callback#onSetCaptioningEnabled(boolean)}. */
+ void onSetCaptioningEnabled(Player player, boolean enabled);
+
+ /**
+ * Returns whether the media currently being played has captions.
+ *
+ * This method is called each time the media session playback state needs to be updated and
+ * published upon a player state change.
+ */
+ boolean hasCaptions(Player player);
+ }
+
+ /** 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.
@@ -312,7 +384,7 @@ public final class MediaSessionConnector {
* @param extras Optional extras sent by a media controller.
*/
void onCustomAction(
- Player player, ControlDispatcher controlDispatcher, String action, Bundle extras);
+ Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras);
/**
* Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media
@@ -330,6 +402,13 @@ public final class MediaSessionConnector {
/**
* Gets the {@link MediaMetadataCompat} to be published to the session.
*
+ *
An app may need to load metadata resources like artwork bitmaps asynchronously. In such a
+ * case the app should return a {@link MediaMetadataCompat} object that does not contain these
+ * resources as a placeholder. The app should start an asynchronous operation to download the
+ * bitmap and put it into a cache. Finally, the app should call {@link
+ * #invalidateMediaSessionMetadata()}. This causes this callback to be called again and the app
+ * can now return a {@link MediaMetadataCompat} object with all the resources included.
+ *
* @param player The player connected to the media session.
* @return The {@link MediaMetadataCompat} to be published to the session.
*/
@@ -342,6 +421,7 @@ public final class MediaSessionConnector {
private final Looper looper;
private final ComponentListener componentListener;
private final ArrayList commandReceivers;
+ private final ArrayList customCommandReceivers;
private ControlDispatcher controlDispatcher;
private CustomActionProvider[] customActionProviders;
@@ -350,10 +430,13 @@ public final class MediaSessionConnector {
@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 CaptionCallback captionCallback;
+ @Nullable private MediaButtonEventHandler mediaButtonEventHandler;
private long enabledPlaybackActions;
private int rewindMs;
@@ -369,6 +452,7 @@ public final class MediaSessionConnector {
looper = Util.getLooper();
componentListener = new ComponentListener();
commandReceivers = new ArrayList<>();
+ customCommandReceivers = new ArrayList<>();
controlDispatcher = new DefaultControlDispatcher();
customActionProviders = new CustomActionProvider[0];
customActionMap = Collections.emptyMap();
@@ -429,6 +513,29 @@ public final class MediaSessionConnector {
}
}
+ /**
+ * Sets the {@link MediaButtonEventHandler}. Pass {@code null} if the media button event should be
+ * handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
+ *
+ * Please note that prior to API 21 MediaButton events are not delivered to the {@link
+ * MediaSessionCompat}. Instead they are delivered as key events (see 'Responding to media
+ * buttons'). In an {@link android.app.Activity Activity}, media button events arrive at the
+ * {@link android.app.Activity#dispatchKeyEvent(KeyEvent)} method.
+ *
+ *
If you are running the player in a foreground service (prior to API 21), you can create an
+ * intent filter and handle the {@code android.intent.action.MEDIA_BUTTON} action yourself. See
+ * Service handling ACTION_MEDIA_BUTTON for more information.
+ *
+ * @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.
*
@@ -487,7 +594,7 @@ public final class MediaSessionConnector {
*
* @param queueNavigator The queue navigator.
*/
- public void setQueueNavigator(QueueNavigator queueNavigator) {
+ public void setQueueNavigator(@Nullable QueueNavigator queueNavigator) {
if (this.queueNavigator != queueNavigator) {
unregisterCommandReceiver(this.queueNavigator);
this.queueNavigator = queueNavigator;
@@ -500,7 +607,7 @@ public final class MediaSessionConnector {
*
* @param queueEditor The queue editor.
*/
- public void setQueueEditor(QueueEditor queueEditor) {
+ public void setQueueEditor(@Nullable QueueEditor queueEditor) {
if (this.queueEditor != queueEditor) {
unregisterCommandReceiver(this.queueEditor);
this.queueEditor = queueEditor;
@@ -515,7 +622,7 @@ public final class MediaSessionConnector {
*
* @param ratingCallback The rating callback.
*/
- public void setRatingCallback(RatingCallback ratingCallback) {
+ public void setRatingCallback(@Nullable RatingCallback ratingCallback) {
if (this.ratingCallback != ratingCallback) {
unregisterCommandReceiver(this.ratingCallback);
this.ratingCallback = ratingCallback;
@@ -523,6 +630,19 @@ public final class MediaSessionConnector {
}
}
+ /**
+ * Sets the {@link CaptionCallback} to handle requests to enable or disable captions.
+ *
+ * @param captionCallback The caption callback.
+ */
+ public void setCaptionCallback(@Nullable CaptionCallback captionCallback) {
+ if (this.captionCallback != captionCallback) {
+ unregisterCommandReceiver(this.captionCallback);
+ this.captionCallback = captionCallback;
+ registerCommandReceiver(this.captionCallback);
+ }
+ }
+
/**
* Sets a custom error on the session.
*
@@ -544,7 +664,20 @@ 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();
}
@@ -562,7 +695,8 @@ public final class MediaSessionConnector {
}
/**
- * Sets a provider of metadata to be published to the media session.
+ * 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.
@@ -579,12 +713,15 @@ public final class MediaSessionConnector {
*
*
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() {
MediaMetadataCompat metadata =
mediaMetadataProvider != null && player != null
? mediaMetadataProvider.getMetadata(player)
- : null;
+ : METADATA_EMPTY;
mediaSession.setMetadata(metadata);
}
@@ -596,14 +733,25 @@ public final class MediaSessionConnector {
*/
public final void invalidateMediaSessionPlaybackState() {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+ @Nullable Player player = this.player;
if (player == null) {
- builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
+ builder
+ .setActions(buildPrepareActions())
+ .setState(
+ PlaybackStateCompat.STATE_NONE,
+ /* position= */ 0,
+ /* playbackSpeed= */ 0,
+ /* updateTime= */ SystemClock.elapsedRealtime());
+
+ mediaSession.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_NONE);
+ mediaSession.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE);
mediaSession.setPlaybackState(builder.build());
return;
}
Map currentActions = new HashMap<>();
for (CustomActionProvider customActionProvider : customActionProviders) {
+ @Nullable
PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(player);
if (customAction != null) {
currentActions.put(customAction.getAction(), customActionProvider);
@@ -612,16 +760,18 @@ public final class MediaSessionConnector {
}
customActionMap = Collections.unmodifiableMap(currentActions);
- int playbackState = player.getPlaybackState();
- ExoPlaybackException playbackError =
- playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
+ Bundle extras = new Bundle();
+ @Nullable ExoPlaybackException playbackError = player.getPlaybackError();
boolean reportError = playbackError != null || customError != null;
int sessionPlaybackState =
reportError
? PlaybackStateCompat.STATE_ERROR
- : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
+ : getMediaSessionPlaybackState(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);
@@ -630,8 +780,10 @@ public final class MediaSessionConnector {
queueNavigator != null
? queueNavigator.getActiveQueueItemId(player)
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
- Bundle extras = new Bundle();
- extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
+ PlaybackParameters playbackParameters = player.getPlaybackParameters();
+ extras.putFloat(EXTRAS_SPEED, playbackParameters.speed);
+ extras.putFloat(EXTRAS_PITCH, playbackParameters.pitch);
+ float sessionPlaybackSpeed = player.isPlaying() ? playbackParameters.speed : 0f;
builder
.setActions(buildPrepareActions() | buildPlaybackActions(player))
.setActiveQueueItemId(activeQueueItemId)
@@ -639,9 +791,21 @@ public final class MediaSessionConnector {
.setState(
sessionPlaybackState,
player.getCurrentPosition(),
- player.getPlaybackParameters().speed,
- SystemClock.elapsedRealtime())
+ sessionPlaybackSpeed,
+ /* updateTime= */ SystemClock.elapsedRealtime())
.setExtras(extras);
+
+ @Player.RepeatMode int repeatMode = player.getRepeatMode();
+ mediaSession.setRepeatMode(
+ repeatMode == Player.REPEAT_MODE_ONE
+ ? PlaybackStateCompat.REPEAT_MODE_ONE
+ : repeatMode == Player.REPEAT_MODE_ALL
+ ? PlaybackStateCompat.REPEAT_MODE_ALL
+ : PlaybackStateCompat.REPEAT_MODE_NONE);
+ mediaSession.setShuffleMode(
+ player.getShuffleModeEnabled()
+ ? PlaybackStateCompat.SHUFFLE_MODE_ALL
+ : PlaybackStateCompat.SHUFFLE_MODE_NONE);
mediaSession.setPlaybackState(builder.build());
}
@@ -658,14 +822,41 @@ public final class MediaSessionConnector {
}
}
- private void registerCommandReceiver(CommandReceiver commandReceiver) {
- if (!commandReceivers.contains(commandReceiver)) {
+ /**
+ * 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(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null && !customCommandReceivers.contains(commandReceiver)) {
+ customCommandReceivers.add(commandReceiver);
+ }
+ }
+
+ /**
+ * Unregisters a previously registered custom command receiver.
+ *
+ * @param commandReceiver The command receiver to unregister.
+ */
+ public void unregisterCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null) {
+ customCommandReceivers.remove(commandReceiver);
+ }
+ }
+
+ private void registerCommandReceiver(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null && !commandReceivers.contains(commandReceiver)) {
commandReceivers.add(commandReceiver);
}
}
- private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
- commandReceivers.remove(commandReceiver);
+ private void unregisterCommandReceiver(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null) {
+ commandReceivers.remove(commandReceiver);
+ }
}
private long buildPrepareActions() {
@@ -679,12 +870,14 @@ public final class MediaSessionConnector {
boolean enableRewind = false;
boolean enableFastForward = false;
boolean enableSetRating = false;
+ boolean enableSetCaptioningEnabled = false;
Timeline timeline = player.getCurrentTimeline();
if (!timeline.isEmpty() && !player.isPlayingAd()) {
enableSeeking = player.isCurrentWindowSeekable();
enableRewind = enableSeeking && rewindMs > 0;
enableFastForward = enableSeeking && fastForwardMs > 0;
- enableSetRating = true;
+ enableSetRating = ratingCallback != null;
+ enableSetCaptioningEnabled = captionCallback != null && captionCallback.hasCaptions(player);
}
long playbackActions = BASE_PLAYBACK_ACTIONS;
@@ -704,13 +897,91 @@ public final class MediaSessionConnector {
actions |=
(QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
}
- if (ratingCallback != null && enableSetRating) {
+ if (enableSetRating) {
actions |= PlaybackStateCompat.ACTION_SET_RATING;
}
+ if (enableSetCaptioningEnabled) {
+ actions |= PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED;
+ }
return actions;
}
- private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) {
+ @EnsuresNonNullIf(result = true, expression = "player")
+ private boolean canDispatchPlaybackAction(long action) {
+ return player != null && (enabledPlaybackActions & action) != 0;
+ }
+
+ @EnsuresNonNullIf(result = true, expression = "playbackPreparer")
+ private boolean canDispatchToPlaybackPreparer(long action) {
+ return playbackPreparer != null
+ && (playbackPreparer.getSupportedPrepareActions() & action) != 0;
+ }
+
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "queueNavigator"})
+ private boolean canDispatchToQueueNavigator(long action) {
+ return player != null
+ && queueNavigator != null
+ && (queueNavigator.getSupportedQueueNavigatorActions(player) & action) != 0;
+ }
+
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "ratingCallback"})
+ private boolean canDispatchSetRating() {
+ return player != null && ratingCallback != null;
+ }
+
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "captionCallback"})
+ private boolean canDispatchSetCaptioningEnabled() {
+ return player != null && captionCallback != null;
+ }
+
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "queueEditor"})
+ private boolean canDispatchQueueEdit() {
+ return player != null && queueEditor != null;
+ }
+
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "mediaButtonEventHandler"})
+ private boolean canDispatchMediaButtonEvent() {
+ return player != null && mediaButtonEventHandler != null;
+ }
+
+ private void rewind(Player player) {
+ if (player.isCurrentWindowSeekable() && rewindMs > 0) {
+ seekToOffset(player, /* offsetMs= */ -rewindMs);
+ }
+ }
+
+ private void fastForward(Player player) {
+ if (player.isCurrentWindowSeekable() && fastForwardMs > 0) {
+ seekToOffset(player, /* offsetMs= */ fastForwardMs);
+ }
+ }
+
+ private void seekToOffset(Player player, long offsetMs) {
+ long positionMs = player.getCurrentPosition() + offsetMs;
+ long durationMs = player.getDuration();
+ if (durationMs != C.TIME_UNSET) {
+ positionMs = Math.min(positionMs, durationMs);
+ }
+ positionMs = Math.max(positionMs, 0);
+ seekTo(player, player.getCurrentWindowIndex(), positionMs);
+ }
+
+ private void seekTo(Player player, int windowIndex, long positionMs) {
+ controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
+ }
+
+ private static int getMediaSessionPlaybackState(
+ @Player.State int exoPlayerPlaybackState, boolean playWhenReady) {
switch (exoPlayerPlaybackState) {
case Player.STATE_BUFFERING:
return PlaybackStateCompat.STATE_BUFFERING;
@@ -718,69 +989,15 @@ public final class MediaSessionConnector {
return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
case Player.STATE_ENDED:
return PlaybackStateCompat.STATE_STOPPED;
+ case Player.STATE_IDLE:
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() & action) != 0;
- }
-
- private boolean canDispatchToQueueNavigator(long action) {
- 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 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 {
@@ -803,7 +1020,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()) {
@@ -811,7 +1028,9 @@ public final class MediaSessionConnector {
}
builder.putLong(
MediaMetadataCompat.METADATA_KEY_DURATION,
- player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration());
+ player.isCurrentWindowDynamic() || player.getDuration() == C.TIME_UNSET
+ ? -1
+ : player.getDuration());
long activeQueueItemId = mediaController.getPlaybackState().getActiveQueueItemId();
if (activeQueueItemId != MediaSessionCompat.QueueItem.UNKNOWN_ID) {
List queue = mediaController.getQueue();
@@ -819,10 +1038,10 @@ public final class MediaSessionConnector {
MediaSessionCompat.QueueItem queueItem = queue.get(i);
if (queueItem.getQueueId() == activeQueueItemId) {
MediaDescriptionCompat description = queueItem.getDescription();
- Bundle extras = description.getExtras();
+ @Nullable Bundle extras = description.getExtras();
if (extras != null) {
for (String key : extras.keySet()) {
- Object value = extras.get(key);
+ @Nullable Object value = extras.get(key);
if (value instanceof String) {
builder.putString(metadataExtrasPrefix + key, (String) value);
} else if (value instanceof CharSequence) {
@@ -838,39 +1057,40 @@ public final class MediaSessionConnector {
}
}
}
- if (description.getTitle() != null) {
- String title = String.valueOf(description.getTitle());
- builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
- builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title);
+ @Nullable CharSequence title = description.getTitle();
+ if (title != null) {
+ String titleString = String.valueOf(title);
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, titleString);
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, titleString);
}
- if (description.getSubtitle() != null) {
+ @Nullable CharSequence subtitle = description.getSubtitle();
+ if (subtitle != null) {
builder.putString(
- MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
- String.valueOf(description.getSubtitle()));
+ MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(subtitle));
}
- if (description.getDescription() != null) {
+ @Nullable CharSequence displayDescription = description.getDescription();
+ if (displayDescription != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
- String.valueOf(description.getDescription()));
+ String.valueOf(displayDescription));
}
- if (description.getIconBitmap() != null) {
- builder.putBitmap(
- MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap());
+ @Nullable Bitmap iconBitmap = description.getIconBitmap();
+ if (iconBitmap != null) {
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, iconBitmap);
}
- if (description.getIconUri() != null) {
+ @Nullable Uri iconUri = description.getIconUri();
+ if (iconUri != null) {
builder.putString(
- MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
- String.valueOf(description.getIconUri()));
+ MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(iconUri));
}
- if (description.getMediaId() != null) {
- builder.putString(
- MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
- String.valueOf(description.getMediaId()));
+ @Nullable String mediaId = description.getMediaId();
+ if (mediaId != null) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId);
}
- if (description.getMediaUri() != null) {
+ @Nullable Uri mediaUri = description.getMediaUri();
+ if (mediaUri != null) {
builder.putString(
- MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
- String.valueOf(description.getMediaUri()));
+ MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(mediaUri));
}
break;
}
@@ -889,8 +1109,8 @@ public final class MediaSessionConnector {
// Player.EventListener implementation.
@Override
- public void onTimelineChanged(
- Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
+ public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
+ Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) {
@@ -906,32 +1126,29 @@ public final class MediaSessionConnector {
}
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
+ invalidateMediaSessionPlaybackState();
+ }
+
+ @Override
+ public void onIsPlayingChanged(boolean isPlaying) {
invalidateMediaSessionPlaybackState();
}
@Override
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
- mediaSession.setRepeatMode(
- repeatMode == Player.REPEAT_MODE_ONE
- ? PlaybackStateCompat.REPEAT_MODE_ONE
- : repeatMode == Player.REPEAT_MODE_ALL
- ? PlaybackStateCompat.REPEAT_MODE_ALL
- : PlaybackStateCompat.REPEAT_MODE_NONE);
invalidateMediaSessionPlaybackState();
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
- mediaSession.setShuffleMode(
- shuffleModeEnabled
- ? PlaybackStateCompat.SHUFFLE_MODE_ALL
- : PlaybackStateCompat.SHUFFLE_MODE_NONE);
invalidateMediaSessionPlaybackState();
+ invalidateMediaSessionQueue();
}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
if (currentWindowIndex != player.getCurrentWindowIndex()) {
if (queueNavigator != null) {
queueNavigator.onCurrentWindowIndexChanged(player);
@@ -958,12 +1175,13 @@ public final class MediaSessionConnector {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
- playbackPreparer.onPrepare();
+ playbackPreparer.onPrepare(/* playWhenReady= */ true);
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
- controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
+ seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
- controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);
+ controlDispatcher.dispatchSetPlayWhenReady(
+ Assertions.checkNotNull(player), /* playWhenReady= */ true);
}
}
@@ -977,7 +1195,7 @@ public final class MediaSessionConnector {
@Override
public void onSeekTo(long positionMs) {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SEEK_TO)) {
- seekTo(player, positionMs);
+ seekTo(player, player.getCurrentWindowIndex(), positionMs);
}
}
@@ -1003,17 +1221,26 @@ public final class MediaSessionConnector {
}
@Override
- public void onSetShuffleMode(int shuffleMode) {
+ public void onSetShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {
- boolean shuffleModeEnabled =
- shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL
- || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP;
+ 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 mediaSessionRepeatMode) {
+ public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int mediaSessionRepeatMode) {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
@RepeatModeUtil.RepeatToggleModes int repeatMode;
switch (mediaSessionRepeatMode) {
@@ -1024,6 +1251,8 @@ public final class MediaSessionConnector {
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;
@@ -1054,7 +1283,7 @@ public final class MediaSessionConnector {
}
@Override
- public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
+ public void onCustomAction(String action, @Nullable Bundle extras) {
if (player != null && customActionMap.containsKey(action)) {
customActionMap.get(action).onCustomAction(player, controlDispatcher, action, extras);
invalidateMediaSessionPlaybackState();
@@ -1069,62 +1298,62 @@ public final class MediaSessionConnector {
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)) {
- stopPlayerForPrepare(/* playWhenReady= */ false);
- playbackPreparer.onPrepare();
+ playbackPreparer.onPrepare(/* playWhenReady= */ false);
}
}
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
- stopPlayerForPrepare(/* playWhenReady= */ false);
- playbackPreparer.onPrepareFromMediaId(mediaId, extras);
+ playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
- stopPlayerForPrepare(/* playWhenReady= */ false);
- playbackPreparer.onPrepareFromSearch(query, extras);
+ playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
- stopPlayerForPrepare(/* playWhenReady= */ false);
- playbackPreparer.onPrepareFromUri(uri, extras);
+ playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
- stopPlayerForPrepare(/* playWhenReady= */ true);
- playbackPreparer.onPrepareFromMediaId(mediaId, extras);
+ playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
- stopPlayerForPrepare(/* playWhenReady= */ true);
- playbackPreparer.onPrepareFromSearch(query, extras);
+ playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
- stopPlayerForPrepare(/* playWhenReady= */ true);
- playbackPreparer.onPrepareFromUri(uri, extras);
+ playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras);
}
}
@@ -1162,5 +1391,21 @@ public final class MediaSessionConnector {
queueEditor.onRemoveQueueItem(player, description);
}
}
+
+ @Override
+ public void onSetCaptioningEnabled(boolean enabled) {
+ if (canDispatchSetCaptioningEnabled()) {
+ captionCallback.onSetCaptioningEnabled(player, enabled);
+ }
+ }
+
+ @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 617b8781f4..87b9447f7c 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,6 +18,7 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.media.session.PlaybackStateCompat;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.util.RepeatModeUtil;
@@ -65,7 +66,7 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
@Override
public void onCustomAction(
- Player player, ControlDispatcher controlDispatcher, String action, Bundle extras) {
+ Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras) {
int mode = player.getRepeatMode();
int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
if (mode != proposedMode) {
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 b92d7a27b7..41bda3bf44 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,11 +17,10 @@ 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 android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
@@ -129,10 +128,10 @@ public final class TimelineQueueEditor
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
*/
public TimelineQueueEditor(
- @NonNull MediaControllerCompat mediaController,
- @NonNull ConcatenatingMediaSource queueMediaSource,
- @NonNull QueueDataAdapter queueDataAdapter,
- @NonNull MediaSourceFactory sourceFactory) {
+ MediaControllerCompat mediaController,
+ ConcatenatingMediaSource queueMediaSource,
+ QueueDataAdapter queueDataAdapter,
+ MediaSourceFactory sourceFactory) {
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
new MediaIdEqualityChecker());
}
@@ -147,11 +146,11 @@ public final class TimelineQueueEditor
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
*/
public TimelineQueueEditor(
- @NonNull MediaControllerCompat mediaController,
- @NonNull ConcatenatingMediaSource queueMediaSource,
- @NonNull QueueDataAdapter queueDataAdapter,
- @NonNull MediaSourceFactory sourceFactory,
- @NonNull MediaDescriptionEqualityChecker equalityChecker) {
+ MediaControllerCompat mediaController,
+ ConcatenatingMediaSource queueMediaSource,
+ QueueDataAdapter queueDataAdapter,
+ MediaSourceFactory sourceFactory,
+ MediaDescriptionEqualityChecker equalityChecker) {
this.mediaController = mediaController;
this.queueMediaSource = queueMediaSource;
this.queueDataAdapter = queueDataAdapter;
@@ -166,7 +165,7 @@ public final class TimelineQueueEditor
@Override
public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {
- MediaSource mediaSource = sourceFactory.createMediaSource(description);
+ @Nullable MediaSource mediaSource = sourceFactory.createMediaSource(description);
if (mediaSource != null) {
queueDataAdapter.add(index, description);
queueMediaSource.addMediaSource(index, mediaSource);
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 d0047637dd..fc4cc11b58 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,18 +17,18 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
-import android.support.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
+import androidx.annotation.Nullable;
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
@@ -67,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;
@@ -76,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 this method again.
+ *
* @param player The current player.
* @param windowIndex The timeline window index for which to provide a description.
* @return A {@link MediaDescriptionCompat}.
@@ -188,22 +194,50 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
// 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/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java
new file mode 100644
index 0000000000..65c0ce080e
--- /dev/null
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.mediasession;
+
+import com.google.android.exoplayer2.util.NonNullApi;
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 4e6b11c495..3af38397a8 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,13 +27,21 @@ 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:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
- api 'com.squareup.okhttp3:okhttp:3.11.0'
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ // Do not update to 3.13.X or later until minSdkVersion is increased to 21:
+ // https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5
+ // Since OkHttp is distributed as a jar rather than an aar, Gradle wont stop
+ // us from making this mistake!
+ api 'com.squareup.okhttp3:okhttp:3.12.5'
}
ext {
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 dd1db8211a..3053961f49 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,10 +18,9 @@ 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;
@@ -34,6 +33,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.CacheControl;
@@ -45,7 +45,13 @@ import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
-/** An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}. */
+/**
+ * An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}.
+ *
+ * Note: HTTP request headers will be set using all parameters passed via (in order of decreasing
+ * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to
+ * construct the instance.
+ */
public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
static {
@@ -57,14 +63,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private final Call.Factory callFactory;
private final RequestProperties requestProperties;
- private final @Nullable String userAgent;
- private final @Nullable Predicate contentTypePredicate;
- private final @Nullable CacheControl cacheControl;
- private final @Nullable RequestProperties defaultRequestProperties;
+ @Nullable private final String userAgent;
+ @Nullable private final CacheControl cacheControl;
+ @Nullable private final RequestProperties defaultRequestProperties;
- private @Nullable DataSpec dataSpec;
- private @Nullable Response response;
- private @Nullable InputStream responseByteStream;
+ @Nullable private Predicate contentTypePredicate;
+ @Nullable private DataSpec dataSpec;
+ @Nullable private Response response;
+ @Nullable private InputStream responseByteStream;
private boolean opened;
private long bytesToSkip;
@@ -73,6 +79,36 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private long bytesSkipped;
private long bytesRead;
+ /**
+ * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
+ * by the source.
+ * @param userAgent An optional User-Agent string.
+ */
+ public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) {
+ this(callFactory, userAgent, /* cacheControl= */ null, /* defaultRequestProperties= */ null);
+ }
+
+ /**
+ * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
+ * by the source.
+ * @param userAgent An optional User-Agent string.
+ * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ */
+ public OkHttpDataSource(
+ Call.Factory callFactory,
+ @Nullable String userAgent,
+ @Nullable CacheControl cacheControl,
+ @Nullable RequestProperties defaultRequestProperties) {
+ super(/* isNetwork= */ true);
+ this.callFactory = Assertions.checkNotNull(callFactory);
+ this.userAgent = userAgent;
+ this.cacheControl = cacheControl;
+ this.defaultRequestProperties = defaultRequestProperties;
+ this.requestProperties = new RequestProperties();
+ }
+
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
@@ -80,7 +116,10 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from {@link
* #open(DataSpec)}.
+ * @deprecated Use {@link #OkHttpDataSource(Call.Factory, String)} and {@link
+ * #setContentTypePredicate(Predicate)}.
*/
+ @Deprecated
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@@ -101,9 +140,12 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* predicate then a {@link InvalidContentTypeException} is thrown from {@link
* #open(DataSpec)}.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
- * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
- * the server as HTTP headers on every request.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ * @deprecated Use {@link #OkHttpDataSource(Call.Factory, String, CacheControl,
+ * RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
*/
+ @Deprecated
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@@ -119,11 +161,28 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
this.requestProperties = new RequestProperties();
}
+ /**
+ * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
+ * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
+ *
+ * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
+ * predicate that was previously set.
+ */
+ public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) {
+ this.contentTypePredicate = contentTypePredicate;
+ }
+
@Override
- public @Nullable Uri getUri() {
+ @Nullable
+ public Uri getUri() {
return response == null ? null : Uri.parse(response.request().url().toString());
}
+ @Override
+ public int getResponseCode() {
+ return response == null ? -1 : response.code();
+ }
+
@Override
public Map> getResponseHeaders() {
return response == null ? Collections.emptyMap() : response.headers().toMultimap();
@@ -275,14 +334,19 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
if (cacheControl != null) {
builder.cacheControl(cacheControl);
}
+
+ Map headers = new HashMap<>();
if (defaultRequestProperties != null) {
- for (Map.Entry property : defaultRequestProperties.getSnapshot().entrySet()) {
- builder.header(property.getKey(), property.getValue());
- }
+ headers.putAll(defaultRequestProperties.getSnapshot());
}
- for (Map.Entry property : requestProperties.getSnapshot().entrySet()) {
- builder.header(property.getKey(), property.getValue());
+
+ headers.putAll(requestProperties.getSnapshot());
+ headers.putAll(dataSpec.httpRequestHeaders);
+
+ for (Map.Entry header : headers.entrySet()) {
+ builder.header(header.getKey(), header.getValue());
}
+
if (!(position == 0 && length == C.LENGTH_UNSET)) {
String rangeRequest = "bytes=" + position + "-";
if (length != C.LENGTH_UNSET) {
@@ -296,11 +360,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
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..f3d74f9233 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;
@@ -29,9 +29,9 @@ import okhttp3.Call;
public final class OkHttpDataSourceFactory extends BaseFactory {
private final Call.Factory callFactory;
- private final @Nullable String userAgent;
- private final @Nullable TransferListener listener;
- private final @Nullable CacheControl cacheControl;
+ @Nullable private final String userAgent;
+ @Nullable private final TransferListener listener;
+ @Nullable private final CacheControl cacheControl;
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
@@ -89,7 +89,6 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
new OkHttpDataSource(
callFactory,
userAgent,
- /* contentTypePredicate= */ null,
cacheControl,
defaultRequestProperties);
if (listener != null) {
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java
new file mode 100644
index 0000000000..54eb4d5967
--- /dev/null
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.okhttp;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/okhttp/src/test/AndroidManifest.xml b/extensions/okhttp/src/test/AndroidManifest.xml
new file mode 100644
index 0000000000..3efff43f6b
--- /dev/null
+++ b/extensions/okhttp/src/test/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/extensions/okhttp/src/test/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceTest.java b/extensions/okhttp/src/test/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceTest.java
new file mode 100644
index 0000000000..dab62b06e8
--- /dev/null
+++ b/extensions/okhttp/src/test/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.okhttp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import java.util.HashMap;
+import java.util.Map;
+import okhttp3.Call;
+import okhttp3.MediaType;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link OkHttpDataSource}. */
+@RunWith(AndroidJUnit4.class)
+public class OkHttpDataSourceTest {
+
+ @Test
+ public void open_setsCorrectHeaders() throws HttpDataSource.HttpDataSourceException {
+ /*
+ * This test will set HTTP default request parameters (1) in the OkHttpDataSource, (2) via
+ * OkHttpDataSource.setRequestProperty() and (3) in the DataSpec instance according to the table
+ * below. Values wrapped in '*' are the ones that should be set in the connection request.
+ *
+ * +-----------------------+---+-----+-----+-----+-----+-----+
+ * | | Header Key |
+ * +-----------------------+---+-----+-----+-----+-----+-----+
+ * | Location | 0 | 1 | 2 | 3 | 4 | 5 |
+ * +-----------------------+---+-----+-----+-----+-----+-----+
+ * | Default |*Y*| Y | Y | | | |
+ * | OkHttpDataSource | | *Y* | Y | Y | *Y* | |
+ * | DataSpec | | | *Y* | *Y* | | *Y* |
+ * +-----------------------+---+-----+-----+-----+-----+-----+
+ */
+
+ String defaultValue = "Default";
+ String okHttpDataSourceValue = "OkHttpDataSource";
+ String dataSpecValue = "DataSpec";
+
+ // 1. Default properties on OkHttpDataSource
+ HttpDataSource.RequestProperties defaultRequestProperties =
+ new HttpDataSource.RequestProperties();
+ defaultRequestProperties.set("0", defaultValue);
+ defaultRequestProperties.set("1", defaultValue);
+ defaultRequestProperties.set("2", defaultValue);
+
+ Call.Factory mockCallFactory = Mockito.mock(Call.Factory.class);
+ OkHttpDataSource okHttpDataSource =
+ new OkHttpDataSource(
+ mockCallFactory, "testAgent", /* cacheControl= */ null, defaultRequestProperties);
+
+ // 2. Additional properties set with setRequestProperty().
+ okHttpDataSource.setRequestProperty("1", okHttpDataSourceValue);
+ okHttpDataSource.setRequestProperty("2", okHttpDataSourceValue);
+ okHttpDataSource.setRequestProperty("3", okHttpDataSourceValue);
+ okHttpDataSource.setRequestProperty("4", okHttpDataSourceValue);
+
+ // 3. DataSpec properties
+ Map dataSpecRequestProperties = new HashMap<>();
+ dataSpecRequestProperties.put("2", dataSpecValue);
+ dataSpecRequestProperties.put("3", dataSpecValue);
+ dataSpecRequestProperties.put("5", dataSpecValue);
+
+ DataSpec dataSpec =
+ new DataSpec(
+ /* uri= */ Uri.parse("http://www.google.com"),
+ /* httpMethod= */ 1,
+ /* httpBody= */ null,
+ /* absoluteStreamPosition= */ 1000,
+ /* position= */ 1000,
+ /* length= */ 5000,
+ /* key= */ null,
+ /* flags= */ 0,
+ dataSpecRequestProperties);
+
+ Mockito.doAnswer(
+ invocation -> {
+ Request request = invocation.getArgument(0);
+ assertThat(request.header("0")).isEqualTo(defaultValue);
+ assertThat(request.header("1")).isEqualTo(okHttpDataSourceValue);
+ assertThat(request.header("2")).isEqualTo(dataSpecValue);
+ assertThat(request.header("3")).isEqualTo(dataSpecValue);
+ assertThat(request.header("4")).isEqualTo(okHttpDataSourceValue);
+ assertThat(request.header("5")).isEqualTo(dataSpecValue);
+
+ // return a Call whose .execute() will return a mock Response
+ Call returnValue = Mockito.mock(Call.class);
+ Mockito.doReturn(
+ new Response.Builder()
+ .request(request)
+ .protocol(Protocol.HTTP_1_1)
+ .code(200)
+ .message("OK")
+ .body(ResponseBody.create(MediaType.parse("text/plain"), ""))
+ .build())
+ .when(returnValue)
+ .execute();
+ return returnValue;
+ })
+ .when(mockCallFactory)
+ .newCall(ArgumentMatchers.any());
+ okHttpDataSource.open(dataSpec);
+ }
+}
diff --git a/extensions/opus/README.md b/extensions/opus/README.md
index 15c3e5413d..d3691b07bd 100644
--- a/extensions/opus/README.md
+++ b/extensions/opus/README.md
@@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately.
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
-## Build instructions ##
+## Build instructions (Linux, macOS) ##
To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's
@@ -28,7 +28,8 @@ EXOPLAYER_ROOT="$(pwd)"
OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main"
```
-* Download the [Android NDK][] and set its location in an environment variable:
+* Download the [Android NDK][] and set its location in an environment variable.
+ This build configuration has been tested on NDK r20.
```
NDK_PATH=""
@@ -57,6 +58,13 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
+## Build instructions (Windows) ##
+
+We do not provide support for building this extension on Windows, however it
+should be possible to follow the Linux instructions in [Windows PowerShell][].
+
+[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
+
## Notes ##
* Every time there is a change to the libopus checkout:
@@ -71,31 +79,39 @@ Once you've followed the instructions above to check out, build and depend on
the extension, the next step is to tell ExoPlayer to use `LibopusAudioRenderer`.
How you do this depends on which player API you're using:
-* If you're passing a `DefaultRenderersFactory` to
- `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by
- setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`
- constructor to `EXTENSION_RENDERER_MODE_ON`. This will use
- `LibopusAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't
- support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give
- `LibopusAudioRenderer` priority over `MediaCodecAudioRenderer`.
+* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
+ you can enable using the extension by setting the `extensionRendererMode`
+ parameter of the `DefaultRenderersFactory` constructor to
+ `EXTENSION_RENDERER_MODE_ON`. This will use `LibopusAudioRenderer` for
+ playback if `MediaCodecAudioRenderer` doesn't support the input format. Pass
+ `EXTENSION_RENDERER_MODE_PREFER` to give `LibopusAudioRenderer` priority over
+ `MediaCodecAudioRenderer`.
* If you've subclassed `DefaultRenderersFactory`, add a `LibopusAudioRenderer`
to the output list in `buildAudioRenderers`. ExoPlayer will use the first
`Renderer` in the list that supports the input media format.
* If you've implemented your own `RenderersFactory`, return a
`LibopusAudioRenderer` instance from `createRenderers`. ExoPlayer will use the
first `Renderer` in the returned array that supports the input media format.
-* If you're using `ExoPlayerFactory.newInstance`, pass a `LibopusAudioRenderer`
- in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the
- list that supports the input media format.
+* If you're using `ExoPlayer.Builder`, pass a `LibopusAudioRenderer` in the
+ array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
+ supports the input media format.
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation,
so you need to make sure you are passing an `LibopusAudioRenderer` to the
player, then implement your own logic to use the renderer for a given track.
+## Using the extension in the demo application ##
+
+To try out playback using the extension in the [demo application][], see
+[enabling extension decoders][].
+
+[demo application]: https://exoplayer.dev/demo-application.html
+[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
+
## Links ##
* [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..28cf8f138f 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,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')
- androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
+ implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
+ androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
}
ext {
diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml
index 9c1241a512..7f75cbccea 100644
--- a/extensions/opus/src/androidTest/AndroidManifest.xml
+++ b/extensions/opus/src/androidTest/AndroidManifest.xml
@@ -19,8 +19,9 @@
package="com.google.android.exoplayer2.ext.opus.test">
+
-
@@ -28,6 +29,6 @@
+ android:name="androidx.test.runner.AndroidJUnitRunner"/>
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 5ad864c597..3b5239e2fa 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,22 +15,19 @@
*/
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.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
import org.junit.Test;
@@ -56,7 +53,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();
@@ -82,16 +79,15 @@ public class OpusPlaybackTest {
public void run() {
Looper.prepare();
LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer();
- DefaultTrackSelector trackSelector = new DefaultTrackSelector();
- player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
+ player = new ExoPlayer.Builder(context, audioRenderer).build();
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);
+ player.play();
Looper.loop();
}
@@ -101,7 +97,7 @@ public class OpusPlaybackTest {
}
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playbackState == Player.STATE_ENDED
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
player.release();
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..3592331eff 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
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.opus;
import android.os.Handler;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
@@ -23,22 +24,22 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.MimeTypes;
-/**
- * Decodes and renders audio using the native Opus decoder.
- */
-public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
+/** Decodes and renders audio using the native Opus decoder. */
+public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
/** The number of input and output buffers. */
private static final int NUM_BUFFERS = 16;
/** The default input buffer size. */
private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
- private OpusDecoder decoder;
+ private int channelCount;
+ private int sampleRate;
public LibopusAudioRenderer() {
- this(null, null);
+ this(/* eventHandler= */ null, /* eventListener= */ null);
}
/**
@@ -47,7 +48,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(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
@@ -64,23 +67,36 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* 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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
+ * @deprecated Use {@link #LibopusAudioRenderer(Handler, AudioRendererEventListener,
+ * AudioProcessor...)} instead, and pass DRM-related parameters to the {@link MediaSource}
+ * factories.
*/
- public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
- DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
+ @Deprecated
+ public LibopusAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys,
audioProcessors);
}
@Override
- protected int supportsFormatInternal(DrmSessionManager drmSessionManager,
- Format format) {
+ @FormatSupport
+ protected int supportsFormatInternal(
+ @Nullable DrmSessionManager drmSessionManager, Format format) {
+ boolean drmIsSupported =
+ format.drmInitData == null
+ || OpusLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType)
+ || (format.exoMediaCryptoType == null
+ && supportsFormatDrm(drmSessionManager, format.drmInitData));
if (!OpusLibrary.isAvailable()
|| !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
- } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
+ } else if (!drmIsSupported) {
return FORMAT_UNSUPPORTED_DRM;
} else {
return FORMAT_HANDLED;
@@ -88,25 +104,36 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
- protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
+ protected OpusDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws OpusDecoderException {
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
- decoder =
+ OpusDecoder decoder =
new OpusDecoder(
NUM_BUFFERS,
NUM_BUFFERS,
initialInputBufferSize,
format.initializationData,
mediaCrypto);
+ channelCount = decoder.getChannelCount();
+ sampleRate = decoder.getSampleRate();
return decoder;
}
@Override
protected Format getOutputFormat() {
- return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
- Format.NO_VALUE, decoder.getChannelCount(), decoder.getSampleRate(), C.ENCODING_PCM_16BIT,
- null, null, 0, null);
+ return Format.createAudioSampleFormat(
+ /* id= */ null,
+ MimeTypes.AUDIO_RAW,
+ /* codecs= */ null,
+ Format.NO_VALUE,
+ Format.NO_VALUE,
+ channelCount,
+ sampleRate,
+ C.ENCODING_PCM_16BIT,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null);
}
-
}
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
index f8ec477b88..f0e993e3b9 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.opus;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
@@ -22,6 +23,7 @@ import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.drm.DecryptionException;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
@@ -43,7 +45,7 @@ import java.util.List;
private static final int DECODE_ERROR = -1;
private static final int DRM_ERROR = -2;
- private final ExoMediaCrypto exoMediaCrypto;
+ @Nullable private final ExoMediaCrypto exoMediaCrypto;
private final int channelCount;
private final int headerSkipSamples;
@@ -65,8 +67,13 @@ import java.util.List;
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder.
*/
- public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
- List initializationData, ExoMediaCrypto exoMediaCrypto) throws OpusDecoderException {
+ public OpusDecoder(
+ int numInputBuffers,
+ int numOutputBuffers,
+ int initialInputBufferSize,
+ List initializationData,
+ @Nullable ExoMediaCrypto exoMediaCrypto)
+ throws OpusDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (!OpusLibrary.isAvailable()) {
throw new OpusDecoderException("Failed to load decoder native libraries.");
@@ -150,6 +157,7 @@ import java.util.List;
}
@Override
+ @Nullable
protected OpusDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
@@ -158,7 +166,7 @@ import java.util.List;
// any other time, skip number of samples as specified by seek preroll.
skipSamples = (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples;
}
- ByteBuffer inputData = inputBuffer.data;
+ ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
int result = inputBuffer.isEncrypted()
? opusSecureDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(),
@@ -178,7 +186,7 @@ import java.util.List;
}
}
- ByteBuffer outputData = outputBuffer.data;
+ ByteBuffer outputData = Util.castNonNull(outputBuffer.data);
outputData.position(0);
outputData.limit(result);
if (skipSamples > 0) {
@@ -230,10 +238,22 @@ import java.util.List;
int gain, byte[] streamMap);
private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize,
SimpleOutputBuffer outputBuffer);
- private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer,
- int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate,
- ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
- int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
+
+ private native int opusSecureDecode(
+ long decoder,
+ long timeUs,
+ ByteBuffer inputBuffer,
+ int inputSize,
+ SimpleOutputBuffer outputBuffer,
+ int sampleRate,
+ @Nullable ExoMediaCrypto mediaCrypto,
+ int inputMode,
+ byte[] key,
+ byte[] iv,
+ int numSubSamples,
+ int[] numBytesOfClearData,
+ int[] numBytesOfEncryptedData);
+
private native void opusClose(long decoder);
private native void opusReset(long decoder);
private native int opusGetErrorCode(long decoder);
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..d09d69bf03 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
@@ -15,8 +15,11 @@
*/
package com.google.android.exoplayer2.ext.opus;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.LibraryLoader;
+import com.google.android.exoplayer2.util.Util;
/**
* Configures and queries the underlying native library.
@@ -27,7 +30,8 @@ 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");
+ @Nullable private static Class extends ExoMediaCrypto> exoMediaCryptoType;
private OpusLibrary() {}
@@ -36,10 +40,14 @@ public final class OpusLibrary {
* it must do so before calling any other method defined by this class, and before instantiating a
* {@link LibopusAudioRenderer} instance.
*
+ * @param exoMediaCryptoType The {@link ExoMediaCrypto} type expected for decoding protected
+ * content.
* @param libraries The names of the Opus native libraries.
*/
- public static void setLibraries(String... libraries) {
+ public static void setLibraries(
+ Class extends ExoMediaCrypto> exoMediaCryptoType, String... libraries) {
LOADER.setLibraries(libraries);
+ OpusLibrary.exoMediaCryptoType = exoMediaCryptoType;
}
/**
@@ -49,13 +57,21 @@ public final class OpusLibrary {
return LOADER.isAvailable();
}
- /**
- * Returns the version of the underlying library if available, or null otherwise.
- */
+ /** Returns the version of the underlying library if available, or null otherwise. */
+ @Nullable
public static String getVersion() {
return isAvailable() ? opusGetVersion() : null;
}
+ /**
+ * Returns whether the given {@link ExoMediaCrypto} type matches the one required for decoding
+ * protected content.
+ */
+ public static boolean matchesExpectedExoMediaCryptoType(
+ @Nullable Class extends ExoMediaCrypto> exoMediaCryptoType) {
+ return Util.areEqual(OpusLibrary.exoMediaCryptoType, exoMediaCryptoType);
+ }
+
public static native String opusGetVersion();
public static native boolean opusIsSecureDecodeSupported();
}
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java
new file mode 100644
index 0000000000..0848937fdc
--- /dev/null
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.opus;
+
+import com.google.android.exoplayer2.util.NonNullApi;
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/main/jni/Application.mk b/extensions/opus/src/main/jni/Application.mk
index 59bf5f8f87..7d6f732548 100644
--- a/extensions/opus/src/main/jni/Application.mk
+++ b/extensions/opus/src/main/jni/Application.mk
@@ -15,6 +15,6 @@
#
APP_OPTIM := release
-APP_STL := gnustl_static
+APP_STL := c++_static
APP_CPPFLAGS := -frtti
APP_PLATFORM := android-9
diff --git a/extensions/opus/src/test/AndroidManifest.xml b/extensions/opus/src/test/AndroidManifest.xml
new file mode 100644
index 0000000000..d17f889d17
--- /dev/null
+++ b/extensions/opus/src/test/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
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..88d3524d72 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,16 +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
+ implementation 'net.butterflytv.utils:rtmp-client:3.1.0'
+ implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
+ testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
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..587e310d64 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
@@ -15,14 +15,15 @@
*/
package com.google.android.exoplayer2.ext.rtmp;
+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.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
-import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException;
import net.butterflytv.rtmp_client.RtmpClient;
import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException;
@@ -34,25 +35,13 @@ public final class RtmpDataSource extends BaseDataSource {
ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp");
}
- private RtmpClient rtmpClient;
- private Uri uri;
+ @Nullable private RtmpClient rtmpClient;
+ @Nullable private Uri uri;
public RtmpDataSource() {
super(/* isNetwork= */ true);
}
- /**
- * @param listener An optional listener.
- * @deprecated Use {@link #RtmpDataSource()} and {@link #addTransferListener(TransferListener)}.
- */
- @Deprecated
- public RtmpDataSource(@Nullable TransferListener listener) {
- this();
- if (listener != null) {
- addTransferListener(listener);
- }
- }
-
@Override
public long open(DataSpec dataSpec) throws RtmpIOException {
transferInitializing(dataSpec);
@@ -66,7 +55,7 @@ public final class RtmpDataSource extends BaseDataSource {
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
- int bytesRead = rtmpClient.read(buffer, offset, readLength);
+ int bytesRead = castNonNull(rtmpClient).read(buffer, offset, readLength);
if (bytesRead == -1) {
return C.RESULT_END_OF_INPUT;
}
@@ -87,6 +76,7 @@ public final class RtmpDataSource extends BaseDataSource {
}
@Override
+ @Nullable
public Uri getUri() {
return uri;
}
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..db60eea269 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;
@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.upstream.TransferListener;
*/
public final class RtmpDataSourceFactory implements DataSource.Factory {
- private final @Nullable TransferListener listener;
+ @Nullable private final TransferListener listener;
public RtmpDataSourceFactory() {
this(null);
@@ -37,7 +37,7 @@ public final class RtmpDataSourceFactory implements DataSource.Factory {
}
@Override
- public DataSource createDataSource() {
+ public RtmpDataSource createDataSource() {
RtmpDataSource dataSource = new RtmpDataSource();
if (listener != null) {
dataSource.addTransferListener(listener);
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java
new file mode 100644
index 0000000000..cb16630bd3
--- /dev/null
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.rtmp;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/rtmp/src/test/AndroidManifest.xml b/extensions/rtmp/src/test/AndroidManifest.xml
index 7eab4e2d59..b2e19827d9 100644
--- a/extensions/rtmp/src/test/AndroidManifest.xml
+++ b/extensions/rtmp/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
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..fd0836648a 100644
--- a/extensions/vp9/README.md
+++ b/extensions/vp9/README.md
@@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately.
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
-## Build instructions ##
+## Build instructions (Linux, macOS) ##
To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's
@@ -29,36 +29,33 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
```
* Download the [Android NDK][] and set its location in an environment variable.
+ This build configuration has been tested on NDK r20.
```
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:
```
cd ${VP9_EXT_PATH}/jni && \
-./generate_libvpx_android_configs.sh "${NDK_PATH}"
+./generate_libvpx_android_configs.sh
```
* Build the JNI native libraries from the command line:
@@ -70,7 +67,13 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
-[#3520]: https://github.com/google/ExoPlayer/issues/3520
+
+## Build instructions (Windows) ##
+
+We do not provide support for building this extension on Windows, however it
+should be possible to follow the Linux instructions in [Windows PowerShell][].
+
+[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
## Notes ##
@@ -78,10 +81,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 ##
@@ -89,38 +92,61 @@ Once you've followed the instructions above to check out, build and depend on
the extension, the next step is to tell ExoPlayer to use `LibvpxVideoRenderer`.
How you do this depends on which player API you're using:
-* If you're passing a `DefaultRenderersFactory` to
- `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by
- setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`
- constructor to `EXTENSION_RENDERER_MODE_ON`. This will use
- `LibvpxVideoRenderer` for playback if `MediaCodecVideoRenderer` doesn't
- support decoding the input VP9 stream. Pass `EXTENSION_RENDERER_MODE_PREFER`
- to give `LibvpxVideoRenderer` priority over `MediaCodecVideoRenderer`.
+* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
+ you can enable using the extension by setting the `extensionRendererMode`
+ parameter of the `DefaultRenderersFactory` constructor to
+ `EXTENSION_RENDERER_MODE_ON`. This will use `LibvpxVideoRenderer` for playback
+ if `MediaCodecVideoRenderer` doesn't support decoding the input VP9 stream.
+ Pass `EXTENSION_RENDERER_MODE_PREFER` to give `LibvpxVideoRenderer` priority
+ over `MediaCodecVideoRenderer`.
* If you've subclassed `DefaultRenderersFactory`, add a `LibvpxVideoRenderer`
to the output list in `buildVideoRenderers`. ExoPlayer will use the first
`Renderer` in the list that supports the input media format.
* If you've implemented your own `RenderersFactory`, return a
`LibvpxVideoRenderer` instance from `createRenderers`. ExoPlayer will use the
first `Renderer` in the returned array that supports the input media format.
-* If you're using `ExoPlayerFactory.newInstance`, pass a `LibvpxVideoRenderer`
- in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the
- list that supports the input media format.
+* If you're using `ExoPlayer.Builder`, pass a `LibvpxVideoRenderer` in the array
+ of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
+ supports the input media format.
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation,
so you need to make sure you are passing an `LibvpxVideoRenderer` to the
player, then implement your own logic to use the renderer for a given track.
-`LibvpxVideoRenderer` can optionally output to a `VpxVideoSurfaceView` when not
-being used via `SimpleExoPlayer`, in which case color space conversion will be
-performed using a GL shader. To enable this mode, send the renderer a message of
-type `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` with the
-`VpxVideoSurfaceView` as its object, instead of sending `MSG_SET_SURFACE` with a
-`Surface`.
+## Using the extension in the demo application ##
+
+To try out playback using the extension in the [demo application][], see
+[enabling extension decoders][].
+
+[demo application]: https://exoplayer.dev/demo-application.html
+[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
+
+## Rendering options ##
+
+There are two possibilities for rendering the output `LibvpxVideoRenderer`
+gets from the libvpx decoder:
+
+* GL rendering using GL shader for color space conversion
+ * If you are using `SimpleExoPlayer` with `PlayerView`, enable this option by
+ setting `surface_type` of `PlayerView` to be
+ `video_decoder_gl_surface_view`.
+ * Otherwise, enable this option by sending `LibvpxVideoRenderer` a message of
+ type `C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER` with an instance of
+ `VideoDecoderOutputBufferRenderer` as its object.
+
+* Native rendering using `ANativeWindow`
+ * If you are using `SimpleExoPlayer` with `PlayerView`, this option is enabled
+ by default.
+ * Otherwise, enable this option by sending `LibvpxVideoRenderer` a message of
+ type `C.MSG_SET_SURFACE` with an instance of `SurfaceView` as its object.
+
+Note: Although the default option uses `ANativeWindow`, based on our testing the
+GL rendering mode has better performance, so should be preferred.
## Links ##
* [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..80239beb22 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,12 +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
+ implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
+ androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
}
diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml
index aef376237e..4d0832d198 100644
--- a/extensions/vp9/src/androidTest/AndroidManifest.xml
+++ b/extensions/vp9/src/androidTest/AndroidManifest.xml
@@ -19,8 +19,9 @@
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 c6d1e667e0..ed8bd00fca 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,25 +15,24 @@
*/
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.C;
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.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.video.VideoDecoderGLSurfaceView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -89,7 +88,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,22 +113,21 @@ public class VpxPlaybackTest {
@Override
public void run() {
Looper.prepare();
- LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0);
- DefaultTrackSelector trackSelector = new DefaultTrackSelector();
- player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector);
+ LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0);
+ player = new ExoPlayer.Builder(context, videoRenderer).build();
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)
- .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER)
- .setPayload(new VpxVideoSurfaceView(context))
+ .setType(C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER)
+ .setPayload(new VideoDecoderGLSurfaceView(context).getVideoDecoderOutputBufferRenderer())
.send();
player.prepare(mediaSource);
- player.setPlayWhenReady(true);
+ player.play();
Looper.loop();
}
@@ -139,7 +137,7 @@ public class VpxPlaybackTest {
}
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playbackState == Player.STATE_ENDED
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
player.release();
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 e61030a2e1..28cb35e60f 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,39 +15,30 @@
*/
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 android.view.Surface;
-import com.google.android.exoplayer2.BaseRenderer;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.PlayerMessage.Target;
-import com.google.android.exoplayer2.decoder.DecoderCounters;
-import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
-import com.google.android.exoplayer2.drm.DrmSession;
-import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
-import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.MimeTypes;
-import com.google.android.exoplayer2.util.TimedValueQueue;
import com.google.android.exoplayer2.util.TraceUtil;
-import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer;
+import com.google.android.exoplayer2.video.VideoDecoderException;
+import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
+import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
+import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;
import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
-import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
/**
* Decodes and renders video using the native VP9 decoder.
@@ -58,116 +49,40 @@ import java.lang.annotation.RetentionPolicy;
*
* - Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload
* should be the target {@link Surface}, or null.
- *
- Message with type {@link #MSG_SET_OUTPUT_BUFFER_RENDERER} to set the output buffer
- * renderer. The message payload should be the target {@link VpxOutputBufferRenderer}, or
- * null.
+ *
- Message with type {@link C#MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER} to set the output
+ * buffer renderer. The message payload should be the target {@link
+ * VideoDecoderOutputBufferRenderer}, or null.
*
*/
-public class LibvpxVideoRenderer extends BaseRenderer {
+public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer {
- @Documented
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- REINITIALIZATION_STATE_NONE,
- REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
- REINITIALIZATION_STATE_WAIT_END_OF_STREAM
- })
- private @interface ReinitializationState {}
- /**
- * The decoder does not need to be re-initialized.
- */
- private static final int REINITIALIZATION_STATE_NONE = 0;
- /**
- * The input format has changed in a way that requires the decoder to be re-initialized, but we
- * haven't yet signaled an end of stream to the existing decoder. We need to do so in order to
- * ensure that it outputs any remaining buffers before we release it.
- */
- private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
- /**
- * The input format has changed in a way that requires the decoder to be re-initialized, and we've
- * signaled an end of stream to the existing decoder. We're waiting for the decoder to output an
- * end of stream signal to indicate that it has output any remaining buffers before we release it.
- */
- private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
-
- /**
- * The type of a message that can be passed to an instance of this class via {@link
- * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link
- * VpxOutputBufferRenderer}, or null.
- */
- 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;
- /** The default input buffer size. */
- private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
+ private final int numOutputBuffers;
+ /**
+ * The default input buffer size. The value is based on SoftVPX.cpp.
+ */
+ private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024;
- private final boolean scaleToFit;
- private final boolean disableLoopFilter;
- private final long allowedJoiningTimeMs;
- private final int maxDroppedFramesToNotify;
- private final boolean playClearSamplesWithoutKeys;
- private final EventDispatcher eventDispatcher;
- private final FormatHolder formatHolder;
- 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;
- private Format outputFormat;
- private VpxDecoder decoder;
- private VpxInputBuffer inputBuffer;
- private VpxOutputBuffer outputBuffer;
- @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;
- private Surface surface;
- private VpxOutputBufferRenderer outputBufferRenderer;
- private int outputMode;
- private boolean waitingForKeys;
-
- private boolean inputStreamEnded;
- private boolean outputStreamEnded;
- private int reportedWidth;
- private int reportedHeight;
-
- private long droppedFrameAccumulationStartTimeMs;
- private int droppedFrames;
- private int consecutiveDroppedFrameCount;
- private int buffersInCodecCount;
- private long lastRenderTimeUs;
- private long outputStreamOffsetUs;
- private VideoFrameMetadataListener frameMetadataListener;
-
- protected DecoderCounters decoderCounters;
+ @Nullable private VpxDecoder decoder;
+ @Nullable private VideoFrameMetadataListener frameMetadataListener;
/**
- * @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 +91,21 @@ 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,
+ @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
this(
- scaleToFit,
allowedJoiningTimeMs,
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* drmSessionManager= */ null,
- /* playClearSamplesWithoutKeys= */ false,
- /* disableLoopFilter= */ false,
- /* useSurfaceYuvOutput= */ false);
+ /* playClearSamplesWithoutKeys= */ 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
@@ -207,445 +120,171 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* 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 useSurfaceYuvOutput Directly output YUV to the Surface via ANativeWindow.
+ * @deprecated Use {@link #LibvpxVideoRenderer(long, Handler, VideoRendererEventListener, int,
+ * int, int, int)}} instead, and pass DRM-related parameters to the {@link MediaSource}
+ * factories.
*/
+ @Deprecated
+ @SuppressWarnings("deprecation")
public LibvpxVideoRenderer(
- boolean scaleToFit,
long allowedJoiningTimeMs,
- Handler eventHandler,
- VideoRendererEventListener eventListener,
+ @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify,
- DrmSessionManager drmSessionManager,
+ @Nullable DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys) {
+ this(
+ allowedJoiningTimeMs,
+ eventHandler,
+ eventListener,
+ maxDroppedFramesToNotify,
+ drmSessionManager,
+ playClearSamplesWithoutKeys,
+ 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 threads Number of threads libvpx will use to decode.
+ * @param numInputBuffers Number of input buffers.
+ * @param numOutputBuffers Number of output buffers.
+ */
+ @SuppressWarnings("deprecation")
+ public LibvpxVideoRenderer(
+ long allowedJoiningTimeMs,
+ @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener,
+ int maxDroppedFramesToNotify,
+ int threads,
+ int numInputBuffers,
+ int numOutputBuffers) {
+ this(
+ allowedJoiningTimeMs,
+ eventHandler,
+ eventListener,
+ maxDroppedFramesToNotify,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ false,
+ threads,
+ numInputBuffers,
+ numOutputBuffers);
+ }
+
+ /**
+ * @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 threads Number of threads libvpx will use to decode.
+ * @param numInputBuffers Number of input buffers.
+ * @param numOutputBuffers Number of output buffers.
+ * @deprecated Use {@link #LibvpxVideoRenderer(long, Handler, VideoRendererEventListener, int,
+ * int, int, int)}} instead, and pass DRM-related parameters to the {@link MediaSource}
+ * factories.
+ */
+ @Deprecated
+ public LibvpxVideoRenderer(
+ long allowedJoiningTimeMs,
+ @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener,
+ int maxDroppedFramesToNotify,
+ @Nullable DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys,
- boolean disableLoopFilter,
- boolean useSurfaceYuvOutput) {
- 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;
- joiningDeadlineMs = C.TIME_UNSET;
- clearReportedVideoSize();
- formatHolder = new FormatHolder();
- formatQueue = new TimedValueQueue<>();
- flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
- eventDispatcher = new EventDispatcher(eventHandler, eventListener);
- outputMode = VpxDecoder.OUTPUT_MODE_NONE;
- decoderReinitializationState = REINITIALIZATION_STATE_NONE;
+ int threads,
+ int numInputBuffers,
+ int numOutputBuffers) {
+ super(
+ allowedJoiningTimeMs,
+ eventHandler,
+ eventListener,
+ maxDroppedFramesToNotify,
+ drmSessionManager,
+ playClearSamplesWithoutKeys);
+ this.threads = threads;
+ this.numInputBuffers = numInputBuffers;
+ this.numOutputBuffers = numOutputBuffers;
}
- // BaseRenderer implementation.
-
@Override
- public int supportsFormat(Format format) {
+ @Capabilities
+ protected int supportsFormatInternal(
+ @Nullable DrmSessionManager drmSessionManager, Format format) {
if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) {
- return FORMAT_UNSUPPORTED_TYPE;
- } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
- return FORMAT_UNSUPPORTED_DRM;
+ return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
}
- return FORMAT_HANDLED | ADAPTIVE_SEAMLESS;
+ boolean drmIsSupported =
+ format.drmInitData == null
+ || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType)
+ || (format.exoMediaCryptoType == null
+ && supportsFormatDrm(drmSessionManager, format.drmInitData));
+ if (!drmIsSupported) {
+ return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
+ }
+ return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
}
@Override
- public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
- if (outputStreamEnded) {
- return;
- }
-
- if (format == null) {
- // We don't have a format yet, so try and read one.
- flagsOnlyBuffer.clear();
- int result = readSource(formatHolder, flagsOnlyBuffer, true);
- if (result == C.RESULT_FORMAT_READ) {
- onInputFormatChanged(formatHolder.format);
- } else if (result == C.RESULT_BUFFER_READ) {
- // End of stream read having not read a format.
- Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
- inputStreamEnded = true;
- outputStreamEnded = true;
- return;
- } else {
- // We still don't have a format and can't make progress without one.
- return;
- }
- }
-
- // If we don't have a decoder yet, we need to instantiate one.
- maybeInitDecoder();
-
- if (decoder != null) {
- try {
- // Rendering loop.
- TraceUtil.beginSection("drainAndFeed");
- while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
- while (feedInputBuffer()) {}
- TraceUtil.endSection();
- } catch (VpxDecoderException e) {
- throw ExoPlaybackException.createForRenderer(e, getIndex());
- }
- decoderCounters.ensureUpdated();
- }
- }
-
-
- @Override
- public boolean isEnded() {
- return outputStreamEnded;
+ protected SimpleDecoder<
+ VideoDecoderInputBuffer,
+ ? extends VideoDecoderOutputBuffer,
+ ? extends VideoDecoderException>
+ createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
+ throws VideoDecoderException {
+ TraceUtil.beginSection("createVpxDecoder");
+ int initialInputBufferSize =
+ format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
+ VpxDecoder decoder =
+ new VpxDecoder(
+ numInputBuffers, numOutputBuffers, initialInputBufferSize, mediaCrypto, threads);
+ this.decoder = decoder;
+ TraceUtil.endSection();
+ return decoder;
}
@Override
- public boolean isReady() {
- if (waitingForKeys) {
- return false;
- }
- if (format != null && (isSourceReady() || outputBuffer != null)
- && (renderedFirstFrame || outputMode == VpxDecoder.OUTPUT_MODE_NONE)) {
- // Ready. If we were joining then we've now joined, so clear the joining deadline.
- joiningDeadlineMs = C.TIME_UNSET;
- return true;
- } else if (joiningDeadlineMs == C.TIME_UNSET) {
- // Not joining.
- return false;
- } else if (SystemClock.elapsedRealtime() < joiningDeadlineMs) {
- // Joining and still within the joining deadline.
- return true;
- } else {
- // The joining deadline has been exceeded. Give up and clear the deadline.
- joiningDeadlineMs = C.TIME_UNSET;
- return false;
+ protected void renderOutputBuffer(
+ VideoDecoderOutputBuffer outputBuffer, long presentationTimeUs, Format outputFormat)
+ throws VideoDecoderException {
+ if (frameMetadataListener != null) {
+ frameMetadataListener.onVideoFrameAboutToBeRendered(
+ presentationTimeUs, System.nanoTime(), outputFormat, /* mediaFormat= */ null);
}
+ super.renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat);
}
@Override
- protected void onEnabled(boolean joining) throws ExoPlaybackException {
- decoderCounters = new DecoderCounters();
- eventDispatcher.enabled(decoderCounters);
- }
-
- @Override
- protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
- inputStreamEnded = false;
- outputStreamEnded = false;
- clearRenderedFirstFrame();
- initialPositionUs = C.TIME_UNSET;
- consecutiveDroppedFrameCount = 0;
- if (decoder != null) {
- flushDecoder();
+ protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
+ throws VpxDecoderException {
+ if (decoder == null) {
+ throw new VpxDecoderException(
+ "Failed to render output buffer to surface: decoder is not initialized.");
}
- if (joining) {
- setJoiningDeadlineMs();
- } else {
- joiningDeadlineMs = C.TIME_UNSET;
- }
- formatQueue.clear();
- }
-
- @Override
- protected void onStarted() {
- droppedFrames = 0;
- droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
- lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
- }
-
- @Override
- protected void onStopped() {
- joiningDeadlineMs = C.TIME_UNSET;
- maybeNotifyDroppedFrames();
- }
-
- @Override
- protected void onDisabled() {
- format = null;
- waitingForKeys = false;
- clearReportedVideoSize();
- clearRenderedFirstFrame();
- try {
- setSourceDrmSession(null);
- releaseDecoder();
- } finally {
- eventDispatcher.disabled(decoderCounters);
- }
- }
-
- @Override
- protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
- outputStreamOffsetUs = offsetUs;
- super.onStreamChanged(formats, offsetUs);
- }
-
- /**
- * Called when a decoder has been created and configured.
- *
- * The default implementation is a no-op.
- *
- * @param name The name of the decoder that was initialized.
- * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
- * finished.
- * @param initializationDurationMs The time taken to initialize the decoder, in milliseconds.
- */
- @CallSuper
- protected void onDecoderInitialized(
- String name, long initializedTimestampMs, long initializationDurationMs) {
- eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);
- }
-
- /**
- * Flushes the decoder.
- *
- * @throws ExoPlaybackException If an error occurs reinitializing a decoder.
- */
- @CallSuper
- protected void flushDecoder() throws ExoPlaybackException {
- waitingForKeys = false;
- buffersInCodecCount = 0;
- if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
- releaseDecoder();
- maybeInitDecoder();
- } else {
- inputBuffer = null;
- if (outputBuffer != null) {
- outputBuffer.release();
- outputBuffer = null;
- }
- decoder.flush();
- decoderReceivedBuffers = false;
- }
- }
-
- /** Releases the decoder. */
- @CallSuper
- protected void releaseDecoder() {
- inputBuffer = null;
- outputBuffer = null;
- 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);
- }
- }
-
- /**
- * Called when a new format is read from the upstream source.
- *
- * @param newFormat The new format.
- * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder.
- */
- @CallSuper
- protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
- Format oldFormat = format;
- format = newFormat;
- pendingFormat = newFormat;
-
- boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null
- : oldFormat.drmInitData);
- if (drmInitDataChanged) {
- if (format.drmInitData != null) {
- if (drmSessionManager == null) {
- throw ExoPlaybackException.createForRenderer(
- new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
- }
- 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 {
- setSourceDrmSession(null);
- }
- }
-
- 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;
- } else {
- // There aren't any final output buffers, so release the decoder immediately.
- releaseDecoder();
- maybeInitDecoder();
- }
- }
-
- eventDispatcher.inputFormatChanged(format);
- }
-
- /**
- * Called immediately before an input buffer is queued into the decoder.
- *
- * The default implementation is a no-op.
- *
- * @param buffer The buffer that will be queued.
- */
- protected void onQueueInputBuffer(VpxInputBuffer buffer) {
- // Do nothing.
- }
-
- /**
- * Called when an output buffer is successfully processed.
- *
- * @param presentationTimeUs The timestamp associated with the output buffer.
- */
- @CallSuper
- protected void onProcessedOutputBuffer(long presentationTimeUs) {
- buffersInCodecCount--;
- }
-
- /**
- * Returns whether the buffer being processed should be dropped.
- *
- * @param earlyUs The time until the buffer should be presented in microseconds. A negative value
- * indicates that the buffer is late.
- * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
- * measured at the start of the current iteration of the rendering loop.
- */
- protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) {
- return isBufferLate(earlyUs);
- }
-
- /**
- * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after
- * the current playback position, if possible.
- *
- * @param earlyUs The time until the current buffer should be presented in microseconds. A
- * negative value indicates that the buffer is late.
- * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
- * measured at the start of the current iteration of the rendering loop.
- */
- protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) {
- return isBufferVeryLate(earlyUs);
- }
-
- /**
- * Returns whether to force rendering an output buffer.
- *
- * @param earlyUs The time until the current buffer should be presented in microseconds. A
- * negative value indicates that the buffer is late.
- * @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in
- * microseconds.
- * @return Returns whether to force rendering an output buffer.
- */
- protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) {
- return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000;
- }
-
- /**
- * Skips the specified output buffer and releases it.
- *
- * @param outputBuffer The output buffer to skip.
- */
- protected void skipOutputBuffer(VpxOutputBuffer outputBuffer) {
- decoderCounters.skippedOutputBufferCount++;
+ decoder.renderToSurface(outputBuffer, surface);
outputBuffer.release();
}
- /**
- * Drops the specified output buffer and releases it.
- *
- * @param outputBuffer The output buffer to drop.
- */
- protected void dropOutputBuffer(VpxOutputBuffer outputBuffer) {
- updateDroppedBufferCounters(1);
- outputBuffer.release();
- }
-
- /**
- * Renders the specified output buffer.
- *
- *
The implementation of this method takes ownership of the output buffer and is responsible
- * for calling {@link VpxOutputBuffer#release()} either immediately or in the future.
- *
- * @param outputBuffer The buffer to render.
- */
- 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) {
- dropOutputBuffer(outputBuffer);
- } else {
- maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
- if (renderRgb) {
- renderRgbFrame(outputBuffer, scaleToFit);
- outputBuffer.release();
- } else if (renderYuv) {
- outputBufferRenderer.setOutputBuffer(outputBuffer);
- // The renderer will release the buffer.
- } else { // renderSurface
- decoder.renderToSurface(outputBuffer, surface);
- outputBuffer.release();
- }
- consecutiveDroppedFrameCount = 0;
- decoderCounters.renderedOutputBufferCount++;
- maybeNotifyRenderedFirstFrame();
- }
- }
-
- /**
- * Drops frames from the current output buffer to the next keyframe at or before the playback
- * position. If no such keyframe exists, as the playback position is inside the same group of
- * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise.
- *
- * @param positionUs The current playback position, in microseconds.
- * @return Whether any buffers were dropped.
- * @throws ExoPlaybackException If an error occurs flushing the decoder.
- */
- protected boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException {
- int droppedSourceBufferCount = skipSource(positionUs);
- if (droppedSourceBufferCount == 0) {
- return false;
- }
- decoderCounters.droppedToKeyframeCount++;
- // We dropped some buffers to catch up, so update the decoder counters and flush the decoder,
- // which releases all pending buffers buffers including the current output buffer.
- updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount);
- flushDecoder();
- return true;
- }
-
- /**
- * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were
- * dropped.
- *
- * @param droppedBufferCount The number of additional dropped buffers.
- */
- protected void updateDroppedBufferCounters(int droppedBufferCount) {
- decoderCounters.droppedBufferCount += droppedBufferCount;
- droppedFrames += droppedBufferCount;
- consecutiveDroppedFrameCount += droppedBufferCount;
- decoderCounters.maxConsecutiveDroppedBufferCount =
- Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount);
- if (maxDroppedFramesToNotify > 0 && droppedFrames >= maxDroppedFramesToNotify) {
- maybeNotifyDroppedFrames();
+ @Override
+ protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
+ if (decoder != null) {
+ decoder.setOutputMode(outputMode);
}
}
@@ -654,368 +293,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
@Override
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
if (messageType == C.MSG_SET_SURFACE) {
- setOutput((Surface) message, null);
- } else if (messageType == MSG_SET_OUTPUT_BUFFER_RENDERER) {
- setOutput(null, (VpxOutputBufferRenderer) message);
+ setOutputSurface((Surface) message);
+ } else if (messageType == C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) {
+ setOutputBufferRenderer((VideoDecoderOutputBufferRenderer) message);
} else if (messageType == C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) {
frameMetadataListener = (VideoFrameMetadataListener) message;
} else {
super.handleMessage(messageType, message);
}
}
-
- // Internal methods.
-
- private void setOutput(
- @Nullable Surface surface, @Nullable VpxOutputBufferRenderer outputBufferRenderer) {
- // At most one output may be non-null. Both may be null if the output is being cleared.
- Assertions.checkState(surface == null || outputBufferRenderer == null);
- if (this.surface != surface || this.outputBufferRenderer != outputBufferRenderer) {
- // The output has changed.
- this.surface = surface;
- this.outputBufferRenderer = outputBufferRenderer;
- if (surface != null) {
- outputMode =
- useSurfaceYuvOutput ? VpxDecoder.OUTPUT_MODE_SURFACE_YUV : VpxDecoder.OUTPUT_MODE_RGB;
- } else {
- outputMode =
- outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE;
- }
- if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) {
- if (decoder != null) {
- decoder.setOutputMode(outputMode);
- }
- // If we know the video size, report it again immediately.
- maybeRenotifyVideoSizeChanged();
- // We haven't rendered to the new output yet.
- clearRenderedFirstFrame();
- if (getState() == STATE_STARTED) {
- setJoiningDeadlineMs();
- }
- } else {
- // The output has been removed. We leave the outputMode of the underlying decoder unchanged
- // in anticipation that a subsequent output will likely be of the same type.
- clearReportedVideoSize();
- clearRenderedFirstFrame();
- }
- } else if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) {
- // The output is unchanged and non-null. If we know the video size and/or have already
- // rendered to the output, report these again immediately.
- maybeRenotifyVideoSizeChanged();
- maybeRenotifyRenderedFirstFrame();
- }
- }
-
- private void maybeInitDecoder() throws ExoPlaybackException {
- if (decoder != null) {
- return;
- }
-
- setDecoderDrmSession(sourceDrmSession);
-
- ExoMediaCrypto mediaCrypto = null;
- if (decoderDrmSession != null) {
- mediaCrypto = decoderDrmSession.getMediaCrypto();
- if (mediaCrypto == null) {
- 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.
- } else {
- // The drm session isn't open yet.
- return;
- }
- }
- }
-
- try {
- long decoderInitializingTimestamp = SystemClock.elapsedRealtime();
- TraceUtil.beginSection("createVpxDecoder");
- int initialInputBufferSize =
- format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
- decoder =
- new VpxDecoder(
- NUM_INPUT_BUFFERS,
- NUM_OUTPUT_BUFFERS,
- initialInputBufferSize,
- mediaCrypto,
- disableLoopFilter,
- useSurfaceYuvOutput);
- decoder.setOutputMode(outputMode);
- TraceUtil.endSection();
- long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
- onDecoderInitialized(
- decoder.getName(),
- decoderInitializedTimestamp,
- decoderInitializedTimestamp - decoderInitializingTimestamp);
- decoderCounters.decoderInitCount++;
- } catch (VpxDecoderException e) {
- throw ExoPlaybackException.createForRenderer(e, getIndex());
- }
- }
-
- private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException {
- if (decoder == null
- || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
- || inputStreamEnded) {
- // We need to reinitialize the decoder or the input stream has ended.
- return false;
- }
-
- if (inputBuffer == null) {
- inputBuffer = decoder.dequeueInputBuffer();
- if (inputBuffer == null) {
- return false;
- }
- }
-
- if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
- inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
- decoder.queueInputBuffer(inputBuffer);
- inputBuffer = null;
- decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
- return false;
- }
-
- int result;
- if (waitingForKeys) {
- // We've already read an encrypted sample into buffer, and are waiting for keys.
- result = C.RESULT_BUFFER_READ;
- } else {
- result = readSource(formatHolder, inputBuffer, false);
- }
-
- if (result == C.RESULT_NOTHING_READ) {
- return false;
- }
- if (result == C.RESULT_FORMAT_READ) {
- onInputFormatChanged(formatHolder.format);
- return true;
- }
- if (inputBuffer.isEndOfStream()) {
- inputStreamEnded = true;
- decoder.queueInputBuffer(inputBuffer);
- inputBuffer = null;
- return false;
- }
- boolean bufferEncrypted = inputBuffer.isEncrypted();
- waitingForKeys = shouldWaitForKeys(bufferEncrypted);
- if (waitingForKeys) {
- return false;
- }
- if (pendingFormat != null) {
- formatQueue.add(inputBuffer.timeUs, pendingFormat);
- pendingFormat = null;
- }
- inputBuffer.flip();
- inputBuffer.colorInfo = formatHolder.format.colorInfo;
- onQueueInputBuffer(inputBuffer);
- decoder.queueInputBuffer(inputBuffer);
- buffersInCodecCount++;
- decoderReceivedBuffers = true;
- decoderCounters.inputBufferCount++;
- inputBuffer = null;
- return true;
- }
-
- /**
- * Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link
- * #processOutputBuffer(long, long)}.
- *
- * @param positionUs The player's current position.
- * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
- * measured at the start of the current iteration of the rendering loop.
- * @return Whether it may be possible to drain more output data.
- * @throws ExoPlaybackException If an error occurs draining the output buffer.
- */
- private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
- throws ExoPlaybackException, VpxDecoderException {
- if (outputBuffer == null) {
- outputBuffer = decoder.dequeueOutputBuffer();
- if (outputBuffer == null) {
- return false;
- }
- decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
- buffersInCodecCount -= outputBuffer.skippedOutputBufferCount;
- }
-
- if (outputBuffer.isEndOfStream()) {
- if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
- // We're waiting to re-initialize the decoder, and have now processed all final buffers.
- releaseDecoder();
- maybeInitDecoder();
- } else {
- outputBuffer.release();
- outputBuffer = null;
- outputStreamEnded = true;
- }
- return false;
- }
-
- boolean processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs);
- if (processedOutputBuffer) {
- onProcessedOutputBuffer(outputBuffer.timeUs);
- outputBuffer = null;
- }
- return processedOutputBuffer;
- }
-
- /**
- * Processes {@link #outputBuffer} by rendering it, skipping it or doing nothing, and returns
- * whether it may be possible to process another output buffer.
- *
- * @param positionUs The player's current position.
- * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
- * measured at the start of the current iteration of the rendering loop.
- * @return Whether it may be possible to drain another output buffer.
- * @throws ExoPlaybackException If an error occurs processing the output buffer.
- */
- private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs)
- throws ExoPlaybackException, VpxDecoderException {
- if (initialPositionUs == C.TIME_UNSET) {
- initialPositionUs = positionUs;
- }
-
- long earlyUs = outputBuffer.timeUs - positionUs;
- if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
- // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
- if (isBufferLate(earlyUs)) {
- skipOutputBuffer(outputBuffer);
- return true;
- }
- return false;
- }
-
- long presentationTimeUs = outputBuffer.timeUs - outputStreamOffsetUs;
- Format format = formatQueue.pollFloor(presentationTimeUs);
- if (format != null) {
- outputFormat = format;
- }
-
- long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
- boolean isStarted = getState() == STATE_STARTED;
- if (!renderedFirstFrame
- || (isStarted
- && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {
- if (frameMetadataListener != null) {
- frameMetadataListener.onVideoFrameAboutToBeRendered(
- presentationTimeUs, System.nanoTime(), outputFormat);
- }
- renderOutputBuffer(outputBuffer);
- return true;
- }
-
- if (!isStarted || positionUs == initialPositionUs) {
- return false;
- }
-
- if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs)
- && maybeDropBuffersToKeyframe(positionUs)) {
- return false;
- } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
- dropOutputBuffer(outputBuffer);
- return true;
- }
-
- if (earlyUs < 30000) {
- if (frameMetadataListener != null) {
- frameMetadataListener.onVideoFrameAboutToBeRendered(
- presentationTimeUs, System.nanoTime(), outputFormat);
- }
- renderOutputBuffer(outputBuffer);
- return true;
- }
-
- return false;
- }
-
- private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
- return false;
- }
- @DrmSession.State int drmSessionState = decoderDrmSession.getState();
- if (drmSessionState == DrmSession.STATE_ERROR) {
- 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;
- }
-
- private void clearRenderedFirstFrame() {
- renderedFirstFrame = false;
- }
-
- private void maybeNotifyRenderedFirstFrame() {
- if (!renderedFirstFrame) {
- renderedFirstFrame = true;
- eventDispatcher.renderedFirstFrame(surface);
- }
- }
-
- private void maybeRenotifyRenderedFirstFrame() {
- if (renderedFirstFrame) {
- eventDispatcher.renderedFirstFrame(surface);
- }
- }
-
- private void clearReportedVideoSize() {
- reportedWidth = Format.NO_VALUE;
- reportedHeight = Format.NO_VALUE;
- }
-
- private void maybeNotifyVideoSizeChanged(int width, int height) {
- if (reportedWidth != width || reportedHeight != height) {
- reportedWidth = width;
- reportedHeight = height;
- eventDispatcher.videoSizeChanged(width, height, 0, 1);
- }
- }
-
- private void maybeRenotifyVideoSizeChanged() {
- if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) {
- eventDispatcher.videoSizeChanged(reportedWidth, reportedHeight, 0, 1);
- }
- }
-
- private void maybeNotifyDroppedFrames() {
- if (droppedFrames > 0) {
- long now = SystemClock.elapsedRealtime();
- long elapsedMs = now - droppedFrameAccumulationStartTimeMs;
- eventDispatcher.droppedFrames(droppedFrames, elapsedMs);
- droppedFrames = 0;
- droppedFrameAccumulationStartTimeMs = now;
- }
- }
-
- private static boolean isBufferLate(long earlyUs) {
- // Class a buffer as late if it should have been presented more than 30 ms ago.
- return earlyUs < -30000;
- }
-
- private static boolean isBufferVeryLate(long earlyUs) {
- // Class a buffer as very late if it should have been presented more than 500 ms ago.
- return earlyUs < -500000;
- }
-
}
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..98a26727ee 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
@@ -16,32 +16,34 @@
package com.google.android.exoplayer2.ext.vp9;
import android.view.Surface;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DecryptionException;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
+import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
import java.nio.ByteBuffer;
-/**
- * Vpx decoder.
- */
-/* package */ final class VpxDecoder extends
- SimpleDecoder {
-
- 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;
+/** Vpx decoder. */
+/* package */ final class VpxDecoder
+ extends SimpleDecoder {
+ // These constants should match the codes returned from vpxDecode and vpxSecureDecode functions in
+ // https://github.com/google/ExoPlayer/blob/release-v2/extensions/vp9/src/main/jni/vpx_jni.cc.
private static final int NO_ERROR = 0;
- private static final int DECODE_ERROR = 1;
- private static final int DRM_ERROR = 2;
+ private static final int DECODE_ERROR = -1;
+ private static final int DRM_ERROR = -2;
- private final ExoMediaCrypto exoMediaCrypto;
+ @Nullable private final ExoMediaCrypto exoMediaCrypto;
private final long vpxDecContext;
- private volatile int outputMode;
+ @Nullable private ByteBuffer lastSupplementalData;
+
+ @C.VideoOutputMode private volatile int outputMode;
/**
* Creates a VP9 decoder.
@@ -51,19 +53,19 @@ import java.nio.ByteBuffer;
* @param initialInputBufferSize The initial size of each input buffer.
* @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 threads Number of threads libvpx will use to decode.
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
*/
public VpxDecoder(
int numInputBuffers,
int numOutputBuffers,
int initialInputBufferSize,
- ExoMediaCrypto exoMediaCrypto,
- boolean disableLoopFilter,
- boolean enableSurfaceYuvOutputMode)
+ @Nullable ExoMediaCrypto exoMediaCrypto,
+ int threads)
throws VpxDecoderException {
- super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
+ super(
+ new VideoDecoderInputBuffer[numInputBuffers],
+ new VideoDecoderOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) {
throw new VpxDecoderException("Failed to load decoder native libraries.");
}
@@ -71,7 +73,8 @@ 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= */ false, /* enableRowMultiThreadMode= */ false, threads);
if (vpxDecContext == 0) {
throw new VpxDecoderException("Failed to initialize decoder");
}
@@ -86,28 +89,27 @@ 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.
*/
- public void setOutputMode(int outputMode) {
+ public void setOutputMode(@C.VideoOutputMode int outputMode) {
this.outputMode = outputMode;
}
@Override
- protected VpxInputBuffer createInputBuffer() {
- return new VpxInputBuffer();
+ protected VideoDecoderInputBuffer createInputBuffer() {
+ return new VideoDecoderInputBuffer();
}
@Override
- protected VpxOutputBuffer createOutputBuffer() {
- return new VpxOutputBuffer(this);
+ protected VideoDecoderOutputBuffer createOutputBuffer() {
+ return new VideoDecoderOutputBuffer(this::releaseOutputBuffer);
}
@Override
- protected void releaseOutputBuffer(VpxOutputBuffer buffer) {
+ protected void releaseOutputBuffer(VideoDecoderOutputBuffer buffer) {
// Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
// require a call to vpxReleaseFrame.
- if (outputMode == OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
+ if (outputMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
vpxReleaseFrame(vpxDecContext, buffer);
}
super.releaseOutputBuffer(buffer);
@@ -119,9 +121,15 @@ import java.nio.ByteBuffer;
}
@Override
- protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
- boolean reset) {
- ByteBuffer inputData = inputBuffer.data;
+ @Nullable
+ protected VpxDecoderException decode(
+ VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) {
+ if (reset && lastSupplementalData != null) {
+ // Don't propagate supplemental data across calls to flush the decoder.
+ lastSupplementalData.clear();
+ }
+
+ ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
int inputSize = inputData.limit();
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
final long result = inputBuffer.isEncrypted()
@@ -140,8 +148,22 @@ import java.nio.ByteBuffer;
}
}
+ if (inputBuffer.hasSupplementalData()) {
+ ByteBuffer supplementalData = Assertions.checkNotNull(inputBuffer.supplementalData);
+ int size = supplementalData.remaining();
+ if (size > 0) {
+ if (lastSupplementalData == null || lastSupplementalData.capacity() < size) {
+ lastSupplementalData = ByteBuffer.allocate(size);
+ } else {
+ lastSupplementalData.clear();
+ }
+ lastSupplementalData.put(supplementalData);
+ lastSupplementalData.flip();
+ }
+ }
+
if (!inputBuffer.isDecodeOnly()) {
- outputBuffer.init(inputBuffer.timeUs, outputMode);
+ outputBuffer.init(inputBuffer.timeUs, outputMode, lastSupplementalData);
int getFrameResult = vpxGetFrame(vpxDecContext, outputBuffer);
if (getFrameResult == 1) {
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
@@ -156,11 +178,12 @@ import java.nio.ByteBuffer;
@Override
public void release() {
super.release();
+ lastSupplementalData = null;
vpxClose(vpxDecContext);
}
/** Renders the outputBuffer to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. */
- public void renderToSurface(VpxOutputBuffer outputBuffer, Surface surface)
+ public void renderToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
throws VpxDecoderException {
int getFrameResult = vpxRenderFrame(vpxDecContext, surface, outputBuffer);
if (getFrameResult == -1) {
@@ -168,26 +191,38 @@ 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);
- private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
- ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
- int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
- private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
+
+ private native long vpxSecureDecode(
+ long context,
+ ByteBuffer encoded,
+ int length,
+ @Nullable ExoMediaCrypto mediaCrypto,
+ int inputMode,
+ byte[] key,
+ byte[] iv,
+ int numSubSamples,
+ int[] numBytesOfClearData,
+ int[] numBytesOfEncryptedData);
+
+ private native int vpxGetFrame(long context, VideoDecoderOutputBuffer outputBuffer);
/**
* Renders the frame to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called
* if {@link #vpxInit} was called with {@code enableBufferManager = true}.
*/
- private native int vpxRenderFrame(long context, Surface surface, VpxOutputBuffer outputBuffer);
+ private native int vpxRenderFrame(
+ long context, Surface surface, VideoDecoderOutputBuffer outputBuffer);
/**
* Releases the frame. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called if {@link
* #vpxInit} was called with {@code enableBufferManager = true}.
*/
- private native int vpxReleaseFrame(long context, VpxOutputBuffer outputBuffer);
+ private native int vpxReleaseFrame(long context, VideoDecoderOutputBuffer outputBuffer);
private native int vpxGetErrorCode(long context);
private native String vpxGetErrorMessage(long context);
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
index 8de14629d3..b2da9a7ff8 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
@@ -15,8 +15,10 @@
*/
package com.google.android.exoplayer2.ext.vp9;
+import com.google.android.exoplayer2.video.VideoDecoderException;
+
/** Thrown when a libvpx decoder error occurs. */
-public final class VpxDecoderException extends Exception {
+public final class VpxDecoderException extends VideoDecoderException {
/* package */ VpxDecoderException(String message) {
super(message);
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 5a65fc56ff..e620332fc8 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
@@ -15,8 +15,11 @@
*/
package com.google.android.exoplayer2.ext.vp9;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.LibraryLoader;
+import com.google.android.exoplayer2.util.Util;
/**
* Configures and queries the underlying native library.
@@ -28,6 +31,7 @@ public final class VpxLibrary {
}
private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxV2JNI");
+ @Nullable private static Class extends ExoMediaCrypto> exoMediaCryptoType;
private VpxLibrary() {}
@@ -36,10 +40,14 @@ public final class VpxLibrary {
* it must do so before calling any other method defined by this class, and before instantiating a
* {@link LibvpxVideoRenderer} instance.
*
+ * @param exoMediaCryptoType The {@link ExoMediaCrypto} type required for decoding protected
+ * content.
* @param libraries The names of the Vpx native libraries.
*/
- public static void setLibraries(String... libraries) {
+ public static void setLibraries(
+ Class extends ExoMediaCrypto> exoMediaCryptoType, String... libraries) {
LOADER.setLibraries(libraries);
+ VpxLibrary.exoMediaCryptoType = exoMediaCryptoType;
}
/**
@@ -49,9 +57,8 @@ public final class VpxLibrary {
return LOADER.isAvailable();
}
- /**
- * Returns the version of the underlying library if available, or null otherwise.
- */
+ /** Returns the version of the underlying library if available, or null otherwise. */
+ @Nullable
public static String getVersion() {
return isAvailable() ? vpxGetVersion() : null;
}
@@ -60,6 +67,7 @@ public final class VpxLibrary {
* Returns the configuration string with which the underlying library was built if available, or
* null otherwise.
*/
+ @Nullable
public static String getBuildConfig() {
return isAvailable() ? vpxGetBuildConfig() : null;
}
@@ -74,6 +82,15 @@ public final class VpxLibrary {
return indexHbd >= 0;
}
+ /**
+ * Returns whether the given {@link ExoMediaCrypto} type matches the one required for decoding
+ * protected content.
+ */
+ public static boolean matchesExpectedExoMediaCryptoType(
+ @Nullable Class extends ExoMediaCrypto> exoMediaCryptoType) {
+ return Util.areEqual(VpxLibrary.exoMediaCryptoType, exoMediaCryptoType);
+ }
+
private static native String vpxGetVersion();
private static native String vpxGetBuildConfig();
public static native boolean vpxIsSecureDecodeSupported();
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 725d94819b..1c434032d0 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
@@ -15,132 +15,24 @@
*/
package com.google.android.exoplayer2.ext.vp9;
-import com.google.android.exoplayer2.decoder.OutputBuffer;
-import com.google.android.exoplayer2.video.ColorInfo;
-import java.nio.ByteBuffer;
+import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
-/** 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;
- public static final int COLORSPACE_BT709 = 2;
- public static final int COLORSPACE_BT2020 = 3;
-
- private final VpxDecoder owner;
- /** Decoder private data. */
- public int decoderPrivate;
-
- public int mode;
- /**
- * RGB buffer for RGB mode.
- */
- public ByteBuffer data;
- public int width;
- public int height;
- public ColorInfo colorInfo;
+// TODO(b/139174707): Delete this class once binaries in WVVp9OpusPlaybackTest are updated to depend
+// on VideoDecoderOutputBuffer. Also mark VideoDecoderOutputBuffer as final.
+/**
+ * Video output buffer, populated by {@link VpxDecoder}.
+ *
+ * @deprecated Use {@link VideoDecoderOutputBuffer} instead.
+ */
+@Deprecated
+public final class VpxOutputBuffer extends VideoDecoderOutputBuffer {
/**
- * YUV planes for YUV mode.
- */
- public ByteBuffer[] yuvPlanes;
- public int[] yuvStrides;
- public int colorspace;
-
- public VpxOutputBuffer(VpxDecoder owner) {
- this.owner = owner;
- }
-
- @Override
- public void release() {
- owner.releaseOutputBuffer(this);
- }
-
- /**
- * Initializes the buffer.
+ * Creates VpxOutputBuffer.
*
- * @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 owner Buffer owner.
*/
- public void init(long timeUs, int mode) {
- this.timeUs = timeUs;
- this.mode = mode;
+ public VpxOutputBuffer(VideoDecoderOutputBuffer.Owner owner) {
+ super(owner);
}
-
- /**
- * 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) {
- this.width = width;
- this.height = height;
- this.colorspace = colorspace;
- int uvHeight = (int) (((long) height + 1) / 2);
- if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) {
- return false;
- }
- int yLength = yStride * height;
- int uvLength = uvStride * uvHeight;
- int minimumYuvSize = yLength + (uvLength * 2);
- if (!isSafeToMultiply(uvLength, 2) || minimumYuvSize < yLength) {
- return false;
- }
- initData(minimumYuvSize);
-
- if (yuvPlanes == null) {
- yuvPlanes = new ByteBuffer[3];
- }
- // Rewrapping has to be done on every frame since the stride might have changed.
- yuvPlanes[0] = data.slice();
- yuvPlanes[0].limit(yLength);
- data.position(yLength);
- yuvPlanes[1] = data.slice();
- yuvPlanes[1].limit(uvLength);
- data.position(yLength + uvLength);
- yuvPlanes[2] = data.slice();
- yuvPlanes[2].limit(uvLength);
- if (yuvStrides == null) {
- yuvStrides = new int[3];
- }
- yuvStrides[0] = yStride;
- yuvStrides[1] = uvStride;
- yuvStrides[2] = uvStride;
- return true;
- }
-
- private void initData(int size) {
- if (data == null || data.capacity() < size) {
- data = ByteBuffer.allocateDirect(size);
- } else {
- data.position(0);
- data.limit(size);
- }
- }
-
- /**
- * Ensures that the result of multiplying individual numbers can fit into the size limit of an
- * integer.
- */
- private boolean isSafeToMultiply(int a, int b) {
- return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b);
- }
-
}
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
deleted file mode 100644
index d82f5a6071..0000000000
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.opengl.GLES20;
-import android.opengl.GLSurfaceView;
-import com.google.android.exoplayer2.util.GlUtil;
-import java.nio.FloatBuffer;
-import java.util.concurrent.atomic.AtomicReference;
-import javax.microedition.khronos.egl.EGLConfig;
-import javax.microedition.khronos.opengles.GL10;
-
-/**
- * GLSurfaceView.Renderer implementation that can render YUV Frames returned by libvpx after
- * decoding. It does the YUV to RGB color conversion in the Fragment Shader.
- */
-/* package */ class VpxRenderer implements GLSurfaceView.Renderer {
-
- private static final float[] kColorConversion601 = {
- 1.164f, 1.164f, 1.164f,
- 0.0f, -0.392f, 2.017f,
- 1.596f, -0.813f, 0.0f,
- };
-
- private static final float[] kColorConversion709 = {
- 1.164f, 1.164f, 1.164f,
- 0.0f, -0.213f, 2.112f,
- 1.793f, -0.533f, 0.0f,
- };
-
- private static final float[] kColorConversion2020 = {
- 1.168f, 1.168f, 1.168f,
- 0.0f, -0.188f, 2.148f,
- 1.683f, -0.652f, 0.0f,
- };
-
- private static final String VERTEX_SHADER =
- "varying vec2 interp_tc;\n"
- + "attribute vec4 in_pos;\n"
- + "attribute vec2 in_tc;\n"
- + "void main() {\n"
- + " gl_Position = in_pos;\n"
- + " interp_tc = in_tc;\n"
- + "}\n";
- private static final String[] TEXTURE_UNIFORMS = {"y_tex", "u_tex", "v_tex"};
- private static final String FRAGMENT_SHADER =
- "precision mediump float;\n"
- + "varying vec2 interp_tc;\n"
- + "uniform sampler2D y_tex;\n"
- + "uniform sampler2D u_tex;\n"
- + "uniform sampler2D v_tex;\n"
- + "uniform mat3 mColorConversion;\n"
- + "void main() {\n"
- + " vec3 yuv;\n"
- + " yuv.x = texture2D(y_tex, interp_tc).r - 0.0625;\n"
- + " yuv.y = texture2D(u_tex, interp_tc).r - 0.5;\n"
- + " yuv.z = texture2D(v_tex, interp_tc).r - 0.5;\n"
- + " gl_FragColor = vec4(mColorConversion * yuv, 1.0);\n"
- + "}\n";
-
- 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;
-
- // Kept in a field rather than a local variable so that it doesn't get garbage collected before
- // glDrawArrays uses it.
- @SuppressWarnings("FieldCanBeLocal")
- private FloatBuffer textureCoords;
- private int program;
- private int texLocation;
- private int colorMatrixLocation;
- private int previousWidth;
- private int previousStride;
-
- private VpxOutputBuffer renderedOutputBuffer; // Accessed only from the GL thread.
-
- public VpxRenderer() {
- previousWidth = -1;
- previousStride = -1;
- pendingOutputBufferReference = new AtomicReference<>();
- }
-
- /**
- * Set a frame to be rendered. This should be followed by a call to
- * VpxVideoSurfaceView.requestRender() to actually render the frame.
- *
- * @param outputBuffer OutputBuffer containing the YUV Frame to be rendered
- */
- public void setFrame(VpxOutputBuffer outputBuffer) {
- VpxOutputBuffer oldPendingOutputBuffer = pendingOutputBufferReference.getAndSet(outputBuffer);
- if (oldPendingOutputBuffer != null) {
- // The old pending output buffer will never be used for rendering, so release it now.
- oldPendingOutputBuffer.release();
- }
- }
-
- @Override
- public void onSurfaceCreated(GL10 unused, EGLConfig config) {
- program = GlUtil.compileProgram(VERTEX_SHADER, FRAGMENT_SHADER);
- GLES20.glUseProgram(program);
- int posLocation = GLES20.glGetAttribLocation(program, "in_pos");
- GLES20.glEnableVertexAttribArray(posLocation);
- GLES20.glVertexAttribPointer(
- posLocation, 2, GLES20.GL_FLOAT, false, 0, TEXTURE_VERTICES);
- texLocation = GLES20.glGetAttribLocation(program, "in_tc");
- GLES20.glEnableVertexAttribArray(texLocation);
- GlUtil.checkGlError();
- colorMatrixLocation = GLES20.glGetUniformLocation(program, "mColorConversion");
- GlUtil.checkGlError();
- setupTextures();
- GlUtil.checkGlError();
- }
-
- @Override
- public void onSurfaceChanged(GL10 unused, int width, int height) {
- GLES20.glViewport(0, 0, width, height);
- }
-
- @Override
- public void onDrawFrame(GL10 unused) {
- VpxOutputBuffer pendingOutputBuffer = pendingOutputBufferReference.getAndSet(null);
- if (pendingOutputBuffer == null && renderedOutputBuffer == null) {
- // There is no output buffer to render at the moment.
- return;
- }
- if (pendingOutputBuffer != null) {
- if (renderedOutputBuffer != null) {
- renderedOutputBuffer.release();
- }
- renderedOutputBuffer = pendingOutputBuffer;
- }
- VpxOutputBuffer outputBuffer = renderedOutputBuffer;
- // Set color matrix. Assume BT709 if the color space is unknown.
- float[] colorConversion = kColorConversion709;
- switch (outputBuffer.colorspace) {
- case VpxOutputBuffer.COLORSPACE_BT601:
- colorConversion = kColorConversion601;
- break;
- case VpxOutputBuffer.COLORSPACE_BT2020:
- colorConversion = kColorConversion2020;
- break;
- case VpxOutputBuffer.COLORSPACE_BT709:
- default:
- break; // Do nothing
- }
- GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0);
-
- for (int i = 0; i < 3; i++) {
- int h = (i == 0) ? outputBuffer.height : (outputBuffer.height + 1) / 2;
- GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
- GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]);
- GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
- GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
- outputBuffer.yuvStrides[i], h, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE,
- outputBuffer.yuvPlanes[i]);
- }
- // Set cropping of stride if either width or stride has changed.
- if (previousWidth != outputBuffer.width || previousStride != outputBuffer.yuvStrides[0]) {
- 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 =
- 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;
- previousStride = outputBuffer.yuvStrides[0];
- }
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
- GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
- GlUtil.checkGlError();
- }
-
- private void setupTextures() {
- GLES20.glGenTextures(3, yuvTextures, 0);
- for (int i = 0; i < 3; i++) {
- GLES20.glUniform1i(GLES20.glGetUniformLocation(program, TEXTURE_UNIFORMS[i]), i);
- GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
- GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]);
- GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
- GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
- GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
- GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
- GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
- GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
- GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
- GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
- }
- GlUtil.checkGlError();
- }
-}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java
deleted file mode 100644
index 8c765952e7..0000000000
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.content.Context;
-import android.opengl.GLSurfaceView;
-import android.util.AttributeSet;
-
-/**
- * A GLSurfaceView extension that scales itself to the given aspect ratio.
- */
-public class VpxVideoSurfaceView extends GLSurfaceView implements VpxOutputBufferRenderer {
-
- private final VpxRenderer renderer;
-
- public VpxVideoSurfaceView(Context context) {
- this(context, null);
- }
-
- public VpxVideoSurfaceView(Context context, AttributeSet attrs) {
- super(context, attrs);
- renderer = new VpxRenderer();
- setPreserveEGLContextOnPause(true);
- setEGLContextClientVersion(2);
- setRenderer(renderer);
- setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
- }
-
- @Override
- public void setOutputBuffer(VpxOutputBuffer outputBuffer) {
- renderer.setFrame(outputBuffer);
- requestRender();
- }
-
-}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java
new file mode 100644
index 0000000000..b8725607a5
--- /dev/null
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.vp9;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/vp9/src/main/jni/Android.mk b/extensions/vp9/src/main/jni/Android.mk
index fdcdc57b41..cb7571a1b0 100644
--- a/extensions/vp9/src/main/jni/Android.mk
+++ b/extensions/vp9/src/main/jni/Android.mk
@@ -17,12 +17,6 @@
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)
@@ -37,7 +31,7 @@ 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/Application.mk b/extensions/vp9/src/main/jni/Application.mk
index 59bf5f8f87..ed28f07acb 100644
--- a/extensions/vp9/src/main/jni/Application.mk
+++ b/extensions/vp9/src/main/jni/Application.mk
@@ -15,6 +15,6 @@
#
APP_OPTIM := release
-APP_STL := gnustl_static
+APP_STL := c++_static
APP_CPPFLAGS := -frtti
-APP_PLATFORM := android-9
+APP_PLATFORM := android-16
diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
index eab6862555..18f1dd5c69 100755
--- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
+++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
@@ -20,46 +20,33 @@
set -e
-if [ $# -ne 1 ]; then
- echo "Usage: ${0} "
+if [ $# -ne 0 ]; then
+ echo "Usage: ${0}"
exit
fi
-ndk="${1}"
-shift 1
-
# configuration parameters common to all architectures
common_params="--disable-examples --disable-docs --enable-realtime-only"
common_params+=" --disable-vp8 --disable-vp9-encoder --disable-webm-io"
common_params+=" --disable-libyuv --disable-runtime-cpu-detect"
+common_params+=" --enable-external-build"
# configuration parameters for various architectures
arch[0]="armeabi-v7a"
-config[0]="--target=armv7-android-gcc --sdk-path=$ndk --enable-neon"
-config[0]+=" --enable-neon-asm"
+config[0]="--target=armv7-android-gcc --enable-neon --enable-neon-asm"
-arch[1]="armeabi"
-config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon"
-config[1]+=" --disable-neon-asm"
+arch[1]="x86"
+config[1]="--force-target=x86-android-gcc --disable-sse2"
+config[1]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
+config[1]+=" --disable-avx2 --enable-pic"
-arch[2]="mips"
-config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk"
+arch[2]="arm64-v8a"
+config[2]="--force-target=armv8-android-gcc --enable-neon"
-arch[3]="x86"
-config[3]="--force-target=x86-android-gcc --sdk-path=$ndk --disable-sse2"
+arch[3]="x86_64"
+config[3]="--force-target=x86_64-android-gcc --disable-sse2"
config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
-config[3]+=" --disable-avx2 --enable-pic"
-
-arch[4]="arm64-v8a"
-config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon"
-
-arch[5]="x86_64"
-config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"
-config[5]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
-config[5]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm"
-
-arch[6]="mips64"
-config[6]="--force-target=mips64-android-gcc --sdk-path=$ndk"
+config[3]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm"
limit=$((${#arch[@]} - 1))
@@ -102,10 +89,7 @@ for i in $(seq 0 ${limit}); do
# configure and make
echo "build_android_configs: "
echo "configure ${config[${i}]} ${common_params}"
- ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \
- -isystem $ndk/sysroot/usr/include/arm-linux-androideabi \
- -isystem $ndk/sysroot/usr/include \
- "
+ ../../libvpx/configure ${config[${i}]} ${common_params}
rm -f libvpx_srcs.txt
for f in ${allowed_files}; do
# the build system supports multiple different configurations. avoid
diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc
index 875e46d40f..823f9b8cab 100644
--- a/extensions/vp9/src/main/jni/vpx_jni.cc
+++ b/extensions/vp9/src/main/jni/vpx_jni.cc
@@ -30,8 +30,6 @@
#include