Ogg/Opus and Ogg/Flac search seeking
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=122977123
This commit is contained in:
parent
7465db2a22
commit
731d4283ab
BIN
library/src/androidTest/assets/ogg/bear_flac.ogg
Normal file
BIN
library/src/androidTest/assets/ogg/bear_flac.ogg
Normal file
Binary file not shown.
BIN
library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg
Normal file
BIN
library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg
Normal file
Binary file not shown.
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.
|
||||||
}
|
}
|
||||||
|
@ -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,18 +36,42 @@ 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) {
|
||||||
|
try {
|
||||||
readResult = extractor.read(input, seekPositionHolder);
|
readResult = extractor.read(input, seekPositionHolder);
|
||||||
if (readResult == Extractor.RESULT_SEEK) {
|
if (readResult == Extractor.RESULT_SEEK) {
|
||||||
long seekPosition = seekPositionHolder.position;
|
long seekPosition = seekPositionHolder.position;
|
||||||
Assertions.checkState(0 < seekPosition && seekPosition <= Integer.MAX_VALUE);
|
Assertions.checkState(0 <= seekPosition && seekPosition <= Integer.MAX_VALUE);
|
||||||
input.setPosition((int) seekPosition);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 getFlacFrameBlockSize(packet);
|
||||||
return Extractor.RESULT_END_OF_INPUT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getFlacFrameBlockSize(ParsableByteArray packet) {
|
||||||
|
int blockSizeCode = (packet.data[2] & 0xFF) >> 4;
|
||||||
|
switch (blockSizeCode) {
|
||||||
|
case 1:
|
||||||
|
return 192;
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
case 5:
|
||||||
|
return 576 << (blockSizeCode - 2);
|
||||||
|
case 6:
|
||||||
|
case 7:
|
||||||
|
// skip the sample number
|
||||||
|
packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);
|
||||||
|
packet.readUtf8EncodedLong();
|
||||||
|
if (blockSizeCode == 6) {
|
||||||
|
return packet.readUnsignedByte() + 1;
|
||||||
} else {
|
} else {
|
||||||
extractorOutput.seekMap(new SeekMap.Unseekable(streamInfo.durationUs()));
|
return packet.readUnsignedShort() + 1;
|
||||||
}
|
}
|
||||||
firstAudioPacketProcessed = true;
|
case 8:
|
||||||
|
case 9:
|
||||||
|
case 10:
|
||||||
|
case 11:
|
||||||
|
case 12:
|
||||||
|
case 13:
|
||||||
|
case 14:
|
||||||
|
case 15:
|
||||||
|
return 256 << (blockSizeCode - 8);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
trackOutput.sampleData(scratch, scratch.limit());
|
private class FlacOggSeeker implements OggSeeker, SeekMap {
|
||||||
scratch.setPosition(0);
|
|
||||||
long timeUs = FlacUtil.extractSampleTimestamp(streamInfo, scratch);
|
|
||||||
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, scratch.limit(), 0, null);
|
|
||||||
|
|
||||||
} else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE && seekTable == null) {
|
private static final int METADATA_LENGTH_OFFSET = 1;
|
||||||
seekTable = FlacSeekTable.parseSeekTable(scratch);
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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) {
|
||||||
|
return convertTimeToGranule(getPacketDurationUs(packet.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
if (!oggParser.readPacket(input, scratch)) {
|
if (!headerRead) {
|
||||||
return Extractor.RESULT_END_OF_INPUT;
|
byte[] metadata = Arrays.copyOf(packet.data, packet.limit());
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
int channelCount = metadata[9] & 0xFF;
|
||||||
List<byte[]> initializationData = Collections.singletonList(metadata);
|
int preskip = ((metadata[11] & 0xFF) << 8) | (metadata[10] & 0xFF);
|
||||||
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS,
|
|
||||||
Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE,
|
List<byte[]> initializationData = new ArrayList<>(3);
|
||||||
initializationData, null, 0, null));
|
initializationData.add(metadata);
|
||||||
state = STATE_READ_TAGS;
|
putNativeOrderLong(initializationData, preskip);
|
||||||
} break;
|
putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES);
|
||||||
case STATE_READ_TAGS:
|
|
||||||
// skip this packet
|
setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, Format.NO_VALUE,
|
||||||
state = STATE_READ_AUDIO;
|
Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0, "und");
|
||||||
extractorOutput.seekMap(new SeekMap.Unseekable(C.UNSET_TIME_US));
|
headerRead = true;
|
||||||
break;
|
} else if (!tagsSkipped) {
|
||||||
case STATE_READ_AUDIO:
|
// Skip tags packet
|
||||||
trackOutput.sampleData(scratch, dataSize);
|
tagsSkipped = true;
|
||||||
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, dataSize, 0, null);
|
} else {
|
||||||
timeUs += getPacketDuration(data);
|
return false;
|
||||||
break;
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
scratch.reset();
|
private void putNativeOrderLong(List<byte[]> initializationData, int samples) {
|
||||||
return Extractor.RESULT_CONTINUE;
|
long ns = (samples * C.NANOS_PER_SECOND) / SAMPLE_RATE;
|
||||||
|
byte[] array = ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(ns).array();
|
||||||
|
initializationData.add(array);
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getPacketDuration(byte[] packet) {
|
/**
|
||||||
|
* 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) {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
throws IOException, InterruptedException {
|
// if this is not an audio packet...
|
||||||
|
if ((packet.data[0] & 0x01) == 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// setup
|
// ... we need to decode the block size
|
||||||
if (totalSamples == 0) {
|
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 {
|
||||||
|
if (vorbisSetup != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vorbisSetup = readSetupHeaders(packet);
|
||||||
if (vorbisSetup == null) {
|
if (vorbisSetup == null) {
|
||||||
inputLength = input.getLength();
|
return true;
|
||||||
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<>();
|
ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
|
||||||
codecInitialisationData.add(vorbisSetup.idHeader.data);
|
codecInitialisationData.add(vorbisSetup.idHeader.data);
|
||||||
codecInitialisationData.add(vorbisSetup.setupHeaderData);
|
codecInitialisationData.add(vorbisSetup.setupHeaderData);
|
||||||
|
|
||||||
durationUs = inputLength == C.LENGTH_UNBOUNDED ? C.UNSET_TIME_US
|
setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS,
|
||||||
: (totalSamples * C.MICROS_PER_SECOND) / vorbisSetup.idHeader.sampleRate;
|
this.vorbisSetup.idHeader.bitrateNominal, OggPageHeader.MAX_PAGE_PAYLOAD,
|
||||||
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,
|
this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
|
||||||
codecInitialisationData, null, 0, null));
|
codecInitialisationData, null, 0, null);
|
||||||
|
return true;
|
||||||
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
|
|
||||||
if (!seenFirstAudioPacket && targetGranule > -1) {
|
|
||||||
OggUtil.skipToNextPage(input);
|
|
||||||
long position = oggSeeker.getNextSeekPosition(targetGranule, input);
|
|
||||||
if (position != -1) {
|
|
||||||
seekPosition.position = position;
|
|
||||||
return Extractor.RESULT_SEEK;
|
|
||||||
} else {
|
|
||||||
elapsedSamples = oggParser.skipToPageOfGranule(input, targetGranule);
|
|
||||||
previousPacketBlockSize = vorbisIdHeader.blockSize0;
|
|
||||||
// we're never at the first packet after seeking
|
|
||||||
seenFirstAudioPacket = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// playback
|
|
||||||
if (oggParser.readPacket(input, scratch)) {
|
|
||||||
// if this is an audio packet...
|
|
||||||
if ((scratch.data[0] & 0x01) != 1) {
|
|
||||||
// ... we need to decode the block size
|
|
||||||
int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup);
|
|
||||||
// a packet contains samples produced from overlapping the previous and current frame data
|
|
||||||
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
|
|
||||||
int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4
|
|
||||||
: 0;
|
|
||||||
if (elapsedSamples + samplesInPacket >= targetGranule) {
|
|
||||||
// codec expects the number of samples appended to audio data
|
|
||||||
appendNumberOfSamples(scratch, samplesInPacket);
|
|
||||||
// calculate time and send audio data to codec
|
|
||||||
long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
|
|
||||||
trackOutput.sampleData(scratch, scratch.limit());
|
|
||||||
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, scratch.limit(), 0, null);
|
|
||||||
targetGranule = -1;
|
|
||||||
}
|
|
||||||
// update state in members for next iteration
|
|
||||||
seenFirstAudioPacket = true;
|
|
||||||
elapsedSamples += samplesInPacket;
|
|
||||||
previousPacketBlockSize = packetBlockSize;
|
|
||||||
}
|
|
||||||
scratch.reset();
|
|
||||||
return Extractor.RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
return Extractor.RESULT_END_OF_INPUT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//@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.
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user