Add a test for MediaCodecRenderer handling of IllegalStateException

This is a regression test for the bug introduced in bb9ff30c3a
which was manually spotted and fixed in 0d2bf49d6a.

Reverting the fix causes this test to fail.

This test is a bit hacky because we have to munge the stack trace of
the `IllegalStateException` to make it look like it was thrown from
inside `MediaCodec`. We deliberately do this 'badly' (e.g. using
`fakeMethod`) to avoid a future reader being confused by a
fake-but-plausible stack trace.

PiperOrigin-RevId: 652820878
This commit is contained in:
ibaker 2024-07-16 06:45:09 -07:00 committed by Copybara-Service
parent d4c6e39dfb
commit 99679645fc

View file

@ -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<FakeSampleStream.FakeSampleStreamItem> 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<RuntimeException> exceptionSupplier;
public Factory(Supplier<RuntimeException> exceptionSupplier) {
this.exceptionSupplier = exceptionSupplier;
}
@Override
public MediaCodecAdapter createAdapter(Configuration configuration) throws IOException {
return new ThrowingMediaCodecAdapter(exceptionSupplier);
}
}
private final Supplier<RuntimeException> exceptionSupplier;
private ThrowingMediaCodecAdapter(Supplier<RuntimeException> 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 {