Add initial MediaParser tests

Bug: 147324074
Bug: 147308781
Test: Not applicable.
Change-Id: Ia77749122dc74f31d30abb9af03330e0ae50d334
diff --git a/tests/tests/mediaparser/Android.bp b/tests/tests/mediaparser/Android.bp
index 08b646c..8b3697c 100644
--- a/tests/tests/mediaparser/Android.bp
+++ b/tests/tests/mediaparser/Android.bp
@@ -18,6 +18,7 @@
     static_libs: [
         "ctstestrunner-axt",
         "androidx.test.ext.junit",
+        "exoplayer2-extractor-test-utils",
         // TODO: Remove the assets folder once b/146655310 is fixed.
         // Current assets version in the asset folder from ExoPlayer SHA
         // 2fc426214f5cfdde2e15a9f08c397a961fa0e249.
diff --git a/tests/tests/mediaparser/AndroidTest.xml b/tests/tests/mediaparser/AndroidTest.xml
index ea39d58..905e7e4 100644
--- a/tests/tests/mediaparser/AndroidTest.xml
+++ b/tests/tests/mediaparser/AndroidTest.xml
@@ -26,8 +26,8 @@
         <option name="package" value="android.media.mediaparser.cts" />
         <!-- setup can be expensive so limit the number of shards -->
         <option name="ajur-max-shard" value="5" />
-        <!-- test-timeout unit is ms, value = 30 min -->
-        <option name="test-timeout" value="10000" />
-        <option name="runtime-hint" value="10m" />
+        <!-- test-timeout unit is ms -->
+        <option name="test-timeout" value="30000" />
+        <option name="runtime-hint" value="30ms" />
     </test>
 </configuration>
diff --git a/tests/tests/mediaparser/src/android/media/mediaparser/cts/MediaParserTest.java b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MediaParserTest.java
index b3f59d1..818ce18 100644
--- a/tests/tests/mediaparser/src/android/media/mediaparser/cts/MediaParserTest.java
+++ b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MediaParserTest.java
@@ -16,10 +16,361 @@
 
 package android.media.mediaparser.cts;
 
-import junit.framework.TestCase;
+import static com.google.common.truth.Truth.assertThat;
 
-public class MediaParserTest extends TestCase {
+import android.media.MediaParser;
 
-    // TODO: Implement tests.
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.google.android.exoplayer2.testutil.FakeExtractorInput;
+import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
+import com.google.android.exoplayer2.testutil.TestUtil;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+
+@RunWith(AndroidJUnit4.class)
+public class MediaParserTest {
+
+    // OGG.
+
+    @Test
+    public void testBearVorbisOgg() throws IOException, InterruptedException {
+        testExtractAsset("ogg/bear_vorbis.ogg");
+    }
+
+    @Test
+    public void testBearOgg() throws IOException, InterruptedException {
+        testExtractAsset("ogg/bear.opus");
+    }
+
+    @Test
+    public void testBearFlacOgg() throws IOException, InterruptedException {
+        testExtractAsset("ogg/bear_flac.ogg");
+    }
+
+    @Test
+    public void testNoFlacSeekTableOgg() throws IOException, InterruptedException {
+        testExtractAsset("ogg/bear_flac_noseektable.ogg");
+    }
+
+    @Test
+    public void testFlacHeaderOggSniff() throws IOException, InterruptedException {
+        testSniffAsset("ogg/flac_header", /* expectedExtractorName= */ "exo.OggExtractor");
+    }
+
+    @Test
+    public void testOpusHeaderOggSniff() throws IOException, InterruptedException {
+        try {
+            testSniffAsset("ogg/opus_header", /* expectedExtractorName= */ "exo.OggExtractor");
+            Assert.fail();
+        } catch (MediaParser.UnrecognizedInputFormatException e) {
+            // Expected.
+        }
+    }
+
+    @Test
+    public void testInvalidHeaderOggSniff() throws IOException, InterruptedException {
+        try {
+            testSniffAsset(
+                    "ogg/invalid_ogg_header", /* expectedExtractorName= */ "exo.OggExtractor");
+            Assert.fail();
+        } catch (MediaParser.UnrecognizedInputFormatException e) {
+            // Expected.
+        }
+    }
+
+    @Test
+    public void testInvalidHeaderSniff() throws IOException, InterruptedException {
+        try {
+            testSniffAsset("ogg/invalid_header", /* expectedExtractorName= */ "exo.OggExtractor");
+            Assert.fail();
+        } catch (MediaParser.UnrecognizedInputFormatException e) {
+            // Expected.
+        }
+    }
+
+    // FLAC.
+
+    @Test
+    public void testBearUncommonSampleRateFlac() throws IOException, InterruptedException {
+        testExtractAsset("flac/bear_uncommon_sample_rate.flac");
+    }
+
+    @Test
+    public void testBearNoSeekTableAndNoNumSamplesFlac() throws IOException, InterruptedException {
+        testExtractAsset("flac/bear_no_seek_table_no_num_samples.flac");
+    }
+
+    @Test
+    public void testBearWithPictureFlac() throws IOException, InterruptedException {
+        testExtractAsset("flac/bear_with_picture.flac");
+    }
+
+    @Test
+    public void testBearWithVorbisCommentsFlac() throws IOException, InterruptedException {
+        testExtractAsset("flac/bear_with_vorbis_comments.flac");
+    }
+
+    @Test
+    public void testOneMetadataBlockFlac() throws IOException, InterruptedException {
+        testExtractAsset("flac/bear_one_metadata_block.flac");
+    }
+
+    @Test
+    public void testBearNoMinMaxFrameSizeFlac() throws IOException, InterruptedException {
+        testExtractAsset("flac/bear_no_min_max_frame_size.flac");
+    }
+
+    @Test
+    public void testNoNumSamplesFlac() throws IOException, InterruptedException {
+        testExtractAsset("flac/bear_no_num_samples.flac");
+    }
+
+    @Test
+    public void testBearNoId3Flac() throws IOException, InterruptedException {
+        testExtractAsset("flac/bear_with_id3_disabled.flac");
+    }
+
+    @Test
+    public void testBearWithId3Flac() throws IOException, InterruptedException {
+        testExtractAsset("flac/bear_with_id3_enabled.flac");
+    }
+
+    @Test
+    public void testBearFlac() throws IOException, InterruptedException {
+        testExtractAsset("flac/bear.flac");
+    }
+
+    // MP3.
+
+    @Test
+    public void testTrimmedMp3() throws IOException, InterruptedException {
+        testExtractAsset("mp3/play-trimmed.mp3");
+    }
+
+    @Test
+    public void testBearMp3() throws IOException, InterruptedException {
+        testExtractAsset("mp3/bear.mp3");
+    }
+
+    // WAV.
+
+    @Test
+    public void testWavWithImaAdpcm() throws IOException, InterruptedException {
+        testExtractAsset("wav/sample_ima_adpcm.wav");
+    }
+
+    @Test
+    public void testWav() throws IOException, InterruptedException {
+        testExtractAsset("wav/sample.wav");
+    }
+
+    // AMR.
+
+    @Test
+    public void testNarrowBandSamplesWithConstantBitrateSeeking()
+            throws IOException, InterruptedException {
+        testExtractAsset("amr/sample_nb_cbr.amr");
+    }
+
+    @Test
+    public void testNarrowBandSamples() throws IOException, InterruptedException {
+        testExtractAsset("amr/sample_nb.amr");
+    }
+
+    @Test
+    public void testWideBandSamples() throws IOException, InterruptedException {
+        testExtractAsset("amr/sample_wb.amr");
+    }
+
+    @Test
+    public void testWideBandSamplesWithConstantBitrateSeeking()
+            throws IOException, InterruptedException {
+        testExtractAsset("amr/sample_wb_cbr.amr");
+    }
+
+    // FLV.
+
+    @Test
+    public void testFlv() throws IOException, InterruptedException {
+        testExtractAsset("flv/sample.flv");
+    }
+
+    // PS.
+
+    // TODO: Enable once the timeout is fixed.
+    @Test
+    @Ignore
+    public void testElphantsDreamPs() throws IOException, InterruptedException {
+        testExtractAsset("ts/elephants_dream.mpg");
+    }
+
+    @Test
+    public void testProgramStream() throws IOException, InterruptedException {
+        testExtractAsset("ts/sample.ps");
+    }
+
+    // ADTS.
+
+    @Test
+    public void testTruncatedAdtsWithConstantBitrateSeeking()
+            throws IOException, InterruptedException {
+        testExtractAsset("ts/sample_cbs_truncated.adts");
+    }
+
+    @Test
+    public void testAdts() throws IOException, InterruptedException {
+        testExtractAsset("ts/sample.adts");
+    }
+
+    @Test
+    public void testAdtsWithConstantBitrateSeeking() throws IOException, InterruptedException {
+        testExtractAsset("ts/sample_cbs.adts");
+    }
+
+    // AC-3.
+
+    @Test
+    public void testAc3() throws IOException, InterruptedException {
+        testExtractAsset("ts/sample.ac3");
+    }
+
+    // AC-4.
+
+    @Test
+    public void testAc4() throws IOException, InterruptedException {
+        testExtractAsset("ts/sample.ac4");
+    }
+
+    // EAC-3.
+
+    @Test
+    public void testEac3() throws IOException, InterruptedException {
+        testExtractAsset("ts/sample.eac3");
+    }
+
+    // TS.
+
+    @Test
+    public void testBigBuckBunnyTs() throws IOException, InterruptedException {
+        testExtractAsset("ts/bbb_2500ms.ts");
+    }
+
+    @Test
+    public void testTransportStream() throws IOException, InterruptedException {
+        testExtractAsset("ts/sample.ts");
+    }
+
+    @Test
+    public void testTransportStreamWithSdt() throws IOException, InterruptedException {
+        testExtractAsset("ts/sample_with_sdt.ts");
+    }
+
+    // MKV.
+
+    @Test
+    public void testSubsampleEncryptedNoAltref() throws IOException, InterruptedException {
+        testExtractAsset("mkv/subsample_encrypted_noaltref.webm");
+    }
+
+    @Test
+    public void testMatroskaFile() throws IOException, InterruptedException {
+        testExtractAsset("mkv/sample.mkv");
+    }
+
+    @Test
+    public void testFullBlocks() throws IOException, InterruptedException {
+        testExtractAsset("mkv/full_blocks.mkv");
+    }
+
+    @Test
+    public void testSubsampleEncryptedAltref() throws IOException, InterruptedException {
+        testExtractAsset("mkv/subsample_encrypted_altref.webm");
+    }
+
+    // MP4.
+
+    @Test
+    public void testAc4Fragmented() throws IOException, InterruptedException {
+        testExtractAsset("mp4/sample_ac4_fragmented.mp4");
+    }
+
+    @Test
+    public void testAndrdoidSlowMotion() throws IOException, InterruptedException {
+        testExtractAsset("mp4/sample_android_slow_motion.mp4");
+    }
+
+    @Test
+    public void testFragmentedSei() throws IOException, InterruptedException {
+        testExtractAsset("mp4/sample_fragmented_sei.mp4");
+    }
+
+    @Test
+    public void testMp4WithAc4() throws IOException, InterruptedException {
+        testExtractAsset("mp4/sample_ac4.mp4");
+    }
+
+    @Test
+    public void testFragmentedSeekable() throws IOException, InterruptedException {
+        testExtractAsset("mp4/sample_fragmented_seekable.mp4");
+    }
+
+    @Test
+    public void testAc4Protected() throws IOException, InterruptedException {
+        testExtractAsset("mp4/sample_ac4_protected.mp4");
+    }
+
+    @Test
+    public void testMp4() throws IOException, InterruptedException {
+        testExtractAsset("mp4/sample.mp4");
+    }
+
+    @Test
+    public void testMdatTooLong() throws IOException, InterruptedException {
+        testExtractAsset("mp4/sample_mdat_too_long.mp4");
+    }
+
+    @Test
+    public void testFragmented() throws IOException, InterruptedException {
+        testExtractAsset("mp4/sample_fragmented.mp4");
+    }
+
+    private static void testSniffAsset(String assetPath, String expectedExtractorName)
+            throws IOException, InterruptedException {
+        extractAsset(assetPath, expectedExtractorName);
+    }
+
+    private static void testExtractAsset(String assetPath)
+            throws IOException, InterruptedException {
+        extractAsset(assetPath, /* expectedExtractorName= */ null);
+    }
+
+    private static void extractAsset(String assetPath, String expectedExtractorName)
+            throws IOException, InterruptedException {
+        byte[] assetBytes =
+                TestUtil.getByteArray(
+                        InstrumentationRegistry.getInstrumentation().getContext(), assetPath);
+        MockMediaParserInputReader mockInput =
+                new MockMediaParserInputReader(
+                        new FakeExtractorInput.Builder().setData(assetBytes).build());
+        MediaParser mediaParser =
+                MediaParser.create(new MockMediaParserOutputConsumer(new FakeExtractorOutput()));
+
+        mediaParser.advance(mockInput);
+        if (expectedExtractorName != null) {
+            assertThat(expectedExtractorName).isEqualTo(mediaParser.getExtractorName());
+            // We are only checking that the extractor is the right one.
+            return;
+        }
+
+        while (mediaParser.advance(mockInput)) {
+            // Do nothing.
+        }
+    }
 }
diff --git a/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserInputReader.java b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserInputReader.java
new file mode 100644
index 0000000..d7fe90d
--- /dev/null
+++ b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserInputReader.java
@@ -0,0 +1,53 @@
+/*
+ * 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 android.media.mediaparser.cts;
+
+import android.media.MediaParser;
+
+import com.google.android.exoplayer2.testutil.FakeExtractorInput;
+
+import java.io.IOException;
+
+public class MockMediaParserInputReader implements MediaParser.SeekableInputReader {
+
+    private final FakeExtractorInput mFakeExtractorInput;
+
+    public MockMediaParserInputReader(FakeExtractorInput fakeExtractorInput) throws IOException {
+        mFakeExtractorInput = fakeExtractorInput;
+    }
+
+    @Override
+    public int read(byte[] buffer, int offset, int readLength)
+            throws IOException, InterruptedException {
+        return mFakeExtractorInput.read(buffer, offset, readLength);
+    }
+
+    @Override
+    public long getPosition() {
+        return mFakeExtractorInput.getPosition();
+    }
+
+    @Override
+    public long getLength() {
+        return mFakeExtractorInput.getLength();
+    }
+
+    @Override
+    public void seekToPosition(long position) {
+        mFakeExtractorInput.setPosition((int) position);
+    }
+}
diff --git a/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserOutputConsumer.java b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserOutputConsumer.java
new file mode 100644
index 0000000..1bef85e
--- /dev/null
+++ b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MockMediaParserOutputConsumer.java
@@ -0,0 +1,202 @@
+/*
+ * 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 android.media.mediaparser.cts;
+
+import android.media.MediaCodec;
+import android.media.MediaParser;
+import android.util.Pair;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.SeekPoint;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class MockMediaParserOutputConsumer implements MediaParser.OutputConsumer {
+
+    private final FakeExtractorOutput mFakeExtractorOutput;
+    private final ArrayList<TrackOutput> mTrackOutputs;
+
+    public MockMediaParserOutputConsumer(FakeExtractorOutput fakeExtractorOutput) {
+        mFakeExtractorOutput = fakeExtractorOutput;
+        mTrackOutputs = new ArrayList<>();
+    }
+
+    @Override
+    public void onSeekMap(MediaParser.SeekMap seekMap) {
+        mFakeExtractorOutput.seekMap(
+                new SeekMap() {
+                    @Override
+                    public boolean isSeekable() {
+                        return seekMap.isSeekable();
+                    }
+
+                    @Override
+                    public long getDurationUs() {
+                        return seekMap.getDurationUs();
+                    }
+
+                    @Override
+                    public SeekPoints getSeekPoints(long timeUs) {
+                        return toExoPlayerSeekPoints(seekMap.getSeekPoints(timeUs));
+                    }
+                });
+    }
+
+    private static SeekMap.SeekPoints toExoPlayerSeekPoints(
+            Pair<MediaParser.SeekPoint, MediaParser.SeekPoint> seekPoints) {
+        return new SeekMap.SeekPoints(
+                toExoPlayerSeekPoint(seekPoints.first), toExoPlayerSeekPoint(seekPoints.second));
+    }
+
+    private static SeekPoint toExoPlayerSeekPoint(MediaParser.SeekPoint seekPoint) {
+        return new SeekPoint(seekPoint.timeUs, seekPoint.position);
+    }
+
+    @Override
+    public void onTracksFound(int numberOfTracks) {
+        // Do nothing.
+    }
+
+    @Override
+    public void onTrackData(int trackIndex, MediaParser.TrackData trackData) {
+        while (mTrackOutputs.size() < trackIndex) {
+            mTrackOutputs.add(mFakeExtractorOutput.track(trackIndex, C.TRACK_TYPE_UNKNOWN));
+        }
+    }
+
+    @Override
+    public void onSampleData(int trackIndex, MediaParser.InputReader inputReader)
+            throws IOException, InterruptedException {
+        mFakeExtractorOutput
+                .track(trackIndex, C.TRACK_TYPE_UNKNOWN)
+                .sampleData(
+                        new ExtractorInputAdapter(inputReader),
+                        (int) inputReader.getLength(),
+                        false);
+    }
+
+    @Override
+    public void onSampleCompleted(
+            int trackIndex,
+            long timeUs,
+            int flags,
+            int size,
+            int offset,
+            MediaCodec.CryptoInfo cryptoData) {}
+
+    private class ExtractorInputAdapter implements ExtractorInput {
+
+        private final MediaParser.InputReader mInputReader;
+
+        private ExtractorInputAdapter(MediaParser.InputReader inputReader) {
+            mInputReader = inputReader;
+        }
+
+        @Override
+        public int read(byte[] target, int offset, int length)
+                throws IOException, InterruptedException {
+            return mInputReader.read(target, offset, length);
+        }
+
+        @Override
+        public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+                throws IOException, InterruptedException {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void readFully(byte[] target, int offset, int length)
+                throws IOException, InterruptedException {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int skip(int length) throws IOException, InterruptedException {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean skipFully(int length, boolean allowEndOfInput)
+                throws IOException, InterruptedException {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void skipFully(int length) throws IOException, InterruptedException {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int peek(byte[] target, int offset, int length)
+                throws IOException, InterruptedException {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+                throws IOException, InterruptedException {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void peekFully(byte[] target, int offset, int length)
+                throws IOException, InterruptedException {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean advancePeekPosition(int length, boolean allowEndOfInput)
+                throws IOException, InterruptedException {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void advancePeekPosition(int length) throws IOException, InterruptedException {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void resetPeekPosition() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public long getPeekPosition() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public long getPosition() {
+            return mInputReader.getPosition();
+        }
+
+        @Override
+        public long getLength() {
+            return mInputReader.getLength();
+        }
+
+        @Override
+        public <E extends Throwable> void setRetryPosition(long position, E e) throws E {
+            throw new UnsupportedOperationException();
+        }
+    }
+}