Add parameter to Renderer.enable to allow rendering of first sample.

PiperOrigin-RevId: 295985916
This commit is contained in:
tonihei 2020-02-19 17:25:40 +00:00 committed by Oliver Woodman
parent c95ed7d18c
commit 72f4b964a5
13 changed files with 492 additions and 109 deletions

View file

@ -82,13 +82,19 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
}
@Override
public final void enable(RendererConfiguration configuration, Format[] formats,
SampleStream stream, long positionUs, boolean joining, long offsetUs)
public final void enable(
RendererConfiguration configuration,
Format[] formats,
SampleStream stream,
long positionUs,
boolean joining,
boolean mayRenderStartOfStream,
long offsetUs)
throws ExoPlaybackException {
Assertions.checkState(state == STATE_DISABLED);
this.configuration = configuration;
state = STATE_ENABLED;
onEnabled(joining);
onEnabled(joining, mayRenderStartOfStream);
replaceStream(formats, stream, offsetUs);
onPositionReset(positionUs, joining);
}
@ -193,27 +199,30 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
/**
* Called when the renderer is enabled.
* <p>
* The default implementation is a no-op.
*
* <p>The default implementation is a no-op.
*
* @param joining Whether this renderer is being enabled to join an ongoing playback.
* @param mayRenderStartOfStream Whether this renderer is allowed to render the start of the
* stream even if the state is not {@link #STATE_STARTED} yet.
* @throws ExoPlaybackException If an error occurs.
*/
protected void onEnabled(boolean joining) throws ExoPlaybackException {
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
// Do nothing.
}
/**
* Called when the renderer's stream has changed. This occurs when the renderer is enabled after
* {@link #onEnabled(boolean)} has been called, and also when the stream has been replaced whilst
* the renderer is enabled or started.
* <p>
* The default implementation is a no-op.
* {@link #onEnabled(boolean, boolean)} has been called, and also when the stream has been
* replaced whilst the renderer is enabled or started.
*
* <p>The default implementation is a no-op.
*
* @param formats The enabled formats.
* @param offsetUs The offset that will be added to the timestamps of buffers read via
* {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} so that decoder input
* buffers have monotonically increasing timestamps.
* @param offsetUs The offset that will be added to the timestamps of buffers read via {@link
* #readSource(FormatHolder, DecoderInputBuffer, boolean)} so that decoder input buffers have
* monotonically increasing timestamps.
* @throws ExoPlaybackException If an error occurs.
*/
protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {

View file

@ -2002,6 +2002,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
playingPeriodHolder.sampleStreams[rendererIndex],
rendererPositionUs,
joining,
/* mayRenderStartOfStream= */ true,
playingPeriodHolder.getRendererOffset());
mediaClock.onRendererEnabled(renderer);
// Start the renderer if playing.

View file

@ -60,24 +60,15 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
return state;
}
/**
* Replaces the {@link SampleStream} that will be associated with this renderer.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_DISABLED}.
*
* @param configuration The renderer configuration.
* @param formats The enabled formats. Should be empty.
* @param stream The {@link SampleStream} from which the renderer should consume.
* @param positionUs The player's current position.
* @param joining Whether this renderer is being enabled to join an ongoing playback.
* @param offsetUs The offset that should be subtracted from {@code positionUs}
* to get the playback position with respect to the media.
* @throws ExoPlaybackException If an error occurs.
*/
@Override
public final void enable(RendererConfiguration configuration, Format[] formats,
SampleStream stream, long positionUs, boolean joining, long offsetUs)
public final void enable(
RendererConfiguration configuration,
Format[] formats,
SampleStream stream,
long positionUs,
boolean joining,
boolean mayRenderStartOfStream,
long offsetUs)
throws ExoPlaybackException {
Assertions.checkState(state == STATE_DISABLED);
this.configuration = configuration;
@ -94,18 +85,6 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
onStarted();
}
/**
* Replaces the {@link SampleStream} that will be associated with this renderer.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
*
* @param formats The enabled formats. Should be empty.
* @param stream The {@link SampleStream} to be associated with this renderer.
* @param offsetUs The offset that should be subtracted from {@code positionUs} in
* {@link #render(long, long)} to get the playback position with respect to the media.
* @throws ExoPlaybackException If an error occurs.
*/
@Override
public final void replaceStream(Format[] formats, SampleStream stream, long offsetUs)
throws ExoPlaybackException {

View file

@ -222,21 +222,30 @@ public interface Renderer extends PlayerMessage.Target {
/**
* Enables the renderer to consume from the specified {@link SampleStream}.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_DISABLED}.
*
* <p>This method may be called when the renderer is in the following states: {@link
* #STATE_DISABLED}.
*
* @param configuration The renderer configuration.
* @param formats The enabled formats.
* @param stream The {@link SampleStream} from which the renderer should consume.
* @param positionUs The player's current position.
* @param joining Whether this renderer is being enabled to join an ongoing playback.
* @param offsetUs The offset to be added to timestamps of buffers read from {@code stream}
* before they are rendered.
* @param mayRenderStartOfStream Whether this renderer is allowed to render the start of the
* stream even if the state is not {@link #STATE_STARTED} yet.
* @param offsetUs The offset to be added to timestamps of buffers read from {@code stream} before
* they are rendered.
* @throws ExoPlaybackException If an error occurs.
*/
void enable(RendererConfiguration configuration, Format[] formats, SampleStream stream,
long positionUs, boolean joining, long offsetUs) throws ExoPlaybackException;
void enable(
RendererConfiguration configuration,
Format[] formats,
SampleStream stream,
long positionUs,
boolean joining,
boolean mayRenderStartOfStream,
long offsetUs)
throws ExoPlaybackException;
/**
* Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be
@ -341,21 +350,32 @@ public interface Renderer extends PlayerMessage.Target {
/**
* Incrementally renders the {@link SampleStream}.
* <p>
* If the renderer is in the {@link #STATE_ENABLED} state then each call to this method will do
* work toward being ready to render the {@link SampleStream} when the renderer is started. It may
* also render the very start of the media, for example the first frame of a video stream. If the
*
* <p>If the renderer is in the {@link #STATE_ENABLED} state then each call to this method will do
* work toward being ready to render the {@link SampleStream} when the renderer is started. If the
* renderer is in the {@link #STATE_STARTED} state then calls to this method will render the
* {@link SampleStream} in sync with the specified media positions.
* <p>
* This method should return quickly, and should not block if the renderer is unable to make
* useful progress.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
*
* @param positionUs The current media time in microseconds, measured at the start of the
* current iteration of the rendering loop.
* <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
* allowed to do that in the following two cases:
*
* <ol>
* <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
* useful progress.
*
* <p>This method may be called when the renderer is in the following states: {@link
* #STATE_ENABLED}, {@link #STATE_STARTED}.
*
* @param positionUs The current media time in microseconds, measured at the start of the current
* iteration of the rendering loop.
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
* measured at the start of the current iteration of the rendering loop.
* @throws ExoPlaybackException If an error occurs.

View file

@ -491,8 +491,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
super.onEnabled(joining);
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters);
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {

View file

@ -495,7 +495,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
decoderCounters = new DecoderCounters();
eventDispatcher.enabled(decoderCounters);
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;

View file

@ -679,7 +679,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
decoderCounters = new DecoderCounters();
}

View file

@ -129,7 +129,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private Surface surface;
private Surface dummySurface;
@VideoScalingMode private int scalingMode;
private boolean renderedFirstFrame;
private boolean renderedFirstFrameAfterReset;
private boolean mayRenderFirstFrameAfterEnableIfNotStarted;
private boolean renderedFirstFrameAfterEnable;
private long initialPositionUs;
private long joiningDeadlineMs;
private long droppedFrameAccumulationStartTimeMs;
@ -360,8 +362,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
super.onEnabled(joining);
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream);
int oldTunnelingAudioSessionId = tunnelingAudioSessionId;
tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
tunneling = tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET;
@ -370,6 +373,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
eventDispatcher.enabled(decoderCounters);
frameReleaseTimeHelper.enable();
mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream;
renderedFirstFrameAfterEnable = false;
}
@Override
@ -387,8 +392,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override
public boolean isReady() {
if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface)
|| getCodec() == null || tunneling)) {
if (super.isReady()
&& (renderedFirstFrameAfterReset
|| (dummySurface != null && surface == dummySurface)
|| getCodec() == null
|| tunneling)) {
// Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineMs = C.TIME_UNSET;
return true;
@ -729,11 +737,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
long elapsedSinceLastRenderUs = elapsedRealtimeNowUs - lastRenderTimeUs;
boolean isStarted = getState() == STATE_STARTED;
boolean shouldRenderFirstFrame =
!renderedFirstFrameAfterEnable
? (isStarted || mayRenderFirstFrameAfterEnableIfNotStarted)
: !renderedFirstFrameAfterReset;
// Don't force output until we joined and the position reached the current stream.
boolean forceRenderOutputBuffer =
joiningDeadlineMs == C.TIME_UNSET
&& positionUs >= outputStreamOffsetUs
&& (!renderedFirstFrame
&& (shouldRenderFirstFrame
|| (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs)));
if (forceRenderOutputBuffer) {
long releaseTimeNs = System.nanoTime();
@ -1056,7 +1068,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
private void clearRenderedFirstFrame() {
renderedFirstFrame = false;
renderedFirstFrameAfterReset = false;
// The first frame notification is triggered by renderOutputBuffer or renderOutputBufferV21 for
// non-tunneled playback, onQueueInputBuffer for tunneled playback prior to API level 23, and
// OnFrameRenderedListenerV23.onFrameRenderedListener for tunneled playback on API level 23 and
@ -1071,14 +1083,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
/* package */ void maybeNotifyRenderedFirstFrame() {
if (!renderedFirstFrame) {
renderedFirstFrame = true;
renderedFirstFrameAfterEnable = true;
if (!renderedFirstFrameAfterReset) {
renderedFirstFrameAfterReset = true;
eventDispatcher.renderedFirstFrame(surface);
}
}
private void maybeRenotifyRenderedFirstFrame() {
if (renderedFirstFrame) {
if (renderedFirstFrameAfterReset) {
eventDispatcher.renderedFirstFrame(surface);
}
}

View file

@ -92,7 +92,9 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer {
@ReinitializationState private int decoderReinitializationState;
private boolean decoderReceivedBuffers;
private boolean renderedFirstFrame;
private boolean renderedFirstFrameAfterReset;
private boolean mayRenderFirstFrameAfterEnableIfNotStarted;
private boolean renderedFirstFrameAfterEnable;
private long initialPositionUs;
private long joiningDeadlineMs;
private boolean waitingForKeys;
@ -195,7 +197,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer {
}
if (inputFormat != null
&& (isSourceReady() || outputBuffer != null)
&& (renderedFirstFrame || !hasOutput())) {
&& (renderedFirstFrameAfterReset || !hasOutput())) {
// Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineMs = C.TIME_UNSET;
return true;
@ -215,9 +217,12 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer {
// Protected methods.
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
decoderCounters = new DecoderCounters();
eventDispatcher.enabled(decoderCounters);
mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream;
renderedFirstFrameAfterEnable = false;
}
@Override
@ -267,6 +272,9 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer {
@Override
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
// 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.
outputStreamOffsetUs = offsetUs;
super.onStreamChanged(formats, offsetUs);
}
@ -787,10 +795,15 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer {
}
long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
long elapsedSinceLastRenderUs = elapsedRealtimeNowUs - lastRenderTimeUs;
boolean isStarted = getState() == STATE_STARTED;
if (!renderedFirstFrame
|| (isStarted
&& shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {
boolean shouldRenderFirstFrame =
!renderedFirstFrameAfterEnable
? (isStarted || mayRenderFirstFrameAfterEnableIfNotStarted)
: !renderedFirstFrameAfterReset;
// TODO: We shouldn't force render while we are joining an ongoing playback.
if (shouldRenderFirstFrame
|| (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs))) {
renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat);
return true;
}
@ -799,6 +812,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer {
return false;
}
// TODO: Treat dropped buffers as skipped while we are joining an ongoing playback.
if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs)
&& maybeDropBuffersToKeyframe(positionUs)) {
return false;
@ -862,18 +876,19 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer {
}
private void clearRenderedFirstFrame() {
renderedFirstFrame = false;
renderedFirstFrameAfterReset = false;
}
private void maybeNotifyRenderedFirstFrame() {
if (!renderedFirstFrame) {
renderedFirstFrame = true;
renderedFirstFrameAfterEnable = true;
if (!renderedFirstFrameAfterReset) {
renderedFirstFrameAfterReset = true;
eventDispatcher.renderedFirstFrame(surface);
}
}
private void maybeRenotifyRenderedFirstFrame() {
if (renderedFirstFrame) {
if (renderedFirstFrameAfterReset) {
eventDispatcher.renderedFirstFrame(surface);
}
}

View file

@ -5606,7 +5606,8 @@ public final class ExoPlayerTest {
FakeRenderer audioRenderer =
new FakeRenderer(Builder.AUDIO_FORMAT) {
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
// Fail when enabling the renderer. This will happen during the period transition.
throw createRendererException(new IllegalStateException(), Builder.AUDIO_FORMAT);
}
@ -5672,7 +5673,8 @@ public final class ExoPlayerTest {
FakeRenderer audioRenderer =
new FakeRenderer(Builder.AUDIO_FORMAT) {
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
// Fail when enabling the renderer. This will happen during the playlist update.
throw createRendererException(new IllegalStateException(), Builder.AUDIO_FORMAT);
}

View file

@ -241,8 +241,8 @@ public final class AnalyticsCollectorTest {
period0 /* audio */, period0 /* video */, period1 /* audio */, period1 /* video */);
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period1);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0, period1);
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0, period1);
assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)).containsExactly(period1);
listener.assertNoMoreEvents();
}
@ -444,8 +444,10 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1Seq2);
assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES))
.containsExactly(period0, period1Seq2, period1Seq2);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0, period0);
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0, period0);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED))
.containsExactly(period0, period1Seq1, period0, period1Seq2);
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))
.containsExactly(period0, period1Seq1, period0, period1Seq2);
assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET))
.containsExactly(period0, period1Seq2, period1Seq2);
listener.assertNoMoreEvents();
@ -672,9 +674,9 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(window0Period1Seq0);
assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(window0Period1Seq0);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED))
.containsExactly(window0Period1Seq0, period1Seq0);
.containsExactly(window0Period1Seq0, window1Period0Seq1, period1Seq0);
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))
.containsExactly(window0Period1Seq0, period1Seq0);
.containsExactly(window0Period1Seq0, window1Period0Seq1, period1Seq0);
assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET))
.containsExactly(window0Period1Seq0);
listener.assertNoMoreEvents();
@ -964,8 +966,22 @@ public final class AnalyticsCollectorTest {
contentAfterPostroll);
assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES))
.containsExactly(contentAfterPreroll, contentAfterMidroll, contentAfterPostroll);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(prerollAd);
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(prerollAd);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED))
.containsExactly(
prerollAd,
contentAfterPreroll,
midrollAd,
contentAfterMidroll,
postrollAd,
contentAfterPostroll);
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))
.containsExactly(
prerollAd,
contentAfterPreroll,
midrollAd,
contentAfterMidroll,
postrollAd,
contentAfterPostroll);
assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET))
.containsExactly(contentAfterPreroll, contentAfterMidroll, contentAfterPostroll);
listener.assertNoMoreEvents();
@ -1082,9 +1098,9 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(contentBeforeMidroll);
assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(contentAfterMidroll);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED))
.containsExactly(contentBeforeMidroll, midrollAd);
.containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll);
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))
.containsExactly(contentBeforeMidroll, midrollAd);
.containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll);
assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET))
.containsExactly(contentAfterMidroll);
listener.assertNoMoreEvents();
@ -1194,7 +1210,10 @@ public final class AnalyticsCollectorTest {
private final VideoRendererEventListener.EventDispatcher eventDispatcher;
private final DecoderCounters decoderCounters;
private Format format;
private boolean renderedFirstFrame;
private long streamOffsetUs;
private boolean renderedFirstFrameAfterReset;
private boolean mayRenderFirstFrameAfterStreamChangeIfNotStarted;
private boolean renderedFirstFrameAfterStreamChange;
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {
super(ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
@ -1203,10 +1222,23 @@ public final class AnalyticsCollectorTest {
}
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
super.onEnabled(joining);
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters);
renderedFirstFrame = false;
mayRenderFirstFrameAfterStreamChangeIfNotStarted = mayRenderStartOfStream;
renderedFirstFrameAfterStreamChange = false;
}
@Override
protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
super.onStreamChanged(formats, offsetUs);
streamOffsetUs = offsetUs;
if (renderedFirstFrameAfterReset) {
renderedFirstFrameAfterReset = false;
renderedFirstFrameAfterStreamChange = false;
mayRenderFirstFrameAfterStreamChangeIfNotStarted = false;
}
}
@Override
@ -1226,7 +1258,7 @@ public final class AnalyticsCollectorTest {
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
super.onPositionReset(positionUs, joining);
renderedFirstFrame = false;
renderedFirstFrameAfterReset = false;
}
@Override
@ -1242,11 +1274,18 @@ public final class AnalyticsCollectorTest {
@Override
protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) {
boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
if (shouldProcess && !renderedFirstFrame) {
boolean shouldRenderFirstFrame =
!renderedFirstFrameAfterStreamChange
? (getState() == Renderer.STATE_STARTED
|| mayRenderFirstFrameAfterStreamChangeIfNotStarted)
: !renderedFirstFrameAfterReset;
shouldProcess |= shouldRenderFirstFrame && playbackPositionUs >= streamOffsetUs;
if (shouldProcess && !renderedFirstFrameAfterReset) {
eventDispatcher.videoSizeChanged(
format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio);
eventDispatcher.renderedFirstFrame(/* surface= */ null);
renderedFirstFrame = true;
renderedFirstFrameAfterReset = true;
renderedFirstFrameAfterStreamChange = true;
}
return shouldProcess;
}
@ -1265,8 +1304,9 @@ public final class AnalyticsCollectorTest {
}
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
super.onEnabled(joining);
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters);
notifiedAudioSessionId = false;
}

View file

@ -97,9 +97,10 @@ public class SimpleDecoderAudioRendererTest {
RendererConfiguration.DEFAULT,
new Format[] {FORMAT},
new FakeSampleStream(FORMAT, /* eventDispatcher= */ null, /* shouldOutputSample= */ false),
0,
false,
0);
/* positionUs= */ 0,
/* joining= */ false,
/* mayRenderStartOfStream= */ true,
/* offsetUs= */ 0);
audioRenderer.setCurrentStreamFinal();
when(mockAudioSink.isEnded()).thenReturn(true);
while (!audioRenderer.isEnded()) {

View file

@ -0,0 +1,300 @@
/*
* Copyright (C) 2020 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.video;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.graphics.SurfaceTexture;
import android.os.Handler;
import android.os.SystemClock;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.RendererConfiguration;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.testutil.FakeSampleStream;
import com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem;
import com.google.android.exoplayer2.util.MimeTypes;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Unit test for {@link SimpleDecoderVideoRenderer}. */
@RunWith(AndroidJUnit4.class)
public final class SimpleDecoderVideoRendererTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private static final Format BASIC_MP4_1080 =
Format.createVideoSampleFormat(
/* id= */ null,
/* sampleMimeType= */ MimeTypes.VIDEO_MP4,
/* codecs= */ null,
/* bitrate= */ Format.NO_VALUE,
/* maxInputSize= */ Format.NO_VALUE,
/* width= */ 1920,
/* height= */ 1080,
/* frameRate= */ Format.NO_VALUE,
/* initializationData= */ null,
/* rotationDegrees= */ 0,
/* pixelWidthHeightRatio= */ 1f,
/* drmInitData= */ null);
private SimpleDecoderVideoRenderer renderer;
@Mock private VideoRendererEventListener eventListener;
@Before
public void setUp() {
renderer =
new SimpleDecoderVideoRenderer(
/* allowedJoiningTimeMs= */ 0,
new Handler(),
eventListener,
/* maxDroppedFramesToNotify= */ -1) {
@C.VideoOutputMode private int outputMode;
@Override
@Capabilities
public int supportsFormat(Format format) {
return RendererCapabilities.create(FORMAT_HANDLED);
}
@Override
protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
this.outputMode = outputMode;
}
@Override
protected void renderOutputBufferToSurface(
VideoDecoderOutputBuffer outputBuffer, Surface surface) {
// Do nothing.
}
@Override
protected SimpleDecoder<
VideoDecoderInputBuffer,
? extends VideoDecoderOutputBuffer,
? extends VideoDecoderException>
createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) {
return new SimpleDecoder<
VideoDecoderInputBuffer, VideoDecoderOutputBuffer, VideoDecoderException>(
new VideoDecoderInputBuffer[10], new VideoDecoderOutputBuffer[10]) {
@Override
protected VideoDecoderInputBuffer createInputBuffer() {
return new VideoDecoderInputBuffer();
}
@Override
protected VideoDecoderOutputBuffer createOutputBuffer() {
return new VideoDecoderOutputBuffer(this::releaseOutputBuffer);
}
@Override
protected VideoDecoderException createUnexpectedDecodeException(Throwable error) {
return new VideoDecoderException("error", error);
}
@Nullable
@Override
protected VideoDecoderException decode(
VideoDecoderInputBuffer inputBuffer,
VideoDecoderOutputBuffer outputBuffer,
boolean reset) {
outputBuffer.init(inputBuffer.timeUs, outputMode, /* supplementalData= */ null);
return null;
}
@Override
public String getName() {
return "TestDecoder";
}
};
}
};
renderer.setOutputSurface(new Surface(new SurfaceTexture(/* texName= */ 0)));
}
@Test
public void enable_withMayRenderStartOfStream_rendersFirstFrameBeforeStart() throws Exception {
FakeSampleStream fakeSampleStream =
new FakeSampleStream(
/* format= */ BASIC_MP4_1080,
/* eventDispatcher= */ null,
/* firstSampleTimeUs= */ 0,
/* timeUsIncrement= */ 50,
new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME));
renderer.enable(
RendererConfiguration.DEFAULT,
new Format[] {BASIC_MP4_1080},
fakeSampleStream,
/* positionUs= */ 0,
/* joining= */ false,
/* mayRenderStartOfStream= */ true,
/* offsetUs */ 0);
for (int i = 0; i < 10; i++) {
renderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000);
}
verify(eventListener).onRenderedFirstFrame(any());
}
@Test
public void enable_withoutMayRenderStartOfStream_doesNotRenderFirstFrameBeforeStart()
throws Exception {
FakeSampleStream fakeSampleStream =
new FakeSampleStream(
/* format= */ BASIC_MP4_1080,
/* eventDispatcher= */ null,
/* firstSampleTimeUs= */ 0,
/* timeUsIncrement= */ 50,
new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME));
renderer.enable(
RendererConfiguration.DEFAULT,
new Format[] {BASIC_MP4_1080},
fakeSampleStream,
/* positionUs= */ 0,
/* joining= */ false,
/* mayRenderStartOfStream= */ false,
/* offsetUs */ 0);
for (int i = 0; i < 10; i++) {
renderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000);
}
verify(eventListener, never()).onRenderedFirstFrame(any());
}
@Test
public void enable_withoutMayRenderStartOfStream_rendersFirstFrameAfterStart() throws Exception {
FakeSampleStream fakeSampleStream =
new FakeSampleStream(
/* format= */ BASIC_MP4_1080,
/* eventDispatcher= */ null,
/* firstSampleTimeUs= */ 0,
/* timeUsIncrement= */ 50,
new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME));
renderer.enable(
RendererConfiguration.DEFAULT,
new Format[] {BASIC_MP4_1080},
fakeSampleStream,
/* positionUs= */ 0,
/* joining= */ false,
/* mayRenderStartOfStream= */ false,
/* offsetUs */ 0);
renderer.start();
for (int i = 0; i < 10; i++) {
renderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000);
}
verify(eventListener).onRenderedFirstFrame(any());
}
// TODO: First frame of replaced stream are not yet reported.
@Ignore
@Test
public void replaceStream_whenStarted_rendersFirstFrameOfNewStream() throws Exception {
FakeSampleStream fakeSampleStream1 =
new FakeSampleStream(
/* format= */ BASIC_MP4_1080,
/* eventDispatcher= */ null,
/* firstSampleTimeUs= */ 0,
/* timeUsIncrement= */ 50,
new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME),
FakeSampleStreamItem.END_OF_STREAM_ITEM);
FakeSampleStream fakeSampleStream2 =
new FakeSampleStream(
/* format= */ BASIC_MP4_1080,
/* eventDispatcher= */ null,
/* firstSampleTimeUs= */ 0,
/* timeUsIncrement= */ 50,
new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME),
FakeSampleStreamItem.END_OF_STREAM_ITEM);
renderer.enable(
RendererConfiguration.DEFAULT,
new Format[] {BASIC_MP4_1080},
fakeSampleStream1,
/* positionUs= */ 0,
/* joining= */ false,
/* mayRenderStartOfStream= */ true,
/* offsetUs */ 0);
renderer.start();
boolean replacedStream = false;
for (int i = 0; i < 200; i += 10) {
renderer.render(/* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000);
if (!replacedStream && renderer.hasReadStreamToEnd()) {
renderer.replaceStream(
new Format[] {BASIC_MP4_1080}, fakeSampleStream2, /* offsetUs= */ 100);
replacedStream = true;
}
}
verify(eventListener, times(2)).onRenderedFirstFrame(any());
}
@Test
public void replaceStream_whenNotStarted_doesNotRenderFirstFrameOfNewStream() throws Exception {
FakeSampleStream fakeSampleStream1 =
new FakeSampleStream(
/* format= */ BASIC_MP4_1080,
/* eventDispatcher= */ null,
/* firstSampleTimeUs= */ 0,
/* timeUsIncrement= */ 50,
new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME),
FakeSampleStreamItem.END_OF_STREAM_ITEM);
FakeSampleStream fakeSampleStream2 =
new FakeSampleStream(
/* format= */ BASIC_MP4_1080,
/* eventDispatcher= */ null,
/* firstSampleTimeUs= */ 0,
/* timeUsIncrement= */ 50,
new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME),
FakeSampleStreamItem.END_OF_STREAM_ITEM);
renderer.enable(
RendererConfiguration.DEFAULT,
new Format[] {BASIC_MP4_1080},
fakeSampleStream1,
/* positionUs= */ 0,
/* joining= */ false,
/* mayRenderStartOfStream= */ true,
/* offsetUs */ 0);
boolean replacedStream = false;
for (int i = 0; i < 200; i += 10) {
renderer.render(/* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000);
if (!replacedStream && renderer.hasReadStreamToEnd()) {
renderer.replaceStream(
new Format[] {BASIC_MP4_1080}, fakeSampleStream2, /* offsetUs= */ 100);
replacedStream = true;
}
}
verify(eventListener).onRenderedFirstFrame(any());
}
}