From 731d4283abf18acc9caa7d502998b9854022d522 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 23 May 2016 02:09:36 -0700 Subject: [PATCH] Ogg/Opus and Ogg/Flac search seeking ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=122977123 --- .../src/androidTest/assets/ogg/bear_flac.ogg | Bin 0 -> 173796 bytes .../assets/ogg/bear_flac_noseektable.ogg | Bin 0 -> 173746 bytes .../extractor/ogg/DefaultOggSeekerTest.java | 108 ++++++ .../ogg/DefaultOggSeekerUtilMethodsTest.java | 234 +++++++++++ .../extractor/ogg/OggExtractorFileTests.java | 94 +++++ .../extractor/ogg/OggExtractorTest.java | 39 +- .../extractor/ogg/OggPacketTest.java | 248 ++++++++++++ .../extractor/ogg/OggPageHeaderTest.java | 95 +++++ .../extractor/ogg/OggParserTest.java | 365 ------------------ .../extractor/ogg/OggSeekerTest.java | 132 ------- .../exoplayer/extractor/ogg/OggUtilTest.java | 190 --------- .../extractor/ogg/OpusReaderTest.java | 104 ----- .../extractor/ogg/VorbisReaderTest.java | 18 +- .../android/exoplayer/testutil/TestUtil.java | 42 +- .../java/com/google/android/exoplayer/C.java | 5 + .../extractor/ogg/DefaultOggSeeker.java | 297 ++++++++++++++ .../exoplayer/extractor/ogg/FlacReader.java | 174 +++++++-- .../exoplayer/extractor/ogg/OggExtractor.java | 16 +- .../exoplayer/extractor/ogg/OggPacket.java | 141 +++++++ .../extractor/ogg/OggPageHeader.java | 131 +++++++ .../exoplayer/extractor/ogg/OggParser.java | 173 --------- .../exoplayer/extractor/ogg/OggSeeker.java | 70 ++-- .../exoplayer/extractor/ogg/OggUtil.java | 211 ---------- .../exoplayer/extractor/ogg/OpusReader.java | 93 ++--- .../exoplayer/extractor/ogg/StreamReader.java | 196 +++++++++- .../exoplayer/extractor/ogg/VorbisReader.java | 186 +++------ .../android/exoplayer/util/FlacSeekTable.java | 91 ----- .../android/exoplayer/util/FlacUtil.java | 50 --- 28 files changed, 1882 insertions(+), 1621 deletions(-) create mode 100644 library/src/androidTest/assets/ogg/bear_flac.ogg create mode 100644 library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg create mode 100644 library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/DefaultOggSeekerTest.java create mode 100644 library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java create mode 100644 library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorFileTests.java create mode 100644 library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggPacketTest.java create mode 100644 library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggPageHeaderTest.java delete mode 100644 library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggParserTest.java delete mode 100644 library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggSeekerTest.java delete mode 100644 library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggUtilTest.java delete mode 100644 library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OpusReaderTest.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/ogg/DefaultOggSeeker.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggPacket.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggPageHeader.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggParser.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/util/FlacSeekTable.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/util/FlacUtil.java diff --git a/library/src/androidTest/assets/ogg/bear_flac.ogg b/library/src/androidTest/assets/ogg/bear_flac.ogg new file mode 100644 index 0000000000000000000000000000000000000000..68e8091ed04df15bbffd278745a28e21d3f719ef GIT binary patch literal 173796 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 Jrqp5wC0nk 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℘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 0 && granuleDiff <= DefaultOggSeeker.MATCH_RANGE) { + expectedPosition = -1; + } else { + long doublePageSize = (27 + 2 + 54 + 55) * (granuleDiff <= 0 ? 2 : 1); + expectedPosition = pagePosition - doublePageSize + granuleDiff; + expectedPosition = Math.max(expectedPosition, START_POSITION); + expectedPosition = Math.min(expectedPosition, END_POSITION - 1); + } + assertGetNextSeekPosition(expectedPosition, targetGranule, input); + } + + private void assertGetNextSeekPosition(long expectedPosition, long targetGranule, + FakeExtractorInput input) throws IOException, InterruptedException { + while (true) { + try { + assertEquals(expectedPosition, oggSeeker.getNextSeekPosition(targetGranule, input)); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { + // ignored + } + } + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java new file mode 100644 index 0000000000..961a230634 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2015 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.exoplayer.extractor.ogg; + +import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.extractor.ExtractorInput; +import com.google.android.exoplayer.testutil.FakeExtractorInput; +import com.google.android.exoplayer.testutil.TestUtil; + +import junit.framework.TestCase; + +import java.io.EOFException; +import java.io.IOException; +import java.util.Random; + +/** + * Unit test for {@link DefaultOggSeeker} utility methods. + */ +public class DefaultOggSeekerUtilMethodsTest extends TestCase { + + private Random random = new Random(0); + private DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, 100, new FlacReader()); + + public void testSkipToNextPage() throws Exception { + FakeExtractorInput extractorInput = TestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(4000, random), + new byte[]{'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random) + ), false); + skipToNextPage(extractorInput); + assertEquals(4000, extractorInput.getPosition()); + } + + public void testSkipToNextPageUnbounded() throws Exception { + FakeExtractorInput extractorInput = TestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(4000, random), + new byte[]{'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random) + ), true); + skipToNextPage(extractorInput); + assertEquals(4000, extractorInput.getPosition()); + } + + public void testSkipToNextPageOverlap() throws Exception { + FakeExtractorInput extractorInput = TestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(2046, random), + new byte[]{'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random) + ), false); + skipToNextPage(extractorInput); + assertEquals(2046, extractorInput.getPosition()); + } + + public void testSkipToNextPageOverlapUnbounded() throws Exception { + FakeExtractorInput extractorInput = TestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(2046, random), + new byte[]{'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random) + ), true); + skipToNextPage(extractorInput); + assertEquals(2046, extractorInput.getPosition()); + } + + public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { + FakeExtractorInput extractorInput = TestData.createInput( + TestUtil.joinByteArrays( + new byte[]{'x', 'O', 'g', 'g', 'S'} + ), false); + skipToNextPage(extractorInput); + assertEquals(1, extractorInput.getPosition()); + } + + public void testSkipToNextPageNoMatch() throws Exception { + FakeExtractorInput extractorInput = TestData.createInput( + new byte[]{'g', 'g', 'S', 'O', 'g', 'g'}, false); + try { + skipToNextPage(extractorInput); + fail(); + } catch (EOFException e) { + // expected + } + } + + private static void skipToNextPage(ExtractorInput extractorInput) + throws IOException, InterruptedException { + while (true) { + try { + DefaultOggSeeker.skipToNextPage(extractorInput); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { /* ignored */ } + } + } + + public void testSkipToPageOfGranule() throws IOException, InterruptedException { + byte[] packet = TestUtil.buildTestData(3 * 254, random); + FakeExtractorInput input = TestData.createInput( + TestUtil.joinByteArrays( + TestData.buildOggHeader(0x01, 20000, 1000, 0x03), + TestUtil.createByteArray(254, 254, 254), // Laces. + packet, + TestData.buildOggHeader(0x04, 40000, 1001, 0x03), + TestUtil.createByteArray(254, 254, 254), // Laces. + packet, + TestData.buildOggHeader(0x04, 60000, 1002, 0x03), + TestUtil.createByteArray(254, 254, 254), // Laces. + packet), false); + + // expect to be granule of the previous page returned as elapsedSamples + skipToPageOfGranule(input, 54000, 40000); + // expect to be at the start of the third page + assertEquals(2 * (30 + (3 * 254)), input.getPosition()); + } + + public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException { + byte[] packet = TestUtil.buildTestData(3 * 254, random); + FakeExtractorInput input = TestData.createInput( + TestUtil.joinByteArrays( + TestData.buildOggHeader(0x01, 20000, 1000, 0x03), + TestUtil.createByteArray(254, 254, 254), // Laces. + packet, + TestData.buildOggHeader(0x04, 40000, 1001, 0x03), + TestUtil.createByteArray(254, 254, 254), // Laces. + packet, + TestData.buildOggHeader(0x04, 60000, 1002, 0x03), + TestUtil.createByteArray(254, 254, 254), // Laces. + packet), false); + + skipToPageOfGranule(input, 40000, 20000); + // expect to be at the start of the second page + assertEquals((30 + (3 * 254)), input.getPosition()); + } + + public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException { + byte[] packet = TestUtil.buildTestData(3 * 254, random); + FakeExtractorInput input = TestData.createInput( + TestUtil.joinByteArrays( + TestData.buildOggHeader(0x01, 20000, 1000, 0x03), + TestUtil.createByteArray(254, 254, 254), // Laces. + packet, + TestData.buildOggHeader(0x04, 40000, 1001, 0x03), + TestUtil.createByteArray(254, 254, 254), // Laces. + packet, + TestData.buildOggHeader(0x04, 60000, 1002, 0x03), + TestUtil.createByteArray(254, 254, 254), // Laces. + packet), false); + + try { + skipToPageOfGranule(input, 10000, 20000); + fail(); + } catch (ParserException e) { + // ignored + } + assertEquals(0, input.getPosition()); + } + + private void skipToPageOfGranule(ExtractorInput input, long granule, + long elapsedSamplesExpected) throws IOException, InterruptedException { + while (true) { + try { + assertEquals(elapsedSamplesExpected, oggSeeker.skipToPageOfGranule(input, granule)); + return; + } catch (FakeExtractorInput.SimulatedIOException e) { + input.resetPeekPosition(); + } + } + } + + public void testReadGranuleOfLastPage() throws IOException, InterruptedException { + FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( + TestUtil.buildTestData(100, random), + TestData.buildOggHeader(0x00, 20000, 66, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random), + TestData.buildOggHeader(0x00, 40000, 67, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random), + TestData.buildOggHeader(0x05, 60000, 68, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random) + ), false); + assertReadGranuleOfLastPage(input, 60000); + } + + public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { + FakeExtractorInput input = TestData.createInput(TestUtil.buildTestData(100, random), false); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (EOFException e) { + // ignored + } + } + + public void testReadGranuleOfLastPageWithUnboundedLength() + throws IOException, InterruptedException { + FakeExtractorInput input = TestData.createInput(new byte[0], true); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (IllegalArgumentException e) { + // ignored + } + } + + private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) + throws IOException, InterruptedException { + while (true) { + try { + assertEquals(expected, oggSeeker.readGranuleOfLastPage(input)); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { + // ignored + } + } + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorFileTests.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorFileTests.java new file mode 100644 index 0000000000..7342dde691 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorFileTests.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2016 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.exoplayer.extractor.ogg; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.Format; +import com.google.android.exoplayer.extractor.SeekMap; +import com.google.android.exoplayer.testutil.FakeExtractorInput; +import com.google.android.exoplayer.testutil.FakeExtractorOutput; +import com.google.android.exoplayer.testutil.FakeTrackOutput; +import com.google.android.exoplayer.testutil.TestUtil; +import com.google.android.exoplayer.util.MimeTypes; + +import android.test.InstrumentationTestCase; + +/** + * Unit test for {@link OpusReader}. + */ +public final class OggExtractorFileTests extends InstrumentationTestCase { + + public static final String OPUS_TEST_FILE = "ogg/bear.opus"; + public static final String FLAC_TEST_FILE = "ogg/bear_flac.ogg"; + public static final String FLAC_NS_TEST_FILE = "ogg/bear_flac_noseektable.ogg"; + + public void testOpus() throws Exception { + parseFile(OPUS_TEST_FILE, false, false, false, MimeTypes.AUDIO_OPUS, 2747500, 275); + parseFile(OPUS_TEST_FILE, false, true, false, MimeTypes.AUDIO_OPUS, C.UNSET_TIME_US, 275); + parseFile(OPUS_TEST_FILE, true, false, true, MimeTypes.AUDIO_OPUS, 2747500, 275); + parseFile(OPUS_TEST_FILE, true, true, true, MimeTypes.AUDIO_OPUS, C.UNSET_TIME_US, 275); + } + + public void testFlac() throws Exception { + parseFile(FLAC_TEST_FILE, false, false, false, MimeTypes.AUDIO_FLAC, 2741000, 33); + parseFile(FLAC_TEST_FILE, false, true, false, MimeTypes.AUDIO_FLAC, 2741000, 33); + parseFile(FLAC_TEST_FILE, true, false, true, MimeTypes.AUDIO_FLAC, 2741000, 33); + parseFile(FLAC_TEST_FILE, true, true, true, MimeTypes.AUDIO_FLAC, 2741000, 33); + } + + public void testFlacNoSeektable() throws Exception { + parseFile(FLAC_NS_TEST_FILE, false, false, false, MimeTypes.AUDIO_FLAC, 2741000, 33); + parseFile(FLAC_NS_TEST_FILE, false, true, false, MimeTypes.AUDIO_FLAC, C.UNSET_TIME_US, 33); + parseFile(FLAC_NS_TEST_FILE, true, false, true, MimeTypes.AUDIO_FLAC, 2741000, 33); + parseFile(FLAC_NS_TEST_FILE, true, true, true, MimeTypes.AUDIO_FLAC, C.UNSET_TIME_US, 33); + } + + private void parseFile(String testFile, boolean simulateIOErrors, boolean simulateUnknownLength, + boolean simulatePartialReads, String expectedMimeType, long expectedDuration, + int expectedSampleCount) + throws Exception { + byte[] fileData = TestUtil.getByteArray(getInstrumentation(), testFile); + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) + .setSimulateIOErrors(simulateIOErrors) + .setSimulateUnknownLength(simulateUnknownLength) + .setSimulatePartialReads(simulatePartialReads).build(); + + OggExtractor extractor = new OggExtractor(); + assertTrue(TestUtil.sniffTestData(extractor, input)); + input.resetPeekPosition(); + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + extractor.init(extractorOutput); + TestUtil.consumeTestData(extractor, input); + + assertEquals(1, extractorOutput.trackOutputs.size()); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertNotNull(trackOutput); + + Format format = trackOutput.format; + assertNotNull(format); + assertEquals(expectedMimeType, format.sampleMimeType); + assertEquals(48000, format.sampleRate); + assertEquals(2, format.channelCount); + + SeekMap seekMap = extractorOutput.seekMap; + assertNotNull(seekMap); + assertEquals(expectedDuration, seekMap.getDurationUs()); + assertEquals(expectedDuration != C.UNSET_TIME_US, seekMap.isSeekable()); + + trackOutput.assertSampleCount(expectedSampleCount); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorTest.java index 0913d456aa..df119fb7e9 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorTest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer.extractor.ogg; import com.google.android.exoplayer.testutil.FakeExtractorInput; -import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer.testutil.TestUtil; import junit.framework.TestCase; @@ -40,56 +39,48 @@ public final class OggExtractorTest extends TestCase { byte[] data = TestUtil.joinByteArrays( TestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(7), // Laces - new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'}); - assertTrue(sniff(createInput(data))); + new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); + assertTrue(sniff(data)); } public void testSniffFlac() throws Exception { byte[] data = TestUtil.joinByteArrays( TestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(5), // Laces - new byte[]{0x7F, 'F', 'L', 'A', 'C'}); - assertTrue(sniff(createInput(data))); + new byte[] {0x7F, 'F', 'L', 'A', 'C'}); + assertTrue(sniff(data)); } public void testSniffFailsOpusFile() throws Exception { byte[] data = TestUtil.joinByteArrays( TestData.buildOggHeader(0x02, 0, 1000, 0x00), - new byte[]{'O', 'p', 'u', 's'}); - assertFalse(sniff(createInput(data))); + new byte[] {'O', 'p', 'u', 's'}); + assertFalse(sniff(data)); } public void testSniffFailsInvalidOggHeader() throws Exception { byte[] data = TestData.buildOggHeader(0x00, 0, 1000, 0x00); - assertFalse(sniff(createInput(data))); + assertFalse(sniff(data)); } public void testSniffInvalidHeader() throws Exception { byte[] data = TestUtil.joinByteArrays( TestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(7), // Laces - new byte[]{0x7F, 'X', 'o', 'r', 'b', 'i', 's'}); - assertFalse(sniff(createInput(data))); + new byte[] {0x7F, 'X', 'o', 'r', 'b', 'i', 's'}); + assertFalse(sniff(data)); } public void testSniffFailsEOF() throws Exception { byte[] data = TestData.buildOggHeader(0x02, 0, 1000, 0x00); - assertFalse(sniff(createInput(data))); + assertFalse(sniff(data)); } - private static FakeExtractorInput createInput(byte[] data) { - return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) - .setSimulateUnknownLength(true).setSimulatePartialReads(true).build(); - } - - private boolean sniff(FakeExtractorInput input) throws InterruptedException, IOException { - while (true) { - try { - return extractor.sniff(input); - } catch (SimulatedIOException e) { - // Ignore. - } - } + private boolean sniff(byte[] data) throws InterruptedException, IOException { + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data) + .setSimulateIOErrors(true).setSimulateUnknownLength(true).setSimulatePartialReads(true) + .build(); + return TestUtil.sniffTestData(extractor, input); } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggPacketTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggPacketTest.java new file mode 100644 index 0000000000..17c6f37c6b --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggPacketTest.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2015 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.exoplayer.extractor.ogg; + +import com.google.android.exoplayer.testutil.FakeExtractorInput; +import com.google.android.exoplayer.testutil.TestUtil; +import com.google.android.exoplayer.util.ParsableByteArray; + +import android.test.InstrumentationTestCase; +import android.test.MoreAsserts; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Random; + +/** + * Unit test for {@link OggPacket}. + */ +public final class OggPacketTest extends InstrumentationTestCase { + + private static final String TEST_FILE = "ogg/bear.opus"; + + private Random random; + private OggPacket oggPacket; + + @Override + public void setUp() throws Exception { + super.setUp(); + random = new Random(0); + oggPacket = new OggPacket(); + } + + public void testReadPacketsWithEmptyPage() throws Exception { + byte[] firstPacket = TestUtil.buildTestData(8, random); + byte[] secondPacket = TestUtil.buildTestData(272, random); + byte[] thirdPacket = TestUtil.buildTestData(256, random); + byte[] fourthPacket = TestUtil.buildTestData(271, random); + + FakeExtractorInput input = TestData.createInput( + TestUtil.joinByteArrays( + // First page with a single packet. + TestData.buildOggHeader(0x02, 0, 1000, 0x01), + TestUtil.createByteArray(0x08), // Laces + firstPacket, + // Second page with a single packet. + TestData.buildOggHeader(0x00, 16, 1001, 0x02), + TestUtil.createByteArray(0xFF, 0x11), // Laces + secondPacket, + // Third page with zero packets. + TestData.buildOggHeader(0x00, 16, 1002, 0x00), + // Fourth page with two packets. + TestData.buildOggHeader(0x04, 128, 1003, 0x04), + TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces + thirdPacket, + fourthPacket), true); + + assertReadPacket(input, firstPacket); + assertTrue((oggPacket.getPageHeader().type & 0x02) == 0x02); + assertFalse((oggPacket.getPageHeader().type & 0x04) == 0x04); + assertEquals(0x02, oggPacket.getPageHeader().type); + assertEquals(27 + 1, oggPacket.getPageHeader().headerSize); + assertEquals(8, oggPacket.getPageHeader().bodySize); + assertEquals(0x00, oggPacket.getPageHeader().revision); + assertEquals(1, oggPacket.getPageHeader().pageSegmentCount); + assertEquals(1000, oggPacket.getPageHeader().pageSequenceNumber); + assertEquals(4096, oggPacket.getPageHeader().streamSerialNumber); + assertEquals(0, oggPacket.getPageHeader().granulePosition); + + assertReadPacket(input, secondPacket); + assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02); + assertFalse((oggPacket.getPageHeader().type & 0x04) == 0x04); + assertEquals(0, oggPacket.getPageHeader().type); + assertEquals(27 + 2, oggPacket.getPageHeader().headerSize); + assertEquals(255 + 17, oggPacket.getPageHeader().bodySize); + assertEquals(2, oggPacket.getPageHeader().pageSegmentCount); + assertEquals(1001, oggPacket.getPageHeader().pageSequenceNumber); + assertEquals(16, oggPacket.getPageHeader().granulePosition); + + assertReadPacket(input, thirdPacket); + assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02); + assertTrue((oggPacket.getPageHeader().type & 0x04) == 0x04); + assertEquals(4, oggPacket.getPageHeader().type); + assertEquals(27 + 4, oggPacket.getPageHeader().headerSize); + assertEquals(255 + 1 + 255 + 16, oggPacket.getPageHeader().bodySize); + assertEquals(4, oggPacket.getPageHeader().pageSegmentCount); + // Page 1002 is empty, so current page is 1003. + assertEquals(1003, oggPacket.getPageHeader().pageSequenceNumber); + assertEquals(128, oggPacket.getPageHeader().granulePosition); + + assertReadPacket(input, fourthPacket); + + assertReadEof(input); + } + + public void testReadPacketWithZeroSizeTerminator() throws Exception { + byte[] firstPacket = TestUtil.buildTestData(255, random); + byte[] secondPacket = TestUtil.buildTestData(8, random); + + FakeExtractorInput input = TestData.createInput( + TestUtil.joinByteArrays( + TestData.buildOggHeader(0x06, 0, 1000, 0x04), + TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces. + firstPacket, + secondPacket), true); + + assertReadPacket(input, firstPacket); + assertReadPacket(input, secondPacket); + assertReadEof(input); + } + + public void testReadContinuedPacketOverTwoPages() throws Exception { + byte[] firstPacket = TestUtil.buildTestData(518); + + FakeExtractorInput input = TestData.createInput( + TestUtil.joinByteArrays( + // First page. + TestData.buildOggHeader(0x02, 0, 1000, 0x02), + TestUtil.createByteArray(0xFF, 0xFF), // Laces. + Arrays.copyOf(firstPacket, 510), + // Second page (continued packet). + TestData.buildOggHeader(0x05, 10, 1001, 0x01), + TestUtil.createByteArray(0x08), // Laces. + Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true); + + assertReadPacket(input, firstPacket); + assertTrue((oggPacket.getPageHeader().type & 0x04) == 0x04); + assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02); + assertEquals(1001, oggPacket.getPageHeader().pageSequenceNumber); + + assertReadEof(input); + } + + public void testReadContinuedPacketOverFourPages() throws Exception { + byte[] firstPacket = TestUtil.buildTestData(1028); + + FakeExtractorInput input = TestData.createInput( + TestUtil.joinByteArrays( + // First page. + TestData.buildOggHeader(0x02, 0, 1000, 0x02), + TestUtil.createByteArray(0xFF, 0xFF), // Laces. + Arrays.copyOf(firstPacket, 510), + // Second page (continued packet). + TestData.buildOggHeader(0x01, 10, 1001, 0x01), + TestUtil.createByteArray(0xFF), // Laces. + Arrays.copyOfRange(firstPacket, 510, 510 + 255), + // Third page (continued packet). + TestData.buildOggHeader(0x01, 10, 1002, 0x01), + TestUtil.createByteArray(0xFF), // Laces. + Arrays.copyOfRange(firstPacket, 510 + 255, 510 + 255 + 255), + // Fourth page (continued packet). + TestData.buildOggHeader(0x05, 10, 1003, 0x01), + TestUtil.createByteArray(0x08), // Laces. + Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true); + + assertReadPacket(input, firstPacket); + assertTrue((oggPacket.getPageHeader().type & 0x04) == 0x04); + assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02); + assertEquals(1003, oggPacket.getPageHeader().pageSequenceNumber); + + assertReadEof(input); + } + + public void testReadDiscardContinuedPacketAtStart() throws Exception { + byte[] pageBody = TestUtil.buildTestData(256 + 8); + + FakeExtractorInput input = TestData.createInput( + TestUtil.joinByteArrays( + // Page with a continued packet at start. + TestData.buildOggHeader(0x01, 10, 1001, 0x03), + TestUtil.createByteArray(255, 1, 8), // Laces. + pageBody), true); + + // Expect the first partial packet to be discarded. + assertReadPacket(input, Arrays.copyOfRange(pageBody, 256, 256 + 8)); + assertReadEof(input); + } + + public void testReadZeroSizedPacketsAtEndOfStream() throws Exception { + byte[] firstPacket = TestUtil.buildTestData(8, random); + byte[] secondPacket = TestUtil.buildTestData(8, random); + byte[] thirdPacket = TestUtil.buildTestData(8, random); + + FakeExtractorInput input = TestData.createInput( + TestUtil.joinByteArrays( + TestData.buildOggHeader(0x02, 0, 1000, 0x01), + TestUtil.createByteArray(0x08), // Laces. + firstPacket, + TestData.buildOggHeader(0x04, 0, 1001, 0x03), + TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. + secondPacket, + TestData.buildOggHeader(0x04, 0, 1002, 0x03), + TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. + thirdPacket), true); + + assertReadPacket(input, firstPacket); + assertReadPacket(input, secondPacket); + assertReadPacket(input, thirdPacket); + assertReadEof(input); + } + + + public void testParseRealFile() throws IOException, InterruptedException { + byte[] data = TestUtil.getByteArray(getInstrumentation(), TEST_FILE); + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); + int packetCounter = 0; + while (readPacket(input)) { + packetCounter++; + } + assertEquals(277, packetCounter); + } + + private void assertReadPacket(FakeExtractorInput extractorInput, byte[] expected) + throws IOException, InterruptedException { + assertTrue(readPacket(extractorInput)); + ParsableByteArray payload = oggPacket.getPayload(); + MoreAsserts.assertEquals(expected, Arrays.copyOf(payload.data, payload.limit())); + } + + private void assertReadEof(FakeExtractorInput extractorInput) + throws IOException, InterruptedException { + assertFalse(readPacket(extractorInput)); + } + + private boolean readPacket(FakeExtractorInput input) + throws InterruptedException, IOException { + while (true) { + try { + return oggPacket.populate(input); + } catch (FakeExtractorInput.SimulatedIOException e) { + // Ignore. + } + } + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggPageHeaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggPageHeaderTest.java new file mode 100644 index 0000000000..da6289ddb3 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggPageHeaderTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2015 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.exoplayer.extractor.ogg; + +import com.google.android.exoplayer.testutil.FakeExtractorInput; +import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException; +import com.google.android.exoplayer.testutil.TestUtil; + +import junit.framework.TestCase; + +import java.io.IOException; + +/** + * Unit test for {@link OggPageHeader}. + */ +public final class OggPageHeaderTest extends TestCase { + + public void testPopulatePageHeader() throws IOException, InterruptedException { + FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( + TestData.buildOggHeader(0x01, 123456, 4, 2), + TestUtil.createByteArray(2, 2) + ), true); + OggPageHeader header = new OggPageHeader(); + populatePageHeader(input, header, false); + + assertEquals(0x01, header.type); + assertEquals(27 + 2, header.headerSize); + assertEquals(4, header.bodySize); + assertEquals(2, header.pageSegmentCount); + assertEquals(123456, header.granulePosition); + assertEquals(4, header.pageSequenceNumber); + assertEquals(0x1000, header.streamSerialNumber); + assertEquals(0x100000, header.pageChecksum); + assertEquals(0, header.revision); + } + + public void testPopulatePageHeaderQuiteOnExceptionLessThan27Bytes() + throws IOException, InterruptedException { + FakeExtractorInput input = TestData.createInput(TestUtil.createByteArray(2, 2), false); + OggPageHeader header = new OggPageHeader(); + assertFalse(populatePageHeader(input, header, true)); + } + + public void testPopulatePageHeaderQuiteOnExceptionNotOgg() + throws IOException, InterruptedException { + byte[] headerBytes = TestUtil.joinByteArrays( + TestData.buildOggHeader(0x01, 123456, 4, 2), + TestUtil.createByteArray(2, 2) + ); + // change from 'O' to 'o' + headerBytes[0] = 'o'; + FakeExtractorInput input = TestData.createInput(headerBytes, false); + OggPageHeader header = new OggPageHeader(); + assertFalse(populatePageHeader(input, header, true)); + } + + public void testPopulatePageHeaderQuiteOnExceptionWrongRevision() + throws IOException, InterruptedException { + byte[] headerBytes = TestUtil.joinByteArrays( + TestData.buildOggHeader(0x01, 123456, 4, 2), + TestUtil.createByteArray(2, 2) + ); + // change revision from 0 to 1 + headerBytes[4] = 0x01; + FakeExtractorInput input = TestData.createInput(headerBytes, false); + OggPageHeader header = new OggPageHeader(); + assertFalse(populatePageHeader(input, header, true)); + } + + private boolean populatePageHeader(FakeExtractorInput input, OggPageHeader header, + boolean quite) throws IOException, InterruptedException { + while (true) { + try { + return header.populate(input, quite); + } catch (SimulatedIOException e) { + // ignored + } + } + } + +} + diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggParserTest.java deleted file mode 100644 index ebacfee144..0000000000 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggParserTest.java +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Copyright (C) 2015 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.exoplayer.extractor.ogg; - -import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.extractor.ExtractorInput; -import com.google.android.exoplayer.testutil.FakeExtractorInput; -import com.google.android.exoplayer.testutil.TestUtil; -import com.google.android.exoplayer.util.ParsableByteArray; - -import android.test.MoreAsserts; - -import junit.framework.TestCase; - -import java.io.EOFException; -import java.io.IOException; -import java.util.Arrays; -import java.util.Random; - -/** - * Unit test for {@link OggParser}. - */ -public final class OggParserTest extends TestCase { - - private Random random; - private OggParser oggParser; - private ParsableByteArray scratch; - - @Override - public void setUp() throws Exception { - super.setUp(); - random = new Random(0); - oggParser = new OggParser(); - scratch = new ParsableByteArray(new byte[255 * 255], 0); - } - - public void testReadPacketsWithEmptyPage() throws Exception { - byte[] firstPacket = TestUtil.buildTestData(8, random); - byte[] secondPacket = TestUtil.buildTestData(272, random); - byte[] thirdPacket = TestUtil.buildTestData(256, random); - byte[] fourthPacket = TestUtil.buildTestData(271, random); - - FakeExtractorInput input = TestData.createInput( - TestUtil.joinByteArrays( - // First page with a single packet. - TestData.buildOggHeader(0x02, 0, 1000, 0x01), - TestUtil.createByteArray(0x08), // Laces - firstPacket, - // Second page with a single packet. - TestData.buildOggHeader(0x00, 16, 1001, 0x02), - TestUtil.createByteArray(0xFF, 0x11), // Laces - secondPacket, - // Third page with zero packets. - TestData.buildOggHeader(0x00, 16, 1002, 0x00), - // Fourth page with two packets. - TestData.buildOggHeader(0x04, 128, 1003, 0x04), - TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces - thirdPacket, - fourthPacket), true); - - assertReadPacket(input, firstPacket); - assertTrue((oggParser.getPageHeader().type & 0x02) == 0x02); - assertFalse((oggParser.getPageHeader().type & 0x04) == 0x04); - assertEquals(0x02, oggParser.getPageHeader().type); - assertEquals(27 + 1, oggParser.getPageHeader().headerSize); - assertEquals(8, oggParser.getPageHeader().bodySize); - assertEquals(0x00, oggParser.getPageHeader().revision); - assertEquals(1, oggParser.getPageHeader().pageSegmentCount); - assertEquals(1000, oggParser.getPageHeader().pageSequenceNumber); - assertEquals(4096, oggParser.getPageHeader().streamSerialNumber); - assertEquals(0, oggParser.getPageHeader().granulePosition); - - assertReadPacket(input, secondPacket); - assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02); - assertFalse((oggParser.getPageHeader().type & 0x04) == 0x04); - assertEquals(0, oggParser.getPageHeader().type); - assertEquals(27 + 2, oggParser.getPageHeader().headerSize); - assertEquals(255 + 17, oggParser.getPageHeader().bodySize); - assertEquals(2, oggParser.getPageHeader().pageSegmentCount); - assertEquals(1001, oggParser.getPageHeader().pageSequenceNumber); - assertEquals(16, oggParser.getPageHeader().granulePosition); - - assertReadPacket(input, thirdPacket); - assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02); - assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04); - assertEquals(4, oggParser.getPageHeader().type); - assertEquals(27 + 4, oggParser.getPageHeader().headerSize); - assertEquals(255 + 1 + 255 + 16, oggParser.getPageHeader().bodySize); - assertEquals(4, oggParser.getPageHeader().pageSegmentCount); - // Page 1002 is empty, so current page is 1003. - assertEquals(1003, oggParser.getPageHeader().pageSequenceNumber); - assertEquals(128, oggParser.getPageHeader().granulePosition); - - assertReadPacket(input, fourthPacket); - - assertReadEof(input); - } - - public void testReadPacketWithZeroSizeTerminator() throws Exception { - byte[] firstPacket = TestUtil.buildTestData(255, random); - byte[] secondPacket = TestUtil.buildTestData(8, random); - - FakeExtractorInput input = TestData.createInput( - TestUtil.joinByteArrays( - TestData.buildOggHeader(0x06, 0, 1000, 0x04), - TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces. - firstPacket, - secondPacket), true); - - assertReadPacket(input, firstPacket); - assertReadPacket(input, secondPacket); - assertReadEof(input); - } - - public void testReadContinuedPacketOverTwoPages() throws Exception { - byte[] firstPacket = TestUtil.buildTestData(518); - - FakeExtractorInput input = TestData.createInput( - TestUtil.joinByteArrays( - // First page. - TestData.buildOggHeader(0x02, 0, 1000, 0x02), - TestUtil.createByteArray(0xFF, 0xFF), // Laces. - Arrays.copyOf(firstPacket, 510), - // Second page (continued packet). - TestData.buildOggHeader(0x05, 10, 1001, 0x01), - TestUtil.createByteArray(0x08), // Laces. - Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true); - - assertReadPacket(input, firstPacket); - assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04); - assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02); - assertEquals(1001, oggParser.getPageHeader().pageSequenceNumber); - - assertReadEof(input); - } - - public void testReadContinuedPacketOverFourPages() throws Exception { - byte[] firstPacket = TestUtil.buildTestData(1028); - - FakeExtractorInput input = TestData.createInput( - TestUtil.joinByteArrays( - // First page. - TestData.buildOggHeader(0x02, 0, 1000, 0x02), - TestUtil.createByteArray(0xFF, 0xFF), // Laces. - Arrays.copyOf(firstPacket, 510), - // Second page (continued packet). - TestData.buildOggHeader(0x01, 10, 1001, 0x01), - TestUtil.createByteArray(0xFF), // Laces. - Arrays.copyOfRange(firstPacket, 510, 510 + 255), - // Third page (continued packet). - TestData.buildOggHeader(0x01, 10, 1002, 0x01), - TestUtil.createByteArray(0xFF), // Laces. - Arrays.copyOfRange(firstPacket, 510 + 255, 510 + 255 + 255), - // Fourth page (continued packet). - TestData.buildOggHeader(0x05, 10, 1003, 0x01), - TestUtil.createByteArray(0x08), // Laces. - Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true); - - assertReadPacket(input, firstPacket); - assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04); - assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02); - assertEquals(1003, oggParser.getPageHeader().pageSequenceNumber); - - assertReadEof(input); - } - - public void testReadDiscardContinuedPacketAtStart() throws Exception { - byte[] pageBody = TestUtil.buildTestData(256 + 8); - - FakeExtractorInput input = TestData.createInput( - TestUtil.joinByteArrays( - // Page with a continued packet at start. - TestData.buildOggHeader(0x01, 10, 1001, 0x03), - TestUtil.createByteArray(255, 1, 8), // Laces. - pageBody), true); - - // Expect the first partial packet to be discarded. - assertReadPacket(input, Arrays.copyOfRange(pageBody, 256, 256 + 8)); - assertReadEof(input); - } - - public void testReadZeroSizedPacketsAtEndOfStream() throws Exception { - byte[] firstPacket = TestUtil.buildTestData(8, random); - byte[] secondPacket = TestUtil.buildTestData(8, random); - byte[] thirdPacket = TestUtil.buildTestData(8, random); - - FakeExtractorInput input = TestData.createInput( - TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 0x01), - TestUtil.createByteArray(0x08), // Laces. - firstPacket, - TestData.buildOggHeader(0x04, 0, 1001, 0x03), - TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. - secondPacket, - TestData.buildOggHeader(0x04, 0, 1002, 0x03), - TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. - thirdPacket), true); - - assertReadPacket(input, firstPacket); - assertReadPacket(input, secondPacket); - assertReadPacket(input, thirdPacket); - assertReadEof(input); - } - - public void testSkipToPageOfGranule() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - FakeExtractorInput input = TestData.createInput( - TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - TestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - TestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet), false); - - // expect to be granule of the previous page returned as elapsedSamples - skipToPageOfGranule(input, 54000, 40000); - // expect to be at the start of the third page - assertEquals(2 * (30 + (3 * 254)), input.getPosition()); - } - - public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - FakeExtractorInput input = TestData.createInput( - TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - TestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - TestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet), false); - - skipToPageOfGranule(input, 40000, 20000); - // expect to be at the start of the second page - assertEquals((30 + (3 * 254)), input.getPosition()); - } - - public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - FakeExtractorInput input = TestData.createInput( - TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - TestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - TestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet), false); - - try { - skipToPageOfGranule(input, 10000, 20000); - fail(); - } catch (ParserException e) { - // ignored - } - assertEquals(0, input.getPosition()); - } - - private void skipToPageOfGranule(ExtractorInput input, long granule, - long elapsedSamplesExpected) throws IOException, InterruptedException { - while (true) { - try { - assertEquals(elapsedSamplesExpected, oggParser.skipToPageOfGranule(input, granule)); - return; - } catch (FakeExtractorInput.SimulatedIOException e) { - input.resetPeekPosition(); - } - } - } - - public void testReadGranuleOfLastPage() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( - TestUtil.buildTestData(100, random), - TestData.buildOggHeader(0x00, 20000, 66, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - TestData.buildOggHeader(0x00, 40000, 67, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - TestData.buildOggHeader(0x05, 60000, 68, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random) - ), false); - assertReadGranuleOfLastPage(input, 60000); - } - - public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.buildTestData(100, random), false); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (EOFException e) { - // ignored - } - } - - public void testReadGranuleOfLastPageWithUnboundedLength() - throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(new byte[0], true); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (IllegalArgumentException e) { - // ignored - } - } - - private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) - throws IOException, InterruptedException { - while (true) { - try { - assertEquals(expected, oggParser.readGranuleOfLastPage(input)); - break; - } catch (FakeExtractorInput.SimulatedIOException e) { - // ignored - } - } - } - - private void assertReadPacket(FakeExtractorInput extractorInput, byte[] expected) - throws IOException, InterruptedException { - scratch.reset(); - assertTrue(readPacket(extractorInput, scratch)); - MoreAsserts.assertEquals(expected, Arrays.copyOf(scratch.data, scratch.limit())); - } - - private void assertReadEof(FakeExtractorInput extractorInput) - throws IOException, InterruptedException { - scratch.reset(); - assertFalse(readPacket(extractorInput, scratch)); - } - - private boolean readPacket(FakeExtractorInput input, ParsableByteArray scratch) - throws InterruptedException, IOException { - while (true) { - try { - return oggParser.readPacket(input, scratch); - } catch (FakeExtractorInput.SimulatedIOException e) { - // Ignore. - } - } - } - -} diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggSeekerTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggSeekerTest.java deleted file mode 100644 index de0034fce4..0000000000 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggSeekerTest.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2015 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.exoplayer.extractor.ogg; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.testutil.FakeExtractorInput; -import com.google.android.exoplayer.testutil.TestUtil; - -import junit.framework.TestCase; - -import java.io.IOException; - -/** - * Unit test for {@link OggSeeker}. - */ -public final class OggSeekerTest extends TestCase { - - private OggSeeker oggSeeker; - - @Override - public void setUp() throws Exception { - super.setUp(); - oggSeeker = new OggSeeker(); - oggSeeker.setup(1, 1); - } - - public void testSetupUnboundAudioLength() { - try { - new OggSeeker().setup(C.LENGTH_UNBOUNDED, 1000); - fail(); - } catch (IllegalArgumentException e) { - // ignored - } - } - - public void testSetupZeroOrNegativeTotalSamples() { - try { - new OggSeeker().setup(1000, 0); - fail(); - } catch (IllegalArgumentException e) { - // ignored - } - try { - new OggSeeker().setup(1000, -1000); - fail(); - } catch (IllegalArgumentException e) { - // ignored - } - } - - public void testGetNextSeekPositionSetupNotCalled() throws IOException, InterruptedException { - try { - new OggSeeker().getNextSeekPosition(1000, TestData.createInput(new byte[0], false)); - fail(); - } catch (IllegalStateException e) { - // ignored - } - } - - public void testGetNextSeekPositionMatch() throws IOException, InterruptedException { - long targetGranule = 100000; - long headerGranule = 52001; - FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( - TestData.buildOggHeader(0x00, headerGranule, 22, 2), - TestUtil.createByteArray(54, 55) // laces - ), false); - long expectedPosition = -1; - assertGetNextSeekPosition(expectedPosition, targetGranule, input); - } - - public void testGetNextSeekPositionTooHigh() throws IOException, InterruptedException { - long targetGranule = 100000; - long headerGranule = 200000; - FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( - TestData.buildOggHeader(0x00, headerGranule, 22, 2), - TestUtil.createByteArray(54, 55) // laces - ), false); - long doublePageSize = 2 * (input.getLength() + 54 + 55); - long expectedPosition = -doublePageSize + (targetGranule - headerGranule); - assertGetNextSeekPosition(expectedPosition, targetGranule, input); - } - - public void testGetNextSeekPositionTooHighDistanceLower48000() - throws IOException, InterruptedException { - long targetGranule = 199999; - long headerGranule = 200000; - FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( - TestData.buildOggHeader(0x00, headerGranule, 22, 2), - TestUtil.createByteArray(54, 55) // laces - ), false); - long doublePageSize = 2 * (input.getLength() + 54 + 55); - long expectedPosition = -doublePageSize - 1; - assertGetNextSeekPosition(expectedPosition, targetGranule, input); - } - - public void testGetNextSeekPositionTooLow() throws IOException, InterruptedException { - long headerGranule = 200000; - FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( - TestData.buildOggHeader(0x00, headerGranule, 22, 2), - TestUtil.createByteArray(54, 55) // laces - ), false); - long targetGranule = 300000; - long expectedPosition = -(27 + 2 + 54 + 55) + (targetGranule - headerGranule); - assertGetNextSeekPosition(expectedPosition, targetGranule, input); - } - - private void assertGetNextSeekPosition(long expectedPosition, long targetGranule, - FakeExtractorInput input) throws IOException, InterruptedException { - while (true) { - try { - assertEquals(expectedPosition, oggSeeker.getNextSeekPosition(targetGranule, input)); - break; - } catch (FakeExtractorInput.SimulatedIOException e) { - // ignored - } - } - } - -} diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggUtilTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggUtilTest.java deleted file mode 100644 index b5c4ae08e8..0000000000 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggUtilTest.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2015 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.exoplayer.extractor.ogg; - -import com.google.android.exoplayer.extractor.ExtractorInput; -import com.google.android.exoplayer.testutil.FakeExtractorInput; -import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException; -import com.google.android.exoplayer.testutil.TestUtil; -import com.google.android.exoplayer.util.ParsableByteArray; - -import junit.framework.TestCase; - -import java.io.EOFException; -import java.io.IOException; -import java.util.Random; - -/** - * Unit test for {@link OggUtil}. - */ -public final class OggUtilTest extends TestCase { - - private Random random = new Random(0); - - public void testReadBits() throws Exception { - assertEquals(0, OggUtil.readBits((byte) 0x00, 2, 2)); - assertEquals(1, OggUtil.readBits((byte) 0x02, 1, 1)); - assertEquals(15, OggUtil.readBits((byte) 0xF0, 4, 4)); - assertEquals(1, OggUtil.readBits((byte) 0x80, 1, 7)); - } - - public void testPopulatePageHeader() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 123456, 4, 2), - TestUtil.createByteArray(2, 2) - ), true); - OggUtil.PageHeader header = new OggUtil.PageHeader(); - ParsableByteArray byteArray = new ParsableByteArray(27 + 2); - populatePageHeader(input, header, byteArray, false); - - assertEquals(0x01, header.type); - assertEquals(27 + 2, header.headerSize); - assertEquals(4, header.bodySize); - assertEquals(2, header.pageSegmentCount); - assertEquals(123456, header.granulePosition); - assertEquals(4, header.pageSequenceNumber); - assertEquals(0x1000, header.streamSerialNumber); - assertEquals(0x100000, header.pageChecksum); - assertEquals(0, header.revision); - } - - public void testPopulatePageHeaderQuiteOnExceptionLessThan27Bytes() - throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.createByteArray(2, 2), false); - OggUtil.PageHeader header = new OggUtil.PageHeader(); - ParsableByteArray byteArray = new ParsableByteArray(27 + 2); - assertFalse(populatePageHeader(input, header, byteArray, true)); - } - - public void testPopulatePageHeaderQuiteOnExceptionNotOgg() - throws IOException, InterruptedException { - byte[] headerBytes = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 123456, 4, 2), - TestUtil.createByteArray(2, 2) - ); - // change from 'O' to 'o' - headerBytes[0] = 'o'; - FakeExtractorInput input = TestData.createInput(headerBytes, false); - OggUtil.PageHeader header = new OggUtil.PageHeader(); - ParsableByteArray byteArray = new ParsableByteArray(27 + 2); - assertFalse(populatePageHeader(input, header, byteArray, true)); - } - - public void testPopulatePageHeaderQuiteOnExceptionWrongRevision() - throws IOException, InterruptedException { - byte[] headerBytes = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 123456, 4, 2), - TestUtil.createByteArray(2, 2) - ); - // change revision from 0 to 1 - headerBytes[4] = 0x01; - FakeExtractorInput input = TestData.createInput(headerBytes, false); - OggUtil.PageHeader header = new OggUtil.PageHeader(); - ParsableByteArray byteArray = new ParsableByteArray(27 + 2); - assertFalse(populatePageHeader(input, header, byteArray, true)); - } - - private boolean populatePageHeader(FakeExtractorInput input, OggUtil.PageHeader header, - ParsableByteArray byteArray, boolean quite) throws IOException, InterruptedException { - while (true) { - try { - return OggUtil.populatePageHeader(input, header, byteArray, quite); - } catch (SimulatedIOException e) { - // ignored - } - } - } - - public void testSkipToNextPage() throws Exception { - FakeExtractorInput extractorInput = createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(4000, random), - new byte[]{'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), false); - skipToNextPage(extractorInput); - assertEquals(4000, extractorInput.getPosition()); - } - - public void testSkipToNextPageUnbounded() throws Exception { - FakeExtractorInput extractorInput = createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(4000, random), - new byte[]{'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), true); - skipToNextPage(extractorInput); - assertEquals(4000, extractorInput.getPosition()); - } - - public void testSkipToNextPageOverlap() throws Exception { - FakeExtractorInput extractorInput = createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(2046, random), - new byte[]{'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), false); - skipToNextPage(extractorInput); - assertEquals(2046, extractorInput.getPosition()); - } - - public void testSkipToNextPageOverlapUnbounded() throws Exception { - FakeExtractorInput extractorInput = createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(2046, random), - new byte[]{'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), true); - skipToNextPage(extractorInput); - assertEquals(2046, extractorInput.getPosition()); - } - - public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { - FakeExtractorInput extractorInput = createInput( - TestUtil.joinByteArrays( - new byte[]{'x', 'O', 'g', 'g', 'S'} - ), false); - skipToNextPage(extractorInput); - assertEquals(1, extractorInput.getPosition()); - } - - public void testSkipToNextPageNoMatch() throws Exception { - FakeExtractorInput extractorInput = createInput(new byte[]{'g', 'g', 'S', 'O', 'g', 'g'}, - false); - try { - skipToNextPage(extractorInput); - fail(); - } catch (EOFException e) { - // expected - } - } - - private static void skipToNextPage(ExtractorInput extractorInput) - throws IOException, InterruptedException { - while (true) { - try { - OggUtil.skipToNextPage(extractorInput); - break; - } catch (SimulatedIOException e) { /* ignored */ } - } - } - - private static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) { - return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) - .setSimulateUnknownLength(simulateUnknownLength).setSimulatePartialReads(true).build(); - } -} - diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OpusReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OpusReaderTest.java deleted file mode 100644 index 0e9e85e833..0000000000 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OpusReaderTest.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2016 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.exoplayer.extractor.ogg; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.Format; -import com.google.android.exoplayer.extractor.DefaultExtractorInput; -import com.google.android.exoplayer.extractor.Extractor; -import com.google.android.exoplayer.extractor.PositionHolder; -import com.google.android.exoplayer.testutil.FakeExtractorOutput; -import com.google.android.exoplayer.testutil.FakeTrackOutput; -import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.DataSpec; -import com.google.android.exoplayer.upstream.DefaultDataSource; -import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer.util.Util; - -import android.content.Context; -import android.net.Uri; -import android.test.InstrumentationTestCase; - -import java.io.IOException; - -/** - * Unit test for {@link OpusReader}. - */ -public final class OpusReaderTest extends InstrumentationTestCase { - - private static final String TEST_FILE = "asset:///ogg/bear.opus"; - - private OggExtractor extractor; - private FakeExtractorOutput extractorOutput; - private DefaultExtractorInput extractorInput; - - @Override - public void setUp() throws Exception { - super.setUp(); - - Context context = getInstrumentation().getContext(); - DataSource dataSource = new DefaultDataSource(context, null, Util - .getUserAgent(context, "ExoPlayerExtFlacTest"), false); - Uri uri = Uri.parse(TEST_FILE); - long length = dataSource.open(new DataSpec(uri, 0, C.LENGTH_UNBOUNDED, null)); - extractorInput = new DefaultExtractorInput(dataSource, 0, length); - - extractor = new OggExtractor(); - assertTrue(extractor.sniff(extractorInput)); - extractorInput.resetPeekPosition(); - - extractorOutput = new FakeExtractorOutput(); - extractor.init(extractorOutput); - } - - public void testSniffOpus() throws Exception { - // Do nothing. All assertions are in setUp() - } - - public void testParseHeader() throws Exception { - FakeTrackOutput trackOutput = parseFile(false); - - trackOutput.assertSampleCount(0); - - Format format = trackOutput.format; - assertNotNull(format); - assertEquals(MimeTypes.AUDIO_OPUS, format.sampleMimeType); - assertEquals(48000, format.sampleRate); - assertEquals(2, format.channelCount); - } - - public void testParseWholeFile() throws Exception { - FakeTrackOutput trackOutput = parseFile(true); - - trackOutput.assertSampleCount(275); - } - - private FakeTrackOutput parseFile(boolean parseAll) throws IOException, InterruptedException { - PositionHolder seekPositionHolder = new PositionHolder(); - int readResult = Extractor.RESULT_CONTINUE; - do { - readResult = extractor.read(extractorInput, seekPositionHolder); - if (readResult == Extractor.RESULT_SEEK) { - fail("There should be no seek"); - } - } while (readResult != Extractor.RESULT_END_OF_INPUT && parseAll); - - assertEquals(1, extractorOutput.trackOutputs.size()); - FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); - assertNotNull(trackOutput); - return trackOutput; - } -} diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/VorbisReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/VorbisReaderTest.java index a085372d2b..00b08cc9c6 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/VorbisReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/VorbisReaderTest.java @@ -30,13 +30,18 @@ import java.io.IOException; public final class VorbisReaderTest extends TestCase { private VorbisReader extractor; - private ParsableByteArray scratch; @Override public void setUp() throws Exception { super.setUp(); extractor = new VorbisReader(); - scratch = new ParsableByteArray(new byte[255 * 255], 0); + } + + public void testReadBits() throws Exception { + assertEquals(0, VorbisReader.readBits((byte) 0x00, 2, 2)); + assertEquals(1, VorbisReader.readBits((byte) 0x02, 1, 1)); + assertEquals(15, VorbisReader.readBits((byte) 0xF0, 4, 4)); + assertEquals(1, VorbisReader.readBits((byte) 0x80, 1, 7)); } public void testAppendNumberOfSamples() throws Exception { @@ -90,9 +95,16 @@ public final class VorbisReaderTest extends TestCase { private VorbisSetup readSetupHeaders(FakeExtractorInput input) throws IOException, InterruptedException { + OggPacket oggPacket = new OggPacket(); while (true) { try { - return extractor.readSetupHeaders(input, scratch); + if (!oggPacket.populate(input)) { + fail(); + } + VorbisSetup vorbisSetup = extractor.readSetupHeaders(oggPacket.getPayload()); + if (vorbisSetup != null) { + return vorbisSetup; + } } catch (SimulatedIOException e) { // Ignore. } diff --git a/library/src/androidTest/java/com/google/android/exoplayer/testutil/TestUtil.java b/library/src/androidTest/java/com/google/android/exoplayer/testutil/TestUtil.java index fc479bbc28..143783e513 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/testutil/TestUtil.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/testutil/TestUtil.java @@ -17,12 +17,12 @@ package com.google.android.exoplayer.testutil; import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.PositionHolder; +import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; import android.app.Instrumentation; import android.test.InstrumentationTestCase; - import org.mockito.MockitoAnnotations; import java.io.IOException; @@ -36,17 +36,41 @@ public class TestUtil { private TestUtil() {} + public static boolean sniffTestData(Extractor extractor, byte[] data) + throws IOException, InterruptedException { + return sniffTestData(extractor, newExtractorInput(data)); + } + + public static boolean sniffTestData(Extractor extractor, FakeExtractorInput input) + throws IOException, InterruptedException { + while (true) { + try { + return extractor.sniff(input); + } catch (SimulatedIOException e) { + // Ignore. + } + } + } + public static void consumeTestData(Extractor extractor, byte[] data) throws IOException, InterruptedException { - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); + consumeTestData(extractor, newExtractorInput(data)); + } + + public static void consumeTestData(Extractor extractor, FakeExtractorInput input) + throws IOException, InterruptedException { PositionHolder seekPositionHolder = new PositionHolder(); int readResult = Extractor.RESULT_CONTINUE; while (readResult != Extractor.RESULT_END_OF_INPUT) { - readResult = extractor.read(input, seekPositionHolder); - if (readResult == Extractor.RESULT_SEEK) { - long seekPosition = seekPositionHolder.position; - Assertions.checkState(0 < seekPosition && seekPosition <= Integer.MAX_VALUE); - input.setPosition((int) seekPosition); + try { + readResult = extractor.read(input, seekPositionHolder); + if (readResult == Extractor.RESULT_SEEK) { + long seekPosition = seekPositionHolder.position; + Assertions.checkState(0 <= seekPosition && seekPosition <= Integer.MAX_VALUE); + input.setPosition((int) seekPosition); + } + } catch (SimulatedIOException e) { + // Ignore. } } } @@ -107,4 +131,8 @@ public class TestUtil { return Util.toByteArray(is); } + private static FakeExtractorInput newExtractorInput(byte[] data) { + return new FakeExtractorInput.Builder().setData(data).build(); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index 5df0e03f66..ac6d7aae83 100644 --- a/library/src/main/java/com/google/android/exoplayer/C.java +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -43,6 +43,11 @@ public interface C { */ long MICROS_PER_SECOND = 1000000L; + /** + * The number of nanoseconds in one second. + */ + public static final long NANOS_PER_SECOND = 1000000000L; + /** * Represents an unbounded length of data. */ diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/DefaultOggSeeker.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/DefaultOggSeeker.java new file mode 100644 index 0000000000..16ea199a35 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/DefaultOggSeeker.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2015 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.exoplayer.extractor.ogg; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.extractor.ExtractorInput; +import com.google.android.exoplayer.extractor.SeekMap; +import com.google.android.exoplayer.util.Assertions; + +import java.io.EOFException; +import java.io.IOException; + +/** + * Used to seek in an Ogg stream. + */ +/* package */ final class DefaultOggSeeker implements OggSeeker { + + private static final int STATE_SEEK_TO_END = 0; + private static final int STATE_READ_LAST_PAGE = 1; + private static final int STATE_SEEK = 2; + private static final int STATE_IDLE = 3; + + //@VisibleForTesting + public static final int MATCH_RANGE = 72000; + private static final int DEFAULT_OFFSET = 30000; + + private final OggPageHeader pageHeader = new OggPageHeader(); + private final long startPosition; + private final long endPosition; + private final StreamReader streamReader; + + private int state; + private long totalGranules; + private volatile long queriedGranule; + private long positionBeforeSeekToEnd; + private long targetGranule; + private long elapsedSamples; + + public static DefaultOggSeeker createOggSeekerForTesting(long startPosition, long endPosition, + long totalGranules) { + Assertions.checkArgument(totalGranules > 0); + DefaultOggSeeker oggSeeker = new DefaultOggSeeker(startPosition, endPosition, null); + oggSeeker.totalGranules = totalGranules; + return oggSeeker; + } + + /** + * Constructs an OggSeeker. + * @param startPosition Start position of the payload. + * @param endPosition End position of the payload. + * @param streamReader StreamReader instance which owns this OggSeeker + */ + public DefaultOggSeeker(long startPosition, long endPosition, StreamReader streamReader) { + this.streamReader = streamReader; + Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition); + this.startPosition = startPosition; + this.endPosition = endPosition; + this.queriedGranule = 0; + this.state = STATE_SEEK_TO_END; + } + + @Override + public long read(ExtractorInput input) throws IOException, InterruptedException { + switch (state) { + case STATE_IDLE: + return -1; + + case STATE_SEEK_TO_END: + positionBeforeSeekToEnd = input.getPosition(); + state = STATE_READ_LAST_PAGE; + // seek to the end just before the last page of stream to get the duration + long lastPagePosition = input.getLength() - OggPageHeader.MAX_PAGE_SIZE; + if (lastPagePosition > 0) { + return Math.max(lastPagePosition, 0); + } + // fall through + + case STATE_READ_LAST_PAGE: + totalGranules = readGranuleOfLastPage(input); + state = STATE_IDLE; + return positionBeforeSeekToEnd; + + case STATE_SEEK: + long currentGranule; + if (targetGranule == 0) { + currentGranule = 0; + } else { + long position = getNextSeekPosition(targetGranule, input); + if (position != -1) { + return position; + } else { + currentGranule = skipToPageOfGranule(input, targetGranule); + } + } + state = STATE_IDLE; + return -currentGranule - 2; + + default: + // Never happens. + throw new IllegalStateException(); + } + } + + @Override + public long startSeek() { + Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); + targetGranule = queriedGranule; + state = STATE_SEEK; + return targetGranule; + } + + @Override + public OggSeekMap createSeekMap() { + return totalGranules != 0 ? new OggSeekMap() : null; + } + + /** + * Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput} + * has to seek and then be passed for another call until -1 is return. If -1 is returned the + * input is at a position which is before the start of the page before the target page and at + * which it is sensible to just skip pages to the target granule and pre-roll instead of doing + * another seek request. + * + * @param targetGranule the target granule position to seek to. + * @param input the {@link ExtractorInput} to read from. + * @return the position to seek the {@link ExtractorInput} to for a next call or -1 if it's close + * enough to skip to the target page. + * @throws IOException thrown if reading from the input fails. + * @throws InterruptedException thrown if interrupted while reading from the input. + */ + //@VisibleForTesting + public long getNextSeekPosition(long targetGranule, ExtractorInput input) + throws IOException, InterruptedException { + long previousPosition = input.getPosition(); + skipToNextPage(input); + pageHeader.populate(input, false); + long granuleDistance = targetGranule - pageHeader.granulePosition; + if (granuleDistance <= 0 || granuleDistance > MATCH_RANGE) { + // estimated position too high or too low + long offset = (pageHeader.bodySize + pageHeader.headerSize) + * (granuleDistance <= 0 ? 2 : 1); + long estimatedPosition = getEstimatedPosition(input.getPosition(), granuleDistance, offset); + if (estimatedPosition != previousPosition) { // Temporary prevention for simple loops + return estimatedPosition; + } + } + // position accepted (below target granule and within MATCH_RANGE) + input.resetPeekPosition(); + return -1; + } + + private long getEstimatedPosition(long position, long granuleDistance, long offset) { + position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset; + if (position < startPosition) { + position = startPosition; + } + if (position >= endPosition) { + position = endPosition - 1; + } + return position; + } + + private class OggSeekMap implements SeekMap { + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getPosition(long timeUs) { + if (timeUs == 0) { + queriedGranule = 0; + return startPosition; + } + queriedGranule = streamReader.convertTimeToGranule(timeUs); + return getEstimatedPosition(startPosition, queriedGranule, DEFAULT_OFFSET); + } + + @Override + public long getDurationUs() { + return streamReader.convertGranuleToTime(totalGranules); + } + + } + + /** + * Skips to the next page. + * + * @param input The {@code ExtractorInput} to skip to the next page. + * @throws IOException thrown if peeking/reading from the input fails. + * @throws InterruptedException thrown if interrupted while peeking/reading from the input. + */ + //@VisibleForTesting + static void skipToNextPage(ExtractorInput input) + throws IOException, InterruptedException { + + byte[] buffer = new byte[2048]; + int peekLength = buffer.length; + long length = input.getLength(); + while (true) { + if (length != C.LENGTH_UNBOUNDED && input.getPosition() + peekLength > length) { + // Make sure to not peek beyond the end of the input. + peekLength = (int) (length - input.getPosition()); + if (peekLength < 4) { + // Not found until eof. + throw new EOFException(); + } + } + input.peekFully(buffer, 0, peekLength, false); + for (int i = 0; i < peekLength - 3; i++) { + if (buffer[i] == 'O' && buffer[i + 1] == 'g' && buffer[i + 2] == 'g' + && buffer[i + 3] == 'S') { + // Match! Skip to the start of the pattern. + input.skipFully(i); + return; + } + } + // Overlap by not skipping the entire peekLength. + input.skipFully(peekLength - 3); + } + } + + /** + * Skips to the last Ogg page in the stream and reads the header's granule field which is the + * total number of samples per channel. + * + * @param input The {@link ExtractorInput} to read from. + * @return the total number of samples of this input. + * @throws IOException thrown if reading from the input fails. + * @throws InterruptedException thrown if interrupted while reading from the input. + */ + //@VisibleForTesting + long readGranuleOfLastPage(ExtractorInput input) + throws IOException, InterruptedException { + Assertions.checkArgument(input.getLength() != C.LENGTH_UNBOUNDED); // never read forever! + skipToNextPage(input); + pageHeader.reset(); + while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < input.getLength()) { + pageHeader.populate(input, false); + input.skipFully(pageHeader.headerSize + pageHeader.bodySize); + } + return pageHeader.granulePosition; + } + + /** + * Skips to the position of the start of the page containing the {@code targetGranule} and + * returns the elapsed samples which is the granule of the page previous to the target page. + *

+ * Note that the position of the {@code input} must be before the start of the page previous to + * the page containing the targetGranule to get the correct number of elapsed samples. + * Which is in short like: {@code pos(input) <= pos(targetPage.pageSequence - 1)}. + * + * @param input the {@link ExtractorInput} to read from. + * @param targetGranule the target granule (number of frames per channel). + * @return the number of elapsed samples at the start of the target page. + * @throws ParserException thrown if populating the page header fails. + * @throws IOException thrown if reading from the input fails. + * @throws InterruptedException thrown if interrupted while reading from the input. + */ + //@VisibleForTesting + long skipToPageOfGranule(ExtractorInput input, long targetGranule) + throws IOException, InterruptedException { + skipToNextPage(input); + pageHeader.populate(input, false); + while (pageHeader.granulePosition < targetGranule) { + input.skipFully(pageHeader.headerSize + pageHeader.bodySize); + // Store in a member field to be able to resume after IOExceptions. + elapsedSamples = pageHeader.granulePosition; + // Peek next header. + pageHeader.populate(input, false); + } + if (elapsedSamples == 0) { + throw new ParserException(); + } + input.resetPeekPosition(); + long returnValue = elapsedSamples; + // Reset member state. + elapsedSamples = 0; + return returnValue; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/FlacReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/FlacReader.java index 8f65c8edd6..3513484a34 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/FlacReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/FlacReader.java @@ -15,17 +15,14 @@ */ package com.google.android.exoplayer.extractor.ogg; -import com.google.android.exoplayer.C; import com.google.android.exoplayer.Format; -import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.ExtractorInput; -import com.google.android.exoplayer.extractor.PositionHolder; import com.google.android.exoplayer.extractor.SeekMap; -import com.google.android.exoplayer.util.FlacSeekTable; +import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.FlacStreamInfo; -import com.google.android.exoplayer.util.FlacUtil; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableByteArray; +import com.google.android.exoplayer.util.Util; import java.io.IOException; import java.util.Arrays; @@ -40,11 +37,10 @@ import java.util.List; private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF; private static final byte SEEKTABLE_PACKET_TYPE = 0x03; + private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; + private FlacStreamInfo streamInfo; - - private FlacSeekTable seekTable; - - private boolean firstAudioPacketProcessed; + private FlacOggSeeker flacOggSeeker; public static boolean verifyBitstreamType(ParsableByteArray data) { return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type @@ -52,46 +48,150 @@ import java.util.List; } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { - long position = input.getPosition(); - - if (!oggParser.readPacket(input, scratch)) { - return Extractor.RESULT_END_OF_INPUT; + protected long preparePayload(ParsableByteArray packet) { + if (packet.data[0] != AUDIO_PACKET_TYPE) { + return -1; } + return getFlacFrameBlockSize(packet); + } - byte[] data = scratch.data; + @Override + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) + throws IOException, InterruptedException { + byte[] data = packet.data; if (streamInfo == null) { streamInfo = new FlacStreamInfo(data, 17); - byte[] metadata = Arrays.copyOfRange(data, 9, scratch.limit()); + byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks List initializationData = Collections.singletonList(metadata); - trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, - streamInfo.bitRate(), Format.NO_VALUE, streamInfo.channels, streamInfo.sampleRate, - initializationData, null, 0, null)); + setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, Format.NO_VALUE, + streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate, initializationData, + null, 0, null); + } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { + Assertions.checkArgument(flacOggSeeker == null); + flacOggSeeker = new FlacOggSeeker(); + flacOggSeeker.parseSeekTable(packet); } else if (data[0] == AUDIO_PACKET_TYPE) { - if (!firstAudioPacketProcessed) { - if (seekTable != null) { - extractorOutput.seekMap(seekTable.createSeekMap(position, streamInfo.sampleRate, - streamInfo.durationUs())); - seekTable = null; - } else { - extractorOutput.seekMap(new SeekMap.Unseekable(streamInfo.durationUs())); - } - firstAudioPacketProcessed = true; + if (flacOggSeeker != null) { + flacOggSeeker.setFirstFrameOffset(position); + setupData.oggSeeker = flacOggSeeker; } + return false; + } + return true; + } - trackOutput.sampleData(scratch, scratch.limit()); - scratch.setPosition(0); - long timeUs = FlacUtil.extractSampleTimestamp(streamInfo, scratch); - trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, scratch.limit(), 0, null); + private int getFlacFrameBlockSize(ParsableByteArray packet) { + int blockSizeCode = (packet.data[2] & 0xFF) >> 4; + switch (blockSizeCode) { + case 1: + return 192; + case 2: + case 3: + case 4: + case 5: + return 576 << (blockSizeCode - 2); + case 6: + case 7: + // skip the sample number + packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET); + packet.readUtf8EncodedLong(); + if (blockSizeCode == 6) { + return packet.readUnsignedByte() + 1; + } else { + return packet.readUnsignedShort() + 1; + } + case 8: + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + return 256 << (blockSizeCode - 8); + } + return -1; + } - } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE && seekTable == null) { - seekTable = FlacSeekTable.parseSeekTable(scratch); + private class FlacOggSeeker implements OggSeeker, SeekMap { + + private static final int METADATA_LENGTH_OFFSET = 1; + private static final int SEEK_POINT_SIZE = 18; + + private long[] sampleNumbers; + private long[] offsets; + private long firstFrameOffset = -1; + private volatile long queriedGranule; + private volatile long seekedGranule; + private long currentGranule = -1; + + public void setFirstFrameOffset(long firstFrameOffset) { + this.firstFrameOffset = firstFrameOffset; + } + + /** + * Parses a FLAC file seek table metadata structure and initializes internal fields. + * + * @param data + * A ParsableByteArray including whole seek table metadata block. Its position should be set + * to the beginning of the block. + * @see FLAC format + * METADATA_BLOCK_SEEKTABLE + */ + public void parseSeekTable(ParsableByteArray data) { + data.skipBytes(METADATA_LENGTH_OFFSET); + int length = data.readUnsignedInt24(); + int numberOfSeekPoints = length / SEEK_POINT_SIZE; + + sampleNumbers = new long[numberOfSeekPoints]; + offsets = new long[numberOfSeekPoints]; + + for (int i = 0; i < numberOfSeekPoints; i++) { + sampleNumbers[i] = data.readLong(); + offsets[i] = data.readLong(); + data.skipBytes(2); // Skip "Number of samples in the target frame." + } + } + + @Override + public long read(ExtractorInput input) throws IOException, InterruptedException { + if (currentGranule >= 0) { + currentGranule = -currentGranule - 2; + return currentGranule; + } + return -1; + } + + @Override + public synchronized long startSeek() { + currentGranule = seekedGranule; + return queriedGranule; + } + + @Override + public SeekMap createSeekMap() { + return this; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public synchronized long getPosition(long timeUs) { + queriedGranule = convertTimeToGranule(timeUs); + int index = Util.binarySearchFloor(sampleNumbers, queriedGranule, true, true); + seekedGranule = sampleNumbers[index]; + return firstFrameOffset + offsets[index]; + } + + @Override + public long getDurationUs() { + return streamInfo.durationUs(); } - scratch.reset(); - return Extractor.RESULT_CONTINUE; } } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggExtractor.java index 207107a0df..b3ef3675fe 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggExtractor.java @@ -30,19 +30,22 @@ import java.io.IOException; */ public class OggExtractor implements Extractor { + private static final int MAX_VERIFICATION_BYTES = 8; + private StreamReader streamReader; @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { try { - ParsableByteArray scratch = new ParsableByteArray(new byte[OggUtil.PAGE_HEADER_SIZE], 0); - OggUtil.PageHeader header = new OggUtil.PageHeader(); - if (!OggUtil.populatePageHeader(input, header, scratch, true) - || (header.type & 0x02) != 0x02) { + OggPageHeader header = new OggPageHeader(); + if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { return false; } - input.peekFully(scratch.data, 0, header.bodySize); - scratch.setLimit(header.bodySize); + + int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES); + ParsableByteArray scratch = new ParsableByteArray(length); + input.peekFully(scratch.data, 0, length); + if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { streamReader = new FlacReader(); } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { @@ -62,6 +65,7 @@ public class OggExtractor implements Extractor { public void init(ExtractorOutput output) { TrackOutput trackOutput = output.track(0); output.endTracks(); + // TODO: fix the case if sniff() isn't called streamReader.init(output, trackOutput); } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggPacket.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggPacket.java new file mode 100644 index 0000000000..ad80ec5342 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggPacket.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2015 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.exoplayer.extractor.ogg; + +import com.google.android.exoplayer.extractor.ExtractorInput; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * OGG packet class. + */ +/* package */ final class OggPacket { + + private final OggPageHeader pageHeader = new OggPageHeader(); + private final ParsableByteArray packetArray = + new ParsableByteArray(new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0); + + private int currentSegmentIndex = -1; + private int segmentCount; + private boolean populated; + + /** + * Resets this reader. + */ + public void reset() { + pageHeader.reset(); + packetArray.reset(); + currentSegmentIndex = -1; + populated = false; + } + + /** + * Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make + * sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader + * can resume properly from an error while reading a continued packet spanned across multiple + * pages. + * + * @param input the {@link ExtractorInput} to read data from. + * @return {@code true} if the read was successful. {@code false} if the end of the input was + * encountered having read no data. + * @throws IOException thrown if reading from the input fails. + * @throws InterruptedException thrown if interrupted while reading from input. + */ + public boolean populate(ExtractorInput input) throws IOException, InterruptedException { + Assertions.checkState(input != null); + + if (populated) { + populated = false; + packetArray.reset(); + } + + while (!populated) { + if (currentSegmentIndex < 0) { + // We're at the start of a page. + if (!pageHeader.populate(input, true)) { + return false; + } + int segmentIndex = 0; + int bytesToSkip = pageHeader.headerSize; + if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) { + // After seeking, the first packet may be the remainder + // part of a continued packet which has to be discarded. + bytesToSkip += calculatePacketSize(segmentIndex); + segmentIndex += segmentCount; + } + input.skipFully(bytesToSkip); + currentSegmentIndex = segmentIndex; + } + + int size = calculatePacketSize(currentSegmentIndex); + int segmentIndex = currentSegmentIndex + segmentCount; + if (size > 0) { + input.readFully(packetArray.data, packetArray.limit(), size); + packetArray.setLimit(packetArray.limit() + size); + populated = pageHeader.laces[segmentIndex - 1] != 255; + } + // advance now since we are sure reading didn't throw an exception + currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? -1 + : segmentIndex; + } + return true; + } + + /** + * An OGG Packet may span multiple pages. Returns the {@link OggPageHeader} of the last page read, + * or an empty header if the packet has yet to be populated. + *

+ * Note that the returned {@link OggPageHeader} is mutable and may be updated during subsequent + * calls to {@link #populate(ExtractorInput)}. + * + * @return the {@code PageHeader} of the last page read or an empty header if the packet has yet + * to be populated. + */ + //@VisibleForTesting + public OggPageHeader getPageHeader() { + return pageHeader; + } + + /** + * @return A ParsableByteArray containing the payload of the packet. + */ + public ParsableByteArray getPayload() { + return packetArray; + } + + /** + * Calculates the size of the packet starting from {@code startSegmentIndex}. + * + * @param startSegmentIndex the index of the first segment of the packet. + * @return Size of the packet. + */ + private int calculatePacketSize(int startSegmentIndex) { + segmentCount = 0; + int size = 0; + while (startSegmentIndex + segmentCount < pageHeader.pageSegmentCount) { + int segmentLength = pageHeader.laces[startSegmentIndex + segmentCount++]; + size += segmentLength; + if (segmentLength != 255) { + // packets end at first lace < 255 + break; + } + } + return size; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggPageHeader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggPageHeader.java new file mode 100644 index 0000000000..4cff389a98 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggPageHeader.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2015 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.exoplayer.extractor.ogg; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.extractor.ExtractorInput; +import com.google.android.exoplayer.util.ParsableByteArray; +import com.google.android.exoplayer.util.Util; + +import java.io.EOFException; +import java.io.IOException; + +/** + * Data object to store header information. + */ +/* package */ final class OggPageHeader { + + public static final int EMPTY_PAGE_HEADER_SIZE = 27; + public static final int MAX_SEGMENT_COUNT = 255; + public static final int MAX_PAGE_PAYLOAD = 255 * 255; + public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT + + MAX_PAGE_PAYLOAD; + + private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS"); + + public int revision; + public int type; + public long granulePosition; + public long streamSerialNumber; + public long pageSequenceNumber; + public long pageChecksum; + public int pageSegmentCount; + public int headerSize; + public int bodySize; + /** + * Be aware that {@code laces.length} is always {@link #MAX_SEGMENT_COUNT}. Instead use + * {@link #pageSegmentCount} to iterate. + */ + public final int[] laces = new int[MAX_SEGMENT_COUNT]; + + private final ParsableByteArray scratch = new ParsableByteArray(MAX_SEGMENT_COUNT); + + /** + * Resets all primitive member fields to zero. + */ + public void reset() { + revision = 0; + type = 0; + granulePosition = 0; + streamSerialNumber = 0; + pageSequenceNumber = 0; + pageChecksum = 0; + pageSegmentCount = 0; + headerSize = 0; + bodySize = 0; + } + + /** + * Peeks an Ogg page header and updates this {@link OggPageHeader}. + * + * @param input the {@link ExtractorInput} to read from. + * @param quiet if {@code true} no Exceptions are thrown but {@code false} is return if something + * goes wrong. + * @return {@code true} if the read was successful. {@code false} if the end of the input was + * encountered having read no data. + * @throws IOException thrown if reading data fails or the stream is invalid. + * @throws InterruptedException thrown if thread is interrupted when reading/peeking. + */ + public boolean populate(ExtractorInput input, boolean quiet) + throws IOException, InterruptedException { + scratch.reset(); + reset(); + boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNBOUNDED + || input.getLength() - input.getPeekPosition() >= EMPTY_PAGE_HEADER_SIZE; + if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, EMPTY_PAGE_HEADER_SIZE, true)) { + if (quiet) { + return false; + } else { + throw new EOFException(); + } + } + if (scratch.readUnsignedInt() != TYPE_OGGS) { + if (quiet) { + return false; + } else { + throw new ParserException("expected OggS capture pattern at begin of page"); + } + } + + revision = scratch.readUnsignedByte(); + if (revision != 0x00) { + if (quiet) { + return false; + } else { + throw new ParserException("unsupported bit stream revision"); + } + } + type = scratch.readUnsignedByte(); + + granulePosition = scratch.readLittleEndianLong(); + streamSerialNumber = scratch.readLittleEndianUnsignedInt(); + pageSequenceNumber = scratch.readLittleEndianUnsignedInt(); + pageChecksum = scratch.readLittleEndianUnsignedInt(); + pageSegmentCount = scratch.readUnsignedByte(); + headerSize = EMPTY_PAGE_HEADER_SIZE + pageSegmentCount; + + // calculate total size of header including laces + scratch.reset(); + input.peekFully(scratch.data, 0, pageSegmentCount); + for (int i = 0; i < pageSegmentCount; i++) { + laces[i] = scratch.readUnsignedByte(); + bodySize += laces[i]; + } + + return true; + } +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggParser.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggParser.java deleted file mode 100644 index c66adcabe6..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggParser.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2015 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.exoplayer.extractor.ogg; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.extractor.ExtractorInput; -import com.google.android.exoplayer.extractor.ogg.OggUtil.PacketInfoHolder; -import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.ParsableByteArray; - -import java.io.IOException; - -/** - * Reads OGG packets from an {@link ExtractorInput}. - */ -/* package */ final class OggParser { - - public static final int OGG_MAX_SEGMENT_SIZE = 255; - - private final OggUtil.PageHeader pageHeader = new OggUtil.PageHeader(); - private final ParsableByteArray headerArray = new ParsableByteArray(27 + 255); - private final PacketInfoHolder holder = new PacketInfoHolder(); - - private int currentSegmentIndex = -1; - private long elapsedSamples; - - /** - * Resets this reader. - */ - public void reset() { - pageHeader.reset(); - headerArray.reset(); - currentSegmentIndex = -1; - } - - /** - * Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make - * sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader - * can resume properly from an error while reading a continued packet spanned across multiple - * pages. - * - * @param input the {@link ExtractorInput} to read data from. - * @param packetArray the {@link ParsableByteArray} to write the packet data into. - * @return {@code true} if the read was successful. {@code false} if the end of the input was - * encountered having read no data. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from input. - */ - public boolean readPacket(ExtractorInput input, ParsableByteArray packetArray) - throws IOException, InterruptedException { - Assertions.checkState(input != null && packetArray != null); - - boolean packetComplete = false; - while (!packetComplete) { - if (currentSegmentIndex < 0) { - // We're at the start of a page. - if (!OggUtil.populatePageHeader(input, pageHeader, headerArray, true)) { - return false; - } - int segmentIndex = 0; - int bytesToSkip = pageHeader.headerSize; - if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) { - // After seeking, the first packet may be the remainder - // part of a continued packet which has to be discarded. - OggUtil.calculatePacketSize(pageHeader, segmentIndex, holder); - segmentIndex += holder.segmentCount; - bytesToSkip += holder.size; - } - input.skipFully(bytesToSkip); - currentSegmentIndex = segmentIndex; - } - - OggUtil.calculatePacketSize(pageHeader, currentSegmentIndex, holder); - int segmentIndex = currentSegmentIndex + holder.segmentCount; - if (holder.size > 0) { - input.readFully(packetArray.data, packetArray.limit(), holder.size); - packetArray.setLimit(packetArray.limit() + holder.size); - packetComplete = pageHeader.laces[segmentIndex - 1] != 255; - } - // advance now since we are sure reading didn't throw an exception - currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? -1 - : segmentIndex; - } - return true; - } - - /** - * Skips to the last Ogg page in the stream and reads the header's granule field which is the - * total number of samples per channel. - * - * @param input The {@link ExtractorInput} to read from. - * @return the total number of samples of this input. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. - */ - public long readGranuleOfLastPage(ExtractorInput input) - throws IOException, InterruptedException { - Assertions.checkArgument(input.getLength() != C.LENGTH_UNBOUNDED); // never read forever! - OggUtil.skipToNextPage(input); - pageHeader.reset(); - while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < input.getLength()) { - OggUtil.populatePageHeader(input, pageHeader, headerArray, false); - input.skipFully(pageHeader.headerSize + pageHeader.bodySize); - } - return pageHeader.granulePosition; - } - - /** - * Skips to the position of the start of the page containing the {@code targetGranule} and - * returns the elapsed samples which is the granule of the page previous to the target page. - *

- * Note that the position of the {@code input} must be before the start of the page previous to - * the page containing the targetGranule to get the correct number of elapsed samples. - * Which is in short like: {@code pos(input) <= pos(targetPage.pageSequence - 1)}. - * - * @param input the {@link ExtractorInput} to read from. - * @param targetGranule the target granule (number of frames per channel). - * @return the number of elapsed samples at the start of the target page. - * @throws ParserException thrown if populating the page header fails. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. - */ - public long skipToPageOfGranule(ExtractorInput input, long targetGranule) - throws IOException, InterruptedException { - OggUtil.skipToNextPage(input); - OggUtil.populatePageHeader(input, pageHeader, headerArray, false); - while (pageHeader.granulePosition < targetGranule) { - input.skipFully(pageHeader.headerSize + pageHeader.bodySize); - // Store in a member field to be able to resume after IOExceptions. - elapsedSamples = pageHeader.granulePosition; - // Peek next header. - OggUtil.populatePageHeader(input, pageHeader, headerArray, false); - } - if (elapsedSamples == 0) { - throw new ParserException(); - } - input.resetPeekPosition(); - long returnValue = elapsedSamples; - // Reset member state. - elapsedSamples = 0; - currentSegmentIndex = -1; - return returnValue; - } - - /** - * Returns the {@link OggUtil.PageHeader} of the current page. The header might not have been - * populated if the first packet has yet to be read. - *

- * Note that there is only a single instance of {@code OggParser.PageHeader} which is mutable. - * The value of the fields might be changed by the reader when reading the stream advances and - * the next page is read (which implies reading and populating the next header). - * - * @return the {@code PageHeader} of the current page or {@code null}. - */ - public OggUtil.PageHeader getPageHeader() { - return pageHeader; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggSeeker.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggSeeker.java index 40f88b2d27..8e8c99de15 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggSeeker.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggSeeker.java @@ -15,65 +15,45 @@ */ package com.google.android.exoplayer.extractor.ogg; -import com.google.android.exoplayer.C; import com.google.android.exoplayer.extractor.ExtractorInput; -import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.ParsableByteArray; +import com.google.android.exoplayer.extractor.SeekMap; import java.io.IOException; /** - * Used to seek in an Ogg stream. + * Used to seek in an Ogg stream. OggSeeker implementation may do direct seeking or progressive + * seeking. OggSeeker works together with a {@link SeekMap} instance to capture the queried position + * and start the seeking with an initial estimated position. */ -/* package */ final class OggSeeker { - - private static final int MATCH_RANGE = 72000; - - private final OggUtil.PageHeader pageHeader = new OggUtil.PageHeader(); - private final ParsableByteArray headerArray = new ParsableByteArray(27 + 255); - private long audioDataLength = C.LENGTH_UNBOUNDED; - private long totalSamples; +/* package */ interface OggSeeker { /** - * Setup the seeker with the data it needs to to an educated guess of seeking positions. - * - * @param audioDataLength the length of the audio data (total bytes - header bytes). - * @param totalSamples the total number of samples of audio data. + * @return a SeekMap instance which returns an initial estimated position for progressive seeking + * or the final position for direct seeking. Returns null if {@link #read} hasn't returned -1 + * yet. */ - public void setup(long audioDataLength, long totalSamples) { - Assertions.checkArgument(audioDataLength > 0 && totalSamples > 0); - this.audioDataLength = audioDataLength; - this.totalSamples = totalSamples; - } + SeekMap createSeekMap(); /** - * Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput} - * has to seek and then be passed for another call until -1 is return. If -1 is returned the - * input is at a position which is before the start of the page before the target page and at - * which it is sensible to just skip pages to the target granule and pre-roll instead of doing - * another seek request. + * Initializes a seek operation. + * + * @return The granule position targeted by the seek. + */ + long startSeek(); + + /** + * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a + * progressive seek. + *

+ * If more data is required or if the position of the input needs to be modified then a position + * from which data should be provided is returned. Else a negative value is returned. If a seek + * has been completed then the value returned is -(currentGranule + 2). Else -1 is returned. * - * @param targetGranule the target granule position to seek to. * @param input the {@link ExtractorInput} to read from. - * @return the position to seek the {@link ExtractorInput} to for a next call or -1 if it's close - * enough to skip to the target page. + * @return the non-negative position to seek the {@link ExtractorInput} to or -1 seeking not + * necessary or at the end of seeking a negative number < -1 which is -(currentGranule + 2). * @throws IOException thrown if reading from the input fails. * @throws InterruptedException thrown if interrupted while reading from the input. */ - public long getNextSeekPosition(long targetGranule, ExtractorInput input) - throws IOException, InterruptedException { - Assertions.checkState(audioDataLength != C.LENGTH_UNBOUNDED && totalSamples != 0); - OggUtil.populatePageHeader(input, pageHeader, headerArray, false); - long granuleDistance = targetGranule - pageHeader.granulePosition; - if (granuleDistance <= 0 || granuleDistance > MATCH_RANGE) { - // estimated position too high or too low - long offset = (pageHeader.bodySize + pageHeader.headerSize) - * (granuleDistance <= 0 ? 2 : 1); - return input.getPosition() - offset + (granuleDistance * audioDataLength / totalSamples); - } - // position accepted (below target granule and within MATCH_RANGE) - input.resetPeekPosition(); - return -1; - } - + long read(ExtractorInput input) throws IOException, InterruptedException; } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java deleted file mode 100644 index ba7f7ba4ef..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (C) 2015 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.exoplayer.extractor.ogg; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.extractor.ExtractorInput; -import com.google.android.exoplayer.util.ParsableByteArray; -import com.google.android.exoplayer.util.Util; - -import java.io.EOFException; -import java.io.IOException; - -/** - * Utility methods for reading ogg streams. - */ -/* package */ final class OggUtil { - - public static final int PAGE_HEADER_SIZE = 27; - - private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS"); - - /** - * Reads an int of {@code length} bits from {@code src} starting at - * {@code leastSignificantBitIndex}. - * - * @param src the {@code byte} to read from. - * @param length the length in bits of the int to read. - * @param leastSignificantBitIndex the index of the least significant bit of the int to read. - * @return the int value read. - */ - public static int readBits(byte src, int length, int leastSignificantBitIndex) { - return (src >> leastSignificantBitIndex) & (255 >>> (8 - length)); - } - - /** - * Skips to the next page. - * - * @param input The {@code ExtractorInput} to skip to the next page. - * @throws IOException thrown if peeking/reading from the input fails. - * @throws InterruptedException thrown if interrupted while peeking/reading from the input. - */ - public static void skipToNextPage(ExtractorInput input) - throws IOException, InterruptedException { - - byte[] buffer = new byte[2048]; - int peekLength = buffer.length; - while (true) { - if (input.getLength() != C.LENGTH_UNBOUNDED - && input.getPosition() + peekLength > input.getLength()) { - // Make sure to not peek beyond the end of the input. - peekLength = (int) (input.getLength() - input.getPosition()); - if (peekLength < 4) { - // Not found until eof. - throw new EOFException(); - } - } - input.peekFully(buffer, 0, peekLength, false); - for (int i = 0; i < peekLength - 3; i++) { - if (buffer[i] == 'O' && buffer[i + 1] == 'g' && buffer[i + 2] == 'g' - && buffer[i + 3] == 'S') { - // Match! Skip to the start of the pattern. - input.skipFully(i); - return; - } - } - // Overlap by not skipping the entire peekLength. - input.skipFully(peekLength - 3); - } - } - - /** - * Peeks an Ogg page header and stores the data in the {@code header} object passed - * as argument. - * - * @param input the {@link ExtractorInput} to read from. - * @param header the {@link PageHeader} to be populated. - * @param scratch a scratch array temporary use. Its size should be at least PAGE_HEADER_SIZE - * @param quite if {@code true} no Exceptions are thrown but {@code false} is return if something - * goes wrong. - * @return {@code true} if the read was successful. {@code false} if the end of the - * input was encountered having read no data. - * @throws IOException thrown if reading data fails or the stream is invalid. - * @throws InterruptedException thrown if thread is interrupted when reading/peeking. - */ - public static boolean populatePageHeader(ExtractorInput input, PageHeader header, - ParsableByteArray scratch, boolean quite) throws IOException, InterruptedException { - - scratch.reset(); - header.reset(); - boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNBOUNDED - || input.getLength() - input.getPeekPosition() >= PAGE_HEADER_SIZE; - if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, PAGE_HEADER_SIZE, true)) { - if (quite) { - return false; - } else { - throw new EOFException(); - } - } - if (scratch.readUnsignedInt() != TYPE_OGGS) { - if (quite) { - return false; - } else { - throw new ParserException("expected OggS capture pattern at begin of page"); - } - } - - header.revision = scratch.readUnsignedByte(); - if (header.revision != 0x00) { - if (quite) { - return false; - } else { - throw new ParserException("unsupported bit stream revision"); - } - } - header.type = scratch.readUnsignedByte(); - - header.granulePosition = scratch.readLittleEndianLong(); - header.streamSerialNumber = scratch.readLittleEndianUnsignedInt(); - header.pageSequenceNumber = scratch.readLittleEndianUnsignedInt(); - header.pageChecksum = scratch.readLittleEndianUnsignedInt(); - header.pageSegmentCount = scratch.readUnsignedByte(); - - scratch.reset(); - // calculate total size of header including laces - header.headerSize = PAGE_HEADER_SIZE + header.pageSegmentCount; - input.peekFully(scratch.data, 0, header.pageSegmentCount); - for (int i = 0; i < header.pageSegmentCount; i++) { - header.laces[i] = scratch.readUnsignedByte(); - header.bodySize += header.laces[i]; - } - return true; - } - - /** - * Calculates the size of the packet starting from {@code startSegmentIndex}. - * - * @param header the {@link PageHeader} with laces. - * @param startSegmentIndex the index of the first segment of the packet. - * @param holder a position holder to store the resulting size value. - */ - public static void calculatePacketSize(PageHeader header, int startSegmentIndex, - PacketInfoHolder holder) { - holder.segmentCount = 0; - holder.size = 0; - while (startSegmentIndex + holder.segmentCount < header.pageSegmentCount) { - int segmentLength = header.laces[startSegmentIndex + holder.segmentCount++]; - holder.size += segmentLength; - if (segmentLength != 255) { - // packets end at first lace < 255 - break; - } - } - } - - /** - * Data object to store header information. Be aware that {@code laces.length} is always 255. - * Instead use {@code pageSegmentCount} to iterate. - */ - public static final class PageHeader { - - public int revision; - public int type; - public long granulePosition; - public long streamSerialNumber; - public long pageSequenceNumber; - public long pageChecksum; - public int pageSegmentCount; - public int headerSize; - public int bodySize; - public final int[] laces = new int[255]; - - /** - * Resets all primitive member fields to zero. - */ - public void reset() { - revision = 0; - type = 0; - granulePosition = 0; - streamSerialNumber = 0; - pageSequenceNumber = 0; - pageChecksum = 0; - pageSegmentCount = 0; - headerSize = 0; - bodySize = 0; - } - - } - - /** - * Holds size and number of segments of a packet. - */ - public static class PacketInfoHolder { - public int size; - public int segmentCount; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OpusReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OpusReader.java index 677f18506a..b5659ce3e0 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OpusReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OpusReader.java @@ -17,16 +17,14 @@ package com.google.android.exoplayer.extractor.ogg; import com.google.android.exoplayer.C; import com.google.android.exoplayer.Format; -import com.google.android.exoplayer.extractor.Extractor; -import com.google.android.exoplayer.extractor.ExtractorInput; -import com.google.android.exoplayer.extractor.PositionHolder; -import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableByteArray; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; /** @@ -34,19 +32,16 @@ import java.util.List; */ /* package */ final class OpusReader extends StreamReader { + private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840; + /** * Opus streams are always decoded at 48000 Hz. */ private static final int SAMPLE_RATE = 48000; private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}; - - private static final int STATE_READ_HEADER = 0; - private static final int STATE_READ_TAGS = 1; - private static final int STATE_READ_AUDIO = 2; - - private int state = STATE_READ_HEADER; - private long timeUs; + private boolean headerRead; + private boolean tagsSkipped; public static boolean verifyBitstreamType(ParsableByteArray data) { if (data.bytesLeft() < OPUS_SIGNATURE.length) { @@ -58,42 +53,48 @@ import java.util.List; } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { - if (!oggParser.readPacket(input, scratch)) { - return Extractor.RESULT_END_OF_INPUT; - } - - byte[] data = scratch.data; - int dataSize = scratch.limit(); - - switch (state) { - case STATE_READ_HEADER: { - byte[] metadata = Arrays.copyOfRange(data, 0, dataSize); - int channelCount = metadata[9] & 0xFF; - List initializationData = Collections.singletonList(metadata); - trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, - Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, - initializationData, null, 0, null)); - state = STATE_READ_TAGS; - } break; - case STATE_READ_TAGS: - // skip this packet - state = STATE_READ_AUDIO; - extractorOutput.seekMap(new SeekMap.Unseekable(C.UNSET_TIME_US)); - break; - case STATE_READ_AUDIO: - trackOutput.sampleData(scratch, dataSize); - trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, dataSize, 0, null); - timeUs += getPacketDuration(data); - break; - } - - scratch.reset(); - return Extractor.RESULT_CONTINUE; + protected long preparePayload(ParsableByteArray packet) { + return convertTimeToGranule(getPacketDurationUs(packet.data)); } - private long getPacketDuration(byte[] packet) { + @Override + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) + throws IOException, InterruptedException { + if (!headerRead) { + byte[] metadata = Arrays.copyOf(packet.data, packet.limit()); + int channelCount = metadata[9] & 0xFF; + int preskip = ((metadata[11] & 0xFF) << 8) | (metadata[10] & 0xFF); + + List initializationData = new ArrayList<>(3); + initializationData.add(metadata); + putNativeOrderLong(initializationData, preskip); + putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES); + + setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, Format.NO_VALUE, + Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0, "und"); + headerRead = true; + } else if (!tagsSkipped) { + // Skip tags packet + tagsSkipped = true; + } else { + return false; + } + return true; + } + + private void putNativeOrderLong(List initializationData, int samples) { + long ns = (samples * C.NANOS_PER_SECOND) / SAMPLE_RATE; + byte[] array = ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(ns).array(); + initializationData.add(array); + } + + /** + * Returns the duration of the given audio packet. + * + * @param packet Contains audio data. + * @return Returns the duration of the given audio packet. + */ + private long getPacketDurationUs(byte[] packet) { int toc = packet[0] & 0xFF; int frames; switch (toc & 0x3) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/StreamReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/StreamReader.java index af251cfd52..d77076e290 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/StreamReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/StreamReader.java @@ -1,9 +1,12 @@ package com.google.android.exoplayer.extractor.ogg; +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.Format; import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.PositionHolder; +import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.util.ParsableByteArray; @@ -14,31 +17,202 @@ import java.io.IOException; */ /* package */ abstract class StreamReader { - protected final ParsableByteArray scratch = new ParsableByteArray( - new byte[OggParser.OGG_MAX_SEGMENT_SIZE * 255], 0); + private static final int STATE_READ_HEADERS = 0; + private static final int STATE_READ_PAYLOAD = 1; + private static final int STATE_END_OF_INPUT = 2; - protected final OggParser oggParser = new OggParser(); + static class SetupData { + Format format; + OggSeeker oggSeeker; + } - protected TrackOutput trackOutput; - - protected ExtractorOutput extractorOutput; + private OggPacket oggPacket; + private TrackOutput trackOutput; + private ExtractorOutput extractorOutput; + private OggSeeker oggSeeker; + private long targetGranule; + private long payloadStartPosition; + private long currentGranule; + private int state; + private int sampleRate; + private SetupData setupData; + private long lengthOfReadPacket; + private boolean seekMapSet; void init(ExtractorOutput output, TrackOutput trackOutput) { this.extractorOutput = output; this.trackOutput = trackOutput; + this.oggPacket = new OggPacket(); + this.setupData = new SetupData(); + + this.state = STATE_READ_HEADERS; + this.targetGranule = -1; + this.payloadStartPosition = 0; } /** * @see Extractor#seek() */ - void seek() { - oggParser.reset(); - scratch.reset(); + final void seek() { + oggPacket.reset(); + + if (state != STATE_READ_HEADERS) { + targetGranule = oggSeeker.startSeek(); + state = STATE_READ_PAYLOAD; + } } /** * @see Extractor#read(ExtractorInput, PositionHolder) */ - abstract int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException; + final int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + switch (state) { + case STATE_READ_HEADERS: + return readHeaders(input); + + case STATE_READ_PAYLOAD: + return readPayload(input, seekPosition); + + default: + // Never happens. + throw new IllegalStateException(); + } + } + + private int readHeaders(ExtractorInput input) + throws IOException, InterruptedException { + boolean readingHeaders = true; + while (readingHeaders) { + if (!oggPacket.populate(input)) { + state = STATE_END_OF_INPUT; + return Extractor.RESULT_END_OF_INPUT; + } + lengthOfReadPacket = input.getPosition() - payloadStartPosition; + + readingHeaders = readHeaders(oggPacket.getPayload(), payloadStartPosition, setupData); + if (readingHeaders) { + payloadStartPosition = input.getPosition(); + } + } + + sampleRate = setupData.format.sampleRate; + trackOutput.format(setupData.format); + + if (setupData.oggSeeker != null) { + oggSeeker = setupData.oggSeeker; + } else if (input.getLength() == C.LENGTH_UNBOUNDED) { + oggSeeker = new UnseekableOggSeeker(); + } else { + oggSeeker = new DefaultOggSeeker(payloadStartPosition, input.getLength(), this); + } + + setupData = null; + state = STATE_READ_PAYLOAD; + return Extractor.RESULT_CONTINUE; + } + + private int readPayload(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + long position = oggSeeker.read(input); + if (position >= 0) { + seekPosition.position = position; + return Extractor.RESULT_SEEK; + } else if (position < -1) { + onSeekEnd(-position - 2); + } + if (!seekMapSet) { + SeekMap seekMap = oggSeeker.createSeekMap(); + extractorOutput.seekMap(seekMap); + seekMapSet = true; + } + + if (lengthOfReadPacket > 0 || oggPacket.populate(input)) { + lengthOfReadPacket = 0; + ParsableByteArray payload = oggPacket.getPayload(); + long granulesInPacket = preparePayload(payload); + if (granulesInPacket >= 0 && currentGranule + granulesInPacket >= targetGranule) { + // calculate time and send payload data to codec + long timeUs = convertGranuleToTime(currentGranule); + trackOutput.sampleData(payload, payload.limit()); + trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, payload.limit(), 0, null); + targetGranule = -1; + } + currentGranule += granulesInPacket; + } else { + state = STATE_END_OF_INPUT; + return Extractor.RESULT_END_OF_INPUT; + } + return Extractor.RESULT_CONTINUE; + } + + /** + * Converts granule value to time. + * + * @param granule + * granule value. + * @return Returns time in milliseconds. + */ + protected long convertGranuleToTime(long granule) { + return (granule * C.MICROS_PER_SECOND) / sampleRate; + } + + /** + * Converts time value to granule. + * + * @param timeUs + * Time in milliseconds. + * @return Granule value. + */ + protected long convertTimeToGranule(long timeUs) { + return (sampleRate * timeUs) / C.MICROS_PER_SECOND; + } + + /** + * Prepares payload data in the packet for submitting to TrackOutput and returns number of + * granules in the packet. + * + * @param packet + * Ogg payload data packet + * @return Number of granules in the packet or -1 if the packet doesn't contain payload data. + */ + protected abstract long preparePayload(ParsableByteArray packet); + + /** + * Checks if the given packet is a header packet and reads it. + * + * @param packet An ogg packet. + * @param position Position of the given header packet. + * @param setupData Setup data to be filled. + * @return Return true if the packet contains header data. + */ + protected abstract boolean readHeaders(ParsableByteArray packet, long position, + SetupData setupData) throws IOException, InterruptedException; + + /** + * Called on end of seeking. + * + * @param currentGranule Current granule at the current position of input. + */ + protected void onSeekEnd(long currentGranule) { + this.currentGranule = currentGranule; + } + + private class UnseekableOggSeeker implements OggSeeker { + @Override + public long read(ExtractorInput input) throws IOException, InterruptedException { + return -1; + } + + @Override + public long startSeek() { + return 0; + } + + @Override + public SeekMap createSeekMap() { + return new SeekMap.Unseekable(C.UNSET_TIME_US); + } + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisReader.java index 1e9f4b2b45..62d5a5ff04 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisReader.java @@ -15,13 +15,8 @@ */ package com.google.android.exoplayer.extractor.ogg; -import com.google.android.exoplayer.C; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.extractor.Extractor; -import com.google.android.exoplayer.extractor.ExtractorInput; -import com.google.android.exoplayer.extractor.PositionHolder; -import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.ogg.VorbisUtil.Mode; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableByteArray; @@ -32,24 +27,14 @@ import java.util.ArrayList; /** * {@link StreamReader} to extract Vorbis data out of Ogg byte stream. */ -/* package */ final class VorbisReader extends StreamReader implements SeekMap { - - private static final long LARGEST_EXPECTED_PAGE_SIZE = 8000; +/* package */ final class VorbisReader extends StreamReader { private VorbisSetup vorbisSetup; private int previousPacketBlockSize; - private long elapsedSamples; private boolean seenFirstAudioPacket; - private final OggSeeker oggSeeker = new OggSeeker(); - private long targetGranule = -1; - private VorbisUtil.VorbisIdHeader vorbisIdHeader; private VorbisUtil.CommentHeader commentHeader; - private long inputLength; - private long audioStartPosition; - private long totalSamples; - private long durationUs; public static boolean verifyBitstreamType(ParsableByteArray data) { try { @@ -60,114 +45,71 @@ import java.util.ArrayList; } @Override - public void seek() { - super.seek(); - previousPacketBlockSize = 0; - elapsedSamples = 0; - seenFirstAudioPacket = false; + protected void onSeekEnd(long currentGranule) { + super.onSeekEnd(currentGranule); + seenFirstAudioPacket = currentGranule != 0; + previousPacketBlockSize = vorbisIdHeader != null ? vorbisIdHeader.blockSize0 : 0; } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) + protected long preparePayload(ParsableByteArray packet) { + // if this is not an audio packet... + if ((packet.data[0] & 0x01) == 1) { + return -1; + } + + // ... we need to decode the block size + int packetBlockSize = decodeBlockSize(packet.data[0], vorbisSetup); + // a packet contains samples produced from overlapping the previous and current frame data + // (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2) + int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4 + : 0; + // codec expects the number of samples appended to audio data + appendNumberOfSamples(packet, samplesInPacket); + + // update state in members for next iteration + seenFirstAudioPacket = true; + previousPacketBlockSize = packetBlockSize; + return samplesInPacket; + } + + @Override + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) throws IOException, InterruptedException { - - // setup - if (totalSamples == 0) { - if (vorbisSetup == null) { - inputLength = input.getLength(); - vorbisSetup = readSetupHeaders(input, scratch); - audioStartPosition = input.getPosition(); - extractorOutput.seekMap(this); - if (inputLength != C.LENGTH_UNBOUNDED) { - // seek to the end just before the last page of stream to get the duration - seekPosition.position = Math.max(0, input.getLength() - LARGEST_EXPECTED_PAGE_SIZE); - return Extractor.RESULT_SEEK; - } - } - totalSamples = inputLength == C.LENGTH_UNBOUNDED ? -1 - : oggParser.readGranuleOfLastPage(input); - - ArrayList codecInitialisationData = new ArrayList<>(); - codecInitialisationData.add(vorbisSetup.idHeader.data); - codecInitialisationData.add(vorbisSetup.setupHeaderData); - - durationUs = inputLength == C.LENGTH_UNBOUNDED ? C.UNSET_TIME_US - : (totalSamples * C.MICROS_PER_SECOND) / vorbisSetup.idHeader.sampleRate; - trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, - this.vorbisSetup.idHeader.bitrateNominal, OggParser.OGG_MAX_SEGMENT_SIZE * 255, - this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate, - codecInitialisationData, null, 0, null)); - - if (inputLength != C.LENGTH_UNBOUNDED) { - oggSeeker.setup(inputLength - audioStartPosition, totalSamples); - // seek back to resume from where we finished reading vorbis headers - seekPosition.position = audioStartPosition; - return Extractor.RESULT_SEEK; - } + if (vorbisSetup != null) { + return false; } - // seeking requested - if (!seenFirstAudioPacket && targetGranule > -1) { - OggUtil.skipToNextPage(input); - long position = oggSeeker.getNextSeekPosition(targetGranule, input); - if (position != -1) { - seekPosition.position = position; - return Extractor.RESULT_SEEK; - } else { - elapsedSamples = oggParser.skipToPageOfGranule(input, targetGranule); - previousPacketBlockSize = vorbisIdHeader.blockSize0; - // we're never at the first packet after seeking - seenFirstAudioPacket = true; - } + vorbisSetup = readSetupHeaders(packet); + if (vorbisSetup == null) { + return true; } - // playback - if (oggParser.readPacket(input, scratch)) { - // if this is an audio packet... - if ((scratch.data[0] & 0x01) != 1) { - // ... we need to decode the block size - int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup); - // a packet contains samples produced from overlapping the previous and current frame data - // (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2) - int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4 - : 0; - if (elapsedSamples + samplesInPacket >= targetGranule) { - // codec expects the number of samples appended to audio data - appendNumberOfSamples(scratch, samplesInPacket); - // calculate time and send audio data to codec - long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate; - trackOutput.sampleData(scratch, scratch.limit()); - trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, scratch.limit(), 0, null); - targetGranule = -1; - } - // update state in members for next iteration - seenFirstAudioPacket = true; - elapsedSamples += samplesInPacket; - previousPacketBlockSize = packetBlockSize; - } - scratch.reset(); - return Extractor.RESULT_CONTINUE; - } - return Extractor.RESULT_END_OF_INPUT; + ArrayList codecInitialisationData = new ArrayList<>(); + codecInitialisationData.add(vorbisSetup.idHeader.data); + codecInitialisationData.add(vorbisSetup.setupHeaderData); + + setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, + this.vorbisSetup.idHeader.bitrateNominal, OggPageHeader.MAX_PAGE_PAYLOAD, + this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate, + codecInitialisationData, null, 0, null); + return true; } //@VisibleForTesting - /* package */ VorbisSetup readSetupHeaders(ExtractorInput input, ParsableByteArray scratch) + /* package */ VorbisSetup readSetupHeaders(ParsableByteArray scratch) throws IOException, InterruptedException { if (vorbisIdHeader == null) { - oggParser.readPacket(input, scratch); vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch); - scratch.reset(); + return null; } if (commentHeader == null) { - oggParser.readPacket(input, scratch); commentHeader = VorbisUtil.readVorbisCommentHeader(scratch); - scratch.reset(); + return null; } - oggParser.readPacket(input, scratch); // the third packet contains the setup header byte[] setupHeaderData = new byte[scratch.limit()]; // raw data of vorbis setup header has to be passed to decoder as CSD buffer #2 @@ -176,11 +118,24 @@ import java.util.ArrayList; Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels); // we need the ilog of modes all the time when extracting, so we compute it once int iLogModes = VorbisUtil.iLog(modes.length - 1); - scratch.reset(); return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes); } + /** + * Reads an int of {@code length} bits from {@code src} starting at + * {@code leastSignificantBitIndex}. + * + * @param src the {@code byte} to read from. + * @param length the length in bits of the int to read. + * @param leastSignificantBitIndex the index of the least significant bit of the int to read. + * @return the int value read. + */ + //@VisibleForTesting + /* package */ static int readBits(byte src, int length, int leastSignificantBitIndex) { + return (src >> leastSignificantBitIndex) & (255 >>> (8 - length)); + } + //@VisibleForTesting /* package */ static void appendNumberOfSamples(ParsableByteArray buffer, long packetSampleCount) { @@ -196,7 +151,7 @@ import java.util.ArrayList; private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) { // read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1) - int modeNumber = OggUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1); + int modeNumber = readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1); int currentBlockSize; if (!vorbisSetup.modes[modeNumber].blockFlag) { currentBlockSize = vorbisSetup.idHeader.blockSize0; @@ -206,27 +161,6 @@ import java.util.ArrayList; return currentBlockSize; } - @Override - public boolean isSeekable() { - return vorbisSetup != null && inputLength != C.LENGTH_UNBOUNDED; - } - - @Override - public long getPosition(long timeUs) { - if (timeUs == 0) { - targetGranule = -1; - return audioStartPosition; - } - targetGranule = vorbisSetup.idHeader.sampleRate * timeUs / C.MICROS_PER_SECOND; - return Math.max(audioStartPosition, ((inputLength - audioStartPosition) * timeUs - / durationUs) - 4000); - } - - @Override - public long getDurationUs() { - return durationUs; - } - /** * Class to hold all data read from Vorbis setup headers. */ diff --git a/library/src/main/java/com/google/android/exoplayer/util/FlacSeekTable.java b/library/src/main/java/com/google/android/exoplayer/util/FlacSeekTable.java deleted file mode 100644 index 879d33cc70..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/util/FlacSeekTable.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2016 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.exoplayer.util; - -import com.google.android.exoplayer.extractor.SeekMap; - -/** - * FLAC seek table class - */ -public final class FlacSeekTable { - - private static final int METADATA_LENGTH_OFFSET = 1; - private static final int SEEK_POINT_SIZE = 18; - - private final long[] sampleNumbers; - private final long[] offsets; - - /** - * Parses a FLAC file seek table metadata structure and creates a FlacSeekTable instance. - * - * @param data A ParsableByteArray including whole seek table metadata block. Its position should - * be set to the beginning of the block. - * @return A FlacSeekTable instance keeping seek table data - * @see FLAC format - * METADATA_BLOCK_SEEKTABLE - */ - public static FlacSeekTable parseSeekTable(ParsableByteArray data) { - data.skipBytes(METADATA_LENGTH_OFFSET); - int length = data.readUnsignedInt24(); - int numberOfSeekPoints = length / SEEK_POINT_SIZE; - - long[] sampleNumbers = new long[numberOfSeekPoints]; - long[] offsets = new long[numberOfSeekPoints]; - - for (int i = 0; i < numberOfSeekPoints; i++) { - sampleNumbers[i] = data.readLong(); - offsets[i] = data.readLong(); - data.skipBytes(2); // Skip "Number of samples in the target frame." - } - - return new FlacSeekTable(sampleNumbers, offsets); - } - - private FlacSeekTable(long[] sampleNumbers, long[] offsets) { - this.sampleNumbers = sampleNumbers; - this.offsets = offsets; - } - - /** - * Creates a {@link SeekMap} wrapper for this FlacSeekTable. - * - * @param firstFrameOffset Offset of the first FLAC frame - * @param sampleRate Sample rate of the FLAC file. - * @return A SeekMap wrapper for this FlacSeekTable. - */ - public SeekMap createSeekMap(final long firstFrameOffset, final long sampleRate, - final long durationUs) { - return new SeekMap() { - @Override - public boolean isSeekable() { - return true; - } - - @Override - public long getPosition(long timeUs) { - long sample = (timeUs * sampleRate) / 1000000L; - - int index = Util.binarySearchFloor(sampleNumbers, sample, true, true); - return firstFrameOffset + offsets[index]; - } - - @Override - public long getDurationUs() { - return durationUs; - } - }; - } -} diff --git a/library/src/main/java/com/google/android/exoplayer/util/FlacUtil.java b/library/src/main/java/com/google/android/exoplayer/util/FlacUtil.java deleted file mode 100644 index ab070ae194..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/util/FlacUtil.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2016 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.exoplayer.util; - -/** - * Utility functions for FLAC - */ -public final class FlacUtil { - - private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; - - /** - * Prevents initialization. - */ - private FlacUtil() {} - - /** - * Extracts sample timestamp from the given binary FLAC frame header data structure. - * - * @param streamInfo A {@link FlacStreamInfo} instance - * @param frameData A {@link ParsableByteArray} including binary FLAC frame header data structure. - * Its position should be set to the beginning of the structure. - * @return Sample timestamp - * @see FLAC format FRAME_HEADER - */ - public static long extractSampleTimestamp(FlacStreamInfo streamInfo, - ParsableByteArray frameData) { - frameData.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET); - long sampleNumber = frameData.readUtf8EncodedLong(); - if (streamInfo.minBlockSize == streamInfo.maxBlockSize) { - // if fixed block size then sampleNumber is frame number - sampleNumber *= streamInfo.minBlockSize; - } - return (sampleNumber * 1000000L) / streamInfo.sampleRate; - } - -}