From 82d7c628da3ddbaa265d5c0550d39e3fbf172505 Mon Sep 17 00:00:00 2001 From: dancho Date: Thu, 3 Apr 2025 06:28:57 -0700 Subject: [PATCH] Do not drop decoder input buffers close to a reset position This is a workaround for a bug where the positionUs seen by MCVR jumps when audio pre-roll samples are discarded. PiperOrigin-RevId: 743538208 (cherry picked from commit 036bed36326130294f50264659913bdcecb4c9bc) --- .../video/MediaCodecVideoRenderer.java | 22 ++++++++++++++++++- ...arseAv1SampleDependenciesPlaybackTest.java | 15 +++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java index c66d425a35..2da116e860 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java @@ -149,6 +149,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer */ private static final long OFFSET_FROM_PERIOD_END_TO_TREAT_AS_LAST_US = 100_000L; + /** + * The offset from {@link #getLastResetPositionUs()} in microseconds, before which input buffers + * are not allowed to be dropped. + * + *

This value must be greater than the pre-roll distance used by common audio codecs, such as + * 80ms used by Opus Encapsulation of Opus in ISO + * Base Media File Format + */ + private static final long OFFSET_FROM_RESET_POSITION_TO_ALLOW_INPUT_BUFFER_DROPPING_US = 200_000L; + /** * The maximum number of consecutive dropped input buffers that allow discarding frame headers. * @@ -616,7 +627,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer boolean treatDroppedBuffersAsSkipped) throws ExoPlaybackException { if (minEarlyUsToDropDecoderInput != C.TIME_UNSET) { - shouldDropDecoderInputBuffers = earlyUs < minEarlyUsToDropDecoderInput; + // TODO: b/161996553 - Remove the isAwayFromLastResetPosition check when audio pre-rolling + // is implemented correctly. Audio codecs such as Opus require pre-roll samples to be decoded + // and discarded on a seek. Depending on the audio decoder, the positionUs may jump forward + // by the pre-roll duration. Do not drop more frames than necessary when this happens. + boolean isAwayFromLastResetPosition = + positionUs + > getLastResetPositionUs() + + OFFSET_FROM_RESET_POSITION_TO_ALLOW_INPUT_BUFFER_DROPPING_US; + shouldDropDecoderInputBuffers = + isAwayFromLastResetPosition && earlyUs < minEarlyUsToDropDecoderInput; } return shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastFrame) && maybeDropBuffersToKeyframe(positionUs, treatDroppedBuffersAsSkipped); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/ParseAv1SampleDependenciesPlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/ParseAv1SampleDependenciesPlaybackTest.java index aaf4d80be9..15ab905468 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/ParseAv1SampleDependenciesPlaybackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/ParseAv1SampleDependenciesPlaybackTest.java @@ -29,6 +29,7 @@ import androidx.media3.exoplayer.DecoderCounters; import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.Renderer; +import androidx.media3.exoplayer.analytics.AnalyticsListener; import androidx.media3.exoplayer.audio.AudioRendererEventListener; import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; @@ -103,6 +104,14 @@ public class ParseAv1SampleDependenciesPlaybackTest { new ExoPlayer.Builder(applicationContext, renderersFactory) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) .build(); + player.addAnalyticsListener( + new AnalyticsListener() { + @Override + public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { + // Input buffers near the reset position should not be dropped. + assertThat(eventTime.currentPlaybackPositionMs).isAtLeast(200); + } + }); Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); player.setVideoSurface(surface); player.setMediaItem(MediaItem.fromUri(TEST_MP4_URI)); @@ -121,7 +130,7 @@ public class ParseAv1SampleDependenciesPlaybackTest { // Which input buffer is dropped first depends on the number of MediaCodec buffer slots. // This means the asserts cannot be isEqualTo. assertThat(decoderCounters.maxConsecutiveDroppedBufferCount).isAtMost(2); - assertThat(decoderCounters.droppedInputBufferCount).isAtLeast(8); + assertThat(decoderCounters.droppedInputBufferCount).isAtLeast(4); } private static final class CapturingRenderersFactoryWithLateThresholdToDropDecoderInputUs @@ -155,7 +164,6 @@ public class ParseAv1SampleDependenciesPlaybackTest { /* enableDecoderFallback= */ false, eventHandler, videoRendererEventListener, - DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY, /* parseAv1SampleDependencies= */ true, /* lateThresholdToDropDecoderInputUs= */ -100_000_000L) }; @@ -173,7 +181,6 @@ public class ParseAv1SampleDependenciesPlaybackTest { boolean enableDecoderFallback, @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener, - int maxDroppedFramesToNotify, boolean parseAv1SampleDependencies, long lateThresholdToDropDecoderInputUs) { super( @@ -184,7 +191,7 @@ public class ParseAv1SampleDependenciesPlaybackTest { .setEnableDecoderFallback(enableDecoderFallback) .setEventHandler(eventHandler) .setEventListener(eventListener) - .setMaxDroppedFramesToNotify(maxDroppedFramesToNotify) + .setMaxDroppedFramesToNotify(1) .experimentalSetParseAv1SampleDependencies(parseAv1SampleDependencies) .experimentalSetLateThresholdToDropDecoderInputUs( lateThresholdToDropDecoderInputUs));