Commit c6e8e24a by tonihei Committed by Oliver Woodman

Ensure seek and prepare positions never exceed period duration.

Exceeding the period duration may mean that that playback transitions
to another item even if the player is currently paused.

PiperOrigin-RevId: 300133655
parent bb33d568
...@@ -1040,10 +1040,17 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1040,10 +1040,17 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (!newPlayingPeriodHolder.prepared) { if (!newPlayingPeriodHolder.prepared) {
newPlayingPeriodHolder.info = newPlayingPeriodHolder.info =
newPlayingPeriodHolder.info.copyWithStartPositionUs(periodPositionUs); newPlayingPeriodHolder.info.copyWithStartPositionUs(periodPositionUs);
} else if (newPlayingPeriodHolder.hasEnabledTracks) { } else {
periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); if (newPlayingPeriodHolder.info.durationUs != C.TIME_UNSET
newPlayingPeriodHolder.mediaPeriod.discardBuffer( && periodPositionUs >= newPlayingPeriodHolder.info.durationUs) {
periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe); // Make sure seek position doesn't exceed period duration.
periodPositionUs = Math.max(0, newPlayingPeriodHolder.info.durationUs - 1);
}
if (newPlayingPeriodHolder.hasEnabledTracks) {
periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
newPlayingPeriodHolder.mediaPeriod.discardBuffer(
periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
}
} }
resetRendererPosition(periodPositionUs); resetRendererPosition(periodPositionUs);
maybeContinueLoading(); maybeContinueLoading();
...@@ -1862,7 +1869,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1862,7 +1869,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfo = playbackInfo =
handlePositionDiscontinuity( handlePositionDiscontinuity(
playbackInfo.periodId, playbackInfo.periodId,
playbackInfo.positionUs, loadingPeriodHolder.info.startPositionUs,
playbackInfo.requestedContentPositionUs); playbackInfo.requestedContentPositionUs);
} }
maybeContinueLoading(); maybeContinueLoading();
......
...@@ -171,9 +171,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -171,9 +171,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
prepared = true; prepared = true;
trackGroups = mediaPeriod.getTrackGroups(); trackGroups = mediaPeriod.getTrackGroups();
TrackSelectorResult selectorResult = selectTracks(playbackSpeed, timeline); TrackSelectorResult selectorResult = selectTracks(playbackSpeed, timeline);
long requestedStartPositionUs = info.startPositionUs;
if (info.durationUs != C.TIME_UNSET && requestedStartPositionUs >= info.durationUs) {
// Make sure start position doesn't exceed period duration.
requestedStartPositionUs = Math.max(0, info.durationUs - 1);
}
long newStartPositionUs = long newStartPositionUs =
applyTrackSelection( applyTrackSelection(
selectorResult, info.startPositionUs, /* forceRecreateStreams= */ false); selectorResult, requestedStartPositionUs, /* forceRecreateStreams= */ false);
rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs; rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs;
info = info.copyWithStartPositionUs(newStartPositionUs); info = info.copyWithStartPositionUs(newStartPositionUs);
} }
......
...@@ -730,6 +730,10 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -730,6 +730,10 @@ import com.google.android.exoplayer2.util.Assertions;
adIndexInAdGroup == period.getFirstAdIndexToPlay(adGroupIndex) adIndexInAdGroup == period.getFirstAdIndexToPlay(adGroupIndex)
? period.getAdResumePositionUs() ? period.getAdResumePositionUs()
: 0; : 0;
if (durationUs != C.TIME_UNSET && startPositionUs >= durationUs) {
// Ensure start position doesn't exceed duration.
startPositionUs = Math.max(0, durationUs - 1);
}
return new MediaPeriodInfo( return new MediaPeriodInfo(
id, id,
startPositionUs, startPositionUs,
...@@ -761,6 +765,10 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -761,6 +765,10 @@ import com.google.android.exoplayer2.util.Assertions;
endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE
? period.durationUs ? period.durationUs
: endPositionUs; : endPositionUs;
if (durationUs != C.TIME_UNSET && startPositionUs >= durationUs) {
// Ensure start position doesn't exceed duration.
startPositionUs = Math.max(0, durationUs - 1);
}
return new MediaPeriodInfo( return new MediaPeriodInfo(
id, id,
startPositionUs, startPositionUs,
......
...@@ -369,16 +369,10 @@ public interface Renderer extends PlayerMessage.Target { ...@@ -369,16 +369,10 @@ public interface Renderer extends PlayerMessage.Target {
* {@link SampleStream} in sync with the specified media positions. * {@link SampleStream} in sync with the specified media positions.
* *
* <p>The renderer may also render the very start of the media at the current position (e.g. the * <p>The renderer may also render the very start of the media at the current position (e.g. the
* first frame of a video stream) while still in the {@link #STATE_ENABLED} state. It's not * first frame of a video stream) while still in the {@link #STATE_ENABLED} state, unless it's the
* allowed to do that in the following two cases: * initial start of the media after calling {@link #enable(RendererConfiguration, Format[],
* * SampleStream, long, boolean, boolean, long)} with {@code mayRenderStartOfStream} set to {@code
* <ol> * false}.
* <li>The initial start of the media after calling {@link #enable(RendererConfiguration,
* Format[], SampleStream, long, boolean, boolean, long)} with {@code
* mayRenderStartOfStream} set to {@code false}.
* <li>The start of a new stream after calling {@link #replaceStream(Format[], SampleStream,
* long)}.
* </ol>
* *
* <p>This method should return quickly, and should not block if the renderer is unable to make * <p>This method should return quickly, and should not block if the renderer is unable to make
* useful progress. * useful progress.
......
...@@ -91,8 +91,8 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba ...@@ -91,8 +91,8 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba
} }
/** /**
* Overrides the default prepare position at which to prepare the media period. This value is only * Overrides the default prepare position at which to prepare the media period. This method must
* used if called before {@link #createPeriod(MediaPeriodId)}. * be called before {@link #createPeriod(MediaPeriodId)}.
* *
* @param preparePositionUs The default prepare position to use, in microseconds. * @param preparePositionUs The default prepare position to use, in microseconds.
*/ */
...@@ -100,6 +100,11 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba ...@@ -100,6 +100,11 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba
preparePositionOverrideUs = preparePositionUs; preparePositionOverrideUs = preparePositionUs;
} }
/** Returns the prepare position override set by {@link #overridePreparePositionUs(long)}. */
public long getPreparePositionOverrideUs() {
return preparePositionOverrideUs;
}
/** /**
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source
* then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link * then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link
......
...@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.upstream.Allocator; ...@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** /**
* A {@link MediaSource} that masks the {@link Timeline} with a placeholder until the actual media * A {@link MediaSource} that masks the {@link Timeline} with a placeholder until the actual media
...@@ -143,6 +144,11 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> { ...@@ -143,6 +144,11 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
@Nullable MediaPeriodId idForMaskingPeriodPreparation = null; @Nullable MediaPeriodId idForMaskingPeriodPreparation = null;
if (isPrepared) { if (isPrepared) {
timeline = timeline.cloneWithUpdatedTimeline(newTimeline); timeline = timeline.cloneWithUpdatedTimeline(newTimeline);
if (unpreparedMaskingMediaPeriod != null) {
// Reset override in case the duration changed and we need to update our override.
setPreparePositionOverrideToUnpreparedMaskingPeriod(
unpreparedMaskingMediaPeriod.getPreparePositionOverrideUs());
}
} else if (newTimeline.isEmpty()) { } else if (newTimeline.isEmpty()) {
timeline = timeline =
hasRealTimeline hasRealTimeline
...@@ -182,7 +188,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> { ...@@ -182,7 +188,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
: MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid); : MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid);
if (unpreparedMaskingMediaPeriod != null) { if (unpreparedMaskingMediaPeriod != null) {
MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod;
maskingPeriod.overridePreparePositionUs(periodPositionUs); setPreparePositionOverrideToUnpreparedMaskingPeriod(periodPositionUs);
idForMaskingPeriodPreparation = idForMaskingPeriodPreparation =
maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid)); maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid));
} }
...@@ -225,6 +231,19 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> { ...@@ -225,6 +231,19 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
: internalPeriodUid; : internalPeriodUid;
} }
@RequiresNonNull("unpreparedMaskingMediaPeriod")
private void setPreparePositionOverrideToUnpreparedMaskingPeriod(long preparePositionOverrideUs) {
MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod;
long periodDurationUs = timeline.getPeriodByUid(maskingPeriod.id.periodUid, period).durationUs;
if (periodDurationUs != C.TIME_UNSET) {
// Ensure the overridden position doesn't exceed the period duration.
if (preparePositionOverrideUs >= periodDurationUs) {
preparePositionOverrideUs = Math.max(0, periodDurationUs - 1);
}
}
maskingPeriod.overridePreparePositionUs(preparePositionOverrideUs);
}
/** /**
* Timeline used as placeholder for an unprepared media source. After preparation, a * Timeline used as placeholder for an unprepared media source. After preparation, a
* MaskingTimeline is used to keep the originally assigned dummy period ID. * MaskingTimeline is used to keep the originally assigned dummy period ID.
......
...@@ -275,7 +275,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { ...@@ -275,7 +275,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer {
protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
// TODO: This shouldn't just update the output stream offset as long as there are still buffers // TODO: This shouldn't just update the output stream offset as long as there are still buffers
// of the previous stream in the decoder. It should also make sure to render the first frame of // of the previous stream in the decoder. It should also make sure to render the first frame of
// the next stream if the playback position reached the new stream and the renderer is started. // the next stream if the playback position reached the new stream.
outputStreamOffsetUs = offsetUs; outputStreamOffsetUs = offsetUs;
super.onStreamChanged(formats, offsetUs); super.onStreamChanged(formats, offsetUs);
} }
......
...@@ -976,6 +976,70 @@ public final class ExoPlayerTest { ...@@ -976,6 +976,70 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void seekBeforePreparationCompletes_seeksToCorrectPosition() throws Exception {
CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1);
FakeMediaPeriod[] fakeMediaPeriodHolder = new FakeMediaPeriod[1];
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
FakeMediaSource mediaSource =
new FakeMediaSource(/* timeline= */ null, Builder.VIDEO_FORMAT) {
@Override
protected FakeMediaPeriod createFakeMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
EventDispatcher eventDispatcher,
@Nullable TransferListener transferListener) {
// Defer completing preparation of the period until seek has been sent.
fakeMediaPeriodHolder[0] =
new FakeMediaPeriod(trackGroupArray, eventDispatcher, /* deferOnPrepared= */ true);
createPeriodCalledCountDownLatch.countDown();
return fakeMediaPeriodHolder[0];
}
};
AtomicLong positionWhenReady = new AtomicLong();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
// Ensure we use the MaskingMediaPeriod by delaying the initial timeline update.
.delay(1)
.executeRunnable(() -> mediaSource.setNewSourceInfo(timeline))
.waitForTimelineChanged()
// Block until createPeriod has been called on the fake media source.
.executeRunnable(
() -> {
try {
createPeriodCalledCountDownLatch.await();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
})
// Seek before preparation completes.
.seek(5000)
// Complete preparation of the fake media period.
.executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete())
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
positionWhenReady.set(player.getCurrentPosition());
}
})
.play()
.build();
new ExoPlayerTestRunner.Builder()
.initialSeek(/* windowIndex= */ 0, /* positionMs= */ 2000)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build(context)
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(positionWhenReady.get()).isAtLeast(5000);
}
@Test
public void stopDoesNotResetPosition() throws Exception { public void stopDoesNotResetPosition() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final long[] positionHolder = new long[1]; final long[] positionHolder = new long[1];
......
...@@ -184,7 +184,7 @@ public final class MediaPeriodQueueTest { ...@@ -184,7 +184,7 @@ public final class MediaPeriodQueueTest {
advance(); advance();
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* periodUid= */ firstPeriodUid, /* periodUid= */ firstPeriodUid,
/* startPositionUs= */ CONTENT_DURATION_US, /* startPositionUs= */ CONTENT_DURATION_US - 1,
/* requestedContentPositionUs= */ CONTENT_DURATION_US, /* requestedContentPositionUs= */ CONTENT_DURATION_US,
/* endPositionUs= */ C.TIME_UNSET, /* endPositionUs= */ C.TIME_UNSET,
/* durationUs= */ CONTENT_DURATION_US, /* durationUs= */ CONTENT_DURATION_US,
...@@ -209,7 +209,7 @@ public final class MediaPeriodQueueTest { ...@@ -209,7 +209,7 @@ public final class MediaPeriodQueueTest {
setAdGroupFailedToLoad(/* adGroupIndex= */ 0); setAdGroupFailedToLoad(/* adGroupIndex= */ 0);
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* periodUid= */ firstPeriodUid, /* periodUid= */ firstPeriodUid,
/* startPositionUs= */ CONTENT_DURATION_US, /* startPositionUs= */ CONTENT_DURATION_US - 1,
/* requestedContentPositionUs= */ CONTENT_DURATION_US, /* requestedContentPositionUs= */ CONTENT_DURATION_US,
/* endPositionUs= */ C.TIME_UNSET, /* endPositionUs= */ C.TIME_UNSET,
/* durationUs= */ CONTENT_DURATION_US, /* durationUs= */ CONTENT_DURATION_US,
......
...@@ -674,13 +674,14 @@ public final class AnalyticsCollectorTest { ...@@ -674,13 +674,14 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED))
.containsExactly(window0Period1Seq0, window1Period0Seq1); .containsExactly(window0Period1Seq0, window1Period0Seq1);
assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(window0Period1Seq0); assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(window0Period1Seq0);
assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(window0Period1Seq0); assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES))
.containsExactly(window0Period1Seq0, period1Seq0);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED))
.containsExactly(window0Period1Seq0, window1Period0Seq1, period1Seq0); .containsExactly(window0Period1Seq0, window1Period0Seq1, period1Seq0);
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))
.containsExactly(window0Period1Seq0, window1Period0Seq1, period1Seq0); .containsExactly(window0Period1Seq0, window1Period0Seq1, period1Seq0);
assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)) assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET))
.containsExactly(window0Period1Seq0); .containsExactly(window0Period1Seq0, period1Seq0);
listener.assertNoMoreEvents(); listener.assertNoMoreEvents();
} }
...@@ -1216,8 +1217,8 @@ public final class AnalyticsCollectorTest { ...@@ -1216,8 +1217,8 @@ public final class AnalyticsCollectorTest {
private Format format; private Format format;
private long streamOffsetUs; private long streamOffsetUs;
private boolean renderedFirstFrameAfterReset; private boolean renderedFirstFrameAfterReset;
private boolean mayRenderFirstFrameAfterStreamChangeIfNotStarted; private boolean mayRenderFirstFrameAfterEnableIfNotStarted;
private boolean renderedFirstFrameAfterStreamChange; private boolean renderedFirstFrameAfterEnable;
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) { public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {
super(ExoPlayerTestRunner.Builder.VIDEO_FORMAT); super(ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
...@@ -1230,8 +1231,8 @@ public final class AnalyticsCollectorTest { ...@@ -1230,8 +1231,8 @@ public final class AnalyticsCollectorTest {
throws ExoPlaybackException { throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream); super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters); eventDispatcher.enabled(decoderCounters);
mayRenderFirstFrameAfterStreamChangeIfNotStarted = mayRenderStartOfStream; mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream;
renderedFirstFrameAfterStreamChange = false; renderedFirstFrameAfterEnable = false;
} }
@Override @Override
...@@ -1240,8 +1241,6 @@ public final class AnalyticsCollectorTest { ...@@ -1240,8 +1241,6 @@ public final class AnalyticsCollectorTest {
streamOffsetUs = offsetUs; streamOffsetUs = offsetUs;
if (renderedFirstFrameAfterReset) { if (renderedFirstFrameAfterReset) {
renderedFirstFrameAfterReset = false; renderedFirstFrameAfterReset = false;
renderedFirstFrameAfterStreamChange = false;
mayRenderFirstFrameAfterStreamChangeIfNotStarted = false;
} }
} }
...@@ -1279,9 +1278,8 @@ public final class AnalyticsCollectorTest { ...@@ -1279,9 +1278,8 @@ public final class AnalyticsCollectorTest {
protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) { protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) {
boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs); boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
boolean shouldRenderFirstFrame = boolean shouldRenderFirstFrame =
!renderedFirstFrameAfterStreamChange !renderedFirstFrameAfterEnable
? (getState() == Renderer.STATE_STARTED ? (getState() == Renderer.STATE_STARTED || mayRenderFirstFrameAfterEnableIfNotStarted)
|| mayRenderFirstFrameAfterStreamChangeIfNotStarted)
: !renderedFirstFrameAfterReset; : !renderedFirstFrameAfterReset;
shouldProcess |= shouldRenderFirstFrame && playbackPositionUs >= streamOffsetUs; shouldProcess |= shouldRenderFirstFrame && playbackPositionUs >= streamOffsetUs;
if (shouldProcess && !renderedFirstFrameAfterReset) { if (shouldProcess && !renderedFirstFrameAfterReset) {
...@@ -1289,7 +1287,7 @@ public final class AnalyticsCollectorTest { ...@@ -1289,7 +1287,7 @@ public final class AnalyticsCollectorTest {
format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio); format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio);
eventDispatcher.renderedFirstFrame(/* surface= */ null); eventDispatcher.renderedFirstFrame(/* surface= */ null);
renderedFirstFrameAfterReset = true; renderedFirstFrameAfterReset = true;
renderedFirstFrameAfterStreamChange = true; renderedFirstFrameAfterEnable = true;
} }
return shouldProcess; return shouldProcess;
} }
......
...@@ -255,7 +255,7 @@ public final class SimpleDecoderVideoRendererTest { ...@@ -255,7 +255,7 @@ public final class SimpleDecoderVideoRendererTest {
verify(eventListener).onRenderedFirstFrame(any()); verify(eventListener).onRenderedFirstFrame(any());
} }
// TODO: First frame of replaced stream are not yet reported. // TODO: Fix rendering of first frame at stream transition.
@Ignore @Ignore
@Test @Test
public void replaceStream_whenStarted_rendersFirstFrameOfNewStream() throws Exception { public void replaceStream_whenStarted_rendersFirstFrameOfNewStream() throws Exception {
...@@ -286,7 +286,7 @@ public final class SimpleDecoderVideoRendererTest { ...@@ -286,7 +286,7 @@ public final class SimpleDecoderVideoRendererTest {
renderer.start(); renderer.start();
boolean replacedStream = false; boolean replacedStream = false;
for (int i = 0; i < 200; i += 10) { for (int i = 0; i <= 10; i++) {
renderer.render(/* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000); renderer.render(/* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000);
if (!replacedStream && renderer.hasReadStreamToEnd()) { if (!replacedStream && renderer.hasReadStreamToEnd()) {
renderer.replaceStream(new Format[] {H264_FORMAT}, fakeSampleStream2, /* offsetUs= */ 100); renderer.replaceStream(new Format[] {H264_FORMAT}, fakeSampleStream2, /* offsetUs= */ 100);
...@@ -299,6 +299,8 @@ public final class SimpleDecoderVideoRendererTest { ...@@ -299,6 +299,8 @@ public final class SimpleDecoderVideoRendererTest {
verify(eventListener, times(2)).onRenderedFirstFrame(any()); verify(eventListener, times(2)).onRenderedFirstFrame(any());
} }
// TODO: Fix rendering of first frame at stream transition.
@Ignore
@Test @Test
public void replaceStream_whenNotStarted_doesNotRenderFirstFrameOfNewStream() throws Exception { public void replaceStream_whenNotStarted_doesNotRenderFirstFrameOfNewStream() throws Exception {
FakeSampleStream fakeSampleStream1 = FakeSampleStream fakeSampleStream1 =
...@@ -327,7 +329,7 @@ public final class SimpleDecoderVideoRendererTest { ...@@ -327,7 +329,7 @@ public final class SimpleDecoderVideoRendererTest {
/* offsetUs */ 0); /* offsetUs */ 0);
boolean replacedStream = false; boolean replacedStream = false;
for (int i = 0; i < 200; i += 10) { for (int i = 0; i < 10; i++) {
renderer.render(/* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000); renderer.render(/* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000);
if (!replacedStream && renderer.hasReadStreamToEnd()) { if (!replacedStream && renderer.hasReadStreamToEnd()) {
renderer.replaceStream(new Format[] {H264_FORMAT}, fakeSampleStream2, /* offsetUs= */ 100); renderer.replaceStream(new Format[] {H264_FORMAT}, fakeSampleStream2, /* offsetUs= */ 100);
...@@ -338,5 +340,10 @@ public final class SimpleDecoderVideoRendererTest { ...@@ -338,5 +340,10 @@ public final class SimpleDecoderVideoRendererTest {
} }
verify(eventListener).onRenderedFirstFrame(any()); verify(eventListener).onRenderedFirstFrame(any());
// Render to streamOffsetUs and verify the new first frame gets rendered.
renderer.render(/* positionUs= */ 100, SystemClock.elapsedRealtime() * 1000);
verify(eventListener, times(2)).onRenderedFirstFrame(any());
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment