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 50fefa62fa..dfb7030357 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 @@ -18,6 +18,8 @@ package androidx.media3.exoplayer.mediacodec; import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION; import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -26,14 +28,20 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.spy; +import android.media.MediaCodec; import android.media.MediaCrypto; import android.media.MediaFormat; +import android.os.Bundle; +import android.os.Handler; +import android.os.PersistableBundle; import android.os.SystemClock; +import android.view.Surface; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Clock; +import androidx.media3.decoder.CryptoInfo; import androidx.media3.exoplayer.DecoderReuseEvaluation; import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.RendererCapabilities; @@ -46,7 +54,9 @@ import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.test.utils.FakeSampleStream; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; @@ -462,6 +472,59 @@ public class MediaCodecRendererTest { inOrder, renderer, /* presentationTimeUs= */ 500, /* isDecodeOnly= */ false); } + @Test + public void render_wrapsIllegalStateExceptionFromMediaCodecInExoPlaybackException() + throws Exception { + MediaCodecAdapter.Factory throwingMediaCodecAdapterFactory = + new ThrowingMediaCodecAdapter.Factory( + () -> { + IllegalStateException ise = new IllegalStateException("ISE from inside MediaCodec"); + StackTraceElement[] stackTrace = ise.getStackTrace(); + stackTrace[0] = + new StackTraceElement( + "android.media.MediaCodec", + "fakeMethod", + stackTrace[0].getFileName(), + stackTrace[0].getLineNumber()); + ise.setStackTrace(stackTrace); + return ise; + }); + 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() + .hasCauseThat() + .isInstanceOf(IllegalStateException.class); + assertThat(playbackException) + .hasCauseThat() + .hasCauseThat() + .hasMessageThat() + .contains("ISE from inside MediaCodec"); + } + private FakeSampleStream createFakeSampleStream(Format format, long... sampleTimesUs) { ImmutableList.Builder sampleListBuilder = ImmutableList.builder(); @@ -484,9 +547,13 @@ public class MediaCodecRendererTest { private static class TestRenderer extends MediaCodecRenderer { public TestRenderer() { + this(MediaCodecAdapter.Factory.getDefault(ApplicationProvider.getApplicationContext())); + } + + public TestRenderer(MediaCodecAdapter.Factory mediaCodecAdapterFactory) { super( C.TRACK_TYPE_AUDIO, - MediaCodecAdapter.Factory.getDefault(ApplicationProvider.getApplicationContext()), + mediaCodecAdapterFactory, /* mediaCodecSelector= */ (mimeType, requiresSecureDecoder, requiresTunnelingDecoder) -> Collections.singletonList( MediaCodecInfo.newInstance( @@ -570,6 +637,123 @@ public class MediaCodecRendererTest { } } + /** + * A {@link MediaCodecAdapter} that throws a pre-specified exception from every decoding-related + * interaction. + */ + private static class ThrowingMediaCodecAdapter implements MediaCodecAdapter { + + public static class Factory implements MediaCodecAdapter.Factory { + + private final Supplier exceptionSupplier; + + public Factory(Supplier exceptionSupplier) { + this.exceptionSupplier = exceptionSupplier; + } + + @Override + public MediaCodecAdapter createAdapter(Configuration configuration) throws IOException { + return new ThrowingMediaCodecAdapter(exceptionSupplier); + } + } + + private final Supplier exceptionSupplier; + + private ThrowingMediaCodecAdapter(Supplier exceptionSupplier) { + this.exceptionSupplier = exceptionSupplier; + } + + @Override + public int dequeueInputBufferIndex() { + throw exceptionSupplier.get(); + } + + @Override + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + throw exceptionSupplier.get(); + } + + @Override + public MediaFormat getOutputFormat() { + throw exceptionSupplier.get(); + } + + @Nullable + @Override + public ByteBuffer getInputBuffer(int index) { + throw exceptionSupplier.get(); + } + + @Nullable + @Override + public ByteBuffer getOutputBuffer(int index) { + throw exceptionSupplier.get(); + } + + @Override + public void queueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flags) { + throw exceptionSupplier.get(); + } + + @Override + public void queueSecureInputBuffer( + int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { + throw exceptionSupplier.get(); + } + + @Override + public void releaseOutputBuffer(int index, boolean render) { + throw exceptionSupplier.get(); + } + + @Override + public void releaseOutputBuffer(int index, long renderTimeStampNs) { + throw exceptionSupplier.get(); + } + + @Override + public void flush() { + throw exceptionSupplier.get(); + } + + @Override + public void release() {} + + @Override + public void setOnFrameRenderedListener(OnFrameRenderedListener listener, Handler handler) {} + + @Override + public boolean registerOnBufferAvailableListener(OnBufferAvailableListener listener) { + return false; + } + + @Override + public void setOutputSurface(Surface surface) { + throw exceptionSupplier.get(); + } + + @Override + public void setParameters(Bundle params) { + throw exceptionSupplier.get(); + } + + @Override + public void setVideoScalingMode(@C.VideoScalingMode int scalingMode) { + throw exceptionSupplier.get(); + } + + @Override + public boolean needsReconfiguration() { + return false; + } + + @Override + public PersistableBundle getMetrics() { + return new PersistableBundle(); + } + } + private static void verifyProcessOutputBufferDecodeOnly( InOrder inOrder, MediaCodecRenderer renderer, long presentationTimeUs, boolean isDecodeOnly) throws Exception {