pendingDrmSession;
@@ -128,7 +127,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private Bitmap bitmap;
private boolean renderedFirstFrame;
- private boolean forceRenderFrame;
private long joiningDeadlineMs;
private Surface surface;
private VpxOutputBufferRenderer outputBufferRenderer;
@@ -144,6 +142,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private int droppedFrames;
private int consecutiveDroppedFrameCount;
private int buffersInCodecCount;
+ private long lastRenderTimeUs;
protected DecoderCounters decoderCounters;
@@ -254,7 +253,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
try {
// Rendering loop.
TraceUtil.beginSection("drainAndFeed");
- while (drainOutputBuffer(positionUs)) {}
+ while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
while (feedInputBuffer()) {}
TraceUtil.endSection();
} catch (VpxDecoderException e) {
@@ -319,6 +318,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
protected void onStarted() {
droppedFrames = 0;
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
+ lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
}
@Override
@@ -379,7 +379,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
@CallSuper
protected void flushDecoder() throws ExoPlaybackException {
waitingForKeys = false;
- forceRenderFrame = false;
buffersInCodecCount = 0;
if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
releaseDecoder();
@@ -390,10 +389,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
outputBuffer.release();
outputBuffer = null;
}
- if (nextOutputBuffer != null) {
- nextOutputBuffer.release();
- nextOutputBuffer = null;
- }
decoder.flush();
decoderReceivedBuffers = false;
}
@@ -408,13 +403,11 @@ public class LibvpxVideoRenderer extends BaseRenderer {
inputBuffer = null;
outputBuffer = null;
- nextOutputBuffer = null;
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false;
- forceRenderFrame = false;
buffersInCodecCount = 0;
}
@@ -482,22 +475,15 @@ public class LibvpxVideoRenderer extends BaseRenderer {
}
/**
- * Returns whether the current frame should be dropped.
+ * Returns whether the buffer being processed should be dropped.
*
- * @param outputBufferTimeUs The timestamp of the current output buffer.
- * @param nextOutputBufferTimeUs The timestamp of the next output buffer or {@link C#TIME_UNSET}
- * if the next output buffer is unavailable.
- * @param positionUs The current playback position.
- * @param joiningDeadlineMs The joining deadline.
- * @return Returns whether to drop the current output buffer.
+ * @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 outputBufferTimeUs,
- long nextOutputBufferTimeUs,
- long positionUs,
- long joiningDeadlineMs) {
- return isBufferLate(outputBufferTimeUs - positionUs)
- && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET);
+ protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) {
+ return isBufferLate(earlyUs);
}
/**
@@ -506,11 +492,26 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*
* @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) {
+ 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.
*
@@ -543,6 +544,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
int bufferMode = outputBuffer.mode;
boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
+ lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
if (!renderRgb && !renderYuv) {
dropOutputBuffer(outputBuffer);
} else {
@@ -755,22 +757,18 @@ public class LibvpxVideoRenderer extends BaseRenderer {
/**
* Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link
- * #processOutputBuffer(long)}.
+ * #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)
+ private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException, VpxDecoderException {
- // Acquire outputBuffer either from nextOutputBuffer or from the decoder.
if (outputBuffer == null) {
- if (nextOutputBuffer != null) {
- outputBuffer = nextOutputBuffer;
- nextOutputBuffer = null;
- } else {
- outputBuffer = decoder.dequeueOutputBuffer();
- }
+ outputBuffer = decoder.dequeueOutputBuffer();
if (outputBuffer == null) {
return false;
}
@@ -778,10 +776,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
buffersInCodecCount -= outputBuffer.skippedOutputBufferCount;
}
- if (nextOutputBuffer == null) {
- nextOutputBuffer = decoder.dequeueOutputBuffer();
- }
-
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.
@@ -795,7 +789,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
return false;
}
- return processOutputBuffer(positionUs);
+ boolean processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs);
+ if (processedOutputBuffer) {
+ onProcessedOutputBuffer(outputBuffer.timeUs);
+ outputBuffer = null;
+ }
+ return processedOutputBuffer;
}
/**
@@ -803,53 +802,47 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* 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) throws ExoPlaybackException {
+ private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs)
+ throws ExoPlaybackException {
+ 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(outputBuffer.timeUs - positionUs)) {
- forceRenderFrame = false;
+ if (isBufferLate(earlyUs)) {
skipOutputBuffer(outputBuffer);
- onProcessedOutputBuffer(outputBuffer.timeUs);
- outputBuffer = null;
return true;
}
return false;
}
- if (forceRenderFrame) {
- forceRenderFrame = false;
+ long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
+ boolean isStarted = getState() == STATE_STARTED;
+ if (!renderedFirstFrame
+ || (isStarted
+ && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {
renderOutputBuffer(outputBuffer);
- onProcessedOutputBuffer(outputBuffer.timeUs);
- outputBuffer = null;
return true;
}
- long nextOutputBufferTimeUs =
- nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream()
- ? nextOutputBuffer.timeUs
- : C.TIME_UNSET;
-
- long earlyUs = outputBuffer.timeUs - positionUs;
- if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) {
- forceRenderFrame = true;
+ if (!isStarted) {
return false;
- } else if (shouldDropOutputBuffer(
- outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) {
+ }
+
+ if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs)
+ && maybeDropBuffersToKeyframe(positionUs)) {
+ return false;
+ } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
dropOutputBuffer(outputBuffer);
- onProcessedOutputBuffer(outputBuffer.timeUs);
- outputBuffer = null;
return true;
}
- // If we have yet to render a frame to the current output (either initially or immediately
- // following a seek), render one irrespective of the state or current position.
- if (!renderedFirstFrame || (getState() == STATE_STARTED && earlyUs <= 30000)) {
+ if (earlyUs < 30000) {
renderOutputBuffer(outputBuffer);
- onProcessedOutputBuffer(outputBuffer.timeUs);
- outputBuffer = null;
+ return true;
}
return false;
diff --git a/library/all/build.gradle b/library/all/build.gradle
index 79ed9c747b..bb832ba0ff 100644
--- a/library/all/build.gradle
+++ b/library/all/build.gradle
@@ -25,11 +25,11 @@ android {
}
dependencies {
- compile project(modulePrefix + 'library-core')
- compile project(modulePrefix + 'library-dash')
- compile project(modulePrefix + 'library-hls')
- compile project(modulePrefix + 'library-smoothstreaming')
- compile project(modulePrefix + 'library-ui')
+ api project(modulePrefix + 'library-core')
+ api project(modulePrefix + 'library-dash')
+ api project(modulePrefix + 'library-hls')
+ api project(modulePrefix + 'library-smoothstreaming')
+ api project(modulePrefix + 'library-ui')
}
ext {
diff --git a/library/core/build.gradle b/library/core/build.gradle
index a87e11065f..fe6045c2e7 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -31,6 +31,7 @@ android {
}
test {
java.srcDirs += "../../testutils/src/main/java/"
+ java.srcDirs += "../../testutils_robolectric/src/main/java/"
}
}
@@ -44,15 +45,15 @@ android {
}
dependencies {
- compile 'com.android.support:support-annotations:' + supportLibraryVersion
- androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
- androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
- androidTestCompile 'com.google.truth:truth:' + truthVersion
- androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
- testCompile 'com.google.truth:truth:' + truthVersion
- testCompile 'junit:junit:' + junitVersion
- testCompile 'org.mockito:mockito-core:' + mockitoVersion
- testCompile 'org.robolectric:robolectric:' + robolectricVersion
+ implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
+ androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
+ androidTestImplementation 'com.google.truth:truth:' + truthVersion
+ androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
+ testImplementation 'com.google.truth:truth:' + truthVersion
+ testImplementation 'junit:junit:' + junitVersion
+ testImplementation 'org.mockito:mockito-core:' + mockitoVersion
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
index 83a978219e..3465393853 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
@@ -89,7 +89,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase {
ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext());
try {
DataSpec dataSpec = new DataSpec(contentUri, offset, length, null);
- byte[] completeData = TestUtil.getByteArray(instrumentation, DATA_PATH);
+ byte[] completeData = TestUtil.getByteArray(instrumentation.getContext(), DATA_PATH);
byte[] expectedData = Arrays.copyOfRange(completeData, offset,
length == C.LENGTH_UNSET ? completeData.length : offset + length);
TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index 83bbdd1157..0e0a6e3c26 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -170,7 +170,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
// because it uses a callback.
hasPendingPrepare = true;
pendingOperationAcks++;
- internalPlayer.prepare(mediaSource, resetPosition);
+ internalPlayer.prepare(mediaSource, resetPosition, resetState);
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
@@ -567,10 +567,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
@DiscontinuityReason int positionDiscontinuityReason) {
pendingOperationAcks -= operationAcks;
if (pendingOperationAcks == 0) {
- if (playbackInfo.timeline == null) {
- // Replace internal null timeline with externally visible empty timeline.
- playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, playbackInfo.manifest);
- }
if (playbackInfo.startPositionUs == C.TIME_UNSET) {
// Replace internal unset start position with externally visible start position of zero.
playbackInfo =
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index e05068a7b3..24bd31c62f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -154,7 +154,7 @@ import java.util.Collections;
seekParameters = SeekParameters.DEFAULT;
playbackInfo =
new PlaybackInfo(
- /* timeline= */ null, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult);
+ Timeline.EMPTY, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult);
playbackInfoUpdate = new PlaybackInfoUpdate();
rendererCapabilities = new RendererCapabilities[renderers.length];
for (int i = 0; i < renderers.length; i++) {
@@ -176,8 +176,9 @@ import java.util.Collections;
handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
}
- public void prepare(MediaSource mediaSource, boolean resetPosition) {
- handler.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, 0, mediaSource)
+ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ handler
+ .obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)
.sendToTarget();
}
@@ -286,7 +287,10 @@ import java.util.Collections;
try {
switch (msg.what) {
case MSG_PREPARE:
- prepareInternal((MediaSource) msg.obj, msg.arg1 != 0);
+ prepareInternal(
+ (MediaSource) msg.obj,
+ /* resetPosition= */ msg.arg1 != 0,
+ /* resetState= */ msg.arg2 != 0);
break;
case MSG_SET_PLAY_WHEN_READY:
setPlayWhenReadyInternal(msg.arg1 != 0);
@@ -339,7 +343,7 @@ import java.util.Collections;
}
maybeNotifyPlaybackInfoChanged();
} catch (ExoPlaybackException e) {
- Log.e(TAG, "Renderer error.", e);
+ Log.e(TAG, "Playback error.", e);
stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
maybeNotifyPlaybackInfoChanged();
@@ -387,9 +391,9 @@ import java.util.Collections;
}
}
- private void prepareInternal(MediaSource mediaSource, boolean resetPosition) {
+ private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
pendingPrepareCount++;
- resetInternal(/* releaseMediaSource= */ true, resetPosition, /* resetState= */ true);
+ resetInternal(/* releaseMediaSource= */ true, resetPosition, resetState);
loadControl.onPrepared();
this.mediaSource = mediaSource;
setState(Player.STATE_BUFFERING);
@@ -576,7 +580,6 @@ import java.util.Collections;
}
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
- Timeline timeline = playbackInfo.timeline;
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
MediaPeriodId periodId;
@@ -607,7 +610,7 @@ import java.util.Collections;
}
try {
- if (mediaSource == null || timeline == null) {
+ if (mediaSource == null || pendingPrepareCount > 0) {
// Save seek position for later, as we are still waiting for a prepared source.
pendingInitialSeekPosition = seekPosition;
} else if (periodPositionUs == C.TIME_UNSET) {
@@ -752,7 +755,7 @@ import java.util.Collections;
private int getFirstPeriodIndex() {
Timeline timeline = playbackInfo.timeline;
- return timeline == null || timeline.isEmpty()
+ return timeline.isEmpty()
? 0
: timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window)
.firstPeriodIndex;
@@ -779,7 +782,7 @@ import java.util.Collections;
pendingInitialSeekPosition = null;
}
if (resetState) {
- queue.setTimeline(null);
+ queue.setTimeline(Timeline.EMPTY);
for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
}
@@ -788,11 +791,11 @@ import java.util.Collections;
}
playbackInfo =
new PlaybackInfo(
- resetState ? null : playbackInfo.timeline,
+ resetState ? Timeline.EMPTY : playbackInfo.timeline,
resetState ? null : playbackInfo.manifest,
resetPosition ? new MediaPeriodId(getFirstPeriodIndex()) : playbackInfo.periodId,
// Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored.
- resetPosition ? C.TIME_UNSET : playbackInfo.startPositionUs,
+ resetPosition ? C.TIME_UNSET : playbackInfo.positionUs,
resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs,
playbackInfo.playbackState,
/* isLoading= */ false,
@@ -805,11 +808,11 @@ import java.util.Collections;
}
}
- private void sendMessageInternal(PlayerMessage message) {
+ private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackException {
if (message.getPositionMs() == C.TIME_UNSET) {
// If no delivery time is specified, trigger immediate message delivery.
sendMessageToTarget(message);
- } else if (playbackInfo.timeline == null) {
+ } else if (mediaSource == null || pendingPrepareCount > 0) {
// Still waiting for initial timeline to resolve position.
pendingMessages.add(new PendingMessageInfo(message));
} else {
@@ -824,7 +827,7 @@ import java.util.Collections;
}
}
- private void sendMessageToTarget(PlayerMessage message) {
+ private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException {
if (message.getHandler().getLooper() == handler.getLooper()) {
deliverMessage(message);
if (playbackInfo.playbackState == Player.STATE_READY
@@ -838,22 +841,24 @@ import java.util.Collections;
}
private void sendMessageToTargetThread(final PlayerMessage message) {
- message
- .getHandler()
- .post(
- new Runnable() {
- @Override
- public void run() {
- deliverMessage(message);
- }
- });
+ Handler handler = message.getHandler();
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ deliverMessage(message);
+ } catch (ExoPlaybackException e) {
+ Log.e(TAG, "Unexpected error delivering message on external thread.", e);
+ throw new RuntimeException(e);
+ }
+ }
+ });
}
- private void deliverMessage(PlayerMessage message) {
+ private void deliverMessage(PlayerMessage message) throws ExoPlaybackException {
try {
message.getTarget().handleMessage(message.getType(), message.getPayload());
- } catch (ExoPlaybackException e) {
- eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
} finally {
message.markAsProcessed(/* isDelivered= */ true);
}
@@ -899,7 +904,8 @@ import java.util.Collections;
return true;
}
- private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs) {
+ private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs)
+ throws ExoPlaybackException {
if (pendingMessages.isEmpty() || playbackInfo.periodId.isAd()) {
return;
}
@@ -1130,7 +1136,7 @@ import java.util.Collections;
playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest);
resolvePendingMessagePositions();
- if (oldTimeline == null) {
+ if (pendingPrepareCount > 0) {
playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
pendingPrepareCount = 0;
if (pendingInitialSeekPosition != null) {
@@ -1292,8 +1298,8 @@ import java.util.Collections;
SeekPosition seekPosition, boolean trySubsequentPeriods) {
Timeline timeline = playbackInfo.timeline;
Timeline seekTimeline = seekPosition.timeline;
- if (timeline == null) {
- // We don't have a timeline yet, so we can't resolve the position.
+ if (timeline.isEmpty()) {
+ // We don't have a valid timeline yet, so we can't resolve the position.
return null;
}
if (seekTimeline.isEmpty()) {
@@ -1349,7 +1355,7 @@ import java.util.Collections;
// The player has no media source yet.
return;
}
- if (playbackInfo.timeline == null) {
+ if (pendingPrepareCount > 0) {
// We're waiting to get information about periods.
mediaSource.maybeThrowSourceInfoRefreshError();
return;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
index 1dec506ec9..c34145a145 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -27,27 +27,23 @@ public final class ExoPlayerLibraryInfo {
*/
public static final String TAG = "ExoPlayer";
- /**
- * The version of the library expressed as a string, for example "1.2.3".
- */
+ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
- public static final String VERSION = "2.7.0";
+ public static final String VERSION = "2.7.1";
- /**
- * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
- */
+ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final String VERSION_SLASHY = "ExoPlayerLib/2.7.0";
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.7.1";
/**
* The version of the library expressed as an integer, for example 1002003.
- *
- * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
+ *
+ *
Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final int VERSION_INT = 2007000;
+ public static final int VERSION_INT = 2007001;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
index 3ff2ec9461..3a4ee0e501 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
@@ -24,7 +24,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
*/
/* package */ final class PlaybackInfo {
- public final @Nullable Timeline timeline;
+ public final Timeline timeline;
public final @Nullable Object manifest;
public final MediaPeriodId periodId;
public final long startPositionUs;
@@ -37,7 +37,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
public volatile long bufferedPositionUs;
public PlaybackInfo(
- @Nullable Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) {
+ Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) {
this(
timeline,
/* manifest= */ null,
@@ -50,7 +50,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
}
public PlaybackInfo(
- @Nullable Timeline timeline,
+ Timeline timeline,
@Nullable Object manifest,
MediaPeriodId periodId,
long startPositionUs,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
index 1e8a89e102..408cbecaf1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
@@ -33,7 +33,8 @@ public final class PlayerMessage {
*
* @param messageType The message type.
* @param payload The message payload.
- * @throws ExoPlaybackException If an error occurred whilst handling the message.
+ * @throws ExoPlaybackException If an error occurred whilst handling the message. Should only be
+ * thrown by targets that handle messages on the playback thread.
*/
void handleMessage(int messageType, Object payload) throws ExoPlaybackException;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
index bb9135edbf..6d12dc66e8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
@@ -1443,7 +1443,7 @@ public final class DefaultAudioSink implements AudioSink {
rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset;
}
- if (Util.SDK_INT <= 26) {
+ if (Util.SDK_INT <= 28) {
if (rawPlaybackHeadPosition == 0 && lastRawPlaybackHeadPosition > 0
&& state == PLAYSTATE_PLAYING) {
// If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
index 588282bc9b..37cfce7c7c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -49,7 +49,6 @@ import java.util.List;
private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl");
private static final int TYPE_subt = Util.getIntegerCodeForString("subt");
private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
- private static final int TYPE_cenc = Util.getIntegerCodeForString("cenc");
private static final int TYPE_meta = Util.getIntegerCodeForString("meta");
/**
@@ -128,7 +127,8 @@ import java.util.List;
int sampleCount = sampleSizeBox.getSampleCount();
if (sampleCount == 0) {
- return new TrackSampleTable(new long[0], new int[0], 0, new long[0], new int[0]);
+ return new TrackSampleTable(
+ new long[0], new int[0], 0, new long[0], new int[0], C.TIME_UNSET);
}
// Entries are byte offsets of chunks.
@@ -193,6 +193,7 @@ import java.util.List;
long[] timestamps;
int[] flags;
long timestampTimeUnits = 0;
+ long duration;
if (!isRechunkable) {
offsets = new long[sampleCount];
@@ -260,6 +261,7 @@ import java.util.List;
offset += sizes[i];
remainingSamplesInChunk--;
}
+ duration = timestampTimeUnits + timestampOffset;
Assertions.checkArgument(remainingSamplesAtTimestampOffset == 0);
// Remove trailing ctts entries with 0-valued sample counts.
@@ -294,13 +296,15 @@ import java.util.List;
maximumSize = rechunkedResults.maximumSize;
timestamps = rechunkedResults.timestamps;
flags = rechunkedResults.flags;
+ duration = rechunkedResults.duration;
}
+ long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale);
if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) {
// There is no edit list, or we are ignoring it as we already have gapless metadata to apply.
// This implementation does not support applying both gapless metadata and an edit list.
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
- return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
+ return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
// See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a
@@ -317,10 +321,11 @@ import java.util.List;
long editStartTime = track.editListMediaTimes[0];
long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0],
track.timescale, track.movieTimescale);
- long lastSampleEndTime = timestampTimeUnits;
- if (timestamps[0] <= editStartTime && editStartTime < timestamps[1]
- && timestamps[timestamps.length - 1] < editEndTime && editEndTime <= lastSampleEndTime) {
- long paddingTimeUnits = lastSampleEndTime - editEndTime;
+ if (timestamps[0] <= editStartTime
+ && editStartTime < timestamps[1]
+ && timestamps[timestamps.length - 1] < editEndTime
+ && editEndTime <= duration) {
+ long paddingTimeUnits = duration - editEndTime;
long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0],
track.format.sampleRate, track.timescale);
long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits,
@@ -330,7 +335,7 @@ import java.util.List;
gaplessInfoHolder.encoderDelay = (int) encoderDelay;
gaplessInfoHolder.encoderPadding = (int) encoderPadding;
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
- return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
+ return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
}
}
@@ -339,11 +344,15 @@ import java.util.List;
// The current version of the spec leaves handling of an edit with zero segment_duration in
// unfragmented files open to interpretation. We handle this as a special case and include all
// samples in the edit.
+ long editStartTime = track.editListMediaTimes[0];
for (int i = 0; i < timestamps.length; i++) {
- timestamps[i] = Util.scaleLargeTimestamp(timestamps[i] - track.editListMediaTimes[0],
- C.MICROS_PER_SECOND, track.timescale);
+ timestamps[i] =
+ Util.scaleLargeTimestamp(
+ timestamps[i] - editStartTime, C.MICROS_PER_SECOND, track.timescale);
}
- return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
+ durationUs =
+ Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale);
+ return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
// Omit any sample at the end point of an edit for audio tracks.
@@ -354,13 +363,15 @@ import java.util.List;
int nextSampleIndex = 0;
boolean copyMetadata = false;
for (int i = 0; i < track.editListDurations.length; i++) {
- long mediaTime = track.editListMediaTimes[i];
- if (mediaTime != -1) {
- long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale,
- track.movieTimescale);
- int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
- int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample,
- false);
+ long editMediaTime = track.editListMediaTimes[i];
+ if (editMediaTime != -1) {
+ long editDuration =
+ Util.scaleLargeTimestamp(
+ track.editListDurations[i], track.timescale, track.movieTimescale);
+ int startIndex = Util.binarySearchCeil(timestamps, editMediaTime, true, true);
+ int endIndex =
+ Util.binarySearchCeil(
+ timestamps, editMediaTime + editDuration, omitClippedSample, false);
editedSampleCount += endIndex - startIndex;
copyMetadata |= nextSampleIndex != startIndex;
nextSampleIndex = endIndex;
@@ -377,12 +388,13 @@ import java.util.List;
long pts = 0;
int sampleIndex = 0;
for (int i = 0; i < track.editListDurations.length; i++) {
- long mediaTime = track.editListMediaTimes[i];
- long duration = track.editListDurations[i];
- if (mediaTime != -1) {
- long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale,
- track.movieTimescale);
- int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
+ long editMediaTime = track.editListMediaTimes[i];
+ long editDuration = track.editListDurations[i];
+ if (editMediaTime != -1) {
+ long endMediaTime =
+ editMediaTime
+ + Util.scaleLargeTimestamp(editDuration, track.timescale, track.movieTimescale);
+ int startIndex = Util.binarySearchCeil(timestamps, editMediaTime, true, true);
int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false);
if (copyMetadata) {
int count = endIndex - startIndex;
@@ -392,8 +404,9 @@ import java.util.List;
}
for (int j = startIndex; j < endIndex; j++) {
long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
- long timeInSegmentUs = Util.scaleLargeTimestamp(timestamps[j] - mediaTime,
- C.MICROS_PER_SECOND, track.timescale);
+ long timeInSegmentUs =
+ Util.scaleLargeTimestamp(
+ timestamps[j] - editMediaTime, C.MICROS_PER_SECOND, track.timescale);
editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs;
if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) {
editedMaximumSize = sizes[j];
@@ -401,8 +414,9 @@ import java.util.List;
sampleIndex++;
}
}
- pts += duration;
+ pts += editDuration;
}
+ long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.timescale);
boolean hasSyncSample = false;
for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) {
@@ -413,11 +427,16 @@ import java.util.List;
// Such edit lists are often (although not always) broken, so we ignore it and continue.
Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample.");
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
- return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
+ return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
- return new TrackSampleTable(editedOffsets, editedSizes, editedMaximumSize, editedTimestamps,
- editedFlags);
+ return new TrackSampleTable(
+ editedOffsets,
+ editedSizes,
+ editedMaximumSize,
+ editedTimestamps,
+ editedFlags,
+ editedDurationUs);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java
index 5dd6c6ea9f..8336a280a2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java
@@ -33,13 +33,21 @@ import com.google.android.exoplayer2.util.Util;
public final int maximumSize;
public final long[] timestamps;
public final int[] flags;
+ public final long duration;
- private Results(long[] offsets, int[] sizes, int maximumSize, long[] timestamps, int[] flags) {
+ private Results(
+ long[] offsets,
+ int[] sizes,
+ int maximumSize,
+ long[] timestamps,
+ int[] flags,
+ long duration) {
this.offsets = offsets;
this.sizes = sizes;
this.maximumSize = maximumSize;
this.timestamps = timestamps;
this.flags = flags;
+ this.duration = duration;
}
}
@@ -95,8 +103,9 @@ import com.google.android.exoplayer2.util.Util;
newSampleIndex++;
}
}
+ long duration = timestampDeltaInTimeUnits * originalSampleIndex;
- return new Results(offsets, sizes, maximumSize, timestamps, flags);
+ return new Results(offsets, sizes, maximumSize, timestamps, flags, duration);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
index 112c2d1ba0..75bd2c16ee 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
@@ -427,7 +427,10 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
mp4Track.trackOutput.format(format);
- durationUs = Math.max(durationUs, track.durationUs);
+ durationUs =
+ Math.max(
+ durationUs,
+ track.durationUs != C.TIME_UNSET ? track.durationUs : trackSampleTable.durationUs);
if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) {
firstVideoTrackIndex = tracks.size();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
index cf479eaf3e..9f77c49664 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
@@ -48,9 +48,19 @@ import com.google.android.exoplayer2.util.Util;
* Sample flags.
*/
public final int[] flags;
+ /**
+ * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample
+ * table is empty.
+ */
+ public final long durationUs;
- public TrackSampleTable(long[] offsets, int[] sizes, int maximumSize, long[] timestampsUs,
- int[] flags) {
+ public TrackSampleTable(
+ long[] offsets,
+ int[] sizes,
+ int maximumSize,
+ long[] timestampsUs,
+ int[] flags,
+ long durationUs) {
Assertions.checkArgument(sizes.length == timestampsUs.length);
Assertions.checkArgument(offsets.length == timestampsUs.length);
Assertions.checkArgument(flags.length == timestampsUs.length);
@@ -60,6 +70,7 @@ import com.google.android.exoplayer2.util.Util;
this.maximumSize = maximumSize;
this.timestampsUs = timestampsUs;
this.flags = flags;
+ this.durationUs = durationUs;
sampleCount = offsets.length;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
index 62a7657ea7..2978d00d86 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -100,12 +100,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@C.VideoScalingMode
private int scalingMode;
private boolean renderedFirstFrame;
- private boolean forceRenderFrame;
private long joiningDeadlineMs;
private long droppedFrameAccumulationStartTimeMs;
private int droppedFrames;
private int consecutiveDroppedFrameCount;
private int buffersInCodecCount;
+ private long lastRenderTimeUs;
private int pendingRotationDegrees;
private float pendingPixelWidthHeightRatio;
@@ -314,6 +314,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
super.onStarted();
droppedFrames = 0;
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
+ lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
}
@Override
@@ -438,7 +439,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
super.releaseCodec();
} finally {
buffersInCodecCount = 0;
- forceRenderFrame = false;
if (dummySurface != null) {
if (surface == dummySurface) {
surface = null;
@@ -454,7 +454,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
protected void flushCodec() throws ExoPlaybackException {
super.flushCodec();
buffersInCodecCount = 0;
- forceRenderFrame = false;
}
@Override
@@ -546,15 +545,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
if (surface == dummySurface) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
if (isBufferLate(earlyUs)) {
- forceRenderFrame = false;
skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
return false;
}
- if (!renderedFirstFrame || forceRenderFrame) {
- forceRenderFrame = false;
+ long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
+ boolean isStarted = getState() == STATE_STARTED;
+ if (!renderedFirstFrame
+ || (isStarted
+ && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {
if (Util.SDK_INT >= 21) {
renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime());
} else {
@@ -563,13 +564,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return true;
}
- if (getState() != STATE_STARTED) {
+ if (!isStarted) {
return false;
}
// Fine-grained adjustment of earlyUs based on the elapsed time since the start of the current
// iteration of the rendering loop.
- long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs;
+ long elapsedSinceStartOfLoopUs = elapsedRealtimeNowUs - elapsedRealtimeUs;
earlyUs -= elapsedSinceStartOfLoopUs;
// Compute the buffer's desired release time in nanoseconds.
@@ -583,7 +584,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs)
&& maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) {
- forceRenderFrame = true;
return false;
} else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
dropOutputBuffer(codec, bufferIndex, presentationTimeUs);
@@ -607,6 +607,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
Thread.sleep((earlyUs - 10000) / 1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
+ return false;
}
}
renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
@@ -654,6 +655,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
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 output buffer with the specified index.
*
@@ -738,6 +752,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
TraceUtil.beginSection("releaseOutputBuffer");
codec.releaseOutputBuffer(index, true);
TraceUtil.endSection();
+ lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0;
maybeNotifyRenderedFirstFrame();
@@ -753,12 +768,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds.
*/
@TargetApi(21)
- protected void renderOutputBufferV21(MediaCodec codec, int index, long presentationTimeUs,
- long releaseTimeNs) {
+ protected void renderOutputBufferV21(
+ MediaCodec codec, int index, long presentationTimeUs, long releaseTimeNs) {
maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBuffer");
codec.releaseOutputBuffer(index, releaseTimeNs);
TraceUtil.endSection();
+ lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0;
maybeNotifyRenderedFirstFrame();
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
index 3d0cde5df8..b1ddcdb207 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
@@ -42,7 +42,9 @@ import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.FakeTrackSelection;
import com.google.android.exoplayer2.testutil.FakeTrackSelector;
+import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.upstream.Allocator;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -1169,10 +1171,8 @@ public final class ExoPlayerTest {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testReprepareAfterPlaybackError")
- .waitForPlaybackState(Player.STATE_BUFFERING)
- // Cause an internal exception by seeking to an invalid position while the media source
- // is still being prepared and the player doesn't immediately know it will fail.
- .seek(/* windowIndex= */ 100, /* positionMs= */ 0)
+ .waitForPlaybackState(Player.STATE_READY)
+ .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE)
.prepareSource(
new FakeMediaSource(timeline, /* manifest= */ null),
@@ -1203,11 +1203,8 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testReprepareAfterPlaybackError")
.pause()
- .waitForPlaybackState(Player.STATE_BUFFERING)
- // Cause an internal exception by seeking to an invalid position while the media source
- // is still being prepared and the player doesn't immediately know it will fail.
- .seek(/* windowIndex= */ 100, /* positionMs= */ 0)
- .waitForSeekProcessed()
+ .waitForPlaybackState(Player.STATE_READY)
+ .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE)
.seek(/* positionMs= */ 50)
.waitForSeekProcessed()
@@ -1246,8 +1243,7 @@ public final class ExoPlayerTest {
testRunner.assertTimelinesEqual(timeline, timeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);
- testRunner.assertPositionDiscontinuityReasonsEqual(
- Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_SEEK);
+ testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
assertThat(positionHolder[0]).isEqualTo(50);
assertThat(positionHolder[1]).isEqualTo(50);
}
@@ -1288,6 +1284,104 @@ public final class ExoPlayerTest {
testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
}
+ @Test
+ public void testPlaybackErrorAndReprepareDoesNotResetPosition() throws Exception {
+ final Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
+ final long[] positionHolder = new long[3];
+ final int[] windowIndexHolder = new int[3];
+ final FakeMediaSource secondMediaSource =
+ new FakeMediaSource(/* timeline= */ null, /* manifest= */ null);
+ ActionSchedule actionSchedule =
+ new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition")
+ .pause()
+ .waitForPlaybackState(Player.STATE_READY)
+ .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 500)
+ .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
+ .waitForPlaybackState(Player.STATE_IDLE)
+ .executeRunnable(
+ new PlayerRunnable() {
+ @Override
+ public void run(SimpleExoPlayer player) {
+ // Position while in error state
+ positionHolder[0] = player.getCurrentPosition();
+ windowIndexHolder[0] = player.getCurrentWindowIndex();
+ }
+ })
+ .prepareSource(secondMediaSource, /* resetPosition= */ false, /* resetState= */ false)
+ .waitForPlaybackState(Player.STATE_BUFFERING)
+ .executeRunnable(
+ new PlayerRunnable() {
+ @Override
+ public void run(SimpleExoPlayer player) {
+ // Position while repreparing.
+ positionHolder[1] = player.getCurrentPosition();
+ windowIndexHolder[1] = player.getCurrentWindowIndex();
+ secondMediaSource.setNewSourceInfo(timeline, /* newManifest= */ null);
+ }
+ })
+ .waitForPlaybackState(Player.STATE_READY)
+ .executeRunnable(
+ new PlayerRunnable() {
+ @Override
+ public void run(SimpleExoPlayer player) {
+ // Position after repreparation finished.
+ positionHolder[2] = player.getCurrentPosition();
+ windowIndexHolder[2] = player.getCurrentWindowIndex();
+ }
+ })
+ .play()
+ .build();
+ ExoPlayerTestRunner testRunner =
+ new ExoPlayerTestRunner.Builder()
+ .setTimeline(timeline)
+ .setActionSchedule(actionSchedule)
+ .build();
+ try {
+ testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
+ fail();
+ } catch (ExoPlaybackException e) {
+ // Expected exception.
+ }
+ assertThat(positionHolder[0]).isAtLeast(500L);
+ assertThat(positionHolder[1]).isEqualTo(positionHolder[0]);
+ assertThat(positionHolder[2]).isEqualTo(positionHolder[0]);
+ assertThat(windowIndexHolder[0]).isEqualTo(1);
+ assertThat(windowIndexHolder[1]).isEqualTo(1);
+ assertThat(windowIndexHolder[2]).isEqualTo(1);
+ }
+
+ @Test
+ public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception {
+ final Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ final FakeMediaSource mediaSource2 =
+ new FakeMediaSource(/* timeline= */ null, /* manifest= */ null);
+ ActionSchedule actionSchedule =
+ new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition")
+ .pause()
+ .waitForPlaybackState(Player.STATE_READY)
+ .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
+ .waitForPlaybackState(Player.STATE_IDLE)
+ .prepareSource(mediaSource2, /* resetPosition= */ false, /* resetState= */ false)
+ .waitForPlaybackState(Player.STATE_BUFFERING)
+ .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
+ .waitForPlaybackState(Player.STATE_IDLE)
+ .build();
+ ExoPlayerTestRunner testRunner =
+ new ExoPlayerTestRunner.Builder()
+ .setTimeline(timeline)
+ .setActionSchedule(actionSchedule)
+ .build();
+ try {
+ testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
+ fail();
+ } catch (ExoPlaybackException e) {
+ // Expected exception.
+ }
+ testRunner.assertTimelinesEqual(timeline, timeline);
+ testRunner.assertTimelineChangeReasonsEqual(
+ Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);
+ }
+
@Test
public void testSendMessagesDuringPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
@@ -1421,7 +1515,8 @@ public final class ExoPlayerTest {
new FakeMediaSource(timeline, null),
/* resetPosition= */ false,
/* resetState= */ true)
- .waitForPlaybackState(Player.STATE_READY)
+ .waitForPlaybackState(Player.STATE_BUFFERING)
+ .waitForPlaybackState(Player.STATE_ENDED)
.build();
new Builder()
.setTimeline(timeline)
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
index f67301f017..6f62b7fcfc 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java
@@ -22,8 +22,8 @@ import static org.mockito.Mockito.when;
import android.util.Pair;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.testutil.RobolectricUtil;
import java.util.HashMap;
import org.junit.After;
import org.junit.Before;
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java
index 1e0d8681c5..a4aa3eb938 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java
@@ -20,7 +20,6 @@ import static org.junit.Assert.fail;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window;
@@ -29,6 +28,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
+import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException;
import org.junit.Before;
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java
index ccc3ddea46..465e08b5d2 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java
@@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
@@ -27,6 +26,7 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
+import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException;
import org.junit.Test;
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java
index c1537c50d3..24f1ddd5ed 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java
@@ -24,7 +24,6 @@ import android.os.Handler;
import android.os.HandlerThread;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
@@ -32,6 +31,7 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
+import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException;
import java.util.Arrays;
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java
index f0b4772422..6aa710aff4 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java
@@ -17,12 +17,12 @@ package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
+import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException;
import org.junit.Before;
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java
index b03a76c23e..839492f196 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java
@@ -19,13 +19,13 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MergingMediaSource.IllegalMergeException;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
+import com.google.android.exoplayer2.testutil.RobolectricUtil;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/library/dash/build.gradle b/library/dash/build.gradle
index 99441a2849..d2692eb7d9 100644
--- a/library/dash/build.gradle
+++ b/library/dash/build.gradle
@@ -33,17 +33,9 @@ android {
}
dependencies {
- compile project(modulePrefix + 'library-core')
- compile 'com.android.support:support-annotations:' + supportLibraryVersion
- androidTestCompile project(modulePrefix + 'testutils')
- androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
- androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
- androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
- testCompile project(modulePrefix + 'testutils')
- testCompile 'com.google.truth:truth:' + truthVersion
- testCompile 'junit:junit:' + junitVersion
- testCompile 'org.mockito:mockito-core:' + mockitoVersion
- testCompile 'org.robolectric:robolectric:' + robolectricVersion
+ implementation project(modulePrefix + 'library-core')
+ implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+ testImplementation project(modulePrefix + 'testutils-robolectric')
}
ext {
diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java
deleted file mode 100644
index 50752c8a72..0000000000
--- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java
+++ /dev/null
@@ -1,100 +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.source.dash.offline;
-
-import android.net.Uri;
-
-/**
- * Data for DASH downloading tests.
- */
-/* package */ interface DashDownloadTestData {
-
- Uri TEST_MPD_URI = Uri.parse("test.mpd");
-
- byte[] TEST_MPD =
- ("\n"
- + "\n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- // Bounded range data
- + " \n"
- // Unbounded range data
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- // This segment list has a 1 second offset to make sure the progressive download order
- + " \n"
- + " \n"
- + " \n" // 1s offset
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + "").getBytes();
-
- byte[] TEST_MPD_NO_INDEX =
- ("\n"
- + "\n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + " \n"
- + "").getBytes();
-}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
index eb9b18512c..98783ac93e 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
@@ -50,6 +50,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.nio.charset.Charset;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Locale;
@@ -1113,7 +1114,9 @@ public final class DashMediaSource implements MediaSource {
@Override
public Long parse(Uri uri, InputStream inputStream) throws IOException {
- String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine();
+ String firstLine =
+ new BufferedReader(new InputStreamReader(inputStream, Charset.forName(C.UTF8_NAME)))
+ .readLine();
try {
Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine);
if (!matcher.matches()) {
diff --git a/library/dash/src/androidTest/AndroidManifest.xml b/library/dash/src/test/AndroidManifest.xml
similarity index 71%
rename from library/dash/src/androidTest/AndroidManifest.xml
rename to library/dash/src/test/AndroidManifest.xml
index 39596a8165..eecf596b92 100644
--- a/library/dash/src/androidTest/AndroidManifest.xml
+++ b/library/dash/src/test/AndroidManifest.xml
@@ -18,16 +18,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.source.dash.test">
-
-
-
-
-
-
-
+