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;
/**
* 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.
*
@ -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);

View File

@ -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));