mirror of
https://github.com/androidx/media.git
synced 2025-05-17 12:39:52 +08:00
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
This commit is contained in:
parent
bb33d568ca
commit
c6e8e24ada
@ -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;
|
|||||||
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;
|
|||||||
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;
|
|||||||
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;
|
|||||||
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 {
|
|||||||
* {@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
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
|||||||
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;
|
|||||||
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> {
|
|||||||
@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> {
|
|||||||
: 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> {
|
|||||||
: 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 {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -975,6 +975,70 @@ public final class ExoPlayerTest {
|
|||||||
.blockUntilEnded(TIMEOUT_MS);
|
.blockUntilEnded(TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@Test
|
||||||
public void stopDoesNotResetPosition() throws Exception {
|
public void stopDoesNotResetPosition() throws Exception {
|
||||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
||||||
|
@ -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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
streamOffsetUs = offsetUs;
|
streamOffsetUs = offsetUs;
|
||||||
if (renderedFirstFrameAfterReset) {
|
if (renderedFirstFrameAfterReset) {
|
||||||
renderedFirstFrameAfterReset = false;
|
renderedFirstFrameAfterReset = false;
|
||||||
renderedFirstFrameAfterStreamChange = false;
|
|
||||||
mayRenderFirstFrameAfterStreamChangeIfNotStarted = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/* 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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user