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

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 {
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);

View File

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

View File

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

View File

@ -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.
* <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.
* <p>
* 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;
}
}
}

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.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<byte[]> codecInitializationData = new ArrayList<>();
codecInitializationData.clear();
codecInitializationData.add(idHeader.data);
codecInitializationData.add(vorbisSetup.setupHeaderData);
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS,
idHeader.bitrateNominal, OGG_MAX_SEGMENT_SIZE * 255, idHeader.channels,
idHeader.sampleRate, codecInitializationData, null));
// Setup.
if (totalSamples == 0) {
if (vorbisSetup == null) {
inputLength = input.getLength();
vorbisSetup = readSetupHeaders(input, scratch);
audioStartPosition = input.getPosition();
// Output the format.
ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
codecInitialisationData.add(vorbisSetup.idHeader.data);
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 this is an audio packet...
// If this is an audio packet...
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);
// a packet contains samples produced from overlapping the previous and current frame data
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4
: 0;
// codec expects the number of samples appended to audio data
appendNumberOfSamples(scratch, samplesInPacket);
// calculate time and send audio data to codec
long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
trackOutput.sampleData(scratch, scratch.limit());
trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null);
// update state in members for next iteration
// A packet contains samples produced from overlapping the previous and current frame data
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2).
int samplesInPacket = seenFirstAudioPacket
? ((packetBlockSize + previousPacketBlockSize) / 4) : 0;
if (elapsedSamples + samplesInPacket >= targetGranule) {
// Codec expects the number of samples appended to audio data.
appendNumberOfSamples(scratch, samplesInPacket);
// Calculate time and send audio data to codec.
long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
trackOutput.sampleData(scratch, scratch.limit());
trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null);
targetGranule = -1;
}
// Update state in members for next iteration.
seenFirstAudioPacket = true;
elapsedSamples += samplesInPacket;
previousPacketBlockSize = packetBlockSize;
@ -176,7 +224,7 @@ public final class OggVorbisExtractor implements Extractor {
private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) {
// read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1)
int modeNumber = VorbisUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);
int modeNumber = OggUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);
int currentBlockSize;
if (!vorbisSetup.modes[modeNumber].blockFlag) {
currentBlockSize = vorbisSetup.idHeader.blockSize0;
@ -186,6 +234,31 @@ public final class OggVorbisExtractor implements Extractor {
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.
*/

View File

@ -46,19 +46,6 @@ import java.util.Arrays;
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}.
*
@ -71,11 +58,11 @@ import java.util.Arrays;
public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData)
throws ParserException {
captureVorbisHeader(0x01, headerData);
verifyVorbisHeaderCapturePattern(0x01, headerData, false);
long version = headerData.readLittleEndianUnsignedInt();
int channels = headerData.readUnsignedByte();
int sampleRate = (int) headerData.readLittleEndianUnsignedInt();
long sampleRate = headerData.readLittleEndianUnsignedInt();
int bitrateMax = headerData.readLittleEndianInt();
int bitrateNominal = headerData.readLittleEndianInt();
int bitrateMin = headerData.readLittleEndianInt();
@ -104,7 +91,8 @@ import java.util.Arrays;
public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData)
throws ParserException {
int length = captureVorbisHeader(0x03, headerData);
verifyVorbisHeaderCapturePattern(0x03, headerData, false);
int length = 7;
int len = (int) headerData.readLittleEndianUnsignedInt();
length += 4;
@ -127,21 +115,40 @@ import java.util.Arrays;
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 {
if (idHeader.readUnsignedByte() != headerType) {
throw new ParserException("expected header type " + Integer.toHexString(headerType));
if (header.readUnsignedByte() != headerType) {
if (quite) {
return false;
} else {
throw new ParserException("expected header type " + Integer.toHexString(headerType));
}
}
if (!(idHeader.readUnsignedByte() == 'v'
&& idHeader.readUnsignedByte() == 'o'
&& idHeader.readUnsignedByte() == 'r'
&& idHeader.readUnsignedByte() == 'b'
&& idHeader.readUnsignedByte() == 'i'
&& idHeader.readUnsignedByte() == 's')) {
throw new ParserException("expected characters 'vorbis'");
if (!(header.readUnsignedByte() == 'v'
&& header.readUnsignedByte() == 'o'
&& header.readUnsignedByte() == 'r'
&& header.readUnsignedByte() == 'b'
&& header.readUnsignedByte() == 'i'
&& header.readUnsignedByte() == 's')) {
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)
throws ParserException {
captureVorbisHeader(0x05, headerData);
verifyVorbisHeaderCapturePattern(0x05, headerData, false);
int numberOfBooks = headerData.readUnsignedByte() + 1;
@ -433,7 +440,7 @@ import java.util.Arrays;
public final long version;
public final int channels;
public final int sampleRate;
public final long sampleRate;
public final int bitrateMax;
public final int bitrateNominal;
public final int bitrateMin;
@ -442,7 +449,7 @@ import java.util.Arrays;
public final boolean framingFlag;
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,
byte[] data) {
this.version = version;