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)
This commit is contained in:
dancho 2025-04-03 06:28:57 -07:00 committed by tonihei
parent 9cfaf78994
commit 82d7c628da
2 changed files with 32 additions and 5 deletions

View File

@ -149,6 +149,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
*/ */
private static final long OFFSET_FROM_PERIOD_END_TO_TREAT_AS_LAST_US = 100_000L; 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.
*
* <p>This value must be greater than the pre-roll distance used by common audio codecs, such as
* 80ms used by Opus <a
* href="https://opus-codec.org/docs/opus_in_isobmff.html#4.3.6.2">Encapsulation of Opus in ISO
* Base Media File Format</a>
*/
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. * The maximum number of consecutive dropped input buffers that allow discarding frame headers.
* *
@ -616,7 +627,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
boolean treatDroppedBuffersAsSkipped) boolean treatDroppedBuffersAsSkipped)
throws ExoPlaybackException { throws ExoPlaybackException {
if (minEarlyUsToDropDecoderInput != C.TIME_UNSET) { 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) return shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastFrame)
&& maybeDropBuffersToKeyframe(positionUs, treatDroppedBuffersAsSkipped); && maybeDropBuffersToKeyframe(positionUs, treatDroppedBuffersAsSkipped);

View File

@ -29,6 +29,7 @@ import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.Renderer; import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.audio.AudioRendererEventListener; import androidx.media3.exoplayer.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter;
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
@ -103,6 +104,14 @@ public class ParseAv1SampleDependenciesPlaybackTest {
new ExoPlayer.Builder(applicationContext, renderersFactory) new ExoPlayer.Builder(applicationContext, renderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true)) .setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build(); .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)); Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1));
player.setVideoSurface(surface); player.setVideoSurface(surface);
player.setMediaItem(MediaItem.fromUri(TEST_MP4_URI)); 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. // Which input buffer is dropped first depends on the number of MediaCodec buffer slots.
// This means the asserts cannot be isEqualTo. // This means the asserts cannot be isEqualTo.
assertThat(decoderCounters.maxConsecutiveDroppedBufferCount).isAtMost(2); assertThat(decoderCounters.maxConsecutiveDroppedBufferCount).isAtMost(2);
assertThat(decoderCounters.droppedInputBufferCount).isAtLeast(8); assertThat(decoderCounters.droppedInputBufferCount).isAtLeast(4);
} }
private static final class CapturingRenderersFactoryWithLateThresholdToDropDecoderInputUs private static final class CapturingRenderersFactoryWithLateThresholdToDropDecoderInputUs
@ -155,7 +164,6 @@ public class ParseAv1SampleDependenciesPlaybackTest {
/* enableDecoderFallback= */ false, /* enableDecoderFallback= */ false,
eventHandler, eventHandler,
videoRendererEventListener, videoRendererEventListener,
DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY,
/* parseAv1SampleDependencies= */ true, /* parseAv1SampleDependencies= */ true,
/* lateThresholdToDropDecoderInputUs= */ -100_000_000L) /* lateThresholdToDropDecoderInputUs= */ -100_000_000L)
}; };
@ -173,7 +181,6 @@ public class ParseAv1SampleDependenciesPlaybackTest {
boolean enableDecoderFallback, boolean enableDecoderFallback,
@Nullable Handler eventHandler, @Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener, @Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify,
boolean parseAv1SampleDependencies, boolean parseAv1SampleDependencies,
long lateThresholdToDropDecoderInputUs) { long lateThresholdToDropDecoderInputUs) {
super( super(
@ -184,7 +191,7 @@ public class ParseAv1SampleDependenciesPlaybackTest {
.setEnableDecoderFallback(enableDecoderFallback) .setEnableDecoderFallback(enableDecoderFallback)
.setEventHandler(eventHandler) .setEventHandler(eventHandler)
.setEventListener(eventListener) .setEventListener(eventListener)
.setMaxDroppedFramesToNotify(maxDroppedFramesToNotify) .setMaxDroppedFramesToNotify(1)
.experimentalSetParseAv1SampleDependencies(parseAv1SampleDependencies) .experimentalSetParseAv1SampleDependencies(parseAv1SampleDependencies)
.experimentalSetLateThresholdToDropDecoderInputUs( .experimentalSetLateThresholdToDropDecoderInputUs(
lateThresholdToDropDecoderInputUs)); lateThresholdToDropDecoderInputUs));