Make OggVorbisExtractor seekable.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=117252304
This commit is contained in:
olly 2016-03-15 10:29:36 -07:00 committed by Oliver Woodman
parent 4ffa3556dd
commit a067bd0965
11 changed files with 1083 additions and 209 deletions

View File

@ -15,8 +15,9 @@
*/ */
package com.google.android.exoplayer.extractor.ogg; 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;
import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException;
import com.google.android.exoplayer.testutil.TestUtil; import com.google.android.exoplayer.testutil.TestUtil;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
@ -24,12 +25,13 @@ import android.test.MoreAsserts;
import junit.framework.TestCase; import junit.framework.TestCase;
import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Random; import java.util.Random;
/** /**
* Unit test for {@link OggReader} * Unit test for {@link OggReader}.
*/ */
public final class OggReaderTest extends TestCase { public final class OggReaderTest extends TestCase {
@ -51,7 +53,7 @@ public final class OggReaderTest extends TestCase {
byte[] thirdPacket = TestUtil.buildTestData(256, random); byte[] thirdPacket = TestUtil.buildTestData(256, random);
byte[] fourthPacket = TestUtil.buildTestData(271, random); byte[] fourthPacket = TestUtil.buildTestData(271, random);
FakeExtractorInput input = createInput( FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays( TestUtil.joinByteArrays(
// First page with a single packet. // First page with a single packet.
TestData.buildOggHeader(0x02, 0, 1000, 0x01), TestData.buildOggHeader(0x02, 0, 1000, 0x01),
@ -67,7 +69,7 @@ public final class OggReaderTest extends TestCase {
TestData.buildOggHeader(0x04, 128, 1003, 0x04), TestData.buildOggHeader(0x04, 128, 1003, 0x04),
TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces
thirdPacket, thirdPacket,
fourthPacket)); fourthPacket), true);
assertReadPacket(input, firstPacket); assertReadPacket(input, firstPacket);
assertTrue((oggReader.getPageHeader().type & 0x02) == 0x02); assertTrue((oggReader.getPageHeader().type & 0x02) == 0x02);
@ -111,12 +113,12 @@ public final class OggReaderTest extends TestCase {
byte[] firstPacket = TestUtil.buildTestData(255, random); byte[] firstPacket = TestUtil.buildTestData(255, random);
byte[] secondPacket = TestUtil.buildTestData(8, random); byte[] secondPacket = TestUtil.buildTestData(8, random);
FakeExtractorInput input = createInput( FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays( TestUtil.joinByteArrays(
TestData.buildOggHeader(0x06, 0, 1000, 0x04), TestData.buildOggHeader(0x06, 0, 1000, 0x04),
TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces. TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces.
firstPacket, firstPacket,
secondPacket)); secondPacket), true);
assertReadPacket(input, firstPacket); assertReadPacket(input, firstPacket);
assertReadPacket(input, secondPacket); assertReadPacket(input, secondPacket);
@ -126,7 +128,7 @@ public final class OggReaderTest extends TestCase {
public void testReadContinuedPacketOverTwoPages() throws Exception { public void testReadContinuedPacketOverTwoPages() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(518); byte[] firstPacket = TestUtil.buildTestData(518);
FakeExtractorInput input = createInput( FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays( TestUtil.joinByteArrays(
// First page. // First page.
TestData.buildOggHeader(0x02, 0, 1000, 0x02), TestData.buildOggHeader(0x02, 0, 1000, 0x02),
@ -135,7 +137,7 @@ public final class OggReaderTest extends TestCase {
// Second page (continued packet). // Second page (continued packet).
TestData.buildOggHeader(0x05, 10, 1001, 0x01), TestData.buildOggHeader(0x05, 10, 1001, 0x01),
TestUtil.createByteArray(0x08), // Laces. TestUtil.createByteArray(0x08), // Laces.
Arrays.copyOfRange(firstPacket, 510, 510 + 8))); Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true);
assertReadPacket(input, firstPacket); assertReadPacket(input, firstPacket);
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04); assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04);
@ -148,7 +150,7 @@ public final class OggReaderTest extends TestCase {
public void testReadContinuedPacketOverFourPages() throws Exception { public void testReadContinuedPacketOverFourPages() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(1028); byte[] firstPacket = TestUtil.buildTestData(1028);
FakeExtractorInput input = createInput( FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays( TestUtil.joinByteArrays(
// First page. // First page.
TestData.buildOggHeader(0x02, 0, 1000, 0x02), TestData.buildOggHeader(0x02, 0, 1000, 0x02),
@ -165,7 +167,7 @@ public final class OggReaderTest extends TestCase {
// Fourth page (continued packet). // Fourth page (continued packet).
TestData.buildOggHeader(0x05, 10, 1003, 0x01), TestData.buildOggHeader(0x05, 10, 1003, 0x01),
TestUtil.createByteArray(0x08), // Laces. 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); assertReadPacket(input, firstPacket);
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04); assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04);
@ -175,12 +177,27 @@ public final class OggReaderTest extends TestCase {
assertReadEof(input); 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 { public void testReadZeroSizedPacketsAtEndOfStream() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(8, random); byte[] firstPacket = TestUtil.buildTestData(8, random);
byte[] secondPacket = TestUtil.buildTestData(8, random); byte[] secondPacket = TestUtil.buildTestData(8, random);
byte[] thirdPacket = TestUtil.buildTestData(8, random); byte[] thirdPacket = TestUtil.buildTestData(8, random);
FakeExtractorInput input = createInput( FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays( TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x01), TestData.buildOggHeader(0x02, 0, 1000, 0x01),
TestUtil.createByteArray(0x08), // Laces. TestUtil.createByteArray(0x08), // Laces.
@ -190,7 +207,7 @@ public final class OggReaderTest extends TestCase {
secondPacket, secondPacket,
TestData.buildOggHeader(0x04, 0, 1002, 0x03), TestData.buildOggHeader(0x04, 0, 1002, 0x03),
TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces.
thirdPacket)); thirdPacket), true);
assertReadPacket(input, firstPacket); assertReadPacket(input, firstPacket);
assertReadPacket(input, secondPacket); assertReadPacket(input, secondPacket);
@ -198,9 +215,127 @@ public final class OggReaderTest extends TestCase {
assertReadEof(input); assertReadEof(input);
} }
private static FakeExtractorInput createInput(byte[] data) { public void testSkipToPageOfGranule() throws IOException, InterruptedException {
return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) byte[] packet = TestUtil.buildTestData(3 * 254, random);
.setSimulateUnknownLength(true).setSimulatePartialReads(true).build(); 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) private void assertReadPacket(FakeExtractorInput extractorInput, byte[] expected)
@ -221,7 +356,7 @@ public final class OggReaderTest extends TestCase {
while (true) { while (true) {
try { try {
return oggReader.readPacket(input, scratch); return oggReader.readPacket(input, scratch);
} catch (SimulatedIOException e) { } catch (FakeExtractorInput.SimulatedIOException e) {
// Ignore. // Ignore.
} }
} }

View File

@ -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
}
}
}
}

View File

@ -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();
}
}

View File

@ -43,15 +43,36 @@ public final class OggVorbisExtractorTest extends TestCase {
public void testSniff() throws Exception { public void testSniff() throws Exception {
byte[] data = TestUtil.joinByteArrays( byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x02), 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))); 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); byte[] data = TestData.buildOggHeader(0x00, 0, 1000, 0x00);
assertFalse(sniff(createInput(data))); 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 { public void testAppendNumberOfSamples() throws Exception {
ParsableByteArray buffer = new ParsableByteArray(4); ParsableByteArray buffer = new ParsableByteArray(4);
buffer.setLimit(0); buffer.setLimit(0);

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.extractor.ogg; 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.testutil.TestUtil;
/** /**
@ -22,6 +23,11 @@ import com.google.android.exoplayer.testutil.TestUtil;
*/ */
/* package */ final class TestData { /* 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, public static byte[] buildOggHeader(int headerType, long granule, int pageSequenceCounter,
int pageSegmentCount) { int pageSegmentCount) {
return TestUtil.createByteArray( return TestUtil.createByteArray(
@ -46,7 +52,7 @@ import com.google.android.exoplayer.testutil.TestUtil;
(pageSequenceCounter >> 24) & 0xFF, (pageSequenceCounter >> 24) & 0xFF,
0x00, // LSB of page checksum. 0x00, // LSB of page checksum.
0x00, 0x00,
0x00, 0x10,
0x00, // MSB of page checksum. 0x00, // MSB of page checksum.
pageSegmentCount); pageSegmentCount);
} }

View File

@ -38,13 +38,6 @@ public final class VorbisUtilTest extends TestCase {
assertEquals(0, VorbisUtil.iLog(-122)); 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 { public void testReadIdHeader() throws Exception {
byte[] data = TestData.getIdentificationHeaderData(); byte[] data = TestData.getIdentificationHeaderData();
ParsableByteArray headerData = new ParsableByteArray(data, data.length); ParsableByteArray headerData = new ParsableByteArray(data, data.length);
@ -95,4 +88,45 @@ public final class VorbisUtilTest extends TestCase {
assertEquals(0, modes[1].windowType); 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));
}
} }

View File

@ -15,11 +15,12 @@
*/ */
package com.google.android.exoplayer.extractor.ogg; package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput; 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.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import java.io.IOException; import java.io.IOException;
@ -28,12 +29,14 @@ import java.io.IOException;
*/ */
/* package */ final class OggReader { /* 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 ParsableByteArray headerArray = new ParsableByteArray(27 + 255);
private final PacketInfoHolder holder = new PacketInfoHolder();
private int currentSegmentIndex = -1; private int currentSegmentIndex = -1;
private long elapsedSamples;
/** /**
* Resets this reader. * Resets this reader.
@ -65,37 +68,99 @@ import java.io.IOException;
while (!packetComplete) { while (!packetComplete) {
if (currentSegmentIndex < 0) { if (currentSegmentIndex < 0) {
// We're at the start of a page. // We're at the start of a page.
if (!populatePageHeader(input, pageHeader, headerArray, false)) { if (!OggUtil.populatePageHeader(input, pageHeader, headerArray, true)) {
return false; return false;
} }
currentSegmentIndex = 0; int segmentIndex = 0;
} int bytesToSkip = pageHeader.headerSize;
if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) {
int packetSize = 0; // After seeking, the first packet may be the remainder
int segmentIndex = currentSegmentIndex; // part of a continued packet which has to be discarded.
// add up packetSize from laces OggUtil.calculatePacketSize(pageHeader, segmentIndex, holder);
while (segmentIndex < pageHeader.pageSegmentCount) { segmentIndex += holder.segmentCount;
int segmentLength = pageHeader.laces[segmentIndex++]; bytesToSkip += holder.size;
packetSize += segmentLength;
if (segmentLength != 255) {
// packets end at first lace < 255
break;
} }
input.skipFully(bytesToSkip);
currentSegmentIndex = segmentIndex;
} }
if (packetSize > 0) { OggUtil.calculatePacketSize(pageHeader, currentSegmentIndex, holder);
input.readFully(packetArray.data, packetArray.limit(), packetSize); int segmentIndex = currentSegmentIndex + holder.segmentCount;
packetArray.setLimit(packetArray.limit() + packetSize); if (holder.size > 0) {
input.readFully(packetArray.data, packetArray.limit(), holder.size);
packetArray.setLimit(packetArray.limit() + holder.size);
packetComplete = pageHeader.laces[segmentIndex - 1] != 255; packetComplete = pageHeader.laces[segmentIndex - 1] != 255;
} }
// advance now since we are sure reading didn't throw an exception // 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; 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.
* <p>
* 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. * populated if the first packet has yet to be read.
* <p> * <p>
* Note that there is only a single instance of {@code OggReader.PageHeader} which is mutable. * 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}. * @return the {@code PageHeader} of the current page or {@code null}.
*/ */
public PageHeader getPageHeader() { public OggUtil.PageHeader getPageHeader() {
return pageHeader; 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;
}
}
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -25,27 +25,21 @@ import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.extractor.ogg.VorbisUtil.Mode; 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.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Log;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
/** /**
* {@link Extractor} to extract Vorbis data out of Ogg byte stream. * {@link Extractor} to extract Vorbis data out of Ogg byte stream.
*/ */
public final class OggVorbisExtractor implements Extractor { public final class OggVorbisExtractor implements Extractor, SeekMap {
private static final String TAG = "OggVorbisExtractor";
private static final int OGG_MAX_SEGMENT_SIZE = 255;
private final ParsableByteArray scratch = new ParsableByteArray( private final ParsableByteArray scratch = new ParsableByteArray(
new byte[OGG_MAX_SEGMENT_SIZE * 255], 0); new byte[OggReader.OGG_MAX_SEGMENT_SIZE * 255], 0);
private final OggReader oggReader = new OggReader();
private final OggReader oggReader = new OggReader();
private TrackOutput trackOutput; private TrackOutput trackOutput;
private VorbisSetup vorbisSetup; private VorbisSetup vorbisSetup;
@ -53,36 +47,48 @@ public final class OggVorbisExtractor implements Extractor {
private long elapsedSamples; private long elapsedSamples;
private boolean seenFirstAudioPacket; private boolean seenFirstAudioPacket;
private final OggSeeker oggSeeker = new OggSeeker();
private long targetGranule = -1;
private ExtractorOutput extractorOutput;
private VorbisUtil.VorbisIdHeader vorbisIdHeader; private VorbisUtil.VorbisIdHeader vorbisIdHeader;
private VorbisUtil.CommentHeader commentHeader; private VorbisUtil.CommentHeader commentHeader;
private long inputLength;
private long audioStartPosition;
private long totalSamples;
private long durationUs;
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
try { try {
OggReader.PageHeader header = new OggReader.PageHeader(); OggUtil.PageHeader header = new OggUtil.PageHeader();
OggReader.populatePageHeader(input, header, scratch, true); if (!OggUtil.populatePageHeader(input, header, scratch, true)
if ((header.type & 0x02) != 0x02) { || (header.type & 0x02) != 0x02 || header.bodySize < 7) {
throw new ParserException("expected page to be first page of a logical stream"); return false;
} }
input.resetPeekPosition(); scratch.reset();
input.peekFully(scratch.data, 0, 7);
return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, scratch, true);
} catch (ParserException e) { } catch (ParserException e) {
Log.e(TAG, e.getMessage()); // does not happen
return false; } finally {
input.resetPeekPosition();
scratch.reset();
} }
return true; return false;
} }
@Override @Override
public void init(ExtractorOutput output) { public void init(ExtractorOutput output) {
trackOutput = output.track(0); trackOutput = output.track(0);
output.endTracks(); output.endTracks();
output.seekMap(new SeekMap.Unseekable(C.UNKNOWN_TIME_US)); extractorOutput = output;
} }
@Override @Override
public void seek() { public void seek() {
oggReader.reset(); oggReader.reset();
previousPacketBlockSize = -1; previousPacketBlockSize = 0;
elapsedSamples = 0; elapsedSamples = 0;
seenFirstAudioPacket = false; seenFirstAudioPacket = false;
scratch.reset(); scratch.reset();
@ -92,35 +98,77 @@ public final class OggVorbisExtractor implements Extractor {
public int read(ExtractorInput input, PositionHolder seekPosition) public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (vorbisSetup == null) { // Setup.
vorbisSetup = readSetupHeaders(input, scratch); if (totalSamples == 0) {
VorbisIdHeader idHeader = vorbisSetup.idHeader; if (vorbisSetup == null) {
ArrayList<byte[]> codecInitializationData = new ArrayList<>(); inputLength = input.getLength();
codecInitializationData.clear(); vorbisSetup = readSetupHeaders(input, scratch);
codecInitializationData.add(idHeader.data); audioStartPosition = input.getPosition();
codecInitializationData.add(vorbisSetup.setupHeaderData); // Output the format.
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
idHeader.bitrateNominal, OGG_MAX_SEGMENT_SIZE * 255, idHeader.channels, codecInitialisationData.add(vorbisSetup.idHeader.data);
idHeader.sampleRate, codecInitializationData, null)); codecInitialisationData.add(vorbisSetup.setupHeaderData);
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS,
vorbisSetup.idHeader.bitrateNominal, OggReader.OGG_MAX_SEGMENT_SIZE * 255,
vorbisSetup.idHeader.channels, (int) vorbisSetup.idHeader.sampleRate,
codecInitialisationData, null));
if (inputLength == C.LENGTH_UNBOUNDED) {
// If the length is unbounded, we cannot determine the duration or seek.
totalSamples = -1;
durationUs = C.LENGTH_UNBOUNDED;
extractorOutput.seekMap(this);
return RESULT_CONTINUE;
}
// Seek to just before the last page of stream to get the duration.
seekPosition.position = input.getLength() - 8000;
return RESULT_SEEK;
}
totalSamples = oggReader.readGranuleOfLastPage(input);
durationUs = totalSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
oggSeeker.setup(inputLength - audioStartPosition, totalSamples);
extractorOutput.seekMap(this);
// Seek back to resume from where we finished reading vorbis headers.
seekPosition.position = audioStartPosition;
return RESULT_SEEK;
} }
// Seeking requested.
if (!seenFirstAudioPacket && targetGranule > -1) {
OggUtil.skipToNextPage(input);
long position = oggSeeker.getNextSeekPosition(targetGranule, input);
if (position != -1) {
seekPosition.position = position;
return RESULT_SEEK;
} else {
elapsedSamples = oggReader.skipToPageOfGranule(input, targetGranule);
previousPacketBlockSize = vorbisIdHeader.blockSize0;
// We're never at the first packet after seeking.
seenFirstAudioPacket = true;
oggSeeker.reset();
}
}
// Playback.
if (oggReader.readPacket(input, scratch)) { if (oggReader.readPacket(input, scratch)) {
// if this is an audio packet... // If this is an audio packet...
if ((scratch.data[0] & 0x01) != 1) { if ((scratch.data[0] & 0x01) != 1) {
// ... we need to decode the block size // ... Then we need to decode the block size
int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup); int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup);
// a packet contains samples produced from overlapping the previous and current frame data // 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) // (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2).
int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4 int samplesInPacket = seenFirstAudioPacket
: 0; ? ((packetBlockSize + previousPacketBlockSize) / 4) : 0;
// codec expects the number of samples appended to audio data if (elapsedSamples + samplesInPacket >= targetGranule) {
appendNumberOfSamples(scratch, samplesInPacket); // Codec expects the number of samples appended to audio data.
appendNumberOfSamples(scratch, samplesInPacket);
// calculate time and send audio data to codec // Calculate time and send audio data to codec.
long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate; long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
trackOutput.sampleData(scratch, scratch.limit()); trackOutput.sampleData(scratch, scratch.limit());
trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null); trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null);
targetGranule = -1;
// update state in members for next iteration }
// Update state in members for next iteration.
seenFirstAudioPacket = true; seenFirstAudioPacket = true;
elapsedSamples += samplesInPacket; elapsedSamples += samplesInPacket;
previousPacketBlockSize = packetBlockSize; previousPacketBlockSize = packetBlockSize;
@ -176,7 +224,7 @@ public final class OggVorbisExtractor implements Extractor {
private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) { private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) {
// read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1) // read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1)
int modeNumber = VorbisUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1); int modeNumber = OggUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);
int currentBlockSize; int currentBlockSize;
if (!vorbisSetup.modes[modeNumber].blockFlag) { if (!vorbisSetup.modes[modeNumber].blockFlag) {
currentBlockSize = vorbisSetup.idHeader.blockSize0; currentBlockSize = vorbisSetup.idHeader.blockSize0;
@ -186,6 +234,31 @@ public final class OggVorbisExtractor implements Extractor {
return currentBlockSize; return currentBlockSize;
} }
// SeekMap implementation.
@Override
public boolean isSeekable() {
return inputLength != C.LENGTH_UNBOUNDED;
}
@Override
public long getDurationUs() {
return durationUs;
}
@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);
}
// Internal classes.
/** /**
* Class to hold all data read from Vorbis setup headers. * Class to hold all data read from Vorbis setup headers.
*/ */

View File

@ -46,19 +46,6 @@ import java.util.Arrays;
return val; return val;
} }
/**
* 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));
}
/** /**
* Reads a vorbis identification header from {@code headerData}. * Reads a vorbis identification header from {@code headerData}.
* *
@ -71,11 +58,11 @@ import java.util.Arrays;
public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData) public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData)
throws ParserException { throws ParserException {
captureVorbisHeader(0x01, headerData); verifyVorbisHeaderCapturePattern(0x01, headerData, false);
long version = headerData.readLittleEndianUnsignedInt(); long version = headerData.readLittleEndianUnsignedInt();
int channels = headerData.readUnsignedByte(); int channels = headerData.readUnsignedByte();
int sampleRate = (int) headerData.readLittleEndianUnsignedInt(); long sampleRate = headerData.readLittleEndianUnsignedInt();
int bitrateMax = headerData.readLittleEndianInt(); int bitrateMax = headerData.readLittleEndianInt();
int bitrateNominal = headerData.readLittleEndianInt(); int bitrateNominal = headerData.readLittleEndianInt();
int bitrateMin = headerData.readLittleEndianInt(); int bitrateMin = headerData.readLittleEndianInt();
@ -104,7 +91,8 @@ import java.util.Arrays;
public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData) public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData)
throws ParserException { throws ParserException {
int length = captureVorbisHeader(0x03, headerData); verifyVorbisHeaderCapturePattern(0x03, headerData, false);
int length = 7;
int len = (int) headerData.readLittleEndianUnsignedInt(); int len = (int) headerData.readLittleEndianUnsignedInt();
length += 4; length += 4;
@ -127,21 +115,40 @@ import java.util.Arrays;
return new CommentHeader(vendor, comments, length); return new CommentHeader(vendor, comments, length);
} }
private static int captureVorbisHeader(int headerType, ParsableByteArray idHeader) /**
* Verifies whether the next bytes in {@code header} are a vorbis header of the given
* {@code headerType}.
*
* @param headerType the type of the header expected.
* @param header the alleged header bytes.
* @param quite if {@code true} no exceptions are thrown. Instead {@code false} is returned.
* @return the number of bytes read.
* @throws ParserException thrown if header type or capture pattern is not as expected.
*/
public static boolean verifyVorbisHeaderCapturePattern(int headerType, ParsableByteArray header,
boolean quite)
throws ParserException { throws ParserException {
if (idHeader.readUnsignedByte() != headerType) { if (header.readUnsignedByte() != headerType) {
throw new ParserException("expected header type " + Integer.toHexString(headerType)); if (quite) {
return false;
} else {
throw new ParserException("expected header type " + Integer.toHexString(headerType));
}
} }
if (!(idHeader.readUnsignedByte() == 'v' if (!(header.readUnsignedByte() == 'v'
&& idHeader.readUnsignedByte() == 'o' && header.readUnsignedByte() == 'o'
&& idHeader.readUnsignedByte() == 'r' && header.readUnsignedByte() == 'r'
&& idHeader.readUnsignedByte() == 'b' && header.readUnsignedByte() == 'b'
&& idHeader.readUnsignedByte() == 'i' && header.readUnsignedByte() == 'i'
&& idHeader.readUnsignedByte() == 's')) { && header.readUnsignedByte() == 's')) {
throw new ParserException("expected characters 'vorbis'"); if (quite) {
return false;
} else {
throw new ParserException("expected characters 'vorbis'");
}
} }
return 7; // bytes read return true;
} }
/** /**
@ -159,7 +166,7 @@ import java.util.Arrays;
public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels) public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels)
throws ParserException { throws ParserException {
captureVorbisHeader(0x05, headerData); verifyVorbisHeaderCapturePattern(0x05, headerData, false);
int numberOfBooks = headerData.readUnsignedByte() + 1; int numberOfBooks = headerData.readUnsignedByte() + 1;
@ -433,7 +440,7 @@ import java.util.Arrays;
public final long version; public final long version;
public final int channels; public final int channels;
public final int sampleRate; public final long sampleRate;
public final int bitrateMax; public final int bitrateMax;
public final int bitrateNominal; public final int bitrateNominal;
public final int bitrateMin; public final int bitrateMin;
@ -442,7 +449,7 @@ import java.util.Arrays;
public final boolean framingFlag; public final boolean framingFlag;
public final byte[] data; public final byte[] data;
public VorbisIdHeader(long version, int channels, int sampleRate, int bitrateMax, public VorbisIdHeader(long version, int channels, long sampleRate, int bitrateMax,
int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag, int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag,
byte[] data) { byte[] data) {
this.version = version; this.version = version;