Skip to content

Commit 6650270

Browse files
ychaparovcopybara-github
authored andcommitted
MediaCodecVideoRenderer skips decoder inputs unused as reference
During a seek, or when playing a media with clipped start, MCVR encounters preroll decode-only buffers that are not rendered. Use C.BUFFER_FLAG_NO_OTHER_SAMPLE_DEPENDS_ON_THIS to determine whether a decode-only buffer is unused as reference. These buffers can be dropped before the decoder. When this optimization is triggered, increment decoderCounters.skippedInputBufferCount. Tested in ExoPlayer demo app on "One hour frame counter (MP4)" after enabling extractorsFactory.setMp4ExtractorFlags( FLAG_READ_WITHIN_GOP_SAMPLE_DEPENDENCIES); Observe: "sib" increases on each seek. PiperOrigin-RevId: 650566216
1 parent 7d4f623 commit 6650270

File tree

8 files changed

+122
-10
lines changed

8 files changed

+122
-10
lines changed

RELEASENOTES.md

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
* Add `PreloadMediaSource.PreloadControl.onPreloadError` to allow
2020
`PreloadMediaSource.PreloadControl` implementations to take actions when
2121
error occurs.
22+
* `MediaCodecVideoRenderer` avoids decoding samples that are neither
23+
rendered nor used as reference by other samples.
2224
* Transformer:
2325
* Add `SurfaceAssetLoader`, which supports queueing video data to
2426
Transformer via a `Surface`.

libraries/common/src/main/java/androidx/media3/common/C.java

+4-5
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ private C() {}
620620
* <ul>
621621
* <li>{@link #BUFFER_FLAG_KEY_FRAME}
622622
* <li>{@link #BUFFER_FLAG_END_OF_STREAM}
623-
* <li>{@link #BUFFER_FLAG_NO_OTHER_SAMPLE_DEPENDS_ON_THIS}
623+
* <li>{@link #BUFFER_FLAG_NOT_DEPENDED_ON}
624624
* <li>{@link #BUFFER_FLAG_FIRST_SAMPLE}
625625
* <li>{@link #BUFFER_FLAG_LAST_SAMPLE}
626626
* <li>{@link #BUFFER_FLAG_ENCRYPTED}
@@ -635,7 +635,7 @@ private C() {}
635635
value = {
636636
BUFFER_FLAG_KEY_FRAME,
637637
BUFFER_FLAG_END_OF_STREAM,
638-
BUFFER_FLAG_NO_OTHER_SAMPLE_DEPENDS_ON_THIS,
638+
BUFFER_FLAG_NOT_DEPENDED_ON,
639639
BUFFER_FLAG_FIRST_SAMPLE,
640640
BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA,
641641
BUFFER_FLAG_LAST_SAMPLE,
@@ -650,9 +650,8 @@ private C() {}
650650
@UnstableApi
651651
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
652652

653-
/** Indicates that future buffers do not depend on the data in this buffer. */
654-
@UnstableApi
655-
public static final int BUFFER_FLAG_NO_OTHER_SAMPLE_DEPENDS_ON_THIS = 1 << 26; // 0x04000000
653+
/** Indicates that no other buffers depend on the data in this buffer. */
654+
@UnstableApi public static final int BUFFER_FLAG_NOT_DEPENDED_ON = 1 << 26; // 0x04000000
656655

657656
/** Indicates that a buffer is known to contain the first media sample of the stream. */
658657
@UnstableApi public static final int BUFFER_FLAG_FIRST_SAMPLE = 1 << 27; // 0x08000000

libraries/decoder/src/main/java/androidx/media3/decoder/Buffer.java

+5
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ public final boolean hasSupplementalData() {
6060
return getFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA);
6161
}
6262

63+
/** Returns whether the {@link C#BUFFER_FLAG_NOT_DEPENDED_ON} flag is set. */
64+
public final boolean notDependedOn() {
65+
return getFlag(C.BUFFER_FLAG_NOT_DEPENDED_ON);
66+
}
67+
6368
/**
6469
* Replaces this buffer's flags with {@code flags}.
6570
*

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java

+18
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,12 @@ private boolean feedInputBuffer() throws ExoPlaybackException {
14521452
return true;
14531453
}
14541454

1455+
if (shouldSkipDecoderInputBuffer(buffer)) {
1456+
buffer.clear();
1457+
decoderCounters.skippedInputBufferCount += 1;
1458+
return true;
1459+
}
1460+
14551461
boolean bufferEncrypted = buffer.isEncrypted();
14561462
if (bufferEncrypted) {
14571463
buffer.cryptoInfo.increaseClearDataFirstSubSampleBy(adaptiveReconfigurationBytes);
@@ -1754,6 +1760,18 @@ protected int getCodecBufferFlags(DecoderInputBuffer buffer) {
17541760
return 0;
17551761
}
17561762

1763+
/**
1764+
* Returns whether the input buffer should be skipped before the decoder.
1765+
*
1766+
* <p>This can be used to skip decoding of buffers that are not depended on during seeking. See
1767+
* {@link C#BUFFER_FLAG_NOT_DEPENDED_ON}.
1768+
*
1769+
* @param buffer The input buffer.
1770+
*/
1771+
protected boolean shouldSkipDecoderInputBuffer(DecoderInputBuffer buffer) {
1772+
return false;
1773+
}
1774+
17571775
/**
17581776
* Returns the presentation time of the last buffer in the stream.
17591777
*

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java

+22-1
Original file line numberDiff line numberDiff line change
@@ -1247,7 +1247,7 @@ protected void onQueueInputBuffer(DecoderInputBuffer buffer) throws ExoPlaybackE
12471247

12481248
@Override
12491249
protected int getCodecBufferFlags(DecoderInputBuffer buffer) {
1250-
if (Util.SDK_INT >= 34 && tunneling && buffer.timeUs < getLastResetPositionUs()) {
1250+
if (Util.SDK_INT >= 34 && tunneling && isBufferBeforeStartTime(buffer)) {
12511251
// The buffer likely needs to be dropped because its timestamp is less than the start time.
12521252
// We can't decide to do this after decoding because we won't get the buffer back from the
12531253
// codec in tunneling mode. This may not work perfectly, e.g. when the codec is doing frame
@@ -1257,6 +1257,27 @@ protected int getCodecBufferFlags(DecoderInputBuffer buffer) {
12571257
return 0;
12581258
}
12591259

1260+
@Override
1261+
protected boolean shouldSkipDecoderInputBuffer(DecoderInputBuffer buffer) {
1262+
// TODO: b/351164714 - Do not apply this optimization for buffers with timestamp near
1263+
// the media duration.
1264+
if (hasReadStreamToEnd() || buffer.isLastSample()) {
1265+
// Last buffer is always decoded.
1266+
return false;
1267+
}
1268+
if (buffer.isEncrypted()) {
1269+
// Commonly used decryption algorithms require updating the initialization vector for each
1270+
// block processed. Skipping input buffers before the decoder is not allowed.
1271+
return false;
1272+
}
1273+
// Skip buffers without sample dependencies that won't be rendered.
1274+
return isBufferBeforeStartTime(buffer) && buffer.notDependedOn();
1275+
}
1276+
1277+
private boolean isBufferBeforeStartTime(DecoderInputBuffer buffer) {
1278+
return buffer.timeUs < getLastResetPositionUs();
1279+
}
1280+
12601281
@Override
12611282
protected void onOutputFormatChanged(Format format, @Nullable MediaFormat mediaFormat) {
12621283
@Nullable MediaCodecAdapter codec = getCodec();

libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java

+67
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,73 @@ public void render_withBufferLimitEqualToNumberOfSamples_rendersLastFrameAfterEn
356356
assertThat(argumentDecoderCounters.getValue().skippedOutputBufferCount).isEqualTo(2);
357357
}
358358

359+
@Test
360+
public void render_withoutSampleDependencies_rendersLastFrameAfterEndOfStream() throws Exception {
361+
ArgumentCaptor<DecoderCounters> argumentDecoderCounters =
362+
ArgumentCaptor.forClass(DecoderCounters.class);
363+
FakeSampleStream fakeSampleStream =
364+
new FakeSampleStream(
365+
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
366+
/* mediaSourceEventDispatcher= */ null,
367+
DrmSessionManager.DRM_UNSUPPORTED,
368+
new DrmSessionEventListener.EventDispatcher(),
369+
/* initialFormat= */ VIDEO_H264,
370+
ImmutableList.of(
371+
oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), // First buffer.
372+
// Second buffer will be skipped before decoder during a seek.
373+
oneByteSample(/* timeUs= */ 10_000, C.BUFFER_FLAG_NOT_DEPENDED_ON),
374+
// Last buffer without sample dependencies will be rendered.
375+
oneByteSample(/* timeUs= */ 20_000, C.BUFFER_FLAG_NOT_DEPENDED_ON),
376+
END_OF_STREAM_ITEM));
377+
fakeSampleStream.writeData(/* startPositionUs= */ 0);
378+
// Seek to time after samples.
379+
fakeSampleStream.seekToUs(30_000, /* allowTimeBeyondBuffer= */ true);
380+
mediaCodecVideoRenderer =
381+
new MediaCodecVideoRenderer(
382+
ApplicationProvider.getApplicationContext(),
383+
new ForwardingSynchronousMediaCodecAdapterWithBufferLimit.Factory(/* bufferLimit= */ 3),
384+
mediaCodecSelector,
385+
/* allowedJoiningTimeMs= */ 0,
386+
/* enableDecoderFallback= */ false,
387+
/* eventHandler= */ new Handler(testMainLooper),
388+
eventListener,
389+
/* maxDroppedFramesToNotify= */ 1);
390+
mediaCodecVideoRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT);
391+
mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface);
392+
mediaCodecVideoRenderer.enable(
393+
RendererConfiguration.DEFAULT,
394+
new Format[] {VIDEO_H264},
395+
fakeSampleStream,
396+
/* positionUs= */ 0,
397+
/* joining= */ false,
398+
/* mayRenderStartOfStream= */ true,
399+
/* startPositionUs= */ 30_000,
400+
/* offsetUs= */ 0,
401+
new MediaSource.MediaPeriodId(new Object()));
402+
403+
mediaCodecVideoRenderer.start();
404+
mediaCodecVideoRenderer.setCurrentStreamFinal();
405+
mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000);
406+
// Call to render has reads all samples including the END_OF_STREAM_ITEM because the
407+
// previous sample is skipped before decoding.
408+
assertThat(mediaCodecVideoRenderer.hasReadStreamToEnd()).isTrue();
409+
int posUs = 30_000;
410+
while (!mediaCodecVideoRenderer.isEnded()) {
411+
mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000);
412+
posUs += 40_000;
413+
}
414+
shadowOf(testMainLooper).idle();
415+
416+
verify(eventListener).onRenderedFirstFrame(eq(surface), /* renderTimeMs= */ anyLong());
417+
verify(eventListener).onVideoEnabled(argumentDecoderCounters.capture());
418+
// First key frame is decoded and skipped as an output buffer.
419+
assertThat(argumentDecoderCounters.getValue().skippedOutputBufferCount).isEqualTo(1);
420+
// Second frame is skipped before the decoder, as an input buffer.
421+
assertThat(argumentDecoderCounters.getValue().skippedInputBufferCount).isEqualTo(1);
422+
// Last frame is rendered.
423+
assertThat(argumentDecoderCounters.getValue().renderedOutputBufferCount).isEqualTo(1);
424+
}
425+
359426
@Test
360427
public void
361428
render_withClippingMediaPeriodAndBufferContainingLastAndClippingSamples_rendersLastFrame()

libraries/extractor/src/main/java/androidx/media3/extractor/mp4/FragmentedMp4Extractor.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public static ExtractorsFactory newFactory(SubtitleParser.Factory subtitleParser
135135

136136
/**
137137
* Flag to extract additional sample dependency information, and mark output buffers with {@link
138-
* C#BUFFER_FLAG_NO_OTHER_SAMPLE_DEPENDS_ON_THIS}.
138+
* C#BUFFER_FLAG_NOT_DEPENDED_ON}.
139139
*
140140
* <p>This class always marks the samples at the start of each group of picture (GOP) with {@link
141141
* C#BUFFER_FLAG_KEY_FRAME}. Usually, key frames can be decoded independently, without depending
@@ -1661,7 +1661,7 @@ private boolean readSample(ExtractorInput input) throws IOException {
16611661

16621662
@C.BufferFlags int sampleFlags = trackBundle.getCurrentSampleFlags();
16631663
if ((flags & FLAG_READ_WITHIN_GOP_SAMPLE_DEPENDENCIES) != 0 && !isSampleDependedOn) {
1664-
sampleFlags |= C.BUFFER_FLAG_NO_OTHER_SAMPLE_DEPENDS_ON_THIS;
1664+
sampleFlags |= C.BUFFER_FLAG_NOT_DEPENDED_ON;
16651665
}
16661666

16671667
// Encryption data.

libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public static ExtractorsFactory newFactory(SubtitleParser.Factory subtitleParser
125125

126126
/**
127127
* Flag to extract additional sample dependency information, and mark output buffers with {@link
128-
* C#BUFFER_FLAG_NO_OTHER_SAMPLE_DEPENDS_ON_THIS}.
128+
* C#BUFFER_FLAG_NOT_DEPENDED_ON}.
129129
*
130130
* <p>This class always marks the samples at the start of each group of picture (GOP) with {@link
131131
* C#BUFFER_FLAG_KEY_FRAME}. Usually, key frames can be decoded independently, without depending
@@ -804,7 +804,7 @@ private int readSample(ExtractorInput input, PositionHolder positionHolder) thro
804804
long timeUs = track.sampleTable.timestampsUs[sampleIndex];
805805
@C.BufferFlags int sampleFlags = track.sampleTable.flags[sampleIndex];
806806
if (!isSampleDependedOn) {
807-
sampleFlags |= C.BUFFER_FLAG_NO_OTHER_SAMPLE_DEPENDS_ON_THIS;
807+
sampleFlags |= C.BUFFER_FLAG_NOT_DEPENDED_ON;
808808
}
809809
if (trueHdSampleRechunker != null) {
810810
trueHdSampleRechunker.sampleMetadata(

0 commit comments

Comments
 (0)