Ogg/Opus and Ogg/Flac search seeking

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=122977123
This commit is contained in:
eguven 2016-05-23 02:09:36 -07:00 committed by Oliver Woodman
parent 7465db2a22
commit 731d4283ab
28 changed files with 1882 additions and 1621 deletions

Binary file not shown.

View File

@ -0,0 +1,108 @@
/*
* 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 DefaultOggSeeker}.
*/
public final class DefaultOggSeekerTest extends TestCase {
private static final long HEADER_GRANULE = 200000;
private static final int START_POSITION = 0;
private static final int END_POSITION = 1000000;
private static final int TOTAL_SAMPLES = END_POSITION - START_POSITION;
private DefaultOggSeeker oggSeeker;
@Override
public void setUp() throws Exception {
super.setUp();
oggSeeker = DefaultOggSeeker.createOggSeekerForTesting(START_POSITION, END_POSITION,
TOTAL_SAMPLES);
}
public void testSetupUnboundAudioLength() {
try {
new DefaultOggSeeker(0, C.LENGTH_UNBOUNDED, new FlacReader());
fail();
} catch (IllegalArgumentException e) {
// ignored
}
}
public void testGetNextSeekPositionMatch() throws IOException, InterruptedException {
assertGetNextSeekPosition(HEADER_GRANULE + DefaultOggSeeker.MATCH_RANGE);
assertGetNextSeekPosition(HEADER_GRANULE + 1);
}
public void testGetNextSeekPositionTooHigh() throws IOException, InterruptedException {
assertGetNextSeekPosition(HEADER_GRANULE - 100000);
assertGetNextSeekPosition(HEADER_GRANULE);
}
public void testGetNextSeekPositionTooLow() throws IOException, InterruptedException {
assertGetNextSeekPosition(HEADER_GRANULE + DefaultOggSeeker.MATCH_RANGE + 1);
assertGetNextSeekPosition(HEADER_GRANULE + 100000);
}
public void testGetNextSeekPositionBounds() throws IOException, InterruptedException {
assertGetNextSeekPosition(HEADER_GRANULE + TOTAL_SAMPLES);
assertGetNextSeekPosition(HEADER_GRANULE - TOTAL_SAMPLES);
}
private void assertGetNextSeekPosition(long targetGranule)
throws IOException, InterruptedException {
int pagePosition = 500000;
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
new byte[pagePosition],
TestData.buildOggHeader(0x00, HEADER_GRANULE, 22, 2),
TestUtil.createByteArray(54, 55) // laces
), false);
input.setPosition(pagePosition);
long granuleDiff = targetGranule - HEADER_GRANULE;
long expectedPosition;
if (granuleDiff > 0 && granuleDiff <= DefaultOggSeeker.MATCH_RANGE) {
expectedPosition = -1;
} else {
long doublePageSize = (27 + 2 + 54 + 55) * (granuleDiff <= 0 ? 2 : 1);
expectedPosition = pagePosition - doublePageSize + granuleDiff;
expectedPosition = Math.max(expectedPosition, START_POSITION);
expectedPosition = Math.min(expectedPosition, END_POSITION - 1);
}
assertGetNextSeekPosition(expectedPosition, targetGranule, input);
}
private void assertGetNextSeekPosition(long expectedPosition, long targetGranule,
FakeExtractorInput input) throws IOException, InterruptedException {
while (true) {
try {
assertEquals(expectedPosition, oggSeeker.getNextSeekPosition(targetGranule, input));
break;
} catch (FakeExtractorInput.SimulatedIOException e) {
// ignored
}
}
}
}

View File

@ -0,0 +1,234 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.testutil.TestUtil;
import junit.framework.TestCase;
import java.io.EOFException;
import java.io.IOException;
import java.util.Random;
/**
* Unit test for {@link DefaultOggSeeker} utility methods.
*/
public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private Random random = new Random(0);
private DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, 100, new FlacReader());
public void testSkipToNextPage() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(4000, random),
new byte[]{'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
assertEquals(4000, extractorInput.getPosition());
}
public void testSkipToNextPageUnbounded() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(4000, random),
new byte[]{'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), true);
skipToNextPage(extractorInput);
assertEquals(4000, extractorInput.getPosition());
}
public void testSkipToNextPageOverlap() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(2046, random),
new byte[]{'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
assertEquals(2046, extractorInput.getPosition());
}
public void testSkipToNextPageOverlapUnbounded() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(2046, random),
new byte[]{'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), true);
skipToNextPage(extractorInput);
assertEquals(2046, extractorInput.getPosition());
}
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays(
new byte[]{'x', 'O', 'g', 'g', 'S'}
), false);
skipToNextPage(extractorInput);
assertEquals(1, extractorInput.getPosition());
}
public void testSkipToNextPageNoMatch() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput(
new byte[]{'g', 'g', 'S', 'O', 'g', 'g'}, false);
try {
skipToNextPage(extractorInput);
fail();
} catch (EOFException e) {
// expected
}
}
private static void skipToNextPage(ExtractorInput extractorInput)
throws IOException, InterruptedException {
while (true) {
try {
DefaultOggSeeker.skipToNextPage(extractorInput);
break;
} catch (FakeExtractorInput.SimulatedIOException e) { /* ignored */ }
}
}
public void testSkipToPageOfGranule() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet), false);
// expect to be granule of the previous page returned as elapsedSamples
skipToPageOfGranule(input, 54000, 40000);
// expect to be at the start of the third page
assertEquals(2 * (30 + (3 * 254)), input.getPosition());
}
public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet), false);
skipToPageOfGranule(input, 40000, 20000);
// expect to be at the start of the second page
assertEquals((30 + (3 * 254)), input.getPosition());
}
public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet), false);
try {
skipToPageOfGranule(input, 10000, 20000);
fail();
} catch (ParserException e) {
// ignored
}
assertEquals(0, input.getPosition());
}
private void skipToPageOfGranule(ExtractorInput input, long granule,
long elapsedSamplesExpected) throws IOException, InterruptedException {
while (true) {
try {
assertEquals(elapsedSamplesExpected, oggSeeker.skipToPageOfGranule(input, granule));
return;
} catch (FakeExtractorInput.SimulatedIOException e) {
input.resetPeekPosition();
}
}
}
public void testReadGranuleOfLastPage() throws IOException, InterruptedException {
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
TestUtil.buildTestData(100, random),
TestData.buildOggHeader(0x00, 20000, 66, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
TestData.buildOggHeader(0x00, 40000, 67, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
TestData.buildOggHeader(0x05, 60000, 68, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random)
), false);
assertReadGranuleOfLastPage(input, 60000);
}
public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException {
FakeExtractorInput input = TestData.createInput(TestUtil.buildTestData(100, random), false);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (EOFException e) {
// ignored
}
}
public void testReadGranuleOfLastPageWithUnboundedLength()
throws IOException, InterruptedException {
FakeExtractorInput input = TestData.createInput(new byte[0], true);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (IllegalArgumentException e) {
// ignored
}
}
private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected)
throws IOException, InterruptedException {
while (true) {
try {
assertEquals(expected, oggSeeker.readGranuleOfLastPage(input));
break;
} catch (FakeExtractorInput.SimulatedIOException e) {
// ignored
}
}
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.testutil.FakeExtractorOutput;
import com.google.android.exoplayer.testutil.FakeTrackOutput;
import com.google.android.exoplayer.testutil.TestUtil;
import com.google.android.exoplayer.util.MimeTypes;
import android.test.InstrumentationTestCase;
/**
* Unit test for {@link OpusReader}.
*/
public final class OggExtractorFileTests extends InstrumentationTestCase {
public static final String OPUS_TEST_FILE = "ogg/bear.opus";
public static final String FLAC_TEST_FILE = "ogg/bear_flac.ogg";
public static final String FLAC_NS_TEST_FILE = "ogg/bear_flac_noseektable.ogg";
public void testOpus() throws Exception {
parseFile(OPUS_TEST_FILE, false, false, false, MimeTypes.AUDIO_OPUS, 2747500, 275);
parseFile(OPUS_TEST_FILE, false, true, false, MimeTypes.AUDIO_OPUS, C.UNSET_TIME_US, 275);
parseFile(OPUS_TEST_FILE, true, false, true, MimeTypes.AUDIO_OPUS, 2747500, 275);
parseFile(OPUS_TEST_FILE, true, true, true, MimeTypes.AUDIO_OPUS, C.UNSET_TIME_US, 275);
}
public void testFlac() throws Exception {
parseFile(FLAC_TEST_FILE, false, false, false, MimeTypes.AUDIO_FLAC, 2741000, 33);
parseFile(FLAC_TEST_FILE, false, true, false, MimeTypes.AUDIO_FLAC, 2741000, 33);
parseFile(FLAC_TEST_FILE, true, false, true, MimeTypes.AUDIO_FLAC, 2741000, 33);
parseFile(FLAC_TEST_FILE, true, true, true, MimeTypes.AUDIO_FLAC, 2741000, 33);
}
public void testFlacNoSeektable() throws Exception {
parseFile(FLAC_NS_TEST_FILE, false, false, false, MimeTypes.AUDIO_FLAC, 2741000, 33);
parseFile(FLAC_NS_TEST_FILE, false, true, false, MimeTypes.AUDIO_FLAC, C.UNSET_TIME_US, 33);
parseFile(FLAC_NS_TEST_FILE, true, false, true, MimeTypes.AUDIO_FLAC, 2741000, 33);
parseFile(FLAC_NS_TEST_FILE, true, true, true, MimeTypes.AUDIO_FLAC, C.UNSET_TIME_US, 33);
}
private void parseFile(String testFile, boolean simulateIOErrors, boolean simulateUnknownLength,
boolean simulatePartialReads, String expectedMimeType, long expectedDuration,
int expectedSampleCount)
throws Exception {
byte[] fileData = TestUtil.getByteArray(getInstrumentation(), testFile);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData)
.setSimulateIOErrors(simulateIOErrors)
.setSimulateUnknownLength(simulateUnknownLength)
.setSimulatePartialReads(simulatePartialReads).build();
OggExtractor extractor = new OggExtractor();
assertTrue(TestUtil.sniffTestData(extractor, input));
input.resetPeekPosition();
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
extractor.init(extractorOutput);
TestUtil.consumeTestData(extractor, input);
assertEquals(1, extractorOutput.trackOutputs.size());
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertNotNull(trackOutput);
Format format = trackOutput.format;
assertNotNull(format);
assertEquals(expectedMimeType, format.sampleMimeType);
assertEquals(48000, format.sampleRate);
assertEquals(2, format.channelCount);
SeekMap seekMap = extractorOutput.seekMap;
assertNotNull(seekMap);
assertEquals(expectedDuration, seekMap.getDurationUs());
assertEquals(expectedDuration != C.UNSET_TIME_US, seekMap.isSeekable());
trackOutput.assertSampleCount(expectedSampleCount);
}
}

View File

@ -16,7 +16,6 @@
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.FakeExtractorInput;
import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException;
import com.google.android.exoplayer.testutil.TestUtil; import com.google.android.exoplayer.testutil.TestUtil;
import junit.framework.TestCase; import junit.framework.TestCase;
@ -40,56 +39,48 @@ public final class OggExtractorTest extends TestCase {
byte[] data = TestUtil.joinByteArrays( byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 1), TestData.buildOggHeader(0x02, 0, 1000, 1),
TestUtil.createByteArray(7), // Laces TestUtil.createByteArray(7), // Laces
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'}); new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'});
assertTrue(sniff(createInput(data))); assertTrue(sniff(data));
} }
public void testSniffFlac() throws Exception { public void testSniffFlac() throws Exception {
byte[] data = TestUtil.joinByteArrays( byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 1), TestData.buildOggHeader(0x02, 0, 1000, 1),
TestUtil.createByteArray(5), // Laces TestUtil.createByteArray(5), // Laces
new byte[]{0x7F, 'F', 'L', 'A', 'C'}); new byte[] {0x7F, 'F', 'L', 'A', 'C'});
assertTrue(sniff(createInput(data))); assertTrue(sniff(data));
} }
public void testSniffFailsOpusFile() throws Exception { public void testSniffFailsOpusFile() throws Exception {
byte[] data = TestUtil.joinByteArrays( byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x00), TestData.buildOggHeader(0x02, 0, 1000, 0x00),
new byte[]{'O', 'p', 'u', 's'}); new byte[] {'O', 'p', 'u', 's'});
assertFalse(sniff(createInput(data))); assertFalse(sniff(data));
} }
public void testSniffFailsInvalidOggHeader() throws Exception { 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(data));
} }
public void testSniffInvalidHeader() throws Exception { public void testSniffInvalidHeader() throws Exception {
byte[] data = TestUtil.joinByteArrays( byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 1), TestData.buildOggHeader(0x02, 0, 1000, 1),
TestUtil.createByteArray(7), // Laces TestUtil.createByteArray(7), // Laces
new byte[]{0x7F, 'X', 'o', 'r', 'b', 'i', 's'}); new byte[] {0x7F, 'X', 'o', 'r', 'b', 'i', 's'});
assertFalse(sniff(createInput(data))); assertFalse(sniff(data));
} }
public void testSniffFailsEOF() throws Exception { public void testSniffFailsEOF() throws Exception {
byte[] data = TestData.buildOggHeader(0x02, 0, 1000, 0x00); byte[] data = TestData.buildOggHeader(0x02, 0, 1000, 0x00);
assertFalse(sniff(createInput(data))); assertFalse(sniff(data));
} }
private static FakeExtractorInput createInput(byte[] data) { private boolean sniff(byte[] data) throws InterruptedException, IOException {
return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data)
.setSimulateUnknownLength(true).setSimulatePartialReads(true).build(); .setSimulateIOErrors(true).setSimulateUnknownLength(true).setSimulatePartialReads(true)
} .build();
return TestUtil.sniffTestData(extractor, input);
private boolean sniff(FakeExtractorInput input) throws InterruptedException, IOException {
while (true) {
try {
return extractor.sniff(input);
} catch (SimulatedIOException e) {
// Ignore.
}
}
} }
} }

View File

@ -0,0 +1,248 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.testutil.TestUtil;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
/**
* Unit test for {@link OggPacket}.
*/
public final class OggPacketTest extends InstrumentationTestCase {
private static final String TEST_FILE = "ogg/bear.opus";
private Random random;
private OggPacket oggPacket;
@Override
public void setUp() throws Exception {
super.setUp();
random = new Random(0);
oggPacket = new OggPacket();
}
public void testReadPacketsWithEmptyPage() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(8, random);
byte[] secondPacket = TestUtil.buildTestData(272, random);
byte[] thirdPacket = TestUtil.buildTestData(256, random);
byte[] fourthPacket = TestUtil.buildTestData(271, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
// First page with a single packet.
TestData.buildOggHeader(0x02, 0, 1000, 0x01),
TestUtil.createByteArray(0x08), // Laces
firstPacket,
// Second page with a single packet.
TestData.buildOggHeader(0x00, 16, 1001, 0x02),
TestUtil.createByteArray(0xFF, 0x11), // Laces
secondPacket,
// Third page with zero packets.
TestData.buildOggHeader(0x00, 16, 1002, 0x00),
// Fourth page with two packets.
TestData.buildOggHeader(0x04, 128, 1003, 0x04),
TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces
thirdPacket,
fourthPacket), true);
assertReadPacket(input, firstPacket);
assertTrue((oggPacket.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggPacket.getPageHeader().type & 0x04) == 0x04);
assertEquals(0x02, oggPacket.getPageHeader().type);
assertEquals(27 + 1, oggPacket.getPageHeader().headerSize);
assertEquals(8, oggPacket.getPageHeader().bodySize);
assertEquals(0x00, oggPacket.getPageHeader().revision);
assertEquals(1, oggPacket.getPageHeader().pageSegmentCount);
assertEquals(1000, oggPacket.getPageHeader().pageSequenceNumber);
assertEquals(4096, oggPacket.getPageHeader().streamSerialNumber);
assertEquals(0, oggPacket.getPageHeader().granulePosition);
assertReadPacket(input, secondPacket);
assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggPacket.getPageHeader().type & 0x04) == 0x04);
assertEquals(0, oggPacket.getPageHeader().type);
assertEquals(27 + 2, oggPacket.getPageHeader().headerSize);
assertEquals(255 + 17, oggPacket.getPageHeader().bodySize);
assertEquals(2, oggPacket.getPageHeader().pageSegmentCount);
assertEquals(1001, oggPacket.getPageHeader().pageSequenceNumber);
assertEquals(16, oggPacket.getPageHeader().granulePosition);
assertReadPacket(input, thirdPacket);
assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02);
assertTrue((oggPacket.getPageHeader().type & 0x04) == 0x04);
assertEquals(4, oggPacket.getPageHeader().type);
assertEquals(27 + 4, oggPacket.getPageHeader().headerSize);
assertEquals(255 + 1 + 255 + 16, oggPacket.getPageHeader().bodySize);
assertEquals(4, oggPacket.getPageHeader().pageSegmentCount);
// Page 1002 is empty, so current page is 1003.
assertEquals(1003, oggPacket.getPageHeader().pageSequenceNumber);
assertEquals(128, oggPacket.getPageHeader().granulePosition);
assertReadPacket(input, fourthPacket);
assertReadEof(input);
}
public void testReadPacketWithZeroSizeTerminator() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(255, random);
byte[] secondPacket = TestUtil.buildTestData(8, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
TestData.buildOggHeader(0x06, 0, 1000, 0x04),
TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces.
firstPacket,
secondPacket), true);
assertReadPacket(input, firstPacket);
assertReadPacket(input, secondPacket);
assertReadEof(input);
}
public void testReadContinuedPacketOverTwoPages() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(518);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
// First page.
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(0xFF, 0xFF), // Laces.
Arrays.copyOf(firstPacket, 510),
// Second page (continued packet).
TestData.buildOggHeader(0x05, 10, 1001, 0x01),
TestUtil.createByteArray(0x08), // Laces.
Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true);
assertReadPacket(input, firstPacket);
assertTrue((oggPacket.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02);
assertEquals(1001, oggPacket.getPageHeader().pageSequenceNumber);
assertReadEof(input);
}
public void testReadContinuedPacketOverFourPages() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(1028);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
// First page.
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(0xFF, 0xFF), // Laces.
Arrays.copyOf(firstPacket, 510),
// Second page (continued packet).
TestData.buildOggHeader(0x01, 10, 1001, 0x01),
TestUtil.createByteArray(0xFF), // Laces.
Arrays.copyOfRange(firstPacket, 510, 510 + 255),
// Third page (continued packet).
TestData.buildOggHeader(0x01, 10, 1002, 0x01),
TestUtil.createByteArray(0xFF), // Laces.
Arrays.copyOfRange(firstPacket, 510 + 255, 510 + 255 + 255),
// Fourth page (continued packet).
TestData.buildOggHeader(0x05, 10, 1003, 0x01),
TestUtil.createByteArray(0x08), // Laces.
Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true);
assertReadPacket(input, firstPacket);
assertTrue((oggPacket.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02);
assertEquals(1003, oggPacket.getPageHeader().pageSequenceNumber);
assertReadEof(input);
}
public void testReadDiscardContinuedPacketAtStart() throws Exception {
byte[] pageBody = TestUtil.buildTestData(256 + 8);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
// Page with a continued packet at start.
TestData.buildOggHeader(0x01, 10, 1001, 0x03),
TestUtil.createByteArray(255, 1, 8), // Laces.
pageBody), true);
// Expect the first partial packet to be discarded.
assertReadPacket(input, Arrays.copyOfRange(pageBody, 256, 256 + 8));
assertReadEof(input);
}
public void testReadZeroSizedPacketsAtEndOfStream() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(8, random);
byte[] secondPacket = TestUtil.buildTestData(8, random);
byte[] thirdPacket = TestUtil.buildTestData(8, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x01),
TestUtil.createByteArray(0x08), // Laces.
firstPacket,
TestData.buildOggHeader(0x04, 0, 1001, 0x03),
TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces.
secondPacket,
TestData.buildOggHeader(0x04, 0, 1002, 0x03),
TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces.
thirdPacket), true);
assertReadPacket(input, firstPacket);
assertReadPacket(input, secondPacket);
assertReadPacket(input, thirdPacket);
assertReadEof(input);
}
public void testParseRealFile() throws IOException, InterruptedException {
byte[] data = TestUtil.getByteArray(getInstrumentation(), TEST_FILE);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
int packetCounter = 0;
while (readPacket(input)) {
packetCounter++;
}
assertEquals(277, packetCounter);
}
private void assertReadPacket(FakeExtractorInput extractorInput, byte[] expected)
throws IOException, InterruptedException {
assertTrue(readPacket(extractorInput));
ParsableByteArray payload = oggPacket.getPayload();
MoreAsserts.assertEquals(expected, Arrays.copyOf(payload.data, payload.limit()));
}
private void assertReadEof(FakeExtractorInput extractorInput)
throws IOException, InterruptedException {
assertFalse(readPacket(extractorInput));
}
private boolean readPacket(FakeExtractorInput input)
throws InterruptedException, IOException {
while (true) {
try {
return oggPacket.populate(input);
} catch (FakeExtractorInput.SimulatedIOException e) {
// Ignore.
}
}
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException;
import com.google.android.exoplayer.testutil.TestUtil;
import junit.framework.TestCase;
import java.io.IOException;
/**
* Unit test for {@link OggPageHeader}.
*/
public final class OggPageHeaderTest extends TestCase {
public void testPopulatePageHeader() throws IOException, InterruptedException {
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 123456, 4, 2),
TestUtil.createByteArray(2, 2)
), true);
OggPageHeader header = new OggPageHeader();
populatePageHeader(input, header, false);
assertEquals(0x01, header.type);
assertEquals(27 + 2, header.headerSize);
assertEquals(4, header.bodySize);
assertEquals(2, header.pageSegmentCount);
assertEquals(123456, header.granulePosition);
assertEquals(4, header.pageSequenceNumber);
assertEquals(0x1000, header.streamSerialNumber);
assertEquals(0x100000, header.pageChecksum);
assertEquals(0, header.revision);
}
public void testPopulatePageHeaderQuiteOnExceptionLessThan27Bytes()
throws IOException, InterruptedException {
FakeExtractorInput input = TestData.createInput(TestUtil.createByteArray(2, 2), false);
OggPageHeader header = new OggPageHeader();
assertFalse(populatePageHeader(input, header, true));
}
public void testPopulatePageHeaderQuiteOnExceptionNotOgg()
throws IOException, InterruptedException {
byte[] headerBytes = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 123456, 4, 2),
TestUtil.createByteArray(2, 2)
);
// change from 'O' to 'o'
headerBytes[0] = 'o';
FakeExtractorInput input = TestData.createInput(headerBytes, false);
OggPageHeader header = new OggPageHeader();
assertFalse(populatePageHeader(input, header, true));
}
public void testPopulatePageHeaderQuiteOnExceptionWrongRevision()
throws IOException, InterruptedException {
byte[] headerBytes = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 123456, 4, 2),
TestUtil.createByteArray(2, 2)
);
// change revision from 0 to 1
headerBytes[4] = 0x01;
FakeExtractorInput input = TestData.createInput(headerBytes, false);
OggPageHeader header = new OggPageHeader();
assertFalse(populatePageHeader(input, header, true));
}
private boolean populatePageHeader(FakeExtractorInput input, OggPageHeader header,
boolean quite) throws IOException, InterruptedException {
while (true) {
try {
return header.populate(input, quite);
} catch (SimulatedIOException e) {
// ignored
}
}
}
}

View File

@ -1,365 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.testutil.TestUtil;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.test.MoreAsserts;
import junit.framework.TestCase;
import java.io.EOFException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
/**
* Unit test for {@link OggParser}.
*/
public final class OggParserTest extends TestCase {
private Random random;
private OggParser oggParser;
private ParsableByteArray scratch;
@Override
public void setUp() throws Exception {
super.setUp();
random = new Random(0);
oggParser = new OggParser();
scratch = new ParsableByteArray(new byte[255 * 255], 0);
}
public void testReadPacketsWithEmptyPage() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(8, random);
byte[] secondPacket = TestUtil.buildTestData(272, random);
byte[] thirdPacket = TestUtil.buildTestData(256, random);
byte[] fourthPacket = TestUtil.buildTestData(271, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
// First page with a single packet.
TestData.buildOggHeader(0x02, 0, 1000, 0x01),
TestUtil.createByteArray(0x08), // Laces
firstPacket,
// Second page with a single packet.
TestData.buildOggHeader(0x00, 16, 1001, 0x02),
TestUtil.createByteArray(0xFF, 0x11), // Laces
secondPacket,
// Third page with zero packets.
TestData.buildOggHeader(0x00, 16, 1002, 0x00),
// Fourth page with two packets.
TestData.buildOggHeader(0x04, 128, 1003, 0x04),
TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces
thirdPacket,
fourthPacket), true);
assertReadPacket(input, firstPacket);
assertTrue((oggParser.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggParser.getPageHeader().type & 0x04) == 0x04);
assertEquals(0x02, oggParser.getPageHeader().type);
assertEquals(27 + 1, oggParser.getPageHeader().headerSize);
assertEquals(8, oggParser.getPageHeader().bodySize);
assertEquals(0x00, oggParser.getPageHeader().revision);
assertEquals(1, oggParser.getPageHeader().pageSegmentCount);
assertEquals(1000, oggParser.getPageHeader().pageSequenceNumber);
assertEquals(4096, oggParser.getPageHeader().streamSerialNumber);
assertEquals(0, oggParser.getPageHeader().granulePosition);
assertReadPacket(input, secondPacket);
assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggParser.getPageHeader().type & 0x04) == 0x04);
assertEquals(0, oggParser.getPageHeader().type);
assertEquals(27 + 2, oggParser.getPageHeader().headerSize);
assertEquals(255 + 17, oggParser.getPageHeader().bodySize);
assertEquals(2, oggParser.getPageHeader().pageSegmentCount);
assertEquals(1001, oggParser.getPageHeader().pageSequenceNumber);
assertEquals(16, oggParser.getPageHeader().granulePosition);
assertReadPacket(input, thirdPacket);
assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04);
assertEquals(4, oggParser.getPageHeader().type);
assertEquals(27 + 4, oggParser.getPageHeader().headerSize);
assertEquals(255 + 1 + 255 + 16, oggParser.getPageHeader().bodySize);
assertEquals(4, oggParser.getPageHeader().pageSegmentCount);
// Page 1002 is empty, so current page is 1003.
assertEquals(1003, oggParser.getPageHeader().pageSequenceNumber);
assertEquals(128, oggParser.getPageHeader().granulePosition);
assertReadPacket(input, fourthPacket);
assertReadEof(input);
}
public void testReadPacketWithZeroSizeTerminator() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(255, random);
byte[] secondPacket = TestUtil.buildTestData(8, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
TestData.buildOggHeader(0x06, 0, 1000, 0x04),
TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces.
firstPacket,
secondPacket), true);
assertReadPacket(input, firstPacket);
assertReadPacket(input, secondPacket);
assertReadEof(input);
}
public void testReadContinuedPacketOverTwoPages() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(518);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
// First page.
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(0xFF, 0xFF), // Laces.
Arrays.copyOf(firstPacket, 510),
// Second page (continued packet).
TestData.buildOggHeader(0x05, 10, 1001, 0x01),
TestUtil.createByteArray(0x08), // Laces.
Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true);
assertReadPacket(input, firstPacket);
assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertEquals(1001, oggParser.getPageHeader().pageSequenceNumber);
assertReadEof(input);
}
public void testReadContinuedPacketOverFourPages() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(1028);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
// First page.
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(0xFF, 0xFF), // Laces.
Arrays.copyOf(firstPacket, 510),
// Second page (continued packet).
TestData.buildOggHeader(0x01, 10, 1001, 0x01),
TestUtil.createByteArray(0xFF), // Laces.
Arrays.copyOfRange(firstPacket, 510, 510 + 255),
// Third page (continued packet).
TestData.buildOggHeader(0x01, 10, 1002, 0x01),
TestUtil.createByteArray(0xFF), // Laces.
Arrays.copyOfRange(firstPacket, 510 + 255, 510 + 255 + 255),
// Fourth page (continued packet).
TestData.buildOggHeader(0x05, 10, 1003, 0x01),
TestUtil.createByteArray(0x08), // Laces.
Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true);
assertReadPacket(input, firstPacket);
assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertEquals(1003, oggParser.getPageHeader().pageSequenceNumber);
assertReadEof(input);
}
public void testReadDiscardContinuedPacketAtStart() throws Exception {
byte[] pageBody = TestUtil.buildTestData(256 + 8);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
// Page with a continued packet at start.
TestData.buildOggHeader(0x01, 10, 1001, 0x03),
TestUtil.createByteArray(255, 1, 8), // Laces.
pageBody), true);
// Expect the first partial packet to be discarded.
assertReadPacket(input, Arrays.copyOfRange(pageBody, 256, 256 + 8));
assertReadEof(input);
}
public void testReadZeroSizedPacketsAtEndOfStream() throws Exception {
byte[] firstPacket = TestUtil.buildTestData(8, random);
byte[] secondPacket = TestUtil.buildTestData(8, random);
byte[] thirdPacket = TestUtil.buildTestData(8, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x01),
TestUtil.createByteArray(0x08), // Laces.
firstPacket,
TestData.buildOggHeader(0x04, 0, 1001, 0x03),
TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces.
secondPacket,
TestData.buildOggHeader(0x04, 0, 1002, 0x03),
TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces.
thirdPacket), true);
assertReadPacket(input, firstPacket);
assertReadPacket(input, secondPacket);
assertReadPacket(input, thirdPacket);
assertReadEof(input);
}
public void testSkipToPageOfGranule() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet), false);
// expect to be granule of the previous page returned as elapsedSamples
skipToPageOfGranule(input, 54000, 40000);
// expect to be at the start of the third page
assertEquals(2 * (30 + (3 * 254)), input.getPosition());
}
public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet), false);
skipToPageOfGranule(input, 40000, 20000);
// expect to be at the start of the second page
assertEquals((30 + (3 * 254)), input.getPosition());
}
public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
FakeExtractorInput input = TestData.createInput(
TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet), false);
try {
skipToPageOfGranule(input, 10000, 20000);
fail();
} catch (ParserException e) {
// ignored
}
assertEquals(0, input.getPosition());
}
private void skipToPageOfGranule(ExtractorInput input, long granule,
long elapsedSamplesExpected) throws IOException, InterruptedException {
while (true) {
try {
assertEquals(elapsedSamplesExpected, oggParser.skipToPageOfGranule(input, granule));
return;
} catch (FakeExtractorInput.SimulatedIOException e) {
input.resetPeekPosition();
}
}
}
public void testReadGranuleOfLastPage() throws IOException, InterruptedException {
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
TestUtil.buildTestData(100, random),
TestData.buildOggHeader(0x00, 20000, 66, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
TestData.buildOggHeader(0x00, 40000, 67, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
TestData.buildOggHeader(0x05, 60000, 68, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random)
), false);
assertReadGranuleOfLastPage(input, 60000);
}
public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException {
FakeExtractorInput input = TestData.createInput(TestUtil.buildTestData(100, random), false);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (EOFException e) {
// ignored
}
}
public void testReadGranuleOfLastPageWithUnboundedLength()
throws IOException, InterruptedException {
FakeExtractorInput input = TestData.createInput(new byte[0], true);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (IllegalArgumentException e) {
// ignored
}
}
private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected)
throws IOException, InterruptedException {
while (true) {
try {
assertEquals(expected, oggParser.readGranuleOfLastPage(input));
break;
} catch (FakeExtractorInput.SimulatedIOException e) {
// ignored
}
}
}
private void assertReadPacket(FakeExtractorInput extractorInput, byte[] expected)
throws IOException, InterruptedException {
scratch.reset();
assertTrue(readPacket(extractorInput, scratch));
MoreAsserts.assertEquals(expected, Arrays.copyOf(scratch.data, scratch.limit()));
}
private void assertReadEof(FakeExtractorInput extractorInput)
throws IOException, InterruptedException {
scratch.reset();
assertFalse(readPacket(extractorInput, scratch));
}
private boolean readPacket(FakeExtractorInput input, ParsableByteArray scratch)
throws InterruptedException, IOException {
while (true) {
try {
return oggParser.readPacket(input, scratch);
} catch (FakeExtractorInput.SimulatedIOException e) {
// Ignore.
}
}
}
}

View File

@ -1,132 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.testutil.TestUtil;
import junit.framework.TestCase;
import java.io.IOException;
/**
* Unit test for {@link OggSeeker}.
*/
public final class OggSeekerTest extends TestCase {
private OggSeeker oggSeeker;
@Override
public void setUp() throws Exception {
super.setUp();
oggSeeker = new OggSeeker();
oggSeeker.setup(1, 1);
}
public void testSetupUnboundAudioLength() {
try {
new OggSeeker().setup(C.LENGTH_UNBOUNDED, 1000);
fail();
} catch (IllegalArgumentException e) {
// ignored
}
}
public void testSetupZeroOrNegativeTotalSamples() {
try {
new OggSeeker().setup(1000, 0);
fail();
} catch (IllegalArgumentException e) {
// ignored
}
try {
new OggSeeker().setup(1000, -1000);
fail();
} catch (IllegalArgumentException e) {
// ignored
}
}
public void testGetNextSeekPositionSetupNotCalled() throws IOException, InterruptedException {
try {
new OggSeeker().getNextSeekPosition(1000, TestData.createInput(new byte[0], false));
fail();
} catch (IllegalStateException e) {
// ignored
}
}
public void testGetNextSeekPositionMatch() throws IOException, InterruptedException {
long targetGranule = 100000;
long headerGranule = 52001;
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
TestData.buildOggHeader(0x00, headerGranule, 22, 2),
TestUtil.createByteArray(54, 55) // laces
), false);
long expectedPosition = -1;
assertGetNextSeekPosition(expectedPosition, targetGranule, input);
}
public void testGetNextSeekPositionTooHigh() throws IOException, InterruptedException {
long targetGranule = 100000;
long headerGranule = 200000;
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
TestData.buildOggHeader(0x00, headerGranule, 22, 2),
TestUtil.createByteArray(54, 55) // laces
), false);
long doublePageSize = 2 * (input.getLength() + 54 + 55);
long expectedPosition = -doublePageSize + (targetGranule - headerGranule);
assertGetNextSeekPosition(expectedPosition, targetGranule, input);
}
public void testGetNextSeekPositionTooHighDistanceLower48000()
throws IOException, InterruptedException {
long targetGranule = 199999;
long headerGranule = 200000;
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
TestData.buildOggHeader(0x00, headerGranule, 22, 2),
TestUtil.createByteArray(54, 55) // laces
), false);
long doublePageSize = 2 * (input.getLength() + 54 + 55);
long expectedPosition = -doublePageSize - 1;
assertGetNextSeekPosition(expectedPosition, targetGranule, input);
}
public void testGetNextSeekPositionTooLow() throws IOException, InterruptedException {
long headerGranule = 200000;
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
TestData.buildOggHeader(0x00, headerGranule, 22, 2),
TestUtil.createByteArray(54, 55) // laces
), false);
long targetGranule = 300000;
long expectedPosition = -(27 + 2 + 54 + 55) + (targetGranule - headerGranule);
assertGetNextSeekPosition(expectedPosition, targetGranule, input);
}
private void assertGetNextSeekPosition(long expectedPosition, long targetGranule,
FakeExtractorInput input) throws IOException, InterruptedException {
while (true) {
try {
assertEquals(expectedPosition, oggSeeker.getNextSeekPosition(targetGranule, input));
break;
} catch (FakeExtractorInput.SimulatedIOException e) {
// ignored
}
}
}
}

View File

@ -1,190 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException;
import com.google.android.exoplayer.testutil.TestUtil;
import com.google.android.exoplayer.util.ParsableByteArray;
import junit.framework.TestCase;
import java.io.EOFException;
import java.io.IOException;
import java.util.Random;
/**
* Unit test for {@link OggUtil}.
*/
public final class OggUtilTest extends TestCase {
private Random random = new Random(0);
public void testReadBits() throws Exception {
assertEquals(0, OggUtil.readBits((byte) 0x00, 2, 2));
assertEquals(1, OggUtil.readBits((byte) 0x02, 1, 1));
assertEquals(15, OggUtil.readBits((byte) 0xF0, 4, 4));
assertEquals(1, OggUtil.readBits((byte) 0x80, 1, 7));
}
public void testPopulatePageHeader() throws IOException, InterruptedException {
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 123456, 4, 2),
TestUtil.createByteArray(2, 2)
), true);
OggUtil.PageHeader header = new OggUtil.PageHeader();
ParsableByteArray byteArray = new ParsableByteArray(27 + 2);
populatePageHeader(input, header, byteArray, false);
assertEquals(0x01, header.type);
assertEquals(27 + 2, header.headerSize);
assertEquals(4, header.bodySize);
assertEquals(2, header.pageSegmentCount);
assertEquals(123456, header.granulePosition);
assertEquals(4, header.pageSequenceNumber);
assertEquals(0x1000, header.streamSerialNumber);
assertEquals(0x100000, header.pageChecksum);
assertEquals(0, header.revision);
}
public void testPopulatePageHeaderQuiteOnExceptionLessThan27Bytes()
throws IOException, InterruptedException {
FakeExtractorInput input = TestData.createInput(TestUtil.createByteArray(2, 2), false);
OggUtil.PageHeader header = new OggUtil.PageHeader();
ParsableByteArray byteArray = new ParsableByteArray(27 + 2);
assertFalse(populatePageHeader(input, header, byteArray, true));
}
public void testPopulatePageHeaderQuiteOnExceptionNotOgg()
throws IOException, InterruptedException {
byte[] headerBytes = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 123456, 4, 2),
TestUtil.createByteArray(2, 2)
);
// change from 'O' to 'o'
headerBytes[0] = 'o';
FakeExtractorInput input = TestData.createInput(headerBytes, false);
OggUtil.PageHeader header = new OggUtil.PageHeader();
ParsableByteArray byteArray = new ParsableByteArray(27 + 2);
assertFalse(populatePageHeader(input, header, byteArray, true));
}
public void testPopulatePageHeaderQuiteOnExceptionWrongRevision()
throws IOException, InterruptedException {
byte[] headerBytes = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x01, 123456, 4, 2),
TestUtil.createByteArray(2, 2)
);
// change revision from 0 to 1
headerBytes[4] = 0x01;
FakeExtractorInput input = TestData.createInput(headerBytes, false);
OggUtil.PageHeader header = new OggUtil.PageHeader();
ParsableByteArray byteArray = new ParsableByteArray(27 + 2);
assertFalse(populatePageHeader(input, header, byteArray, true));
}
private boolean populatePageHeader(FakeExtractorInput input, OggUtil.PageHeader header,
ParsableByteArray byteArray, boolean quite) throws IOException, InterruptedException {
while (true) {
try {
return OggUtil.populatePageHeader(input, header, byteArray, quite);
} catch (SimulatedIOException e) {
// ignored
}
}
}
public void testSkipToNextPage() throws Exception {
FakeExtractorInput extractorInput = createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(4000, random),
new byte[]{'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
assertEquals(4000, extractorInput.getPosition());
}
public void testSkipToNextPageUnbounded() throws Exception {
FakeExtractorInput extractorInput = createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(4000, random),
new byte[]{'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), true);
skipToNextPage(extractorInput);
assertEquals(4000, extractorInput.getPosition());
}
public void testSkipToNextPageOverlap() throws Exception {
FakeExtractorInput extractorInput = createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(2046, random),
new byte[]{'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
assertEquals(2046, extractorInput.getPosition());
}
public void testSkipToNextPageOverlapUnbounded() throws Exception {
FakeExtractorInput extractorInput = createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(2046, random),
new byte[]{'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), true);
skipToNextPage(extractorInput);
assertEquals(2046, extractorInput.getPosition());
}
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
FakeExtractorInput extractorInput = createInput(
TestUtil.joinByteArrays(
new byte[]{'x', 'O', 'g', 'g', 'S'}
), false);
skipToNextPage(extractorInput);
assertEquals(1, extractorInput.getPosition());
}
public void testSkipToNextPageNoMatch() throws Exception {
FakeExtractorInput extractorInput = createInput(new byte[]{'g', 'g', 'S', 'O', 'g', 'g'},
false);
try {
skipToNextPage(extractorInput);
fail();
} catch (EOFException e) {
// expected
}
}
private static void skipToNextPage(ExtractorInput extractorInput)
throws IOException, InterruptedException {
while (true) {
try {
OggUtil.skipToNextPage(extractorInput);
break;
} catch (SimulatedIOException e) { /* ignored */ }
}
}
private static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) {
return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true)
.setSimulateUnknownLength(simulateUnknownLength).setSimulatePartialReads(true).build();
}
}

View File

@ -1,104 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.testutil.FakeExtractorOutput;
import com.google.android.exoplayer.testutil.FakeTrackOutput;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.DefaultDataSource;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.content.Context;
import android.net.Uri;
import android.test.InstrumentationTestCase;
import java.io.IOException;
/**
* Unit test for {@link OpusReader}.
*/
public final class OpusReaderTest extends InstrumentationTestCase {
private static final String TEST_FILE = "asset:///ogg/bear.opus";
private OggExtractor extractor;
private FakeExtractorOutput extractorOutput;
private DefaultExtractorInput extractorInput;
@Override
public void setUp() throws Exception {
super.setUp();
Context context = getInstrumentation().getContext();
DataSource dataSource = new DefaultDataSource(context, null, Util
.getUserAgent(context, "ExoPlayerExtFlacTest"), false);
Uri uri = Uri.parse(TEST_FILE);
long length = dataSource.open(new DataSpec(uri, 0, C.LENGTH_UNBOUNDED, null));
extractorInput = new DefaultExtractorInput(dataSource, 0, length);
extractor = new OggExtractor();
assertTrue(extractor.sniff(extractorInput));
extractorInput.resetPeekPosition();
extractorOutput = new FakeExtractorOutput();
extractor.init(extractorOutput);
}
public void testSniffOpus() throws Exception {
// Do nothing. All assertions are in setUp()
}
public void testParseHeader() throws Exception {
FakeTrackOutput trackOutput = parseFile(false);
trackOutput.assertSampleCount(0);
Format format = trackOutput.format;
assertNotNull(format);
assertEquals(MimeTypes.AUDIO_OPUS, format.sampleMimeType);
assertEquals(48000, format.sampleRate);
assertEquals(2, format.channelCount);
}
public void testParseWholeFile() throws Exception {
FakeTrackOutput trackOutput = parseFile(true);
trackOutput.assertSampleCount(275);
}
private FakeTrackOutput parseFile(boolean parseAll) throws IOException, InterruptedException {
PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE;
do {
readResult = extractor.read(extractorInput, seekPositionHolder);
if (readResult == Extractor.RESULT_SEEK) {
fail("There should be no seek");
}
} while (readResult != Extractor.RESULT_END_OF_INPUT && parseAll);
assertEquals(1, extractorOutput.trackOutputs.size());
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertNotNull(trackOutput);
return trackOutput;
}
}

View File

@ -30,13 +30,18 @@ import java.io.IOException;
public final class VorbisReaderTest extends TestCase { public final class VorbisReaderTest extends TestCase {
private VorbisReader extractor; private VorbisReader extractor;
private ParsableByteArray scratch;
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
extractor = new VorbisReader(); extractor = new VorbisReader();
scratch = new ParsableByteArray(new byte[255 * 255], 0); }
public void testReadBits() throws Exception {
assertEquals(0, VorbisReader.readBits((byte) 0x00, 2, 2));
assertEquals(1, VorbisReader.readBits((byte) 0x02, 1, 1));
assertEquals(15, VorbisReader.readBits((byte) 0xF0, 4, 4));
assertEquals(1, VorbisReader.readBits((byte) 0x80, 1, 7));
} }
public void testAppendNumberOfSamples() throws Exception { public void testAppendNumberOfSamples() throws Exception {
@ -90,9 +95,16 @@ public final class VorbisReaderTest extends TestCase {
private VorbisSetup readSetupHeaders(FakeExtractorInput input) private VorbisSetup readSetupHeaders(FakeExtractorInput input)
throws IOException, InterruptedException { throws IOException, InterruptedException {
OggPacket oggPacket = new OggPacket();
while (true) { while (true) {
try { try {
return extractor.readSetupHeaders(input, scratch); if (!oggPacket.populate(input)) {
fail();
}
VorbisSetup vorbisSetup = extractor.readSetupHeaders(oggPacket.getPayload());
if (vorbisSetup != null) {
return vorbisSetup;
}
} catch (SimulatedIOException e) { } catch (SimulatedIOException e) {
// Ignore. // Ignore.
} }

View File

@ -17,12 +17,12 @@ package com.google.android.exoplayer.testutil;
import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.PositionHolder; import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.io.IOException; import java.io.IOException;
@ -36,17 +36,41 @@ public class TestUtil {
private TestUtil() {} private TestUtil() {}
public static boolean sniffTestData(Extractor extractor, byte[] data)
throws IOException, InterruptedException {
return sniffTestData(extractor, newExtractorInput(data));
}
public static boolean sniffTestData(Extractor extractor, FakeExtractorInput input)
throws IOException, InterruptedException {
while (true) {
try {
return extractor.sniff(input);
} catch (SimulatedIOException e) {
// Ignore.
}
}
}
public static void consumeTestData(Extractor extractor, byte[] data) public static void consumeTestData(Extractor extractor, byte[] data)
throws IOException, InterruptedException { throws IOException, InterruptedException {
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); consumeTestData(extractor, newExtractorInput(data));
}
public static void consumeTestData(Extractor extractor, FakeExtractorInput input)
throws IOException, InterruptedException {
PositionHolder seekPositionHolder = new PositionHolder(); PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE; int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) { while (readResult != Extractor.RESULT_END_OF_INPUT) {
readResult = extractor.read(input, seekPositionHolder); try {
if (readResult == Extractor.RESULT_SEEK) { readResult = extractor.read(input, seekPositionHolder);
long seekPosition = seekPositionHolder.position; if (readResult == Extractor.RESULT_SEEK) {
Assertions.checkState(0 < seekPosition && seekPosition <= Integer.MAX_VALUE); long seekPosition = seekPositionHolder.position;
input.setPosition((int) seekPosition); Assertions.checkState(0 <= seekPosition && seekPosition <= Integer.MAX_VALUE);
input.setPosition((int) seekPosition);
}
} catch (SimulatedIOException e) {
// Ignore.
} }
} }
} }
@ -107,4 +131,8 @@ public class TestUtil {
return Util.toByteArray(is); return Util.toByteArray(is);
} }
private static FakeExtractorInput newExtractorInput(byte[] data) {
return new FakeExtractorInput.Builder().setData(data).build();
}
} }

View File

@ -43,6 +43,11 @@ public interface C {
*/ */
long MICROS_PER_SECOND = 1000000L; long MICROS_PER_SECOND = 1000000L;
/**
* The number of nanoseconds in one second.
*/
public static final long NANOS_PER_SECOND = 1000000000L;
/** /**
* Represents an unbounded length of data. * Represents an unbounded length of data.
*/ */

View File

@ -0,0 +1,297 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.util.Assertions;
import java.io.EOFException;
import java.io.IOException;
/**
* Used to seek in an Ogg stream.
*/
/* package */ final class DefaultOggSeeker implements OggSeeker {
private static final int STATE_SEEK_TO_END = 0;
private static final int STATE_READ_LAST_PAGE = 1;
private static final int STATE_SEEK = 2;
private static final int STATE_IDLE = 3;
//@VisibleForTesting
public static final int MATCH_RANGE = 72000;
private static final int DEFAULT_OFFSET = 30000;
private final OggPageHeader pageHeader = new OggPageHeader();
private final long startPosition;
private final long endPosition;
private final StreamReader streamReader;
private int state;
private long totalGranules;
private volatile long queriedGranule;
private long positionBeforeSeekToEnd;
private long targetGranule;
private long elapsedSamples;
public static DefaultOggSeeker createOggSeekerForTesting(long startPosition, long endPosition,
long totalGranules) {
Assertions.checkArgument(totalGranules > 0);
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(startPosition, endPosition, null);
oggSeeker.totalGranules = totalGranules;
return oggSeeker;
}
/**
* Constructs an OggSeeker.
* @param startPosition Start position of the payload.
* @param endPosition End position of the payload.
* @param streamReader StreamReader instance which owns this OggSeeker
*/
public DefaultOggSeeker(long startPosition, long endPosition, StreamReader streamReader) {
this.streamReader = streamReader;
Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition);
this.startPosition = startPosition;
this.endPosition = endPosition;
this.queriedGranule = 0;
this.state = STATE_SEEK_TO_END;
}
@Override
public long read(ExtractorInput input) throws IOException, InterruptedException {
switch (state) {
case STATE_IDLE:
return -1;
case STATE_SEEK_TO_END:
positionBeforeSeekToEnd = input.getPosition();
state = STATE_READ_LAST_PAGE;
// seek to the end just before the last page of stream to get the duration
long lastPagePosition = input.getLength() - OggPageHeader.MAX_PAGE_SIZE;
if (lastPagePosition > 0) {
return Math.max(lastPagePosition, 0);
}
// fall through
case STATE_READ_LAST_PAGE:
totalGranules = readGranuleOfLastPage(input);
state = STATE_IDLE;
return positionBeforeSeekToEnd;
case STATE_SEEK:
long currentGranule;
if (targetGranule == 0) {
currentGranule = 0;
} else {
long position = getNextSeekPosition(targetGranule, input);
if (position != -1) {
return position;
} else {
currentGranule = skipToPageOfGranule(input, targetGranule);
}
}
state = STATE_IDLE;
return -currentGranule - 2;
default:
// Never happens.
throw new IllegalStateException();
}
}
@Override
public long startSeek() {
Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK);
targetGranule = queriedGranule;
state = STATE_SEEK;
return targetGranule;
}
@Override
public OggSeekMap createSeekMap() {
return totalGranules != 0 ? new OggSeekMap() : null;
}
/**
* Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput}
* has to seek and then be passed for another call until -1 is return. If -1 is returned the
* input is at a position which is before the start of the page before the target page and at
* which it is sensible to just skip pages to the target granule and pre-roll instead of doing
* another seek request.
*
* @param targetGranule the target granule position to seek to.
* @param input the {@link ExtractorInput} to read from.
* @return the position to seek the {@link ExtractorInput} to for a next call or -1 if it's close
* enough to skip to the target page.
* @throws IOException thrown if reading from the input fails.
* @throws InterruptedException thrown if interrupted while reading from the input.
*/
//@VisibleForTesting
public long getNextSeekPosition(long targetGranule, ExtractorInput input)
throws IOException, InterruptedException {
long previousPosition = input.getPosition();
skipToNextPage(input);
pageHeader.populate(input, false);
long granuleDistance = targetGranule - pageHeader.granulePosition;
if (granuleDistance <= 0 || granuleDistance > MATCH_RANGE) {
// estimated position too high or too low
long offset = (pageHeader.bodySize + pageHeader.headerSize)
* (granuleDistance <= 0 ? 2 : 1);
long estimatedPosition = getEstimatedPosition(input.getPosition(), granuleDistance, offset);
if (estimatedPosition != previousPosition) { // Temporary prevention for simple loops
return estimatedPosition;
}
}
// position accepted (below target granule and within MATCH_RANGE)
input.resetPeekPosition();
return -1;
}
private long getEstimatedPosition(long position, long granuleDistance, long offset) {
position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset;
if (position < startPosition) {
position = startPosition;
}
if (position >= endPosition) {
position = endPosition - 1;
}
return position;
}
private class OggSeekMap implements SeekMap {
@Override
public boolean isSeekable() {
return true;
}
@Override
public long getPosition(long timeUs) {
if (timeUs == 0) {
queriedGranule = 0;
return startPosition;
}
queriedGranule = streamReader.convertTimeToGranule(timeUs);
return getEstimatedPosition(startPosition, queriedGranule, DEFAULT_OFFSET);
}
@Override
public long getDurationUs() {
return streamReader.convertGranuleToTime(totalGranules);
}
}
/**
* Skips to the next page.
*
* @param input The {@code ExtractorInput} to skip to the next page.
* @throws IOException thrown if peeking/reading from the input fails.
* @throws InterruptedException thrown if interrupted while peeking/reading from the input.
*/
//@VisibleForTesting
static void skipToNextPage(ExtractorInput input)
throws IOException, InterruptedException {
byte[] buffer = new byte[2048];
int peekLength = buffer.length;
long length = input.getLength();
while (true) {
if (length != C.LENGTH_UNBOUNDED && input.getPosition() + peekLength > length) {
// Make sure to not peek beyond the end of the input.
peekLength = (int) (length - input.getPosition());
if (peekLength < 4) {
// Not found until eof.
throw new EOFException();
}
}
input.peekFully(buffer, 0, peekLength, false);
for (int i = 0; i < peekLength - 3; i++) {
if (buffer[i] == 'O' && buffer[i + 1] == 'g' && buffer[i + 2] == 'g'
&& buffer[i + 3] == 'S') {
// Match! Skip to the start of the pattern.
input.skipFully(i);
return;
}
}
// Overlap by not skipping the entire peekLength.
input.skipFully(peekLength - 3);
}
}
/**
* Skips to the last Ogg page in the stream and reads the header's granule field which is the
* total number of samples per channel.
*
* @param input The {@link ExtractorInput} to read from.
* @return the total number of samples of this input.
* @throws IOException thrown if reading from the input fails.
* @throws InterruptedException thrown if interrupted while reading from the input.
*/
//@VisibleForTesting
long readGranuleOfLastPage(ExtractorInput input)
throws IOException, InterruptedException {
Assertions.checkArgument(input.getLength() != C.LENGTH_UNBOUNDED); // never read forever!
skipToNextPage(input);
pageHeader.reset();
while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < input.getLength()) {
pageHeader.populate(input, false);
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
}
return pageHeader.granulePosition;
}
/**
* Skips to the position of the start of the page containing the {@code targetGranule} and
* returns the elapsed samples which is the granule of the page previous to the target page.
* <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.
*/
//@VisibleForTesting
long skipToPageOfGranule(ExtractorInput input, long targetGranule)
throws IOException, InterruptedException {
skipToNextPage(input);
pageHeader.populate(input, false);
while (pageHeader.granulePosition < targetGranule) {
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
// Store in a member field to be able to resume after IOExceptions.
elapsedSamples = pageHeader.granulePosition;
// Peek next header.
pageHeader.populate(input, false);
}
if (elapsedSamples == 0) {
throw new ParserException();
}
input.resetPeekPosition();
long returnValue = elapsedSamples;
// Reset member state.
elapsedSamples = 0;
return returnValue;
}
}

View File

@ -15,17 +15,14 @@
*/ */
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.Format; import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
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.util.FlacSeekTable; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.FlacStreamInfo; import com.google.android.exoplayer.util.FlacStreamInfo;
import com.google.android.exoplayer.util.FlacUtil;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
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;
import java.util.Arrays; import java.util.Arrays;
@ -40,11 +37,10 @@ import java.util.List;
private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF; private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF;
private static final byte SEEKTABLE_PACKET_TYPE = 0x03; private static final byte SEEKTABLE_PACKET_TYPE = 0x03;
private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
private FlacStreamInfo streamInfo; private FlacStreamInfo streamInfo;
private FlacOggSeeker flacOggSeeker;
private FlacSeekTable seekTable;
private boolean firstAudioPacketProcessed;
public static boolean verifyBitstreamType(ParsableByteArray data) { public static boolean verifyBitstreamType(ParsableByteArray data) {
return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type
@ -52,46 +48,150 @@ import java.util.List;
} }
@Override @Override
public int read(ExtractorInput input, PositionHolder seekPosition) protected long preparePayload(ParsableByteArray packet) {
throws IOException, InterruptedException { if (packet.data[0] != AUDIO_PACKET_TYPE) {
long position = input.getPosition(); return -1;
if (!oggParser.readPacket(input, scratch)) {
return Extractor.RESULT_END_OF_INPUT;
} }
return getFlacFrameBlockSize(packet);
}
byte[] data = scratch.data; @Override
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
throws IOException, InterruptedException {
byte[] data = packet.data;
if (streamInfo == null) { if (streamInfo == null) {
streamInfo = new FlacStreamInfo(data, 17); streamInfo = new FlacStreamInfo(data, 17);
byte[] metadata = Arrays.copyOfRange(data, 9, scratch.limit()); byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
List<byte[]> initializationData = Collections.singletonList(metadata); List<byte[]> initializationData = Collections.singletonList(metadata);
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, Format.NO_VALUE,
streamInfo.bitRate(), Format.NO_VALUE, streamInfo.channels, streamInfo.sampleRate, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate, initializationData,
initializationData, null, 0, null)); null, 0, null);
} else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) {
Assertions.checkArgument(flacOggSeeker == null);
flacOggSeeker = new FlacOggSeeker();
flacOggSeeker.parseSeekTable(packet);
} else if (data[0] == AUDIO_PACKET_TYPE) { } else if (data[0] == AUDIO_PACKET_TYPE) {
if (!firstAudioPacketProcessed) { if (flacOggSeeker != null) {
if (seekTable != null) { flacOggSeeker.setFirstFrameOffset(position);
extractorOutput.seekMap(seekTable.createSeekMap(position, streamInfo.sampleRate, setupData.oggSeeker = flacOggSeeker;
streamInfo.durationUs()));
seekTable = null;
} else {
extractorOutput.seekMap(new SeekMap.Unseekable(streamInfo.durationUs()));
}
firstAudioPacketProcessed = true;
} }
return false;
}
return true;
}
trackOutput.sampleData(scratch, scratch.limit()); private int getFlacFrameBlockSize(ParsableByteArray packet) {
scratch.setPosition(0); int blockSizeCode = (packet.data[2] & 0xFF) >> 4;
long timeUs = FlacUtil.extractSampleTimestamp(streamInfo, scratch); switch (blockSizeCode) {
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, scratch.limit(), 0, null); case 1:
return 192;
case 2:
case 3:
case 4:
case 5:
return 576 << (blockSizeCode - 2);
case 6:
case 7:
// skip the sample number
packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);
packet.readUtf8EncodedLong();
if (blockSizeCode == 6) {
return packet.readUnsignedByte() + 1;
} else {
return packet.readUnsignedShort() + 1;
}
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
return 256 << (blockSizeCode - 8);
}
return -1;
}
} else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE && seekTable == null) { private class FlacOggSeeker implements OggSeeker, SeekMap {
seekTable = FlacSeekTable.parseSeekTable(scratch);
private static final int METADATA_LENGTH_OFFSET = 1;
private static final int SEEK_POINT_SIZE = 18;
private long[] sampleNumbers;
private long[] offsets;
private long firstFrameOffset = -1;
private volatile long queriedGranule;
private volatile long seekedGranule;
private long currentGranule = -1;
public void setFirstFrameOffset(long firstFrameOffset) {
this.firstFrameOffset = firstFrameOffset;
}
/**
* Parses a FLAC file seek table metadata structure and initializes internal fields.
*
* @param data
* A ParsableByteArray including whole seek table metadata block. Its position should be set
* to the beginning of the block.
* @see <a href="https://xiph.org/flac/format.html#metadata_block_seektable">FLAC format
* METADATA_BLOCK_SEEKTABLE</a>
*/
public void parseSeekTable(ParsableByteArray data) {
data.skipBytes(METADATA_LENGTH_OFFSET);
int length = data.readUnsignedInt24();
int numberOfSeekPoints = length / SEEK_POINT_SIZE;
sampleNumbers = new long[numberOfSeekPoints];
offsets = new long[numberOfSeekPoints];
for (int i = 0; i < numberOfSeekPoints; i++) {
sampleNumbers[i] = data.readLong();
offsets[i] = data.readLong();
data.skipBytes(2); // Skip "Number of samples in the target frame."
}
}
@Override
public long read(ExtractorInput input) throws IOException, InterruptedException {
if (currentGranule >= 0) {
currentGranule = -currentGranule - 2;
return currentGranule;
}
return -1;
}
@Override
public synchronized long startSeek() {
currentGranule = seekedGranule;
return queriedGranule;
}
@Override
public SeekMap createSeekMap() {
return this;
}
@Override
public boolean isSeekable() {
return true;
}
@Override
public synchronized long getPosition(long timeUs) {
queriedGranule = convertTimeToGranule(timeUs);
int index = Util.binarySearchFloor(sampleNumbers, queriedGranule, true, true);
seekedGranule = sampleNumbers[index];
return firstFrameOffset + offsets[index];
}
@Override
public long getDurationUs() {
return streamInfo.durationUs();
} }
scratch.reset();
return Extractor.RESULT_CONTINUE;
} }
} }

View File

@ -30,19 +30,22 @@ import java.io.IOException;
*/ */
public class OggExtractor implements Extractor { public class OggExtractor implements Extractor {
private static final int MAX_VERIFICATION_BYTES = 8;
private StreamReader streamReader; private StreamReader streamReader;
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
try { try {
ParsableByteArray scratch = new ParsableByteArray(new byte[OggUtil.PAGE_HEADER_SIZE], 0); OggPageHeader header = new OggPageHeader();
OggUtil.PageHeader header = new OggUtil.PageHeader(); if (!header.populate(input, true) || (header.type & 0x02) != 0x02) {
if (!OggUtil.populatePageHeader(input, header, scratch, true)
|| (header.type & 0x02) != 0x02) {
return false; return false;
} }
input.peekFully(scratch.data, 0, header.bodySize);
scratch.setLimit(header.bodySize); int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES);
ParsableByteArray scratch = new ParsableByteArray(length);
input.peekFully(scratch.data, 0, length);
if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { if (FlacReader.verifyBitstreamType(resetPosition(scratch))) {
streamReader = new FlacReader(); streamReader = new FlacReader();
} else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) {
@ -62,6 +65,7 @@ public class OggExtractor implements Extractor {
public void init(ExtractorOutput output) { public void init(ExtractorOutput output) {
TrackOutput trackOutput = output.track(0); TrackOutput trackOutput = output.track(0);
output.endTracks(); output.endTracks();
// TODO: fix the case if sniff() isn't called
streamReader.init(output, trackOutput); streamReader.init(output, trackOutput);
} }

View File

@ -0,0 +1,141 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* OGG packet class.
*/
/* package */ final class OggPacket {
private final OggPageHeader pageHeader = new OggPageHeader();
private final ParsableByteArray packetArray =
new ParsableByteArray(new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);
private int currentSegmentIndex = -1;
private int segmentCount;
private boolean populated;
/**
* Resets this reader.
*/
public void reset() {
pageHeader.reset();
packetArray.reset();
currentSegmentIndex = -1;
populated = false;
}
/**
* Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make
* sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader
* can resume properly from an error while reading a continued packet spanned across multiple
* pages.
*
* @param input the {@link ExtractorInput} to read data from.
* @return {@code true} if the read was successful. {@code false} if the end of the input was
* encountered having read no data.
* @throws IOException thrown if reading from the input fails.
* @throws InterruptedException thrown if interrupted while reading from input.
*/
public boolean populate(ExtractorInput input) throws IOException, InterruptedException {
Assertions.checkState(input != null);
if (populated) {
populated = false;
packetArray.reset();
}
while (!populated) {
if (currentSegmentIndex < 0) {
// We're at the start of a page.
if (!pageHeader.populate(input, true)) {
return false;
}
int segmentIndex = 0;
int bytesToSkip = pageHeader.headerSize;
if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) {
// After seeking, the first packet may be the remainder
// part of a continued packet which has to be discarded.
bytesToSkip += calculatePacketSize(segmentIndex);
segmentIndex += segmentCount;
}
input.skipFully(bytesToSkip);
currentSegmentIndex = segmentIndex;
}
int size = calculatePacketSize(currentSegmentIndex);
int segmentIndex = currentSegmentIndex + segmentCount;
if (size > 0) {
input.readFully(packetArray.data, packetArray.limit(), size);
packetArray.setLimit(packetArray.limit() + size);
populated = pageHeader.laces[segmentIndex - 1] != 255;
}
// advance now since we are sure reading didn't throw an exception
currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? -1
: segmentIndex;
}
return true;
}
/**
* An OGG Packet may span multiple pages. Returns the {@link OggPageHeader} of the last page read,
* or an empty header if the packet has yet to be populated.
* <p>
* Note that the returned {@link OggPageHeader} is mutable and may be updated during subsequent
* calls to {@link #populate(ExtractorInput)}.
*
* @return the {@code PageHeader} of the last page read or an empty header if the packet has yet
* to be populated.
*/
//@VisibleForTesting
public OggPageHeader getPageHeader() {
return pageHeader;
}
/**
* @return A ParsableByteArray containing the payload of the packet.
*/
public ParsableByteArray getPayload() {
return packetArray;
}
/**
* Calculates the size of the packet starting from {@code startSegmentIndex}.
*
* @param startSegmentIndex the index of the first segment of the packet.
* @return Size of the packet.
*/
private int calculatePacketSize(int startSegmentIndex) {
segmentCount = 0;
int size = 0;
while (startSegmentIndex + segmentCount < pageHeader.pageSegmentCount) {
int segmentLength = pageHeader.laces[startSegmentIndex + segmentCount++];
size += segmentLength;
if (segmentLength != 255) {
// packets end at first lace < 255
break;
}
}
return size;
}
}

View File

@ -0,0 +1,131 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import java.io.EOFException;
import java.io.IOException;
/**
* Data object to store header information.
*/
/* package */ final class OggPageHeader {
public static final int EMPTY_PAGE_HEADER_SIZE = 27;
public static final int MAX_SEGMENT_COUNT = 255;
public static final int MAX_PAGE_PAYLOAD = 255 * 255;
public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT
+ MAX_PAGE_PAYLOAD;
private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS");
public int revision;
public int type;
public long granulePosition;
public long streamSerialNumber;
public long pageSequenceNumber;
public long pageChecksum;
public int pageSegmentCount;
public int headerSize;
public int bodySize;
/**
* Be aware that {@code laces.length} is always {@link #MAX_SEGMENT_COUNT}. Instead use
* {@link #pageSegmentCount} to iterate.
*/
public final int[] laces = new int[MAX_SEGMENT_COUNT];
private final ParsableByteArray scratch = new ParsableByteArray(MAX_SEGMENT_COUNT);
/**
* Resets all primitive member fields to zero.
*/
public void reset() {
revision = 0;
type = 0;
granulePosition = 0;
streamSerialNumber = 0;
pageSequenceNumber = 0;
pageChecksum = 0;
pageSegmentCount = 0;
headerSize = 0;
bodySize = 0;
}
/**
* Peeks an Ogg page header and updates this {@link OggPageHeader}.
*
* @param input the {@link ExtractorInput} to read from.
* @param quiet if {@code true} no Exceptions are thrown but {@code false} is return if something
* goes wrong.
* @return {@code true} if the read was successful. {@code false} if the end of the input was
* encountered having read no data.
* @throws IOException thrown if reading data fails or the stream is invalid.
* @throws InterruptedException thrown if thread is interrupted when reading/peeking.
*/
public boolean populate(ExtractorInput input, boolean quiet)
throws IOException, InterruptedException {
scratch.reset();
reset();
boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNBOUNDED
|| input.getLength() - input.getPeekPosition() >= EMPTY_PAGE_HEADER_SIZE;
if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, EMPTY_PAGE_HEADER_SIZE, true)) {
if (quiet) {
return false;
} else {
throw new EOFException();
}
}
if (scratch.readUnsignedInt() != TYPE_OGGS) {
if (quiet) {
return false;
} else {
throw new ParserException("expected OggS capture pattern at begin of page");
}
}
revision = scratch.readUnsignedByte();
if (revision != 0x00) {
if (quiet) {
return false;
} else {
throw new ParserException("unsupported bit stream revision");
}
}
type = scratch.readUnsignedByte();
granulePosition = scratch.readLittleEndianLong();
streamSerialNumber = scratch.readLittleEndianUnsignedInt();
pageSequenceNumber = scratch.readLittleEndianUnsignedInt();
pageChecksum = scratch.readLittleEndianUnsignedInt();
pageSegmentCount = scratch.readUnsignedByte();
headerSize = EMPTY_PAGE_HEADER_SIZE + pageSegmentCount;
// calculate total size of header including laces
scratch.reset();
input.peekFully(scratch.data, 0, pageSegmentCount);
for (int i = 0; i < pageSegmentCount; i++) {
laces[i] = scratch.readUnsignedByte();
bodySize += laces[i];
}
return true;
}
}

View File

@ -1,173 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ogg.OggUtil.PacketInfoHolder;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* Reads OGG packets from an {@link ExtractorInput}.
*/
/* package */ final class OggParser {
public static final int OGG_MAX_SEGMENT_SIZE = 255;
private final OggUtil.PageHeader pageHeader = new OggUtil.PageHeader();
private final ParsableByteArray headerArray = new ParsableByteArray(27 + 255);
private final PacketInfoHolder holder = new PacketInfoHolder();
private int currentSegmentIndex = -1;
private long elapsedSamples;
/**
* Resets this reader.
*/
public void reset() {
pageHeader.reset();
headerArray.reset();
currentSegmentIndex = -1;
}
/**
* Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make
* sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader
* can resume properly from an error while reading a continued packet spanned across multiple
* pages.
*
* @param input the {@link ExtractorInput} to read data from.
* @param packetArray the {@link ParsableByteArray} to write the packet data into.
* @return {@code true} if the read was successful. {@code false} if the end of the input was
* encountered having read no data.
* @throws IOException thrown if reading from the input fails.
* @throws InterruptedException thrown if interrupted while reading from input.
*/
public boolean readPacket(ExtractorInput input, ParsableByteArray packetArray)
throws IOException, InterruptedException {
Assertions.checkState(input != null && packetArray != null);
boolean packetComplete = false;
while (!packetComplete) {
if (currentSegmentIndex < 0) {
// We're at the start of a page.
if (!OggUtil.populatePageHeader(input, pageHeader, headerArray, true)) {
return false;
}
int segmentIndex = 0;
int bytesToSkip = pageHeader.headerSize;
if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) {
// After seeking, the first packet may be the remainder
// part of a continued packet which has to be discarded.
OggUtil.calculatePacketSize(pageHeader, segmentIndex, holder);
segmentIndex += holder.segmentCount;
bytesToSkip += holder.size;
}
input.skipFully(bytesToSkip);
currentSegmentIndex = segmentIndex;
}
OggUtil.calculatePacketSize(pageHeader, currentSegmentIndex, holder);
int segmentIndex = currentSegmentIndex + holder.segmentCount;
if (holder.size > 0) {
input.readFully(packetArray.data, packetArray.limit(), holder.size);
packetArray.setLimit(packetArray.limit() + holder.size);
packetComplete = pageHeader.laces[segmentIndex - 1] != 255;
}
// advance now since we are sure reading didn't throw an exception
currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? -1
: segmentIndex;
}
return true;
}
/**
* Skips to the last Ogg page in the stream and reads the header's granule field which is the
* total number of samples per channel.
*
* @param input The {@link ExtractorInput} to read from.
* @return the total number of samples of this input.
* @throws IOException thrown if reading from the input fails.
* @throws InterruptedException thrown if interrupted while reading from the input.
*/
public long readGranuleOfLastPage(ExtractorInput input)
throws IOException, InterruptedException {
Assertions.checkArgument(input.getLength() != C.LENGTH_UNBOUNDED); // never read forever!
OggUtil.skipToNextPage(input);
pageHeader.reset();
while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < input.getLength()) {
OggUtil.populatePageHeader(input, pageHeader, headerArray, false);
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
}
return pageHeader.granulePosition;
}
/**
* Skips to the position of the start of the page containing the {@code targetGranule} and
* returns the elapsed samples which is the granule of the page previous to the target page.
* <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 OggParser.PageHeader} which is mutable.
* The value of the fields might be changed by the reader when reading the stream advances and
* the next page is read (which implies reading and populating the next header).
*
* @return the {@code PageHeader} of the current page or {@code null}.
*/
public OggUtil.PageHeader getPageHeader() {
return pageHeader;
}
}

View File

@ -15,65 +15,45 @@
*/ */
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.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
/** /**
* Used to seek in an Ogg stream. * Used to seek in an Ogg stream. OggSeeker implementation may do direct seeking or progressive
* seeking. OggSeeker works together with a {@link SeekMap} instance to capture the queried position
* and start the seeking with an initial estimated position.
*/ */
/* package */ final class OggSeeker { /* package */ interface 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. * @return a SeekMap instance which returns an initial estimated position for progressive seeking
* * or the final position for direct seeking. Returns null if {@link #read} hasn't returned -1
* @param audioDataLength the length of the audio data (total bytes - header bytes). * yet.
* @param totalSamples the total number of samples of audio data.
*/ */
public void setup(long audioDataLength, long totalSamples) { SeekMap createSeekMap();
Assertions.checkArgument(audioDataLength > 0 && totalSamples > 0);
this.audioDataLength = audioDataLength;
this.totalSamples = totalSamples;
}
/** /**
* Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput} * Initializes a seek operation.
* 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 * @return The granule position targeted by the seek.
* which it is sensible to just skip pages to the target granule and pre-roll instead of doing */
* another seek request. long startSeek();
/**
* Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a
* progressive seek.
* <p/>
* If more data is required or if the position of the input needs to be modified then a position
* from which data should be provided is returned. Else a negative value is returned. If a seek
* has been completed then the value returned is -(currentGranule + 2). Else -1 is returned.
* *
* @param targetGranule the target granule position to seek to.
* @param input the {@link ExtractorInput} to read from. * @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 * @return the non-negative position to seek the {@link ExtractorInput} to or -1 seeking not
* enough to skip to the target page. * necessary or at the end of seeking a negative number < -1 which is -(currentGranule + 2).
* @throws IOException thrown if reading from the input fails. * @throws IOException thrown if reading from the input fails.
* @throws InterruptedException thrown if interrupted while reading from the input. * @throws InterruptedException thrown if interrupted while reading from the input.
*/ */
public long getNextSeekPosition(long targetGranule, ExtractorInput input) long read(ExtractorInput input) throws IOException, InterruptedException;
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

@ -1,211 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import java.io.EOFException;
import java.io.IOException;
/**
* Utility methods for reading ogg streams.
*/
/* package */ final class OggUtil {
public static final int PAGE_HEADER_SIZE = 27;
private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS");
/**
* Reads an int of {@code length} bits from {@code src} starting at
* {@code leastSignificantBitIndex}.
*
* @param src the {@code byte} to read from.
* @param length the length in bits of the int to read.
* @param leastSignificantBitIndex the index of the least significant bit of the int to read.
* @return the int value read.
*/
public static int readBits(byte src, int length, int leastSignificantBitIndex) {
return (src >> leastSignificantBitIndex) & (255 >>> (8 - length));
}
/**
* Skips to the next page.
*
* @param input The {@code ExtractorInput} to skip to the next page.
* @throws IOException thrown if peeking/reading from the input fails.
* @throws InterruptedException thrown if interrupted while peeking/reading from the input.
*/
public static void skipToNextPage(ExtractorInput input)
throws IOException, InterruptedException {
byte[] buffer = new byte[2048];
int peekLength = buffer.length;
while (true) {
if (input.getLength() != C.LENGTH_UNBOUNDED
&& input.getPosition() + peekLength > input.getLength()) {
// Make sure to not peek beyond the end of the input.
peekLength = (int) (input.getLength() - input.getPosition());
if (peekLength < 4) {
// Not found until eof.
throw new EOFException();
}
}
input.peekFully(buffer, 0, peekLength, false);
for (int i = 0; i < peekLength - 3; i++) {
if (buffer[i] == 'O' && buffer[i + 1] == 'g' && buffer[i + 2] == 'g'
&& buffer[i + 3] == 'S') {
// Match! Skip to the start of the pattern.
input.skipFully(i);
return;
}
}
// Overlap by not skipping the entire peekLength.
input.skipFully(peekLength - 3);
}
}
/**
* Peeks an Ogg page header and stores the data in the {@code header} object passed
* as argument.
*
* @param input the {@link ExtractorInput} to read from.
* @param header the {@link PageHeader} to be populated.
* @param scratch a scratch array temporary use. Its size should be at least PAGE_HEADER_SIZE
* @param quite if {@code true} no Exceptions are thrown but {@code false} is return if something
* goes wrong.
* @return {@code true} if the read was successful. {@code false} if the end of the
* input was encountered having read no data.
* @throws IOException thrown if reading data fails or the stream is invalid.
* @throws InterruptedException thrown if thread is interrupted when reading/peeking.
*/
public static boolean populatePageHeader(ExtractorInput input, PageHeader header,
ParsableByteArray scratch, boolean quite) throws IOException, InterruptedException {
scratch.reset();
header.reset();
boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNBOUNDED
|| input.getLength() - input.getPeekPosition() >= PAGE_HEADER_SIZE;
if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, PAGE_HEADER_SIZE, true)) {
if (quite) {
return false;
} else {
throw new EOFException();
}
}
if (scratch.readUnsignedInt() != TYPE_OGGS) {
if (quite) {
return false;
} else {
throw new ParserException("expected OggS capture pattern at begin of page");
}
}
header.revision = scratch.readUnsignedByte();
if (header.revision != 0x00) {
if (quite) {
return false;
} else {
throw new ParserException("unsupported bit stream revision");
}
}
header.type = scratch.readUnsignedByte();
header.granulePosition = scratch.readLittleEndianLong();
header.streamSerialNumber = scratch.readLittleEndianUnsignedInt();
header.pageSequenceNumber = scratch.readLittleEndianUnsignedInt();
header.pageChecksum = scratch.readLittleEndianUnsignedInt();
header.pageSegmentCount = scratch.readUnsignedByte();
scratch.reset();
// calculate total size of header including laces
header.headerSize = PAGE_HEADER_SIZE + header.pageSegmentCount;
input.peekFully(scratch.data, 0, header.pageSegmentCount);
for (int i = 0; i < header.pageSegmentCount; i++) {
header.laces[i] = scratch.readUnsignedByte();
header.bodySize += header.laces[i];
}
return true;
}
/**
* Calculates the size of the packet starting from {@code startSegmentIndex}.
*
* @param header the {@link PageHeader} with laces.
* @param startSegmentIndex the index of the first segment of the packet.
* @param holder a position holder to store the resulting size value.
*/
public static void calculatePacketSize(PageHeader header, int startSegmentIndex,
PacketInfoHolder holder) {
holder.segmentCount = 0;
holder.size = 0;
while (startSegmentIndex + holder.segmentCount < header.pageSegmentCount) {
int segmentLength = header.laces[startSegmentIndex + holder.segmentCount++];
holder.size += segmentLength;
if (segmentLength != 255) {
// packets end at first lace < 255
break;
}
}
}
/**
* Data object to store header information. Be aware that {@code laces.length} is always 255.
* Instead use {@code pageSegmentCount} to iterate.
*/
public static final class PageHeader {
public int revision;
public int type;
public long granulePosition;
public long streamSerialNumber;
public long pageSequenceNumber;
public long pageChecksum;
public int pageSegmentCount;
public int headerSize;
public int bodySize;
public final int[] laces = new int[255];
/**
* Resets all primitive member fields to zero.
*/
public void reset() {
revision = 0;
type = 0;
granulePosition = 0;
streamSerialNumber = 0;
pageSequenceNumber = 0;
pageChecksum = 0;
pageSegmentCount = 0;
headerSize = 0;
bodySize = 0;
}
}
/**
* Holds size and number of segments of a packet.
*/
public static class PacketInfoHolder {
public int size;
public int segmentCount;
}
}

View File

@ -17,16 +17,14 @@ package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.Format; import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
@ -34,19 +32,16 @@ import java.util.List;
*/ */
/* package */ final class OpusReader extends StreamReader { /* package */ final class OpusReader extends StreamReader {
private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840;
/** /**
* Opus streams are always decoded at 48000 Hz. * Opus streams are always decoded at 48000 Hz.
*/ */
private static final int SAMPLE_RATE = 48000; private static final int SAMPLE_RATE = 48000;
private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}; private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
private boolean headerRead;
private static final int STATE_READ_HEADER = 0; private boolean tagsSkipped;
private static final int STATE_READ_TAGS = 1;
private static final int STATE_READ_AUDIO = 2;
private int state = STATE_READ_HEADER;
private long timeUs;
public static boolean verifyBitstreamType(ParsableByteArray data) { public static boolean verifyBitstreamType(ParsableByteArray data) {
if (data.bytesLeft() < OPUS_SIGNATURE.length) { if (data.bytesLeft() < OPUS_SIGNATURE.length) {
@ -58,42 +53,48 @@ import java.util.List;
} }
@Override @Override
public int read(ExtractorInput input, PositionHolder seekPosition) protected long preparePayload(ParsableByteArray packet) {
throws IOException, InterruptedException { return convertTimeToGranule(getPacketDurationUs(packet.data));
if (!oggParser.readPacket(input, scratch)) {
return Extractor.RESULT_END_OF_INPUT;
}
byte[] data = scratch.data;
int dataSize = scratch.limit();
switch (state) {
case STATE_READ_HEADER: {
byte[] metadata = Arrays.copyOfRange(data, 0, dataSize);
int channelCount = metadata[9] & 0xFF;
List<byte[]> initializationData = Collections.singletonList(metadata);
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS,
Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE,
initializationData, null, 0, null));
state = STATE_READ_TAGS;
} break;
case STATE_READ_TAGS:
// skip this packet
state = STATE_READ_AUDIO;
extractorOutput.seekMap(new SeekMap.Unseekable(C.UNSET_TIME_US));
break;
case STATE_READ_AUDIO:
trackOutput.sampleData(scratch, dataSize);
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, dataSize, 0, null);
timeUs += getPacketDuration(data);
break;
}
scratch.reset();
return Extractor.RESULT_CONTINUE;
} }
private long getPacketDuration(byte[] packet) { @Override
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
throws IOException, InterruptedException {
if (!headerRead) {
byte[] metadata = Arrays.copyOf(packet.data, packet.limit());
int channelCount = metadata[9] & 0xFF;
int preskip = ((metadata[11] & 0xFF) << 8) | (metadata[10] & 0xFF);
List<byte[]> initializationData = new ArrayList<>(3);
initializationData.add(metadata);
putNativeOrderLong(initializationData, preskip);
putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES);
setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, Format.NO_VALUE,
Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0, "und");
headerRead = true;
} else if (!tagsSkipped) {
// Skip tags packet
tagsSkipped = true;
} else {
return false;
}
return true;
}
private void putNativeOrderLong(List<byte[]> initializationData, int samples) {
long ns = (samples * C.NANOS_PER_SECOND) / SAMPLE_RATE;
byte[] array = ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(ns).array();
initializationData.add(array);
}
/**
* Returns the duration of the given audio packet.
*
* @param packet Contains audio data.
* @return Returns the duration of the given audio packet.
*/
private long getPacketDurationUs(byte[] packet) {
int toc = packet[0] & 0xFF; int toc = packet[0] & 0xFF;
int frames; int frames;
switch (toc & 0x3) { switch (toc & 0x3) {

View File

@ -1,9 +1,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.Format;
import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.PositionHolder; 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.TrackOutput;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
@ -14,31 +17,202 @@ import java.io.IOException;
*/ */
/* package */ abstract class StreamReader { /* package */ abstract class StreamReader {
protected final ParsableByteArray scratch = new ParsableByteArray( private static final int STATE_READ_HEADERS = 0;
new byte[OggParser.OGG_MAX_SEGMENT_SIZE * 255], 0); private static final int STATE_READ_PAYLOAD = 1;
private static final int STATE_END_OF_INPUT = 2;
protected final OggParser oggParser = new OggParser(); static class SetupData {
Format format;
OggSeeker oggSeeker;
}
protected TrackOutput trackOutput; private OggPacket oggPacket;
private TrackOutput trackOutput;
protected ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
private OggSeeker oggSeeker;
private long targetGranule;
private long payloadStartPosition;
private long currentGranule;
private int state;
private int sampleRate;
private SetupData setupData;
private long lengthOfReadPacket;
private boolean seekMapSet;
void init(ExtractorOutput output, TrackOutput trackOutput) { void init(ExtractorOutput output, TrackOutput trackOutput) {
this.extractorOutput = output; this.extractorOutput = output;
this.trackOutput = trackOutput; this.trackOutput = trackOutput;
this.oggPacket = new OggPacket();
this.setupData = new SetupData();
this.state = STATE_READ_HEADERS;
this.targetGranule = -1;
this.payloadStartPosition = 0;
} }
/** /**
* @see Extractor#seek() * @see Extractor#seek()
*/ */
void seek() { final void seek() {
oggParser.reset(); oggPacket.reset();
scratch.reset();
if (state != STATE_READ_HEADERS) {
targetGranule = oggSeeker.startSeek();
state = STATE_READ_PAYLOAD;
}
} }
/** /**
* @see Extractor#read(ExtractorInput, PositionHolder) * @see Extractor#read(ExtractorInput, PositionHolder)
*/ */
abstract int read(ExtractorInput input, PositionHolder seekPosition) final int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException; throws IOException, InterruptedException {
switch (state) {
case STATE_READ_HEADERS:
return readHeaders(input);
case STATE_READ_PAYLOAD:
return readPayload(input, seekPosition);
default:
// Never happens.
throw new IllegalStateException();
}
}
private int readHeaders(ExtractorInput input)
throws IOException, InterruptedException {
boolean readingHeaders = true;
while (readingHeaders) {
if (!oggPacket.populate(input)) {
state = STATE_END_OF_INPUT;
return Extractor.RESULT_END_OF_INPUT;
}
lengthOfReadPacket = input.getPosition() - payloadStartPosition;
readingHeaders = readHeaders(oggPacket.getPayload(), payloadStartPosition, setupData);
if (readingHeaders) {
payloadStartPosition = input.getPosition();
}
}
sampleRate = setupData.format.sampleRate;
trackOutput.format(setupData.format);
if (setupData.oggSeeker != null) {
oggSeeker = setupData.oggSeeker;
} else if (input.getLength() == C.LENGTH_UNBOUNDED) {
oggSeeker = new UnseekableOggSeeker();
} else {
oggSeeker = new DefaultOggSeeker(payloadStartPosition, input.getLength(), this);
}
setupData = null;
state = STATE_READ_PAYLOAD;
return Extractor.RESULT_CONTINUE;
}
private int readPayload(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
long position = oggSeeker.read(input);
if (position >= 0) {
seekPosition.position = position;
return Extractor.RESULT_SEEK;
} else if (position < -1) {
onSeekEnd(-position - 2);
}
if (!seekMapSet) {
SeekMap seekMap = oggSeeker.createSeekMap();
extractorOutput.seekMap(seekMap);
seekMapSet = true;
}
if (lengthOfReadPacket > 0 || oggPacket.populate(input)) {
lengthOfReadPacket = 0;
ParsableByteArray payload = oggPacket.getPayload();
long granulesInPacket = preparePayload(payload);
if (granulesInPacket >= 0 && currentGranule + granulesInPacket >= targetGranule) {
// calculate time and send payload data to codec
long timeUs = convertGranuleToTime(currentGranule);
trackOutput.sampleData(payload, payload.limit());
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, payload.limit(), 0, null);
targetGranule = -1;
}
currentGranule += granulesInPacket;
} else {
state = STATE_END_OF_INPUT;
return Extractor.RESULT_END_OF_INPUT;
}
return Extractor.RESULT_CONTINUE;
}
/**
* Converts granule value to time.
*
* @param granule
* granule value.
* @return Returns time in milliseconds.
*/
protected long convertGranuleToTime(long granule) {
return (granule * C.MICROS_PER_SECOND) / sampleRate;
}
/**
* Converts time value to granule.
*
* @param timeUs
* Time in milliseconds.
* @return Granule value.
*/
protected long convertTimeToGranule(long timeUs) {
return (sampleRate * timeUs) / C.MICROS_PER_SECOND;
}
/**
* Prepares payload data in the packet for submitting to TrackOutput and returns number of
* granules in the packet.
*
* @param packet
* Ogg payload data packet
* @return Number of granules in the packet or -1 if the packet doesn't contain payload data.
*/
protected abstract long preparePayload(ParsableByteArray packet);
/**
* Checks if the given packet is a header packet and reads it.
*
* @param packet An ogg packet.
* @param position Position of the given header packet.
* @param setupData Setup data to be filled.
* @return Return true if the packet contains header data.
*/
protected abstract boolean readHeaders(ParsableByteArray packet, long position,
SetupData setupData) throws IOException, InterruptedException;
/**
* Called on end of seeking.
*
* @param currentGranule Current granule at the current position of input.
*/
protected void onSeekEnd(long currentGranule) {
this.currentGranule = currentGranule;
}
private class UnseekableOggSeeker implements OggSeeker {
@Override
public long read(ExtractorInput input) throws IOException, InterruptedException {
return -1;
}
@Override
public long startSeek() {
return 0;
}
@Override
public SeekMap createSeekMap() {
return new SeekMap.Unseekable(C.UNSET_TIME_US);
}
}
} }

View File

@ -15,13 +15,8 @@
*/ */
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.Format; import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.ogg.VorbisUtil.Mode; import com.google.android.exoplayer.extractor.ogg.VorbisUtil.Mode;
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;
@ -32,24 +27,14 @@ import java.util.ArrayList;
/** /**
* {@link StreamReader} to extract Vorbis data out of Ogg byte stream. * {@link StreamReader} to extract Vorbis data out of Ogg byte stream.
*/ */
/* package */ final class VorbisReader extends StreamReader implements SeekMap { /* package */ final class VorbisReader extends StreamReader {
private static final long LARGEST_EXPECTED_PAGE_SIZE = 8000;
private VorbisSetup vorbisSetup; private VorbisSetup vorbisSetup;
private int previousPacketBlockSize; private int previousPacketBlockSize;
private long elapsedSamples;
private boolean seenFirstAudioPacket; private boolean seenFirstAudioPacket;
private final OggSeeker oggSeeker = new OggSeeker();
private long targetGranule = -1;
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;
public static boolean verifyBitstreamType(ParsableByteArray data) { public static boolean verifyBitstreamType(ParsableByteArray data) {
try { try {
@ -60,114 +45,71 @@ import java.util.ArrayList;
} }
@Override @Override
public void seek() { protected void onSeekEnd(long currentGranule) {
super.seek(); super.onSeekEnd(currentGranule);
previousPacketBlockSize = 0; seenFirstAudioPacket = currentGranule != 0;
elapsedSamples = 0; previousPacketBlockSize = vorbisIdHeader != null ? vorbisIdHeader.blockSize0 : 0;
seenFirstAudioPacket = false;
} }
@Override @Override
public int read(ExtractorInput input, PositionHolder seekPosition) protected long preparePayload(ParsableByteArray packet) {
// if this is not an audio packet...
if ((packet.data[0] & 0x01) == 1) {
return -1;
}
// ... we need to decode the block size
int packetBlockSize = decodeBlockSize(packet.data[0], vorbisSetup);
// a packet contains samples produced from overlapping the previous and current frame data
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4
: 0;
// codec expects the number of samples appended to audio data
appendNumberOfSamples(packet, samplesInPacket);
// update state in members for next iteration
seenFirstAudioPacket = true;
previousPacketBlockSize = packetBlockSize;
return samplesInPacket;
}
@Override
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (vorbisSetup != null) {
// setup return false;
if (totalSamples == 0) {
if (vorbisSetup == null) {
inputLength = input.getLength();
vorbisSetup = readSetupHeaders(input, scratch);
audioStartPosition = input.getPosition();
extractorOutput.seekMap(this);
if (inputLength != C.LENGTH_UNBOUNDED) {
// seek to the end just before the last page of stream to get the duration
seekPosition.position = Math.max(0, input.getLength() - LARGEST_EXPECTED_PAGE_SIZE);
return Extractor.RESULT_SEEK;
}
}
totalSamples = inputLength == C.LENGTH_UNBOUNDED ? -1
: oggParser.readGranuleOfLastPage(input);
ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
codecInitialisationData.add(vorbisSetup.idHeader.data);
codecInitialisationData.add(vorbisSetup.setupHeaderData);
durationUs = inputLength == C.LENGTH_UNBOUNDED ? C.UNSET_TIME_US
: (totalSamples * C.MICROS_PER_SECOND) / vorbisSetup.idHeader.sampleRate;
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS,
this.vorbisSetup.idHeader.bitrateNominal, OggParser.OGG_MAX_SEGMENT_SIZE * 255,
this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
codecInitialisationData, null, 0, null));
if (inputLength != C.LENGTH_UNBOUNDED) {
oggSeeker.setup(inputLength - audioStartPosition, totalSamples);
// seek back to resume from where we finished reading vorbis headers
seekPosition.position = audioStartPosition;
return Extractor.RESULT_SEEK;
}
} }
// seeking requested vorbisSetup = readSetupHeaders(packet);
if (!seenFirstAudioPacket && targetGranule > -1) { if (vorbisSetup == null) {
OggUtil.skipToNextPage(input); return true;
long position = oggSeeker.getNextSeekPosition(targetGranule, input);
if (position != -1) {
seekPosition.position = position;
return Extractor.RESULT_SEEK;
} else {
elapsedSamples = oggParser.skipToPageOfGranule(input, targetGranule);
previousPacketBlockSize = vorbisIdHeader.blockSize0;
// we're never at the first packet after seeking
seenFirstAudioPacket = true;
}
} }
// playback ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
if (oggParser.readPacket(input, scratch)) { codecInitialisationData.add(vorbisSetup.idHeader.data);
// if this is an audio packet... codecInitialisationData.add(vorbisSetup.setupHeaderData);
if ((scratch.data[0] & 0x01) != 1) {
// ... we need to decode the block size setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS,
int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup); this.vorbisSetup.idHeader.bitrateNominal, OggPageHeader.MAX_PAGE_PAYLOAD,
// a packet contains samples produced from overlapping the previous and current frame data this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2) codecInitialisationData, null, 0, null);
int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4 return true;
: 0;
if (elapsedSamples + samplesInPacket >= targetGranule) {
// codec expects the number of samples appended to audio data
appendNumberOfSamples(scratch, samplesInPacket);
// calculate time and send audio data to codec
long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
trackOutput.sampleData(scratch, scratch.limit());
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, scratch.limit(), 0, null);
targetGranule = -1;
}
// update state in members for next iteration
seenFirstAudioPacket = true;
elapsedSamples += samplesInPacket;
previousPacketBlockSize = packetBlockSize;
}
scratch.reset();
return Extractor.RESULT_CONTINUE;
}
return Extractor.RESULT_END_OF_INPUT;
} }
//@VisibleForTesting //@VisibleForTesting
/* package */ VorbisSetup readSetupHeaders(ExtractorInput input, ParsableByteArray scratch) /* package */ VorbisSetup readSetupHeaders(ParsableByteArray scratch)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (vorbisIdHeader == null) { if (vorbisIdHeader == null) {
oggParser.readPacket(input, scratch);
vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch); vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch);
scratch.reset(); return null;
} }
if (commentHeader == null) { if (commentHeader == null) {
oggParser.readPacket(input, scratch);
commentHeader = VorbisUtil.readVorbisCommentHeader(scratch); commentHeader = VorbisUtil.readVorbisCommentHeader(scratch);
scratch.reset(); return null;
} }
oggParser.readPacket(input, scratch);
// the third packet contains the setup header // the third packet contains the setup header
byte[] setupHeaderData = new byte[scratch.limit()]; byte[] setupHeaderData = new byte[scratch.limit()];
// raw data of vorbis setup header has to be passed to decoder as CSD buffer #2 // raw data of vorbis setup header has to be passed to decoder as CSD buffer #2
@ -176,11 +118,24 @@ import java.util.ArrayList;
Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels); Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels);
// we need the ilog of modes all the time when extracting, so we compute it once // we need the ilog of modes all the time when extracting, so we compute it once
int iLogModes = VorbisUtil.iLog(modes.length - 1); int iLogModes = VorbisUtil.iLog(modes.length - 1);
scratch.reset();
return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes); return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes);
} }
/**
* Reads an int of {@code length} bits from {@code src} starting at
* {@code leastSignificantBitIndex}.
*
* @param src the {@code byte} to read from.
* @param length the length in bits of the int to read.
* @param leastSignificantBitIndex the index of the least significant bit of the int to read.
* @return the int value read.
*/
//@VisibleForTesting
/* package */ static int readBits(byte src, int length, int leastSignificantBitIndex) {
return (src >> leastSignificantBitIndex) & (255 >>> (8 - length));
}
//@VisibleForTesting //@VisibleForTesting
/* package */ static void appendNumberOfSamples(ParsableByteArray buffer, /* package */ static void appendNumberOfSamples(ParsableByteArray buffer,
long packetSampleCount) { long packetSampleCount) {
@ -196,7 +151,7 @@ import java.util.ArrayList;
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 = OggUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1); int modeNumber = 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;
@ -206,27 +161,6 @@ import java.util.ArrayList;
return currentBlockSize; return currentBlockSize;
} }
@Override
public boolean isSeekable() {
return vorbisSetup != null && inputLength != C.LENGTH_UNBOUNDED;
}
@Override
public long getPosition(long timeUs) {
if (timeUs == 0) {
targetGranule = -1;
return audioStartPosition;
}
targetGranule = vorbisSetup.idHeader.sampleRate * timeUs / C.MICROS_PER_SECOND;
return Math.max(audioStartPosition, ((inputLength - audioStartPosition) * timeUs
/ durationUs) - 4000);
}
@Override
public long getDurationUs() {
return durationUs;
}
/** /**
* Class to hold all data read from Vorbis setup headers. * Class to hold all data read from Vorbis setup headers.
*/ */

View File

@ -1,91 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.util;
import com.google.android.exoplayer.extractor.SeekMap;
/**
* FLAC seek table class
*/
public final class FlacSeekTable {
private static final int METADATA_LENGTH_OFFSET = 1;
private static final int SEEK_POINT_SIZE = 18;
private final long[] sampleNumbers;
private final long[] offsets;
/**
* Parses a FLAC file seek table metadata structure and creates a FlacSeekTable instance.
*
* @param data A ParsableByteArray including whole seek table metadata block. Its position should
* be set to the beginning of the block.
* @return A FlacSeekTable instance keeping seek table data
* @see <a href="https://xiph.org/flac/format.html#metadata_block_seektable">FLAC format
* METADATA_BLOCK_SEEKTABLE</a>
*/
public static FlacSeekTable parseSeekTable(ParsableByteArray data) {
data.skipBytes(METADATA_LENGTH_OFFSET);
int length = data.readUnsignedInt24();
int numberOfSeekPoints = length / SEEK_POINT_SIZE;
long[] sampleNumbers = new long[numberOfSeekPoints];
long[] offsets = new long[numberOfSeekPoints];
for (int i = 0; i < numberOfSeekPoints; i++) {
sampleNumbers[i] = data.readLong();
offsets[i] = data.readLong();
data.skipBytes(2); // Skip "Number of samples in the target frame."
}
return new FlacSeekTable(sampleNumbers, offsets);
}
private FlacSeekTable(long[] sampleNumbers, long[] offsets) {
this.sampleNumbers = sampleNumbers;
this.offsets = offsets;
}
/**
* Creates a {@link SeekMap} wrapper for this FlacSeekTable.
*
* @param firstFrameOffset Offset of the first FLAC frame
* @param sampleRate Sample rate of the FLAC file.
* @return A SeekMap wrapper for this FlacSeekTable.
*/
public SeekMap createSeekMap(final long firstFrameOffset, final long sampleRate,
final long durationUs) {
return new SeekMap() {
@Override
public boolean isSeekable() {
return true;
}
@Override
public long getPosition(long timeUs) {
long sample = (timeUs * sampleRate) / 1000000L;
int index = Util.binarySearchFloor(sampleNumbers, sample, true, true);
return firstFrameOffset + offsets[index];
}
@Override
public long getDurationUs() {
return durationUs;
}
};
}
}

View File

@ -1,50 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.util;
/**
* Utility functions for FLAC
*/
public final class FlacUtil {
private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
/**
* Prevents initialization.
*/
private FlacUtil() {}
/**
* Extracts sample timestamp from the given binary FLAC frame header data structure.
*
* @param streamInfo A {@link FlacStreamInfo} instance
* @param frameData A {@link ParsableByteArray} including binary FLAC frame header data structure.
* Its position should be set to the beginning of the structure.
* @return Sample timestamp
* @see <a href="https://xiph.org/flac/format.html#frame_header">FLAC format FRAME_HEADER</a>
*/
public static long extractSampleTimestamp(FlacStreamInfo streamInfo,
ParsableByteArray frameData) {
frameData.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);
long sampleNumber = frameData.readUtf8EncodedLong();
if (streamInfo.minBlockSize == streamInfo.maxBlockSize) {
// if fixed block size then sampleNumber is frame number
sampleNumber *= streamInfo.minBlockSize;
}
return (sampleNumber * 1000000L) / streamInfo.sampleRate;
}
}