From 1a00da4c19a500318f42f382926a9cb13b0c5c31 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 17 Dec 2020 11:30:52 +0000 Subject: [PATCH] Add CapturingRenderersFactory and use it in Mp4PlaybackTest I decided not to migrate all the tests in one CL to keep the diff manageable. I'll make follow-up CLs to migrate the tests, and eventually delete TeeCodec and all associated logic. I couldn't completely remove the dump diff because ShadowMediaCodec.getCodecInfo() (which would give me access to the MIME type) doesn't seem to work properly - it returned video/avc when name=exotest.audio.aac, and looking into the code it looks like there's some native methods that are missing shadow implementations. PiperOrigin-RevId: 347991956 --- .../exoplayer2/DefaultRenderersFactory.java | 12 +- .../exoplayer2/e2etest/Mp4PlaybackTest.java | 13 +- .../robolectric/PlaybackOutput.java | 37 +- .../robolectric/ShadowMediaCodecConfig.java | 7 + .../exoplayer2/robolectric/TeeCodec.java | 10 +- .../playbackdumps/mp4/midroll-5s.mp4.dump | 4 +- .../playbackdumps/mp4/postroll-5s.mp4.dump | 4 +- .../playbackdumps/mp4/preroll-5s.mp4.dump | 4 +- .../assets/playbackdumps/mp4/sample.mp4.dump | 4 +- .../playbackdumps/mp4/sample_ac3.mp4.dump | 2 +- .../mp4/sample_ac3_fragmented.mp4.dump | 2 +- .../playbackdumps/mp4/sample_ac4.mp4.dump | 2 +- .../mp4/sample_ac4_fragmented.mp4.dump | 2 +- .../mp4/sample_android_slow_motion.mp4.dump | 2 +- .../playbackdumps/mp4/sample_eac3.mp4.dump | 2 +- .../mp4/sample_eac3_fragmented.mp4.dump | 2 +- .../playbackdumps/mp4/sample_eac3joc.mp4.dump | 2 +- .../mp4/sample_eac3joc_fragmented.mp4.dump | 2 +- .../mp4/sample_fragmented.mp4.dump | 4 +- .../mp4/sample_fragmented_seekable.mp4.dump | 4 +- .../mp4/sample_fragmented_sei.mp4.dump | 4 +- .../mp4/sample_mdat_too_long.mp4.dump | 4 +- .../playbackdumps/mp4/sample_opus.mp4.dump | 2 +- .../mp4/sample_opus_fragmented.mp4.dump | 2 +- .../mp4/sample_partially_fragmented.mp4.dump | 4 +- .../playbackdumps/mp4/testvid_1022ms.mp4.dump | 4 +- .../testutil/CapturingRenderersFactory.java | 319 ++++++++++++++++++ 27 files changed, 405 insertions(+), 55 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/CapturingRenderersFactory.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index 4657922d45..57f7f65e1f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -77,14 +77,18 @@ public class DefaultRenderersFactory implements RenderersFactory { /** * Allow use of extension renderers. Extension renderers are indexed before core renderers of the * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore - * prefer to use an extension renderer to a core renderer in the case that both are able to play - * a given track. + * prefer to use an extension renderer to a core renderer in the case that both are able to play a + * given track. */ public static final int EXTENSION_RENDERER_MODE_PREFER = 2; - private static final String TAG = "DefaultRenderersFactory"; + /** + * The maximum number of frames that can be dropped between invocations of {@link + * VideoRendererEventListener#onDroppedFrames(int, long)}. + */ + public static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; - protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; + private static final String TAG = "DefaultRenderersFactory"; private final Context context; @ExtensionRendererMode private int extensionRendererMode; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java index 03c4da9a42..492276b659 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.e2etest; +import android.content.Context; import android.graphics.SurfaceTexture; import android.view.Surface; import androidx.test.core.app.ApplicationProvider; @@ -25,6 +26,7 @@ import com.google.android.exoplayer2.robolectric.PlaybackOutput; import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper; import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; +import com.google.android.exoplayer2.testutil.CapturingRenderersFactory; import com.google.android.exoplayer2.testutil.DumpFileAsserts; import com.google.common.collect.ImmutableList; import org.junit.Rule; @@ -75,12 +77,15 @@ public class Mp4PlaybackTest { @Test public void test() throws Exception { + Context applicationContext = ApplicationProvider.getApplicationContext(); + CapturingRenderersFactory renderersFactory = new CapturingRenderersFactory(applicationContext); SimpleExoPlayer player = - new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext()) + new SimpleExoPlayer.Builder(applicationContext, renderersFactory) .setClock(new AutoAdvancingFakeClock()) .build(); player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); - PlaybackOutput playbackOutput = PlaybackOutput.register(player, mediaCodecConfig); + + PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory); player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/" + inputFile)); player.prepare(); @@ -89,8 +94,6 @@ public class Mp4PlaybackTest { player.release(); DumpFileAsserts.assertOutput( - ApplicationProvider.getApplicationContext(), - playbackOutput, - "playbackdumps/mp4/" + inputFile + ".dump"); + applicationContext, playbackOutput, "playbackdumps/mp4/" + inputFile + ".dump"); } } diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java index d4091c37b7..21a891a523 100644 --- a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java @@ -19,6 +19,7 @@ import android.graphics.Bitmap; import androidx.annotation.Nullable; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.testutil.CapturingRenderersFactory; import com.google.android.exoplayer2.testutil.Dumper; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Assertions; @@ -38,13 +39,18 @@ import java.util.List; */ public final class PlaybackOutput implements Dumper.Dumpable { - private final ShadowMediaCodecConfig codecConfig; + @Nullable private final ShadowMediaCodecConfig codecConfig; + @Nullable private final CapturingRenderersFactory capturingRenderersFactory; private final List metadatas; private final List> subtitles; - private PlaybackOutput(SimpleExoPlayer player, ShadowMediaCodecConfig codecConfig) { + private PlaybackOutput( + SimpleExoPlayer player, + @Nullable ShadowMediaCodecConfig codecConfig, + @Nullable CapturingRenderersFactory capturingRenderersFactory) { this.codecConfig = codecConfig; + this.capturingRenderersFactory = capturingRenderersFactory; metadatas = Collections.synchronizedList(new ArrayList<>()); subtitles = Collections.synchronizedList(new ArrayList<>()); @@ -57,27 +63,38 @@ public final class PlaybackOutput implements Dumper.Dumpable { /** * Create an instance that captures the metadata and text output from {@code player} and the audio - * and video output via the {@link TeeCodec TeeCodecs} exposed by {@code mediaCodecConfig}. + * and video output via {@code capturingRenderersFactory}. * *

Must be called before playback to ensure metadata and text output is captured * correctly. * * @param player The {@link SimpleExoPlayer} to capture metadata and text output from. - * @param mediaCodecConfig The {@link ShadowMediaCodecConfig} to capture audio and video output - * from. + * @param capturingRenderersFactory The {@link CapturingRenderersFactory} to capture audio and + * video output from. * @return A new instance that can be used to dump the playback output. */ + public static PlaybackOutput register( + SimpleExoPlayer player, CapturingRenderersFactory capturingRenderersFactory) { + return new PlaybackOutput(player, /* codecConfig= */ null, capturingRenderersFactory); + } + + /** @deprecated Use {@link #register(SimpleExoPlayer, CapturingRenderersFactory)}. */ + @Deprecated public static PlaybackOutput register( SimpleExoPlayer player, ShadowMediaCodecConfig mediaCodecConfig) { - return new PlaybackOutput(player, mediaCodecConfig); + return new PlaybackOutput(player, mediaCodecConfig, /* capturingRenderersFactory= */ null); } @Override public void dump(Dumper dumper) { - ImmutableMap codecs = codecConfig.getCodecs(); - ImmutableList mimeTypes = ImmutableList.sortedCopyOf(codecs.keySet()); - for (String mimeType : mimeTypes) { - dumper.add(Assertions.checkNotNull(codecs.get(mimeType))); + if (codecConfig != null) { + ImmutableMap codecs = codecConfig.getCodecs(); + ImmutableList mimeTypes = ImmutableList.sortedCopyOf(codecs.keySet()); + for (String mimeType : mimeTypes) { + dumper.add(Assertions.checkNotNull(codecs.get(mimeType))); + } + } else { + Assertions.checkNotNull(capturingRenderersFactory).dump(dumper); } dumpMetadata(dumper); diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java index 57b0d1ff21..37f74a255c 100644 --- a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.robolectric; +import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; +import com.google.android.exoplayer2.testutil.CapturingRenderersFactory; import com.google.android.exoplayer2.util.MimeTypes; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -48,6 +50,11 @@ public final class ShadowMediaCodecConfig extends ExternalResource { return new ShadowMediaCodecConfig(); } + /** + * @deprecated Use {@link CapturingRenderersFactory} to access {@link MediaCodec} interactions + * instead. + */ + @Deprecated public ImmutableMap getCodecs() { return ImmutableMap.copyOf(codecsByMimeType); } diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TeeCodec.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TeeCodec.java index 172350414e..69511422eb 100644 --- a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TeeCodec.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TeeCodec.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.robolectric; +import android.media.MediaCodec; +import com.google.android.exoplayer2.testutil.CapturingRenderersFactory; import com.google.android.exoplayer2.testutil.Dumper; import com.google.android.exoplayer2.util.MimeTypes; import com.google.common.collect.ImmutableList; @@ -25,12 +27,10 @@ import java.util.List; import org.robolectric.shadows.ShadowMediaCodec; /** - * A {@link ShadowMediaCodec.CodecConfig.Codec} for Robolectric's {@link ShadowMediaCodec} that - * records the contents of buffers passed to it before copying the contents into the output buffer. - * - *

This also implements {@link Dumper.Dumpable} so the recorded buffers can be written out to a - * dump file. + * @deprecated Use {@link CapturingRenderersFactory} to access {@link MediaCodec} interactions + * instead. */ +@Deprecated public final class TeeCodec implements ShadowMediaCodec.CodecConfig.Codec, Dumper.Dumpable { private final String mimeType; diff --git a/testdata/src/test/assets/playbackdumps/mp4/midroll-5s.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/midroll-5s.mp4.dump index c9bc837956..a895c4c64f 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/midroll-5s.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/midroll-5s.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/mp4a-latm): +MediaCodecAdapter (exotest.audio.aac): buffers.length = 218 buffers[0] = length 21, hash D57A2CCC buffers[1] = length 4, hash EE9DF @@ -218,7 +218,7 @@ MediaCodec (audio/mp4a-latm): buffers[215] = length 4, hash EE9DF buffers[216] = length 4, hash EE9DF buffers[217] = length 0, hash 1 -MediaCodec (video/avc): +MediaCodecAdapter (exotest.video.avc): buffers.length = 126 buffers[0] = length 5252, hash 13893A4C buffers[1] = length 44, hash A05B3BEA diff --git a/testdata/src/test/assets/playbackdumps/mp4/postroll-5s.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/postroll-5s.mp4.dump index b0daee7b9a..c3c11d86f3 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/postroll-5s.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/postroll-5s.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/mp4a-latm): +MediaCodecAdapter (exotest.audio.aac): buffers.length = 218 buffers[0] = length 21, hash D57A2CCC buffers[1] = length 4, hash EE9DF @@ -218,7 +218,7 @@ MediaCodec (audio/mp4a-latm): buffers[215] = length 4, hash EE9DF buffers[216] = length 4, hash EE9DF buffers[217] = length 0, hash 1 -MediaCodec (video/avc): +MediaCodecAdapter (exotest.video.avc): buffers.length = 126 buffers[0] = length 5384, hash F220EEFD buffers[1] = length 58, hash 897F4173 diff --git a/testdata/src/test/assets/playbackdumps/mp4/preroll-5s.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/preroll-5s.mp4.dump index eef5570aba..9004224490 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/preroll-5s.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/preroll-5s.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/mp4a-latm): +MediaCodecAdapter (exotest.audio.aac): buffers.length = 218 buffers[0] = length 21, hash D57A2CCC buffers[1] = length 4, hash EE9DF @@ -218,7 +218,7 @@ MediaCodec (audio/mp4a-latm): buffers[215] = length 4, hash EE9DF buffers[216] = length 4, hash EE9DF buffers[217] = length 0, hash 1 -MediaCodec (video/avc): +MediaCodecAdapter (exotest.video.avc): buffers.length = 126 buffers[0] = length 5245, hash C090A41E buffers[1] = length 63, hash 5141C80D diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample.mp4.dump index 5256ea561e..52f9d9dd07 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/mp4a-latm): +MediaCodecAdapter (exotest.audio.aac): buffers.length = 46 buffers[0] = length 23, hash 47DE9131 buffers[1] = length 6, hash 31EC5206 @@ -46,7 +46,7 @@ MediaCodec (audio/mp4a-latm): buffers[43] = length 229, hash FFF98DF0 buffers[44] = length 6, hash 31B22286 buffers[45] = length 0, hash 1 -MediaCodec (video/avc): +MediaCodecAdapter (exotest.video.avc): buffers.length = 31 buffers[0] = length 36692, hash D216076E buffers[1] = length 5312, hash D45D3CA0 diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_ac3.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_ac3.mp4.dump index ef247edfee..bc812e72d0 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_ac3.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_ac3.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/ac3): +MediaCodecAdapter (exotest.audio.ac3): buffers.length = 10 buffers[0] = length 1536, hash 7108D5C2 buffers[1] = length 1536, hash 80BF3B34 diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_ac3_fragmented.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_ac3_fragmented.mp4.dump index ef247edfee..bc812e72d0 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_ac3_fragmented.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_ac3_fragmented.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/ac3): +MediaCodecAdapter (exotest.audio.ac3): buffers.length = 10 buffers[0] = length 1536, hash 7108D5C2 buffers[1] = length 1536, hash 80BF3B34 diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_ac4.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_ac4.mp4.dump index bd8c08d8ab..77c14b2c9d 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_ac4.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_ac4.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/ac4): +MediaCodecAdapter (exotest.audio.ac4): buffers.length = 20 buffers[0] = length 367, hash D2762FA buffers[1] = length 367, hash BDD3224A diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_ac4_fragmented.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_ac4_fragmented.mp4.dump index bd8c08d8ab..77c14b2c9d 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_ac4_fragmented.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_ac4_fragmented.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/ac4): +MediaCodecAdapter (exotest.audio.ac4): buffers.length = 20 buffers[0] = length 367, hash D2762FA buffers[1] = length 367, hash BDD3224A diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_android_slow_motion.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_android_slow_motion.mp4.dump index 6fead57421..8ab568c01a 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_android_slow_motion.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_android_slow_motion.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (video/avc): +MediaCodecAdapter (exotest.video.avc): buffers.length = 8 buffers[0] = length 34656, hash D92B66FF buffers[1] = length 768, hash D0C3B229 diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_eac3.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_eac3.mp4.dump index 64f09a752b..69b8670a87 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_eac3.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_eac3.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/eac3): +MediaCodecAdapter (exotest.audio.eac3): buffers.length = 55 buffers[0] = length 4000, hash BAEAFB2A buffers[1] = length 4000, hash E3C5EBF0 diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_eac3_fragmented.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_eac3_fragmented.mp4.dump index 64f09a752b..69b8670a87 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_eac3_fragmented.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_eac3_fragmented.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/eac3): +MediaCodecAdapter (exotest.audio.eac3): buffers.length = 55 buffers[0] = length 4000, hash BAEAFB2A buffers[1] = length 4000, hash E3C5EBF0 diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_eac3joc.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_eac3joc.mp4.dump index 6b2dc20c4f..d2c63d4633 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_eac3joc.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_eac3joc.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/eac3-joc): +MediaCodecAdapter (exotest.audio.eac3joc): buffers.length = 65 buffers[0] = length 2560, hash 882594AD buffers[1] = length 2560, hash 41EC8B22 diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_eac3joc_fragmented.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_eac3joc_fragmented.mp4.dump index 6b2dc20c4f..d2c63d4633 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_eac3joc_fragmented.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_eac3joc_fragmented.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/eac3-joc): +MediaCodecAdapter (exotest.audio.eac3joc): buffers.length = 65 buffers[0] = length 2560, hash 882594AD buffers[1] = length 2560, hash 41EC8B22 diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented.mp4.dump index 0f5af3e57f..b8f6657598 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/mp4a-latm): +MediaCodecAdapter (exotest.audio.aac): buffers.length = 47 buffers[0] = length 18, hash 96519432 buffers[1] = length 4, hash EE9DF @@ -47,7 +47,7 @@ MediaCodec (audio/mp4a-latm): buffers[44] = length 446, hash D6735B8A buffers[45] = length 10, hash A453EEBE buffers[46] = length 0, hash 1 -MediaCodec (video/avc): +MediaCodecAdapter (exotest.video.avc): buffers.length = 31 buffers[0] = length 38070, hash B58E1AEE buffers[1] = length 8340, hash 8AC449FF diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented_seekable.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented_seekable.mp4.dump index 0f5af3e57f..b8f6657598 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented_seekable.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented_seekable.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/mp4a-latm): +MediaCodecAdapter (exotest.audio.aac): buffers.length = 47 buffers[0] = length 18, hash 96519432 buffers[1] = length 4, hash EE9DF @@ -47,7 +47,7 @@ MediaCodec (audio/mp4a-latm): buffers[44] = length 446, hash D6735B8A buffers[45] = length 10, hash A453EEBE buffers[46] = length 0, hash 1 -MediaCodec (video/avc): +MediaCodecAdapter (exotest.video.avc): buffers.length = 31 buffers[0] = length 38070, hash B58E1AEE buffers[1] = length 8340, hash 8AC449FF diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented_sei.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented_sei.mp4.dump index 0f5af3e57f..b8f6657598 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented_sei.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_fragmented_sei.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/mp4a-latm): +MediaCodecAdapter (exotest.audio.aac): buffers.length = 47 buffers[0] = length 18, hash 96519432 buffers[1] = length 4, hash EE9DF @@ -47,7 +47,7 @@ MediaCodec (audio/mp4a-latm): buffers[44] = length 446, hash D6735B8A buffers[45] = length 10, hash A453EEBE buffers[46] = length 0, hash 1 -MediaCodec (video/avc): +MediaCodecAdapter (exotest.video.avc): buffers.length = 31 buffers[0] = length 38070, hash B58E1AEE buffers[1] = length 8340, hash 8AC449FF diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_mdat_too_long.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_mdat_too_long.mp4.dump index 5256ea561e..52f9d9dd07 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_mdat_too_long.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_mdat_too_long.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/mp4a-latm): +MediaCodecAdapter (exotest.audio.aac): buffers.length = 46 buffers[0] = length 23, hash 47DE9131 buffers[1] = length 6, hash 31EC5206 @@ -46,7 +46,7 @@ MediaCodec (audio/mp4a-latm): buffers[43] = length 229, hash FFF98DF0 buffers[44] = length 6, hash 31B22286 buffers[45] = length 0, hash 1 -MediaCodec (video/avc): +MediaCodecAdapter (exotest.video.avc): buffers.length = 31 buffers[0] = length 36692, hash D216076E buffers[1] = length 5312, hash D45D3CA0 diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_opus.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_opus.mp4.dump index f9dd3207c4..de5ef21f30 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_opus.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_opus.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/opus): +MediaCodecAdapter (exotest.audio.opus): buffers.length = 102 buffers[0] = length 3, hash 4732 buffers[1] = length 3, hash 4732 diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_opus_fragmented.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_opus_fragmented.mp4.dump index 6c0236719e..cefab5527f 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_opus_fragmented.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_opus_fragmented.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/opus): +MediaCodecAdapter (exotest.audio.opus): buffers.length = 251 buffers[0] = length 326, hash ECC9FF90 buffers[1] = length 326, hash B041EAAC diff --git a/testdata/src/test/assets/playbackdumps/mp4/sample_partially_fragmented.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/sample_partially_fragmented.mp4.dump index 46c780b93b..17f526e228 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/sample_partially_fragmented.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/sample_partially_fragmented.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/mp4a-latm): +MediaCodecAdapter (exotest.audio.aac): buffers.length = 46 buffers[0] = length 21, hash D57A2CCC buffers[1] = length 6, hash 336D5819 @@ -46,7 +46,7 @@ MediaCodec (audio/mp4a-latm): buffers[43] = length 208, hash 4A050A0D buffers[44] = length 13, hash 2555A7DC buffers[45] = length 0, hash 1 -MediaCodec (video/avc): +MediaCodecAdapter (exotest.video.avc): buffers.length = 31 buffers[0] = length 37655, hash 265F7BA7 buffers[1] = length 5023, hash 30768D40 diff --git a/testdata/src/test/assets/playbackdumps/mp4/testvid_1022ms.mp4.dump b/testdata/src/test/assets/playbackdumps/mp4/testvid_1022ms.mp4.dump index d5c15255b8..88a79642be 100644 --- a/testdata/src/test/assets/playbackdumps/mp4/testvid_1022ms.mp4.dump +++ b/testdata/src/test/assets/playbackdumps/mp4/testvid_1022ms.mp4.dump @@ -1,4 +1,4 @@ -MediaCodec (audio/mp4a-latm): +MediaCodecAdapter (exotest.audio.aac): buffers.length = 45 buffers[0] = length 9, hash 67CB703F buffers[1] = length 9, hash A820BF4B @@ -45,7 +45,7 @@ MediaCodec (audio/mp4a-latm): buffers[42] = length 242, hash B5863406 buffers[43] = length 239, hash F56D62C3 buffers[44] = length 0, hash 1 -MediaCodec (video/avc): +MediaCodecAdapter (exotest.video.avc): buffers.length = 31 buffers[0] = length 16086, hash 5D23AFBA buffers[1] = length 2539, hash 597403A0 diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CapturingRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CapturingRenderersFactory.java new file mode 100644 index 0000000000..b3b837a087 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CapturingRenderersFactory.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.testutil; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; + +import android.content.Context; +import android.media.MediaCodec; +import android.media.MediaCrypto; +import android.media.MediaFormat; +import android.os.Bundle; +import android.os.Handler; +import android.util.SparseArray; +import android.view.Surface; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.google.android.exoplayer2.audio.AudioProcessor; +import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.DefaultAudioSink; +import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; +import com.google.android.exoplayer2.decoder.CryptoInfo; +import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; +import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import com.google.android.exoplayer2.metadata.MetadataOutput; +import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.text.TextOutput; +import com.google.android.exoplayer2.text.TextRenderer; +import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; +import com.google.android.exoplayer2.video.VideoRendererEventListener; +import com.google.common.collect.ImmutableList; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A {@link RenderersFactory} that captures interactions with the audio and video {@link + * MediaCodecAdapter} instances. + * + *

The captured interactions can be used in a test assertion via the {@link Dumper.Dumpable} + * interface. + */ +// TODO(internal b/174661563): Add support for capturing subtitles on the output of the +// SubtitleDecoder. And possibly Metadata too (for consistency). +public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpable { + + private final Context context; + private final CapturingMediaCodecAdapter.Factory mediaCodecAdapterFactory; + + public CapturingRenderersFactory(Context context) { + this.context = context; + this.mediaCodecAdapterFactory = new CapturingMediaCodecAdapter.Factory(); + } + + @Override + public Renderer[] createRenderers( + Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput) { + return new Renderer[] { + new MediaCodecVideoRenderer( + context, + mediaCodecAdapterFactory, + MediaCodecSelector.DEFAULT, + DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS, + /* enableDecoderFallback= */ false, + eventHandler, + videoRendererEventListener, + DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY), + new MediaCodecAudioRenderer( + context, + mediaCodecAdapterFactory, + MediaCodecSelector.DEFAULT, + /* enableDecoderFallback= */ false, + eventHandler, + audioRendererEventListener, + new DefaultAudioSink(AudioCapabilities.getCapabilities(context), new AudioProcessor[0])), + new TextRenderer(textRendererOutput, eventHandler.getLooper()), + new MetadataRenderer(metadataRendererOutput, eventHandler.getLooper()) + }; + } + + @Override + public void dump(Dumper dumper) { + mediaCodecAdapterFactory.dump(dumper); + } + + /** + * A {@link MediaCodecAdapter} that captures interactions and exposes them for test assertions via + * {@link Dumper.Dumpable}. + */ + private static class CapturingMediaCodecAdapter implements MediaCodecAdapter, Dumper.Dumpable { + + private static class Factory implements MediaCodecAdapter.Factory, Dumper.Dumpable { + + private final List constructedAdapters; + + private Factory() { + constructedAdapters = new ArrayList<>(); + } + + @RequiresApi(18) + @Override + public MediaCodecAdapter createAdapter(MediaCodec codec) { + CapturingMediaCodecAdapter adapter = + new CapturingMediaCodecAdapter( + MediaCodecAdapter.Factory.DEFAULT.createAdapter(codec), codec.getName()); + constructedAdapters.add(adapter); + return adapter; + } + + @Override + public void dump(Dumper dumper) { + ImmutableList sortedAdapters = + ImmutableList.sortedCopyOf( + (adapter1, adapter2) -> adapter1.codecName.compareTo(adapter2.codecName), + constructedAdapters); + for (int i = 0; i < sortedAdapters.size(); i++) { + sortedAdapters.get(i).dump(dumper); + } + } + } + + private final MediaCodecAdapter delegate; + // TODO(internal b/175710547): Consider using MediaCodecInfo, but currently Robolectric (v4.5) + // doesn't correctly implement MediaCodec#getCodecInfo() (getName() works). + private final String codecName; + + /** + * The client-owned buffers, keyed by the index used by {@link #dequeueInputBufferIndex()} and + * {@link #getInputBuffer(int)}. + */ + private final SparseArray dequeuedInputBuffers; + + /** All interactions recorded with this adapter. */ + private final List capturedInteractions; + + private final AtomicBoolean isReleased; + + private CapturingMediaCodecAdapter(MediaCodecAdapter delegate, String codecName) { + this.delegate = delegate; + this.codecName = codecName; + dequeuedInputBuffers = new SparseArray<>(); + capturedInteractions = new ArrayList<>(); + isReleased = new AtomicBoolean(); + } + + // MediaCodecAdapter implementation + + @Override + public void configure( + @Nullable MediaFormat mediaFormat, + @Nullable Surface surface, + @Nullable MediaCrypto crypto, + int flags) { + delegate.configure(mediaFormat, surface, crypto, flags); + } + + @Override + public void start() { + delegate.start(); + } + + @Override + public int dequeueInputBufferIndex() { + return delegate.dequeueInputBufferIndex(); + } + + @Override + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + return delegate.dequeueOutputBufferIndex(bufferInfo); + } + + @Override + public MediaFormat getOutputFormat() { + return delegate.getOutputFormat(); + } + + @Nullable + @Override + public ByteBuffer getInputBuffer(int index) { + @Nullable ByteBuffer inputBuffer = delegate.getInputBuffer(index); + if (inputBuffer != null) { + dequeuedInputBuffers.put(index, inputBuffer); + } + return inputBuffer; + } + + @Nullable + @Override + public ByteBuffer getOutputBuffer(int index) { + return delegate.getOutputBuffer(index); + } + + @Override + public void queueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flags) { + ByteBuffer inputBuffer = checkNotNull(dequeuedInputBuffers.get(index)); + capturedInteractions.add(new CapturedInputBuffer(peekBytes(inputBuffer, offset, size))); + + delegate.queueInputBuffer(index, offset, size, presentationTimeUs, flags); + dequeuedInputBuffers.delete(index); + } + + @Override + public void queueSecureInputBuffer( + int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { + delegate.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); + } + + @Override + public void releaseOutputBuffer(int index, boolean render) { + delegate.releaseOutputBuffer(index, render); + } + + @RequiresApi(21) + @Override + public void releaseOutputBuffer(int index, long renderTimeStampNs) { + delegate.releaseOutputBuffer(index, renderTimeStampNs); + } + + @Override + public void flush() { + dequeuedInputBuffers.clear(); + delegate.flush(); + } + + @Override + public void release() { + dequeuedInputBuffers.clear(); + isReleased.set(true); + delegate.release(); + } + + @RequiresApi(23) + @Override + public void setOnFrameRenderedListener(OnFrameRenderedListener listener, Handler handler) { + delegate.setOnFrameRenderedListener(listener, handler); + } + + @RequiresApi(23) + @Override + public void setOutputSurface(Surface surface) { + delegate.setOutputSurface(surface); + } + + @RequiresApi(19) + @Override + public void setParameters(Bundle params) { + delegate.setParameters(params); + } + + @Override + public void setVideoScalingMode(int scalingMode) { + delegate.setVideoScalingMode(scalingMode); + } + + // Dumpable implementation + + @Override + public void dump(Dumper dumper) { + checkState(isReleased.get()); + + dumper.startBlock("MediaCodecAdapter (" + codecName + ")"); + // TODO: Update this when capturedInteractions contains more than just input buffers. + dumper.add("buffers.length", capturedInteractions.size()); + for (int i = 0; i < capturedInteractions.size(); i++) { + CapturedInputBuffer inputBuffer = (CapturedInputBuffer) capturedInteractions.get(i); + dumper.add("buffers[" + i + "]", inputBuffer.contents); + } + dumper.endBlock(); + } + + private static byte[] peekBytes(ByteBuffer buffer, int offset, int size) { + int originalPosition = buffer.position(); + buffer.position(offset); + byte[] bytes = new byte[size]; + buffer.get(bytes); + buffer.position(originalPosition); + return bytes; + } + + /** A marker interface for different interactions with {@link CapturingMediaCodecAdapter}. */ + private interface CapturedInteraction {} + + /** + * Records the data passed to {@link CapturingMediaCodecAdapter#queueInputBuffer(int, int, int, + * long, int)}. + */ + private static class CapturedInputBuffer implements CapturedInteraction { + // TODO: Add other fields + private final byte[] contents; + + private CapturedInputBuffer(byte[] contents) { + this.contents = contents; + } + } + } +}