diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggReaderTest.java index ddc645628c..f1270aeb2f 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggReaderTest.java @@ -15,8 +15,9 @@ */ 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.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer.testutil.TestUtil; import com.google.android.exoplayer.util.ParsableByteArray; @@ -24,12 +25,13 @@ 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 OggReader} + * Unit test for {@link OggReader}. */ public final class OggReaderTest extends TestCase { @@ -51,7 +53,7 @@ public final class OggReaderTest extends TestCase { byte[] thirdPacket = TestUtil.buildTestData(256, random); byte[] fourthPacket = TestUtil.buildTestData(271, random); - FakeExtractorInput input = createInput( + FakeExtractorInput input = TestData.createInput( TestUtil.joinByteArrays( // First page with a single packet. TestData.buildOggHeader(0x02, 0, 1000, 0x01), @@ -67,7 +69,7 @@ public final class OggReaderTest extends TestCase { TestData.buildOggHeader(0x04, 128, 1003, 0x04), TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces thirdPacket, - fourthPacket)); + fourthPacket), true); assertReadPacket(input, firstPacket); assertTrue((oggReader.getPageHeader().type & 0x02) == 0x02); @@ -111,12 +113,12 @@ public final class OggReaderTest extends TestCase { byte[] firstPacket = TestUtil.buildTestData(255, random); byte[] secondPacket = TestUtil.buildTestData(8, random); - FakeExtractorInput input = createInput( + FakeExtractorInput input = TestData.createInput( TestUtil.joinByteArrays( TestData.buildOggHeader(0x06, 0, 1000, 0x04), TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces. firstPacket, - secondPacket)); + secondPacket), true); assertReadPacket(input, firstPacket); assertReadPacket(input, secondPacket); @@ -126,7 +128,7 @@ public final class OggReaderTest extends TestCase { public void testReadContinuedPacketOverTwoPages() throws Exception { byte[] firstPacket = TestUtil.buildTestData(518); - FakeExtractorInput input = createInput( + FakeExtractorInput input = TestData.createInput( TestUtil.joinByteArrays( // First page. TestData.buildOggHeader(0x02, 0, 1000, 0x02), @@ -135,7 +137,7 @@ public final class OggReaderTest extends TestCase { // Second page (continued packet). TestData.buildOggHeader(0x05, 10, 1001, 0x01), TestUtil.createByteArray(0x08), // Laces. - Arrays.copyOfRange(firstPacket, 510, 510 + 8))); + Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true); assertReadPacket(input, firstPacket); assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04); @@ -148,7 +150,7 @@ public final class OggReaderTest extends TestCase { public void testReadContinuedPacketOverFourPages() throws Exception { byte[] firstPacket = TestUtil.buildTestData(1028); - FakeExtractorInput input = createInput( + FakeExtractorInput input = TestData.createInput( TestUtil.joinByteArrays( // First page. TestData.buildOggHeader(0x02, 0, 1000, 0x02), @@ -165,7 +167,7 @@ public final class OggReaderTest extends TestCase { // Fourth page (continued packet). TestData.buildOggHeader(0x05, 10, 1003, 0x01), TestUtil.createByteArray(0x08), // Laces. - Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8))); + Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true); assertReadPacket(input, firstPacket); assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04); @@ -175,12 +177,27 @@ public final class OggReaderTest extends TestCase { 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 = createInput( + FakeExtractorInput input = TestData.createInput( TestUtil.joinByteArrays( TestData.buildOggHeader(0x02, 0, 1000, 0x01), TestUtil.createByteArray(0x08), // Laces. @@ -190,7 +207,7 @@ public final class OggReaderTest extends TestCase { secondPacket, TestData.buildOggHeader(0x04, 0, 1002, 0x03), TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. - thirdPacket)); + thirdPacket), true); assertReadPacket(input, firstPacket); assertReadPacket(input, secondPacket); @@ -198,9 +215,127 @@ public final class OggReaderTest extends TestCase { assertReadEof(input); } - private static FakeExtractorInput createInput(byte[] data) { - return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) - .setSimulateUnknownLength(true).setSimulatePartialReads(true).build(); + 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, oggReader.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, oggReader.readGranuleOfLastPage(input)); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { + // ignored + } + } } private void assertReadPacket(FakeExtractorInput extractorInput, byte[] expected) @@ -221,7 +356,7 @@ public final class OggReaderTest extends TestCase { while (true) { try { return oggReader.readPacket(input, scratch); - } catch (SimulatedIOException e) { + } 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 new file mode 100644 index 0000000000..de0034fce4 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggSeekerTest.java @@ -0,0 +1,132 @@ +/* + * 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 new file mode 100644 index 0000000000..b5c4ae08e8 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggUtilTest.java @@ -0,0 +1,190 @@ +/* + * 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/OggVorbisExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractorTest.java index 45b4ad63a6..46f59200c3 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractorTest.java @@ -43,15 +43,36 @@ public final class OggVorbisExtractorTest extends TestCase { public void testSniff() throws Exception { byte[] data = TestUtil.joinByteArrays( TestData.buildOggHeader(0x02, 0, 1000, 0x02), - TestUtil.createByteArray(120, 120)); // Laces + TestUtil.createByteArray(120, 120), // Laces + new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'}); assertTrue(sniff(createInput(data))); } - public void testSniffFails() throws Exception { + 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))); + } + + public void testSniffFailsInvalidOggHeader() throws Exception { byte[] data = TestData.buildOggHeader(0x00, 0, 1000, 0x00); assertFalse(sniff(createInput(data))); } + public void testSniffInvalidVorbisHeader() throws Exception { + byte[] data = TestUtil.joinByteArrays( + TestData.buildOggHeader(0x02, 0, 1000, 0x02), + TestUtil.createByteArray(120, 120), // Laces + new byte[]{0x01, 'X', 'o', 'r', 'b', 'i', 's'}); + assertFalse(sniff(createInput(data))); + } + + public void testSniffFailsEOF() throws Exception { + byte[] data = TestData.buildOggHeader(0x02, 0, 1000, 0x00); + assertFalse(sniff(createInput(data))); + } + public void testAppendNumberOfSamples() throws Exception { ParsableByteArray buffer = new ParsableByteArray(4); buffer.setLimit(0); diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/TestData.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/TestData.java index caf97e32a3..014438ee1f 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/TestData.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/TestData.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.extractor.ogg; +import com.google.android.exoplayer.testutil.FakeExtractorInput; import com.google.android.exoplayer.testutil.TestUtil; /** @@ -22,6 +23,11 @@ import com.google.android.exoplayer.testutil.TestUtil; */ /* package */ final class TestData { + /* package */ static FakeExtractorInput createInput(byte[] data, boolean simulateUnkownLength) { + return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) + .setSimulateUnknownLength(simulateUnkownLength).setSimulatePartialReads(true).build(); + } + public static byte[] buildOggHeader(int headerType, long granule, int pageSequenceCounter, int pageSegmentCount) { return TestUtil.createByteArray( @@ -46,7 +52,7 @@ import com.google.android.exoplayer.testutil.TestUtil; (pageSequenceCounter >> 24) & 0xFF, 0x00, // LSB of page checksum. 0x00, - 0x00, + 0x10, 0x00, // MSB of page checksum. pageSegmentCount); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/VorbisUtilTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/VorbisUtilTest.java index 7bd17fb65a..07b8130026 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/VorbisUtilTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/VorbisUtilTest.java @@ -38,13 +38,6 @@ public final class VorbisUtilTest extends TestCase { assertEquals(0, VorbisUtil.iLog(-122)); } - public void testReadBits() throws Exception { - assertEquals(0, VorbisUtil.readBits((byte) 0x00, 2, 2)); - assertEquals(1, VorbisUtil.readBits((byte) 0x02, 1, 1)); - assertEquals(15, VorbisUtil.readBits((byte) 0xF0, 4, 4)); - assertEquals(1, VorbisUtil.readBits((byte) 0x80, 1, 7)); - } - public void testReadIdHeader() throws Exception { byte[] data = TestData.getIdentificationHeaderData(); ParsableByteArray headerData = new ParsableByteArray(data, data.length); @@ -95,4 +88,45 @@ public final class VorbisUtilTest extends TestCase { assertEquals(0, modes[1].windowType); } + public void testVerifyVorbisHeaderCapturePattern() throws ParserException { + ParsableByteArray header = new ParsableByteArray( + new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'}); + assertEquals(true, VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false)); + } + + public void testVerifyVorbisHeaderCapturePatternInvalidHeader() throws ParserException { + ParsableByteArray header = new ParsableByteArray( + new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'}); + try { + VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, false); + fail(); + } catch (ParserException e) { + assertEquals("expected header type 99", e.getMessage()); + } + } + + public void testVerifyVorbisHeaderCapturePatternInvalidHeaderQuite() throws ParserException { + ParsableByteArray header = new ParsableByteArray( + new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'}); + assertFalse(VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, true)); + } + + public void testVerifyVorbisHeaderCapturePatternInvalidPattern() throws ParserException { + ParsableByteArray header = new ParsableByteArray( + new byte[]{0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); + try { + VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false); + fail(); + } catch (ParserException e) { + assertEquals("expected characters 'vorbis'", e.getMessage()); + } + } + + public void testVerifyVorbisHeaderCapturePatternQuiteInvalidPatternQuite() + throws ParserException { + ParsableByteArray header = new ParsableByteArray( + new byte[]{0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); + assertFalse(VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, true)); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggReader.java index b913aed4c9..3fb2ed473a 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggReader.java @@ -15,11 +15,12 @@ */ 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 com.google.android.exoplayer.util.Util; import java.io.IOException; @@ -28,12 +29,14 @@ import java.io.IOException; */ /* package */ final class OggReader { - private static final String CAPTURE_PATTERN_PAGE = "OggS"; + public static final int OGG_MAX_SEGMENT_SIZE = 255; - private final PageHeader pageHeader = new PageHeader(); + 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. @@ -65,37 +68,99 @@ import java.io.IOException; while (!packetComplete) { if (currentSegmentIndex < 0) { // We're at the start of a page. - if (!populatePageHeader(input, pageHeader, headerArray, false)) { + if (!OggUtil.populatePageHeader(input, pageHeader, headerArray, true)) { return false; } - currentSegmentIndex = 0; - } - - int packetSize = 0; - int segmentIndex = currentSegmentIndex; - // add up packetSize from laces - while (segmentIndex < pageHeader.pageSegmentCount) { - int segmentLength = pageHeader.laces[segmentIndex++]; - packetSize += segmentLength; - if (segmentLength != 255) { - // packets end at first lace < 255 - break; + 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; } - if (packetSize > 0) { - input.readFully(packetArray.data, packetArray.limit(), packetSize); - packetArray.setLimit(packetArray.limit() + packetSize); + 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; + currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? -1 + : segmentIndex; } return true; } /** - * Returns the {@link OggReader.PageHeader} of the current page. The header might not have been + * 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) { + if (pageHeader.bodySize > 0) { + input.skipFully(pageHeader.bodySize); + } + OggUtil.populatePageHeader(input, pageHeader, headerArray, false); + input.skipFully(pageHeader.headerSize); + } + 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 OggReader.PageHeader} which is mutable.
@@ -104,93 +169,8 @@ import java.io.IOException;
*
* @return the {@code PageHeader} of the current page or {@code null}.
*/
- public PageHeader getPageHeader() {
+ public OggUtil.PageHeader getPageHeader() {
return pageHeader;
}
- /**
- * Reads/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 read from.
- * @param scratch a scratch array temporary use.
- * @param peek pass {@code true} if data should only be peeked from current peek position.
- * @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 peek) throws IOException, InterruptedException {
-
- scratch.reset();
- header.reset();
- if (!input.peekFully(scratch.data, 0, 27, true)) {
- return false;
- }
- if (scratch.readUnsignedInt() != Util.getIntegerCodeForString(CAPTURE_PATTERN_PAGE)) {
- throw new ParserException("expected OggS capture pattern at begin of page");
- }
-
- header.revision = scratch.readUnsignedByte();
- if (header.revision != 0x00) {
- 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 = 27 + 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];
- }
- if (!peek) {
- input.skipFully(header.headerSize);
- }
- return true;
- }
-
- /**
- * 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 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;
- }
-
- }
-
}
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
new file mode 100644
index 0000000000..7466c8d2e8
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggSeeker.java
@@ -0,0 +1,87 @@
+/*
+ * 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.extractor.ExtractorInput;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.ParsableByteArray;
+
+import java.io.IOException;
+
+/**
+ * Used to seek in an Ogg stream.
+ */
+/* 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;
+
+ /**
+ * 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.
+ */
+ public void setup(long audioDataLength, long totalSamples) {
+ Assertions.checkArgument(audioDataLength > 0 && totalSamples > 0);
+ this.audioDataLength = audioDataLength;
+ this.totalSamples = totalSamples;
+ }
+
+ /**
+ * Resets this {@code OggSeeker}.
+ */
+ public void reset() {
+ pageHeader.reset();
+ headerArray.reset();
+ }
+
+ /**
+ * 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.
+ */
+ 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;
+ }
+
+}
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
new file mode 100644
index 0000000000..d62ba6ef42
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java
@@ -0,0 +1,209 @@
+/*
+ * 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 {
+
+ 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 read from.
+ * @param scratch a scratch array temporary use.
+ * @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() >= 27;
+ if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, 27, 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 = 27 + 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/OggVorbisExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractor.java
index 7daeb7e2ea..22b6b6db23 100644
--- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractor.java
+++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractor.java
@@ -25,27 +25,21 @@ 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.extractor.ogg.VorbisUtil.Mode;
-import com.google.android.exoplayer.extractor.ogg.VorbisUtil.VorbisIdHeader;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
-import android.util.Log;
-
import java.io.IOException;
import java.util.ArrayList;
/**
* {@link Extractor} to extract Vorbis data out of Ogg byte stream.
*/
-public final class OggVorbisExtractor implements Extractor {
-
- private static final String TAG = "OggVorbisExtractor";
-
- private static final int OGG_MAX_SEGMENT_SIZE = 255;
+public final class OggVorbisExtractor implements Extractor, SeekMap {
private final ParsableByteArray scratch = new ParsableByteArray(
- new byte[OGG_MAX_SEGMENT_SIZE * 255], 0);
- private final OggReader oggReader = new OggReader();
+ new byte[OggReader.OGG_MAX_SEGMENT_SIZE * 255], 0);
+
+ private final OggReader oggReader = new OggReader();
private TrackOutput trackOutput;
private VorbisSetup vorbisSetup;
@@ -53,36 +47,48 @@ public final class OggVorbisExtractor implements Extractor {
private long elapsedSamples;
private boolean seenFirstAudioPacket;
+ private final OggSeeker oggSeeker = new OggSeeker();
+ private long targetGranule = -1;
+
+ private ExtractorOutput extractorOutput;
private VorbisUtil.VorbisIdHeader vorbisIdHeader;
private VorbisUtil.CommentHeader commentHeader;
+ private long inputLength;
+ private long audioStartPosition;
+ private long totalSamples;
+ private long durationUs;
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
try {
- OggReader.PageHeader header = new OggReader.PageHeader();
- OggReader.populatePageHeader(input, header, scratch, true);
- if ((header.type & 0x02) != 0x02) {
- throw new ParserException("expected page to be first page of a logical stream");
+ OggUtil.PageHeader header = new OggUtil.PageHeader();
+ if (!OggUtil.populatePageHeader(input, header, scratch, true)
+ || (header.type & 0x02) != 0x02 || header.bodySize < 7) {
+ return false;
}
- input.resetPeekPosition();
+ scratch.reset();
+ input.peekFully(scratch.data, 0, 7);
+ return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, scratch, true);
} catch (ParserException e) {
- Log.e(TAG, e.getMessage());
- return false;
+ // does not happen
+ } finally {
+ input.resetPeekPosition();
+ scratch.reset();
}
- return true;
+ return false;
}
@Override
public void init(ExtractorOutput output) {
trackOutput = output.track(0);
output.endTracks();
- output.seekMap(new SeekMap.Unseekable(C.UNKNOWN_TIME_US));
+ extractorOutput = output;
}
@Override
public void seek() {
oggReader.reset();
- previousPacketBlockSize = -1;
+ previousPacketBlockSize = 0;
elapsedSamples = 0;
seenFirstAudioPacket = false;
scratch.reset();
@@ -92,35 +98,77 @@ public final class OggVorbisExtractor implements Extractor {
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
- if (vorbisSetup == null) {
- vorbisSetup = readSetupHeaders(input, scratch);
- VorbisIdHeader idHeader = vorbisSetup.idHeader;
- ArrayList