mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
MCVR: drop decoder input buffers when the decoder is late
* Add experimentalSetMinEarlyUsToDropDecoderInput to DefaultRenderersFactory and MediaCodecVideoRenderer * Enable dropping decoder input buffers inside MCVR.shouldIgnoreFrame * Track consecutive dropped buffers via priority queue for reordering PiperOrigin-RevId: 723837356
This commit is contained in:
parent
baf46d36d9
commit
a56a0bd928
@ -10,6 +10,9 @@
|
|||||||
* DataSource:
|
* DataSource:
|
||||||
* Audio:
|
* Audio:
|
||||||
* Video:
|
* Video:
|
||||||
|
* Add experimental `ExoPlayer` API to drop late `MediaCodecVideoRenderer`
|
||||||
|
decoder input buffers that are not depended on. Enable it with
|
||||||
|
`DefaultRenderersFactory.experimentalSetLateThresholdToDropDecoderInputUs`.
|
||||||
* Text:
|
* Text:
|
||||||
* Metadata:
|
* Metadata:
|
||||||
* Image:
|
* Image:
|
||||||
|
@ -110,6 +110,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
private boolean enableAudioTrackPlaybackParams;
|
private boolean enableAudioTrackPlaybackParams;
|
||||||
private boolean enableMediaCodecVideoRendererPrewarming;
|
private boolean enableMediaCodecVideoRendererPrewarming;
|
||||||
private boolean parseAv1SampleDependencies;
|
private boolean parseAv1SampleDependencies;
|
||||||
|
private long lateThresholdToDropDecoderInputUs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param context A {@link Context}.
|
* @param context A {@link Context}.
|
||||||
@ -120,6 +121,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
extensionRendererMode = EXTENSION_RENDERER_MODE_OFF;
|
extensionRendererMode = EXTENSION_RENDERER_MODE_OFF;
|
||||||
allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS;
|
allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS;
|
||||||
mediaCodecSelector = MediaCodecSelector.DEFAULT;
|
mediaCodecSelector = MediaCodecSelector.DEFAULT;
|
||||||
|
lateThresholdToDropDecoderInputUs = C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -313,6 +315,24 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the late threshold for rendered output buffers, in microseconds, after which decoder input
|
||||||
|
* buffers may be dropped.
|
||||||
|
*
|
||||||
|
* <p>The default value is {@link C#TIME_UNSET} and therefore no input buffers will be dropped due
|
||||||
|
* to this logic.
|
||||||
|
*
|
||||||
|
* <p>This method is experimental and will be renamed or removed in a future release.
|
||||||
|
*
|
||||||
|
* @param lateThresholdToDropDecoderInputUs The threshold.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public final DefaultRenderersFactory experimentalSetLateThresholdToDropDecoderInputUs(
|
||||||
|
long lateThresholdToDropDecoderInputUs) {
|
||||||
|
this.lateThresholdToDropDecoderInputUs = lateThresholdToDropDecoderInputUs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Renderer[] createRenderers(
|
public Renderer[] createRenderers(
|
||||||
Handler eventHandler,
|
Handler eventHandler,
|
||||||
@ -396,6 +416,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
.setEventListener(eventListener)
|
.setEventListener(eventListener)
|
||||||
.setMaxDroppedFramesToNotify(MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)
|
.setMaxDroppedFramesToNotify(MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)
|
||||||
.experimentalSetParseAv1SampleDependencies(parseAv1SampleDependencies)
|
.experimentalSetParseAv1SampleDependencies(parseAv1SampleDependencies)
|
||||||
|
.experimentalSetLateThresholdToDropDecoderInputUs(lateThresholdToDropDecoderInputUs)
|
||||||
.build();
|
.build();
|
||||||
out.add(videoRenderer);
|
out.add(videoRenderer);
|
||||||
|
|
||||||
@ -800,6 +821,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
.setEventListener(eventListener)
|
.setEventListener(eventListener)
|
||||||
.setMaxDroppedFramesToNotify(MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)
|
.setMaxDroppedFramesToNotify(MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)
|
||||||
.experimentalSetParseAv1SampleDependencies(parseAv1SampleDependencies)
|
.experimentalSetParseAv1SampleDependencies(parseAv1SampleDependencies)
|
||||||
|
.experimentalSetLateThresholdToDropDecoderInputUs(lateThresholdToDropDecoderInputUs)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -82,6 +82,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.PriorityQueue;
|
||||||
import org.checkerframework.checker.initialization.qual.Initialized;
|
import org.checkerframework.checker.initialization.qual.Initialized;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
@ -159,6 +160,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
private final VideoFrameReleaseControl.FrameReleaseInfo videoFrameReleaseInfo;
|
private final VideoFrameReleaseControl.FrameReleaseInfo videoFrameReleaseInfo;
|
||||||
@Nullable private final Av1SampleDependencyParser av1SampleDependencyParser;
|
@Nullable private final Av1SampleDependencyParser av1SampleDependencyParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The earliest time threshold, in microseconds, after which decoder input buffers may be dropped.
|
||||||
|
*/
|
||||||
|
private final long minEarlyUsToDropDecoderInput;
|
||||||
|
|
||||||
|
private final PriorityQueue<Long> droppedDecoderInputBufferTimestamps;
|
||||||
|
|
||||||
private @MonotonicNonNull CodecMaxValues codecMaxValues;
|
private @MonotonicNonNull CodecMaxValues codecMaxValues;
|
||||||
private boolean codecNeedsSetOutputSurfaceWorkaround;
|
private boolean codecNeedsSetOutputSurfaceWorkaround;
|
||||||
private boolean codecHandlesHdr10PlusOutOfBandMetadata;
|
private boolean codecHandlesHdr10PlusOutOfBandMetadata;
|
||||||
@ -190,6 +198,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
private long periodDurationUs;
|
private long periodDurationUs;
|
||||||
private boolean pendingVideoSinkInputStreamChange;
|
private boolean pendingVideoSinkInputStreamChange;
|
||||||
|
|
||||||
|
private boolean shouldDropDecoderInputBuffers;
|
||||||
|
|
||||||
/** A builder to create {@link MediaCodecVideoRenderer} instances. */
|
/** A builder to create {@link MediaCodecVideoRenderer} instances. */
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -204,6 +214,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
private float assumedMinimumCodecOperatingRate;
|
private float assumedMinimumCodecOperatingRate;
|
||||||
@Nullable private VideoSink videoSink;
|
@Nullable private VideoSink videoSink;
|
||||||
private boolean parseAv1SampleDependencies;
|
private boolean parseAv1SampleDependencies;
|
||||||
|
private long lateThresholdToDropDecoderInputUs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new builder.
|
* Creates a new builder.
|
||||||
@ -215,6 +226,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
this.mediaCodecSelector = MediaCodecSelector.DEFAULT;
|
this.mediaCodecSelector = MediaCodecSelector.DEFAULT;
|
||||||
this.codecAdapterFactory = MediaCodecAdapter.Factory.getDefault(context);
|
this.codecAdapterFactory = MediaCodecAdapter.Factory.getDefault(context);
|
||||||
this.assumedMinimumCodecOperatingRate = 30;
|
this.assumedMinimumCodecOperatingRate = 30;
|
||||||
|
this.lateThresholdToDropDecoderInputUs = C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the {@link MediaCodecSelector decoder selector}. */
|
/** Sets the {@link MediaCodecSelector decoder selector}. */
|
||||||
@ -327,6 +339,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the late threshold for rendered output buffers, in microseconds, after which decoder
|
||||||
|
* input buffers may be dropped.
|
||||||
|
*
|
||||||
|
* <p>The default value is {@link C#TIME_UNSET} and therefore no input buffers will be dropped
|
||||||
|
* due to this logic.
|
||||||
|
*
|
||||||
|
* <p>This method is experimental and will be renamed or removed in a future release.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder experimentalSetLateThresholdToDropDecoderInputUs(
|
||||||
|
long lateThresholdToDropDecoderInputUs) {
|
||||||
|
this.lateThresholdToDropDecoderInputUs = lateThresholdToDropDecoderInputUs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the {@link MediaCodecVideoRenderer}. Must only be called once per Builder instance.
|
* Builds the {@link MediaCodecVideoRenderer}. Must only be called once per Builder instance.
|
||||||
*
|
*
|
||||||
@ -546,6 +574,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
periodDurationUs = C.TIME_UNSET;
|
periodDurationUs = C.TIME_UNSET;
|
||||||
av1SampleDependencyParser =
|
av1SampleDependencyParser =
|
||||||
builder.parseAv1SampleDependencies ? new Av1SampleDependencyParser() : null;
|
builder.parseAv1SampleDependencies ? new Av1SampleDependencyParser() : null;
|
||||||
|
droppedDecoderInputBufferTimestamps = new PriorityQueue<>();
|
||||||
|
minEarlyUsToDropDecoderInput =
|
||||||
|
builder.lateThresholdToDropDecoderInputUs != C.TIME_UNSET
|
||||||
|
? -builder.lateThresholdToDropDecoderInputUs
|
||||||
|
: C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FrameTimingEvaluator methods
|
// FrameTimingEvaluator methods
|
||||||
@ -568,6 +601,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
boolean isLastFrame,
|
boolean isLastFrame,
|
||||||
boolean treatDroppedBuffersAsSkipped)
|
boolean treatDroppedBuffersAsSkipped)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
|
if (minEarlyUsToDropDecoderInput != C.TIME_UNSET) {
|
||||||
|
shouldDropDecoderInputBuffers = earlyUs < minEarlyUsToDropDecoderInput;
|
||||||
|
}
|
||||||
return shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastFrame)
|
return shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastFrame)
|
||||||
&& maybeDropBuffersToKeyframe(positionUs, treatDroppedBuffersAsSkipped);
|
&& maybeDropBuffersToKeyframe(positionUs, treatDroppedBuffersAsSkipped);
|
||||||
}
|
}
|
||||||
@ -1215,6 +1251,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
@Override
|
@Override
|
||||||
protected void resetCodecStateForFlush() {
|
protected void resetCodecStateForFlush() {
|
||||||
super.resetCodecStateForFlush();
|
super.resetCodecStateForFlush();
|
||||||
|
droppedDecoderInputBufferTimestamps.clear();
|
||||||
|
shouldDropDecoderInputBuffers = false;
|
||||||
buffersInCodecCount = 0;
|
buffersInCodecCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1425,8 +1463,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
// block processed. Skipping input buffers before the decoder is not allowed.
|
// block processed. Skipping input buffers before the decoder is not allowed.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Skip buffers without sample dependencies that won't be rendered.
|
boolean shouldSkipDecoderInputBuffer = isBufferBeforeStartTime(buffer);
|
||||||
if (!isBufferBeforeStartTime(buffer)) {
|
if (!shouldSkipDecoderInputBuffer && !shouldDropDecoderInputBuffers) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (buffer.hasSupplementalData()) {
|
if (buffer.hasSupplementalData()) {
|
||||||
@ -1434,7 +1472,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
}
|
}
|
||||||
if (buffer.notDependedOn()) {
|
if (buffer.notDependedOn()) {
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
decoderCounters.skippedInputBufferCount += 1;
|
if (shouldSkipDecoderInputBuffer) {
|
||||||
|
decoderCounters.skippedInputBufferCount += 1;
|
||||||
|
} else if (shouldDropDecoderInputBuffers) {
|
||||||
|
droppedDecoderInputBufferTimestamps.add(buffer.timeUs);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (av1SampleDependencyParser != null
|
if (av1SampleDependencyParser != null
|
||||||
@ -1450,7 +1492,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
if (sampleLimitAfterSkippingNonReferenceFrames != readOnlySample.limit()
|
if (sampleLimitAfterSkippingNonReferenceFrames != readOnlySample.limit()
|
||||||
&& hasSpaceForNextFrame) {
|
&& hasSpaceForNextFrame) {
|
||||||
checkNotNull(buffer.data).position(sampleLimitAfterSkippingNonReferenceFrames);
|
checkNotNull(buffer.data).position(sampleLimitAfterSkippingNonReferenceFrames);
|
||||||
decoderCounters.skippedInputBufferCount += 1;
|
if (shouldSkipDecoderInputBuffer) {
|
||||||
|
decoderCounters.skippedInputBufferCount += 1;
|
||||||
|
} else if (shouldDropDecoderInputBuffers) {
|
||||||
|
droppedDecoderInputBufferTimestamps.add(buffer.timeUs);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -1595,6 +1641,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
|
|
||||||
long outputStreamOffsetUs = getOutputStreamOffsetUs();
|
long outputStreamOffsetUs = getOutputStreamOffsetUs();
|
||||||
long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs;
|
long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs;
|
||||||
|
updateDroppedBufferCountersWithInputBuffers(presentationTimeUs);
|
||||||
|
|
||||||
if (videoSink != null) {
|
if (videoSink != null) {
|
||||||
// Skip decode-only buffers, e.g. after seeking, immediately.
|
// Skip decode-only buffers, e.g. after seeking, immediately.
|
||||||
@ -1867,10 +1914,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
if (treatDroppedBuffersAsSkipped) {
|
if (treatDroppedBuffersAsSkipped) {
|
||||||
decoderCounters.skippedInputBufferCount += droppedSourceBufferCount;
|
decoderCounters.skippedInputBufferCount += droppedSourceBufferCount;
|
||||||
decoderCounters.skippedOutputBufferCount += buffersInCodecCount;
|
decoderCounters.skippedOutputBufferCount += buffersInCodecCount;
|
||||||
|
decoderCounters.skippedInputBufferCount += droppedDecoderInputBufferTimestamps.size();
|
||||||
} else {
|
} else {
|
||||||
decoderCounters.droppedToKeyframeCount++;
|
decoderCounters.droppedToKeyframeCount++;
|
||||||
updateDroppedBufferCounters(
|
updateDroppedBufferCounters(
|
||||||
droppedSourceBufferCount, /* droppedDecoderBufferCount= */ buffersInCodecCount);
|
/* droppedInputBufferCount= */ droppedSourceBufferCount
|
||||||
|
+ droppedDecoderInputBufferTimestamps.size(),
|
||||||
|
/* droppedDecoderBufferCount= */ buffersInCodecCount);
|
||||||
}
|
}
|
||||||
flushOrReinitializeCodec();
|
flushOrReinitializeCodec();
|
||||||
if (videoSink != null) {
|
if (videoSink != null) {
|
||||||
@ -1901,6 +1951,23 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates counters to reflect dropped input buffers prior to {@code presentationTimeUs}.
|
||||||
|
*
|
||||||
|
* @param presentationTimeUs The presentation timestamp of the last processed output buffer, in
|
||||||
|
* microseconds.
|
||||||
|
*/
|
||||||
|
private void updateDroppedBufferCountersWithInputBuffers(long presentationTimeUs) {
|
||||||
|
int droppedInputBufferCount = 0;
|
||||||
|
Long minDroppedDecoderBufferTimeUs;
|
||||||
|
while ((minDroppedDecoderBufferTimeUs = droppedDecoderInputBufferTimestamps.peek()) != null
|
||||||
|
&& minDroppedDecoderBufferTimeUs < presentationTimeUs) {
|
||||||
|
droppedInputBufferCount++;
|
||||||
|
droppedDecoderInputBufferTimestamps.poll();
|
||||||
|
}
|
||||||
|
updateDroppedBufferCounters(droppedInputBufferCount, /* droppedDecoderBufferCount= */ 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates local counters and {@link DecoderCounters} with a new video frame processing offset.
|
* Updates local counters and {@link DecoderCounters} with a new video frame processing offset.
|
||||||
*
|
*
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.video;
|
package androidx.media3.exoplayer.video;
|
||||||
|
|
||||||
|
import static android.media.MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||||
import static android.view.Display.DEFAULT_DISPLAY;
|
import static android.view.Display.DEFAULT_DISPLAY;
|
||||||
import static androidx.media3.common.util.Util.msToUs;
|
import static androidx.media3.common.util.Util.msToUs;
|
||||||
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
|
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
|
||||||
@ -90,6 +91,7 @@ import java.nio.ByteBuffer;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.PriorityQueue;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -960,6 +962,395 @@ public class MediaCodecVideoRendererTest {
|
|||||||
assertThat(currentOutputFormat).isEqualTo(VIDEO_H264);
|
assertThat(currentOutputFormat).isEqualTo(VIDEO_H264);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void render_withLateBufferWithoutDependencies_dropsInputBuffers() throws Exception {
|
||||||
|
FakeTimeline fakeTimeline =
|
||||||
|
new FakeTimeline(
|
||||||
|
new FakeTimeline.TimelineWindowDefinition(
|
||||||
|
/* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 1_000_000));
|
||||||
|
FakeSampleStream fakeSampleStream =
|
||||||
|
new FakeSampleStream(
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
|
||||||
|
/* mediaSourceEventDispatcher= */ null,
|
||||||
|
DrmSessionManager.DRM_UNSUPPORTED,
|
||||||
|
new DrmSessionEventListener.EventDispatcher(),
|
||||||
|
/* initialFormat= */ VIDEO_H264,
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), // First buffer.
|
||||||
|
oneByteSample(
|
||||||
|
/* timeUs= */ 20_000))); // Late buffer triggers input buffer dropping.
|
||||||
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
|
mediaCodecVideoRenderer =
|
||||||
|
new MediaCodecVideoRenderer(
|
||||||
|
new MediaCodecVideoRenderer.Builder(ApplicationProvider.getApplicationContext())
|
||||||
|
.setCodecAdapterFactory(
|
||||||
|
new DefaultMediaCodecAdapterFactory(
|
||||||
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
() -> {
|
||||||
|
callbackThread = new HandlerThread("MCVRTest:MediaCodecAsyncAdapter");
|
||||||
|
return callbackThread;
|
||||||
|
},
|
||||||
|
() -> {
|
||||||
|
queueingThread = new HandlerThread("MCVRTest:MediaCodecQueueingThread");
|
||||||
|
return queueingThread;
|
||||||
|
}))
|
||||||
|
.setMediaCodecSelector(mediaCodecSelector)
|
||||||
|
.setAllowedJoiningTimeMs(0)
|
||||||
|
.setEnableDecoderFallback(false)
|
||||||
|
.setEventHandler(new Handler(testMainLooper))
|
||||||
|
.setEventListener(eventListener)
|
||||||
|
.setMaxDroppedFramesToNotify(1)
|
||||||
|
.experimentalSetLateThresholdToDropDecoderInputUs(50_000)) {
|
||||||
|
@Override
|
||||||
|
protected @Capabilities int supportsFormat(
|
||||||
|
MediaCodecSelector mediaCodecSelector, Format format) {
|
||||||
|
return RendererCapabilities.create(C.FORMAT_HANDLED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaCodecVideoRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT);
|
||||||
|
mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface);
|
||||||
|
mediaCodecVideoRenderer.setTimeline(fakeTimeline);
|
||||||
|
mediaCodecVideoRenderer.enable(
|
||||||
|
RendererConfiguration.DEFAULT,
|
||||||
|
new Format[] {VIDEO_H264},
|
||||||
|
fakeSampleStream,
|
||||||
|
/* positionUs= */ 0,
|
||||||
|
/* joining= */ false,
|
||||||
|
/* mayRenderStartOfStream= */ true,
|
||||||
|
/* startPositionUs= */ 0,
|
||||||
|
/* offsetUs= */ 0,
|
||||||
|
new MediaSource.MediaPeriodId(fakeTimeline.getUidOfPeriod(0)));
|
||||||
|
shadowOf(testMainLooper).idle();
|
||||||
|
ArgumentCaptor<DecoderCounters> argumentDecoderCounters =
|
||||||
|
ArgumentCaptor.forClass(DecoderCounters.class);
|
||||||
|
verify(eventListener).onVideoEnabled(argumentDecoderCounters.capture());
|
||||||
|
DecoderCounters decoderCounters = argumentDecoderCounters.getValue();
|
||||||
|
|
||||||
|
mediaCodecVideoRenderer.start();
|
||||||
|
mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
while (decoderCounters.renderedOutputBufferCount == 0) {
|
||||||
|
mediaCodecVideoRenderer.render(10_000, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
}
|
||||||
|
// Ensure existing buffer will be ~280ms late and new (not yet read) buffers are available
|
||||||
|
// to be dropped.
|
||||||
|
int posUs = 300_000;
|
||||||
|
fakeSampleStream.append(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 30_000, C.BUFFER_FLAG_NOT_DEPENDED_ON), // Dropped on input.
|
||||||
|
oneByteSample(/* timeUs= */ 300_000), // Caught up - render.
|
||||||
|
oneByteSample(/* timeUs= */ 500_000), // Last buffer is always rendered.
|
||||||
|
END_OF_STREAM_ITEM));
|
||||||
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
|
mediaCodecVideoRenderer.setCurrentStreamFinal();
|
||||||
|
// Render until the non-dropped frame is reached and then increase time to reach the end.
|
||||||
|
while (decoderCounters.renderedOutputBufferCount < 2) {
|
||||||
|
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
}
|
||||||
|
while (!mediaCodecVideoRenderer.isEnded()) {
|
||||||
|
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
posUs += 2_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(decoderCounters.droppedInputBufferCount).isEqualTo(1);
|
||||||
|
assertThat(decoderCounters.droppedBufferCount).isEqualTo(2);
|
||||||
|
assertThat(decoderCounters.maxConsecutiveDroppedBufferCount).isEqualTo(2);
|
||||||
|
assertThat(decoderCounters.droppedToKeyframeCount).isEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: b/390604981 - Run the test on older SDK levels to ensure it uses a MediaCodec shadow
|
||||||
|
// with more than one buffer slot.
|
||||||
|
@Config(minSdk = 30)
|
||||||
|
@Test
|
||||||
|
public void render_afterVeryLateBuffer_doesNotDropInputBuffers() throws Exception {
|
||||||
|
FakeTimeline fakeTimeline =
|
||||||
|
new FakeTimeline(
|
||||||
|
new FakeTimeline.TimelineWindowDefinition(
|
||||||
|
/* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 2_000_000));
|
||||||
|
FakeSampleStream fakeSampleStream =
|
||||||
|
new FakeSampleStream(
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
|
||||||
|
/* mediaSourceEventDispatcher= */ null,
|
||||||
|
DrmSessionManager.DRM_UNSUPPORTED,
|
||||||
|
new DrmSessionEventListener.EventDispatcher(),
|
||||||
|
/* initialFormat= */ VIDEO_H264,
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), // First buffer.
|
||||||
|
oneByteSample(/* timeUs= */ 20_000))); // Very late buffer.
|
||||||
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
|
mediaCodecVideoRenderer =
|
||||||
|
new MediaCodecVideoRenderer(
|
||||||
|
new MediaCodecVideoRenderer.Builder(ApplicationProvider.getApplicationContext())
|
||||||
|
.setCodecAdapterFactory(
|
||||||
|
new DefaultMediaCodecAdapterFactory(
|
||||||
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
() -> {
|
||||||
|
callbackThread = new HandlerThread("MCVRTest:MediaCodecAsyncAdapter");
|
||||||
|
return callbackThread;
|
||||||
|
},
|
||||||
|
() -> {
|
||||||
|
queueingThread = new HandlerThread("MCVRTest:MediaCodecQueueingThread");
|
||||||
|
return queueingThread;
|
||||||
|
}))
|
||||||
|
.setMediaCodecSelector(mediaCodecSelector)
|
||||||
|
.setAllowedJoiningTimeMs(0)
|
||||||
|
.setEnableDecoderFallback(false)
|
||||||
|
.setEventHandler(new Handler(testMainLooper))
|
||||||
|
.setEventListener(eventListener)
|
||||||
|
.setMaxDroppedFramesToNotify(1)
|
||||||
|
.experimentalSetLateThresholdToDropDecoderInputUs(50_000)) {
|
||||||
|
@Override
|
||||||
|
protected @Capabilities int supportsFormat(
|
||||||
|
MediaCodecSelector mediaCodecSelector, Format format) {
|
||||||
|
return RendererCapabilities.create(C.FORMAT_HANDLED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaCodecVideoRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT);
|
||||||
|
mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface);
|
||||||
|
mediaCodecVideoRenderer.setTimeline(fakeTimeline);
|
||||||
|
mediaCodecVideoRenderer.enable(
|
||||||
|
RendererConfiguration.DEFAULT,
|
||||||
|
new Format[] {VIDEO_H264},
|
||||||
|
fakeSampleStream,
|
||||||
|
/* positionUs= */ 0,
|
||||||
|
/* joining= */ false,
|
||||||
|
/* mayRenderStartOfStream= */ true,
|
||||||
|
/* startPositionUs= */ 0,
|
||||||
|
/* offsetUs= */ 0,
|
||||||
|
new MediaSource.MediaPeriodId(fakeTimeline.getUidOfPeriod(0)));
|
||||||
|
shadowOf(testMainLooper).idle();
|
||||||
|
ArgumentCaptor<DecoderCounters> argumentDecoderCounters =
|
||||||
|
ArgumentCaptor.forClass(DecoderCounters.class);
|
||||||
|
verify(eventListener).onVideoEnabled(argumentDecoderCounters.capture());
|
||||||
|
DecoderCounters decoderCounters = argumentDecoderCounters.getValue();
|
||||||
|
|
||||||
|
mediaCodecVideoRenderer.start();
|
||||||
|
mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
while (decoderCounters.renderedOutputBufferCount == 0) {
|
||||||
|
mediaCodecVideoRenderer.render(10_000, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
}
|
||||||
|
// Ensure existing buffer will be 1 second late and new (not yet read) buffers are available
|
||||||
|
// to be skipped and to skip to in the input stream.
|
||||||
|
int posUs = 1_020_000;
|
||||||
|
fakeSampleStream.append(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 30_000), // Dropped input buffer when skipping to keyframe.
|
||||||
|
oneByteSample(/* timeUs= */ 1_020_000, C.BUFFER_FLAG_KEY_FRAME),
|
||||||
|
oneByteSample(/* timeUs= */ 1_040_000, C.BUFFER_FLAG_NOT_DEPENDED_ON),
|
||||||
|
oneByteSample(/* timeUs= */ 1_060_000, C.BUFFER_FLAG_NOT_DEPENDED_ON),
|
||||||
|
oneByteSample(/* timeUs= */ 2_000_000, C.BUFFER_FLAG_NOT_DEPENDED_ON),
|
||||||
|
END_OF_STREAM_ITEM));
|
||||||
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
|
mediaCodecVideoRenderer.setCurrentStreamFinal();
|
||||||
|
// Render until the new keyframe has been processed and then increase time to reach the end.
|
||||||
|
while (decoderCounters.renderedOutputBufferCount < 2) {
|
||||||
|
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
maybeIdleAsynchronousMediaCodecAdapterThreads();
|
||||||
|
}
|
||||||
|
while (!mediaCodecVideoRenderer.isEnded()) {
|
||||||
|
maybeIdleAsynchronousMediaCodecAdapterThreads();
|
||||||
|
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
posUs += 1_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(decoderCounters.renderedOutputBufferCount).isEqualTo(5);
|
||||||
|
assertThat(decoderCounters.droppedInputBufferCount).isEqualTo(1);
|
||||||
|
assertThat(decoderCounters.droppedToKeyframeCount).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void render_afterVeryLateBuffer_countsDroppedInputBuffersCorrectly() throws Exception {
|
||||||
|
FakeTimeline fakeTimeline =
|
||||||
|
new FakeTimeline(
|
||||||
|
new FakeTimeline.TimelineWindowDefinition(
|
||||||
|
/* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 3_000_000));
|
||||||
|
FakeSampleStream fakeSampleStream =
|
||||||
|
new FakeSampleStream(
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
|
||||||
|
/* mediaSourceEventDispatcher= */ null,
|
||||||
|
DrmSessionManager.DRM_UNSUPPORTED,
|
||||||
|
new DrmSessionEventListener.EventDispatcher(),
|
||||||
|
/* initialFormat= */ VIDEO_H264,
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), // First buffer.
|
||||||
|
oneByteSample(/* timeUs= */ 20_000))); // Late buffer.
|
||||||
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
|
mediaCodecVideoRenderer =
|
||||||
|
new MediaCodecVideoRenderer(
|
||||||
|
new MediaCodecVideoRenderer.Builder(ApplicationProvider.getApplicationContext())
|
||||||
|
.setCodecAdapterFactory(
|
||||||
|
new ForwardingSynchronousMediaCodecAdapterWithReordering.Factory())
|
||||||
|
.setMediaCodecSelector(mediaCodecSelector)
|
||||||
|
.setAllowedJoiningTimeMs(0)
|
||||||
|
.setEnableDecoderFallback(false)
|
||||||
|
.setEventHandler(new Handler(testMainLooper))
|
||||||
|
.setEventListener(eventListener)
|
||||||
|
.setMaxDroppedFramesToNotify(1)
|
||||||
|
.experimentalSetLateThresholdToDropDecoderInputUs(50_000)) {
|
||||||
|
@Override
|
||||||
|
protected @Capabilities int supportsFormat(
|
||||||
|
MediaCodecSelector mediaCodecSelector, Format format) {
|
||||||
|
return RendererCapabilities.create(C.FORMAT_HANDLED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaCodecVideoRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT);
|
||||||
|
mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface);
|
||||||
|
mediaCodecVideoRenderer.setTimeline(fakeTimeline);
|
||||||
|
mediaCodecVideoRenderer.enable(
|
||||||
|
RendererConfiguration.DEFAULT,
|
||||||
|
new Format[] {VIDEO_H264},
|
||||||
|
fakeSampleStream,
|
||||||
|
/* positionUs= */ 0,
|
||||||
|
/* joining= */ false,
|
||||||
|
/* mayRenderStartOfStream= */ true,
|
||||||
|
/* startPositionUs= */ 0,
|
||||||
|
/* offsetUs= */ 0,
|
||||||
|
new MediaSource.MediaPeriodId(fakeTimeline.getUidOfPeriod(0)));
|
||||||
|
shadowOf(testMainLooper).idle();
|
||||||
|
ArgumentCaptor<DecoderCounters> argumentDecoderCounters =
|
||||||
|
ArgumentCaptor.forClass(DecoderCounters.class);
|
||||||
|
verify(eventListener).onVideoEnabled(argumentDecoderCounters.capture());
|
||||||
|
DecoderCounters decoderCounters = argumentDecoderCounters.getValue();
|
||||||
|
|
||||||
|
mediaCodecVideoRenderer.start();
|
||||||
|
mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
while (decoderCounters.renderedOutputBufferCount == 0) {
|
||||||
|
mediaCodecVideoRenderer.render(10_000, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
}
|
||||||
|
fakeSampleStream.append(
|
||||||
|
ImmutableList.of(
|
||||||
|
// Dropped input buffer.
|
||||||
|
oneByteSample(/* timeUs= */ 500_000, C.BUFFER_FLAG_NOT_DEPENDED_ON),
|
||||||
|
oneByteSample(300_000), // Render.
|
||||||
|
oneByteSample(400_000) // Very late buffer.
|
||||||
|
));
|
||||||
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
|
// Render until the frame at time 300_000 us is displayed.
|
||||||
|
int posUs = 300_000;
|
||||||
|
while (decoderCounters.renderedOutputBufferCount < 2) {
|
||||||
|
maybeIdleAsynchronousMediaCodecAdapterThreads();
|
||||||
|
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
}
|
||||||
|
fakeSampleStream.append(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(
|
||||||
|
/* timeUs= */ 1_000_000), // Dropped input buffer when skipping to keyframe.
|
||||||
|
oneByteSample(/* timeUs= */ 2_020_000, C.BUFFER_FLAG_KEY_FRAME),
|
||||||
|
oneByteSample(/* timeUs= */ 3_000_000),
|
||||||
|
END_OF_STREAM_ITEM));
|
||||||
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
|
mediaCodecVideoRenderer.setCurrentStreamFinal();
|
||||||
|
posUs = 2_020_000;
|
||||||
|
while (decoderCounters.renderedOutputBufferCount + decoderCounters.skippedOutputBufferCount
|
||||||
|
< 3) {
|
||||||
|
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
maybeIdleAsynchronousMediaCodecAdapterThreads();
|
||||||
|
}
|
||||||
|
while (!mediaCodecVideoRenderer.isEnded()) {
|
||||||
|
maybeIdleAsynchronousMediaCodecAdapterThreads();
|
||||||
|
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
posUs += 1_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(decoderCounters.droppedInputBufferCount).isEqualTo(2);
|
||||||
|
assertThat(decoderCounters.droppedToKeyframeCount).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
render_withLateBufferAndOutOfOrderSamplesWithoutDependencies_dropsInputBuffersAndRendersLast()
|
||||||
|
throws Exception {
|
||||||
|
FakeTimeline fakeTimeline =
|
||||||
|
new FakeTimeline(
|
||||||
|
new FakeTimeline.TimelineWindowDefinition(
|
||||||
|
/* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 1_000_000));
|
||||||
|
FakeSampleStream fakeSampleStream =
|
||||||
|
new FakeSampleStream(
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
|
||||||
|
/* mediaSourceEventDispatcher= */ null,
|
||||||
|
DrmSessionManager.DRM_UNSUPPORTED,
|
||||||
|
new DrmSessionEventListener.EventDispatcher(),
|
||||||
|
/* initialFormat= */ VIDEO_H264,
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), // First buffer.
|
||||||
|
oneByteSample(
|
||||||
|
/* timeUs= */ 20_000))); // Late buffer triggers input buffer dropping.
|
||||||
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
|
mediaCodecVideoRenderer =
|
||||||
|
new MediaCodecVideoRenderer(
|
||||||
|
new MediaCodecVideoRenderer.Builder(ApplicationProvider.getApplicationContext())
|
||||||
|
.setCodecAdapterFactory(
|
||||||
|
new ForwardingSynchronousMediaCodecAdapterWithReordering.Factory())
|
||||||
|
.setMediaCodecSelector(mediaCodecSelector)
|
||||||
|
.setAllowedJoiningTimeMs(0)
|
||||||
|
.setEnableDecoderFallback(false)
|
||||||
|
.setEventHandler(new Handler(testMainLooper))
|
||||||
|
.setEventListener(eventListener)
|
||||||
|
.setMaxDroppedFramesToNotify(1)
|
||||||
|
.experimentalSetLateThresholdToDropDecoderInputUs(200_000)) {
|
||||||
|
@Override
|
||||||
|
protected @Capabilities int supportsFormat(
|
||||||
|
MediaCodecSelector mediaCodecSelector, Format format) {
|
||||||
|
return RendererCapabilities.create(C.FORMAT_HANDLED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaCodecVideoRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT);
|
||||||
|
mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface);
|
||||||
|
mediaCodecVideoRenderer.setTimeline(fakeTimeline);
|
||||||
|
mediaCodecVideoRenderer.enable(
|
||||||
|
RendererConfiguration.DEFAULT,
|
||||||
|
new Format[] {VIDEO_H264},
|
||||||
|
fakeSampleStream,
|
||||||
|
/* positionUs= */ 0,
|
||||||
|
/* joining= */ false,
|
||||||
|
/* mayRenderStartOfStream= */ true,
|
||||||
|
/* startPositionUs= */ 0,
|
||||||
|
/* offsetUs= */ 0,
|
||||||
|
new MediaSource.MediaPeriodId(fakeTimeline.getUidOfPeriod(0)));
|
||||||
|
shadowOf(testMainLooper).idle();
|
||||||
|
ArgumentCaptor<DecoderCounters> argumentDecoderCounters =
|
||||||
|
ArgumentCaptor.forClass(DecoderCounters.class);
|
||||||
|
verify(eventListener).onVideoEnabled(argumentDecoderCounters.capture());
|
||||||
|
DecoderCounters decoderCounters = argumentDecoderCounters.getValue();
|
||||||
|
|
||||||
|
mediaCodecVideoRenderer.start();
|
||||||
|
mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
while (decoderCounters.renderedOutputBufferCount == 0) {
|
||||||
|
mediaCodecVideoRenderer.render(10_000, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
}
|
||||||
|
// Ensure existing buffer will be ~280ms late and new (not yet read) buffers are available
|
||||||
|
// to be dropped.
|
||||||
|
int posUs = 300_000;
|
||||||
|
fakeSampleStream.append(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 300_000), // Render.
|
||||||
|
oneByteSample(/* timeUs= */ 320_000), // Render.
|
||||||
|
// Drop consecutive input buffers that aren't consecutive output buffers.
|
||||||
|
oneByteSample(/* timeUs= */ 310_000, C.BUFFER_FLAG_NOT_DEPENDED_ON),
|
||||||
|
oneByteSample(/* timeUs= */ 330_000, C.BUFFER_FLAG_NOT_DEPENDED_ON),
|
||||||
|
// Last buffer is always rendered.
|
||||||
|
oneByteSample(/* timeUs= */ 500_000, C.BUFFER_FLAG_NOT_DEPENDED_ON),
|
||||||
|
END_OF_STREAM_ITEM));
|
||||||
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
|
mediaCodecVideoRenderer.setCurrentStreamFinal();
|
||||||
|
// Render until the first frame is reached and then increase time to reach the end.
|
||||||
|
while (decoderCounters.renderedOutputBufferCount < 2) {
|
||||||
|
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
}
|
||||||
|
while (!mediaCodecVideoRenderer.isEnded()) {
|
||||||
|
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
|
||||||
|
posUs += 2_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(decoderCounters.droppedInputBufferCount).isEqualTo(2);
|
||||||
|
assertThat(decoderCounters.droppedBufferCount).isEqualTo(3);
|
||||||
|
assertThat(decoderCounters.maxConsecutiveDroppedBufferCount).isEqualTo(1);
|
||||||
|
assertThat(decoderCounters.droppedToKeyframeCount).isEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void enable_withMayRenderStartOfStream_rendersFirstFrameBeforeStart() throws Exception {
|
public void enable_withMayRenderStartOfStream_rendersFirstFrameBeforeStart() throws Exception {
|
||||||
FakeSampleStream fakeSampleStream =
|
FakeSampleStream fakeSampleStream =
|
||||||
@ -1995,6 +2386,56 @@ public class MediaCodecVideoRendererTest {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class ForwardingSynchronousMediaCodecAdapterWithReordering
|
||||||
|
extends ForwardingSynchronousMediaCodecAdapter {
|
||||||
|
/** A factory for {@link ForwardingSynchronousMediaCodecAdapterWithReordering} instances. */
|
||||||
|
public static final class Factory implements MediaCodecAdapter.Factory {
|
||||||
|
@Override
|
||||||
|
public MediaCodecAdapter createAdapter(Configuration configuration) throws IOException {
|
||||||
|
return new ForwardingSynchronousMediaCodecAdapterWithReordering(
|
||||||
|
new SynchronousMediaCodecAdapter.Factory().createAdapter(configuration));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final PriorityQueue<Long> timestamps;
|
||||||
|
|
||||||
|
ForwardingSynchronousMediaCodecAdapterWithReordering(MediaCodecAdapter adapter) {
|
||||||
|
super(adapter);
|
||||||
|
timestamps = new PriorityQueue<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
||||||
|
int outputBufferIndex = super.dequeueOutputBufferIndex(bufferInfo);
|
||||||
|
Long smallestTimestamp = timestamps.peek();
|
||||||
|
if (smallestTimestamp != null && outputBufferIndex != INFO_TRY_AGAIN_LATER) {
|
||||||
|
bufferInfo.presentationTimeUs = smallestTimestamp;
|
||||||
|
}
|
||||||
|
return outputBufferIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void queueInputBuffer(
|
||||||
|
int index, int offset, int size, long presentationTimeUs, int flags) {
|
||||||
|
if ((flags & C.BUFFER_FLAG_END_OF_STREAM) == 0) {
|
||||||
|
timestamps.add(presentationTimeUs);
|
||||||
|
}
|
||||||
|
super.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releaseOutputBuffer(int index, boolean render) {
|
||||||
|
timestamps.poll();
|
||||||
|
super.releaseOutputBuffer(index, render);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releaseOutputBuffer(int index, long renderTimeStampNs) {
|
||||||
|
timestamps.poll();
|
||||||
|
super.releaseOutputBuffer(index, renderTimeStampNs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class ForwardingSynchronousMediaCodecAdapterWithBufferLimit
|
private static final class ForwardingSynchronousMediaCodecAdapterWithBufferLimit
|
||||||
extends ForwardingSynchronousMediaCodecAdapter {
|
extends ForwardingSynchronousMediaCodecAdapter {
|
||||||
/** A factory for {@link ForwardingSynchronousMediaCodecAdapterWithBufferLimit} instances. */
|
/** A factory for {@link ForwardingSynchronousMediaCodecAdapterWithBufferLimit} instances. */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user