From 0933f561b7e79306a0fa2b46697ed989528122a6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 4 Sep 2024 06:41:52 -0700 Subject: [PATCH] Move MCR `CryptoException` handling to top-level `render()` method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handling for `MediaCodec.CryptoException` was originally added only around calls to `MediaCodec.queueSecureInputBuffer` and `queueInputBuffer` (because these are the only methods that can throw this exception). When asynchronous interaction with `MediaCodec` was added in , exceptions from `MediaCodec` started being stored and bubbled out of **later** interactions with `MediaCodecAdapter`. This means that `MediaCodecRenderer` can now see `CryptoException` thrown from a different method, like `MediaCodecAdapter.dequeueInputBufferIndex()`, and this ends up missing the `catch (CryptoException)` code in `MediaCodecRenderer`. This results in an "unexpected runtime error" stack trace like [A]. This change fixes the stack trace to: 1. Make it a "renderer exception" instead of "unexpected runtime error" 2. Include the correct DRM error code -> `@PlaybackException.ErrorCode` mapping. You can see the corrected stack trace below [B]. ----- [A] (synthesized from manually throwing a `CryptoException` from `AsynchronousMediaCodecBufferEnqueuer#doQueueSecureInputBuffer`) ``` playerFailed [eventTime=11.56, mediaPos=10.35, window=0, period=0, errorCode=ERROR_CODE_UNSPECIFIED androidx.media3.exoplayer.ExoPlaybackException: Unexpected runtime error at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:729) at android.os.Handler.dispatchMessage(Handler.java:103) at android.os.Looper.loopOnce(Looper.java:232) at android.os.Looper.loop(Looper.java:317) at android.os.HandlerThread.run(HandlerThread.java:85) Caused by: android.media.MediaCodec$CryptoException: Test error message at androidx.media3.exoplayer.mediacodec.AsynchronousMediaCodecBufferEnqueuer.doQueueSecureInputBuffer(AsynchronousMediaCodecBufferEnqueuer.java:232) at androidx.media3.exoplayer.mediacodec.AsynchronousMediaCodecBufferEnqueuer.doHandleMessage(AsynchronousMediaCodecBufferEnqueuer.java:196) at androidx.media3.exoplayer.mediacodec.AsynchronousMediaCodecBufferEnqueuer.access$000(AsynchronousMediaCodecBufferEnqueuer.java:47) at androidx.media3.exoplayer.mediacodec.AsynchronousMediaCodecBufferEnqueuer$1.handleMessage(AsynchronousMediaCodecBufferEnqueuer.java:93) at android.os.Handler.dispatchMessage(Handler.java:107) at android.os.Looper.loopOnce(Looper.java:232)  at android.os.Looper.loop(Looper.java:317)  at android.os.HandlerThread.run(HandlerThread.java:85)  ``` [B] ``` Playback error androidx.media3.exoplayer.ExoPlaybackException: MediaCodecAudioRenderer error, index=1, format=Format(0, null, null, audio/mp4a-latm, mp4a.40.2, 134359, en, [-1, -1, -1.0, null], [2, 44100]), format_supported=YES at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:649) at android.os.Handler.dispatchMessage(Handler.java:103) at android.os.Looper.loopOnce(Looper.java:232) at android.os.Looper.loop(Looper.java:317) at android.os.HandlerThread.run(HandlerThread.java:85) Caused by: android.media.MediaCodec$CryptoException: Test error message at androidx.media3.exoplayer.mediacodec.AsynchronousMediaCodecBufferEnqueuer.doQueueSecureInputBuffer(AsynchronousMediaCodecBufferEnqueuer.java:232) at androidx.media3.exoplayer.mediacodec.AsynchronousMediaCodecBufferEnqueuer.doHandleMessage(AsynchronousMediaCodecBufferEnqueuer.java:196) at androidx.media3.exoplayer.mediacodec.AsynchronousMediaCodecBufferEnqueuer.access$000(AsynchronousMediaCodecBufferEnqueuer.java:47) at androidx.media3.exoplayer.mediacodec.AsynchronousMediaCodecBufferEnqueuer$1.handleMessage(AsynchronousMediaCodecBufferEnqueuer.java:93) at android.os.Handler.dispatchMessage(Handler.java:107) at android.os.Looper.loopOnce(Looper.java:232)  at android.os.Looper.loop(Looper.java:317)  at android.os.HandlerThread.run(HandlerThread.java:85)  ``` PiperOrigin-RevId: 670951229 --- RELEASENOTES.md | 3 + .../mediacodec/MediaCodecRenderer.java | 60 ++++++++----------- .../mediacodec/MediaCodecRendererTest.java | 38 ++++++++++++ 3 files changed, 67 insertions(+), 34 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 39924d8937..0bca120123 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,9 @@ * Common Library: * ExoPlayer: + * Fix `MediaCodec.CryptoException` sometimes being reported as an + "unexpected runtime error" when `MediaCodec` is operated in asynchronous + mode (default behaviour on API 31+). * Transformer: * Track Selection: * Extractors: diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java index 0659032fed..fa05f5271c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java @@ -35,7 +35,6 @@ import static java.lang.annotation.ElementType.TYPE_USE; import android.annotation.TargetApi; import android.media.MediaCodec; import android.media.MediaCodec.CodecException; -import android.media.MediaCodec.CryptoException; import android.media.MediaCrypto; import android.media.MediaCryptoException; import android.media.MediaFormat; @@ -883,6 +882,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { readSourceOmittingSampleData(FLAG_PEEK); } decoderCounters.ensureUpdated(); + } catch (MediaCodec.CryptoException e) { + throw createRendererException( + e, inputFormat, Util.getErrorCodeForMediaDrmErrorCode(e.getErrorCode())); } catch (IllegalStateException e) { if (isMediaCodecException(e)) { onCodecError(e); @@ -1391,22 +1393,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { processEndOfStream(); return false; } - try { - if (codecNeedsEosPropagation) { - // Do nothing. - } else { - codecReceivedEos = true; - codec.queueInputBuffer( - inputIndex, - /* offset= */ 0, - /* size= */ 0, - /* presentationTimeUs= */ 0, - MediaCodec.BUFFER_FLAG_END_OF_STREAM); - resetInputBuffer(); - } - } catch (CryptoException e) { - throw createRendererException( - e, inputFormat, Util.getErrorCodeForMediaDrmErrorCode(e.getErrorCode())); + if (codecNeedsEosPropagation) { + // Do nothing. + } else { + codecReceivedEos = true; + codec.queueInputBuffer( + inputIndex, + /* offset= */ 0, + /* size= */ 0, + /* presentationTimeUs= */ 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM); + resetInputBuffer(); } return false; } @@ -1463,23 +1460,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { onQueueInputBuffer(buffer); int flags = getCodecBufferFlags(buffer); - try { - if (bufferEncrypted) { - checkNotNull(codec) - .queueSecureInputBuffer( - inputIndex, /* offset= */ 0, buffer.cryptoInfo, presentationTimeUs, flags); - } else { - checkNotNull(codec) - .queueInputBuffer( - inputIndex, - /* offset= */ 0, - checkNotNull(buffer.data).limit(), - presentationTimeUs, - flags); - } - } catch (CryptoException e) { - throw createRendererException( - e, inputFormat, Util.getErrorCodeForMediaDrmErrorCode(e.getErrorCode())); + if (bufferEncrypted) { + checkNotNull(codec) + .queueSecureInputBuffer( + inputIndex, /* offset= */ 0, buffer.cryptoInfo, presentationTimeUs, flags); + } else { + checkNotNull(codec) + .queueInputBuffer( + inputIndex, + /* offset= */ 0, + checkNotNull(buffer.data).limit(), + presentationTimeUs, + flags); } resetInputBuffer(); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java index e19f5e7edb..14f200776e 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.spy; import android.media.MediaCodec; import android.media.MediaCrypto; +import android.media.MediaDrm; import android.media.MediaFormat; import android.os.Bundle; import android.os.Handler; @@ -525,6 +526,43 @@ public class MediaCodecRendererTest { .contains("ISE from inside MediaCodec"); } + // b/347367307#comment6 + @Test + public void render_wrapsCryptoExceptionFromAnyMediaCodecMethod() throws Exception { + MediaCodecAdapter.Factory throwingMediaCodecAdapterFactory = + new ThrowingMediaCodecAdapter.Factory( + () -> + new MediaCodec.CryptoException( + MediaDrm.ErrorCodes.ERROR_INSUFFICIENT_OUTPUT_PROTECTION, "Test exception")); + TestRenderer renderer = new TestRenderer(throwingMediaCodecAdapterFactory); + renderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT); + Format format = + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).setAverageBitrate(1000).build(); + FakeSampleStream fakeSampleStream = + createFakeSampleStream(format, /* sampleTimesUs...= */ 0, 100, 200, 300, 400, 500); + MediaSource.MediaPeriodId mediaPeriodId = new MediaSource.MediaPeriodId(new Object()); + renderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {format}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 400, + /* offsetUs= */ 0, + mediaPeriodId); + renderer.start(); + + ExoPlaybackException playbackException = + assertThrows( + ExoPlaybackException.class, + () -> renderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime())); + + assertThat(playbackException.type).isEqualTo(ExoPlaybackException.TYPE_RENDERER); + assertThat(playbackException).hasCauseThat().isInstanceOf(MediaCodec.CryptoException.class); + assertThat(playbackException).hasCauseThat().hasMessageThat().contains("Test exception"); + } + private FakeSampleStream createFakeSampleStream(Format format, long... sampleTimesUs) { ImmutableList.Builder sampleListBuilder = ImmutableList.builder();