mirror of
https://github.com/samsonjs/media.git
synced 2026-04-04 11:05:47 +00:00
Add parameter to Renderer.enable to allow rendering of first sample.
PiperOrigin-RevId: 295985916
This commit is contained in:
parent
c95ed7d18c
commit
72f4b964a5
13 changed files with 492 additions and 109 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue