From 73fa820828fac724e27e72838ade6aaccf4f1e44 Mon Sep 17 00:00:00 2001 From: dancho Date: Mon, 31 Mar 2025 03:32:37 -0700 Subject: [PATCH] Skip decoder input buffers for encrypted content Previous assertion was incorrect. Per-frame initialization vectors are written to the output stream in `FragmentedMp4Extractor` PiperOrigin-RevId: 742203717 --- .../media3/exoplayer/drm/DrmPlaybackTest.java | 75 +++++++++++++++++++ .../video/MediaCodecVideoRenderer.java | 5 -- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/drm/DrmPlaybackTest.java b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/drm/DrmPlaybackTest.java index cd44465fef..d47d1e559b 100644 --- a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/drm/DrmPlaybackTest.java +++ b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/drm/DrmPlaybackTest.java @@ -17,13 +17,19 @@ package androidx.media3.exoplayer.drm; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; +import android.content.Context; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.util.ConditionVariable; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.DecoderCounters; +import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.concurrent.atomic.AtomicReference; import okhttp3.mockwebserver.MockResponse; @@ -97,4 +103,73 @@ public final class DrmPlaybackTest { getInstrumentation().waitForIdleSync(); assertThat(playbackException.get()).isNull(); } + + @Test + public void clearkeyPlayback_withLateThresholdToDropDecoderInput_dropsInputBuffers() + throws Exception { + // The API 21 emulator doesn't have a secure decoder. Due to b/18678462 MediaCodecUtil pretends + // that there is a secure decoder so we must only run this test on API 21 - i.e. we cannot + // assumeTrue() on getDecoderInfos. + assumeTrue(Util.SDK_INT > 21); + Context context = getInstrumentation().getContext(); + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(CLEARKEY_RESPONSE)); + mockWebServer.start(); + + MediaItem mediaItem = + new MediaItem.Builder() + .setUri("asset:///media/drm/sample_fragmented_clearkey.mp4") + .setDrmConfiguration( + new MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID) + .setLicenseUri(mockWebServer.url("license").toString()) + .build()) + .build(); + AtomicReference player = new AtomicReference<>(); + ConditionVariable playbackComplete = new ConditionVariable(); + AtomicReference playbackException = new AtomicReference<>(); + AtomicReference decoderCountersAtomicReference = new AtomicReference<>(); + getInstrumentation() + .runOnMainSync( + () -> { + player.set( + new ExoPlayer.Builder( + context, + new DefaultRenderersFactory(context) + .experimentalSetLateThresholdToDropDecoderInputUs(-100_000_000L), + new DefaultMediaSourceFactory(context) + .experimentalSetCodecsToParseWithinGopSampleDependencies( + C.VIDEO_CODEC_FLAG_H264)) + .build()); + player + .get() + .addListener( + new Player.Listener() { + @Override + public void onPlaybackStateChanged(@Player.State int playbackState) { + if (playbackState == Player.STATE_ENDED) { + decoderCountersAtomicReference.set( + player.get().getVideoDecoderCounters()); + playbackComplete.open(); + } + } + + @Override + public void onPlayerError(PlaybackException error) { + playbackException.set(error); + playbackComplete.open(); + } + }); + player.get().setMediaItem(mediaItem); + player.get().prepare(); + player.get().play(); + }); + + playbackComplete.block(); + getInstrumentation().runOnMainSync(() -> player.get().release()); + getInstrumentation().waitForIdleSync(); + assertThat(playbackException.get()).isNull(); + // Which input buffers are dropped first depends on the number of MediaCodec buffer slots. + // This means the asserts cannot be isEqualTo. + assertThat(decoderCountersAtomicReference.get().droppedInputBufferCount).isAtLeast(1); + } } 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 cbcc07ba1d..142d52a699 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 @@ -1527,11 +1527,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer // Make sure to decode and render the last frame. return false; } - if (buffer.isEncrypted()) { - // Commonly used decryption algorithms require updating the initialization vector for each - // block processed. Skipping input buffers before the decoder is not allowed. - return false; - } boolean shouldSkipDecoderInputBuffer = isBufferBeforeStartTime(buffer); if (!shouldSkipDecoderInputBuffer && !shouldDropDecoderInputBuffers) { return false;