mirror of
https://github.com/androidx/media.git
synced 2025-05-04 22:20:47 +08:00
Make OggVorbisExtractor seekable.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=117252304
This commit is contained in:
parent
4ffa3556dd
commit
a067bd0965
@ -15,8 +15,9 @@
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor.ogg;
|
||||
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException;
|
||||
import com.google.android.exoplayer.testutil.TestUtil;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
@ -24,12 +25,13 @@ import android.test.MoreAsserts;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Unit test for {@link OggReader}
|
||||
* Unit test for {@link OggReader}.
|
||||
*/
|
||||
public final class OggReaderTest extends TestCase {
|
||||
|
||||
@ -51,7 +53,7 @@ public final class OggReaderTest extends TestCase {
|
||||
byte[] thirdPacket = TestUtil.buildTestData(256, random);
|
||||
byte[] fourthPacket = TestUtil.buildTestData(271, random);
|
||||
|
||||
FakeExtractorInput input = createInput(
|
||||
FakeExtractorInput input = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
// First page with a single packet.
|
||||
TestData.buildOggHeader(0x02, 0, 1000, 0x01),
|
||||
@ -67,7 +69,7 @@ public final class OggReaderTest extends TestCase {
|
||||
TestData.buildOggHeader(0x04, 128, 1003, 0x04),
|
||||
TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces
|
||||
thirdPacket,
|
||||
fourthPacket));
|
||||
fourthPacket), true);
|
||||
|
||||
assertReadPacket(input, firstPacket);
|
||||
assertTrue((oggReader.getPageHeader().type & 0x02) == 0x02);
|
||||
@ -111,12 +113,12 @@ public final class OggReaderTest extends TestCase {
|
||||
byte[] firstPacket = TestUtil.buildTestData(255, random);
|
||||
byte[] secondPacket = TestUtil.buildTestData(8, random);
|
||||
|
||||
FakeExtractorInput input = createInput(
|
||||
FakeExtractorInput input = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x06, 0, 1000, 0x04),
|
||||
TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces.
|
||||
firstPacket,
|
||||
secondPacket));
|
||||
secondPacket), true);
|
||||
|
||||
assertReadPacket(input, firstPacket);
|
||||
assertReadPacket(input, secondPacket);
|
||||
@ -126,7 +128,7 @@ public final class OggReaderTest extends TestCase {
|
||||
public void testReadContinuedPacketOverTwoPages() throws Exception {
|
||||
byte[] firstPacket = TestUtil.buildTestData(518);
|
||||
|
||||
FakeExtractorInput input = createInput(
|
||||
FakeExtractorInput input = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
// First page.
|
||||
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
|
||||
@ -135,7 +137,7 @@ public final class OggReaderTest extends TestCase {
|
||||
// Second page (continued packet).
|
||||
TestData.buildOggHeader(0x05, 10, 1001, 0x01),
|
||||
TestUtil.createByteArray(0x08), // Laces.
|
||||
Arrays.copyOfRange(firstPacket, 510, 510 + 8)));
|
||||
Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true);
|
||||
|
||||
assertReadPacket(input, firstPacket);
|
||||
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04);
|
||||
@ -148,7 +150,7 @@ public final class OggReaderTest extends TestCase {
|
||||
public void testReadContinuedPacketOverFourPages() throws Exception {
|
||||
byte[] firstPacket = TestUtil.buildTestData(1028);
|
||||
|
||||
FakeExtractorInput input = createInput(
|
||||
FakeExtractorInput input = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
// First page.
|
||||
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
|
||||
@ -165,7 +167,7 @@ public final class OggReaderTest extends TestCase {
|
||||
// Fourth page (continued packet).
|
||||
TestData.buildOggHeader(0x05, 10, 1003, 0x01),
|
||||
TestUtil.createByteArray(0x08), // Laces.
|
||||
Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)));
|
||||
Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true);
|
||||
|
||||
assertReadPacket(input, firstPacket);
|
||||
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04);
|
||||
@ -175,12 +177,27 @@ public final class OggReaderTest extends TestCase {
|
||||
assertReadEof(input);
|
||||
}
|
||||
|
||||
public void testReadDiscardContinuedPacketAtStart() throws Exception {
|
||||
byte[] pageBody = TestUtil.buildTestData(256 + 8);
|
||||
|
||||
FakeExtractorInput input = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
// Page with a continued packet at start.
|
||||
TestData.buildOggHeader(0x01, 10, 1001, 0x03),
|
||||
TestUtil.createByteArray(255, 1, 8), // Laces.
|
||||
pageBody), true);
|
||||
|
||||
// Expect the first partial packet to be discarded.
|
||||
assertReadPacket(input, Arrays.copyOfRange(pageBody, 256, 256 + 8));
|
||||
assertReadEof(input);
|
||||
}
|
||||
|
||||
public void testReadZeroSizedPacketsAtEndOfStream() throws Exception {
|
||||
byte[] firstPacket = TestUtil.buildTestData(8, random);
|
||||
byte[] secondPacket = TestUtil.buildTestData(8, random);
|
||||
byte[] thirdPacket = TestUtil.buildTestData(8, random);
|
||||
|
||||
FakeExtractorInput input = createInput(
|
||||
FakeExtractorInput input = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x02, 0, 1000, 0x01),
|
||||
TestUtil.createByteArray(0x08), // Laces.
|
||||
@ -190,7 +207,7 @@ public final class OggReaderTest extends TestCase {
|
||||
secondPacket,
|
||||
TestData.buildOggHeader(0x04, 0, 1002, 0x03),
|
||||
TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces.
|
||||
thirdPacket));
|
||||
thirdPacket), true);
|
||||
|
||||
assertReadPacket(input, firstPacket);
|
||||
assertReadPacket(input, secondPacket);
|
||||
@ -198,9 +215,127 @@ public final class OggReaderTest extends TestCase {
|
||||
assertReadEof(input);
|
||||
}
|
||||
|
||||
private static FakeExtractorInput createInput(byte[] data) {
|
||||
return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true)
|
||||
.setSimulateUnknownLength(true).setSimulatePartialReads(true).build();
|
||||
public void testSkipToPageOfGranule() throws IOException, InterruptedException {
|
||||
byte[] packet = TestUtil.buildTestData(3 * 254, random);
|
||||
FakeExtractorInput input = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
|
||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
||||
packet,
|
||||
TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
|
||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
||||
packet,
|
||||
TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
|
||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
||||
packet), false);
|
||||
|
||||
// expect to be granule of the previous page returned as elapsedSamples
|
||||
skipToPageOfGranule(input, 54000, 40000);
|
||||
// expect to be at the start of the third page
|
||||
assertEquals(2 * (30 + (3 * 254)), input.getPosition());
|
||||
}
|
||||
|
||||
public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException {
|
||||
byte[] packet = TestUtil.buildTestData(3 * 254, random);
|
||||
FakeExtractorInput input = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
|
||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
||||
packet,
|
||||
TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
|
||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
||||
packet,
|
||||
TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
|
||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
||||
packet), false);
|
||||
|
||||
skipToPageOfGranule(input, 40000, 20000);
|
||||
// expect to be at the start of the second page
|
||||
assertEquals((30 + (3 * 254)), input.getPosition());
|
||||
}
|
||||
|
||||
public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException {
|
||||
byte[] packet = TestUtil.buildTestData(3 * 254, random);
|
||||
FakeExtractorInput input = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
|
||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
||||
packet,
|
||||
TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
|
||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
||||
packet,
|
||||
TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
|
||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
||||
packet), false);
|
||||
|
||||
try {
|
||||
skipToPageOfGranule(input, 10000, 20000);
|
||||
fail();
|
||||
} catch (ParserException e) {
|
||||
// ignored
|
||||
}
|
||||
assertEquals(0, input.getPosition());
|
||||
}
|
||||
|
||||
private void skipToPageOfGranule(ExtractorInput input, long granule,
|
||||
long elapsedSamplesExpected) throws IOException, InterruptedException {
|
||||
while (true) {
|
||||
try {
|
||||
assertEquals(elapsedSamplesExpected, oggReader.skipToPageOfGranule(input, granule));
|
||||
return;
|
||||
} catch (FakeExtractorInput.SimulatedIOException e) {
|
||||
input.resetPeekPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testReadGranuleOfLastPage() throws IOException, InterruptedException {
|
||||
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
|
||||
TestUtil.buildTestData(100, random),
|
||||
TestData.buildOggHeader(0x00, 20000, 66, 3),
|
||||
TestUtil.createByteArray(254, 254, 254), // laces
|
||||
TestUtil.buildTestData(3 * 254, random),
|
||||
TestData.buildOggHeader(0x00, 40000, 67, 3),
|
||||
TestUtil.createByteArray(254, 254, 254), // laces
|
||||
TestUtil.buildTestData(3 * 254, random),
|
||||
TestData.buildOggHeader(0x05, 60000, 68, 3),
|
||||
TestUtil.createByteArray(254, 254, 254), // laces
|
||||
TestUtil.buildTestData(3 * 254, random)
|
||||
), false);
|
||||
assertReadGranuleOfLastPage(input, 60000);
|
||||
}
|
||||
|
||||
public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException {
|
||||
FakeExtractorInput input = TestData.createInput(TestUtil.buildTestData(100, random), false);
|
||||
try {
|
||||
assertReadGranuleOfLastPage(input, 60000);
|
||||
fail();
|
||||
} catch (EOFException e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
public void testReadGranuleOfLastPageWithUnboundedLength()
|
||||
throws IOException, InterruptedException {
|
||||
FakeExtractorInput input = TestData.createInput(new byte[0], true);
|
||||
try {
|
||||
assertReadGranuleOfLastPage(input, 60000);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected)
|
||||
throws IOException, InterruptedException {
|
||||
while (true) {
|
||||
try {
|
||||
assertEquals(expected, oggReader.readGranuleOfLastPage(input));
|
||||
break;
|
||||
} catch (FakeExtractorInput.SimulatedIOException e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertReadPacket(FakeExtractorInput extractorInput, byte[] expected)
|
||||
@ -221,7 +356,7 @@ public final class OggReaderTest extends TestCase {
|
||||
while (true) {
|
||||
try {
|
||||
return oggReader.readPacket(input, scratch);
|
||||
} catch (SimulatedIOException e) {
|
||||
} catch (FakeExtractorInput.SimulatedIOException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor.ogg;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer.testutil.TestUtil;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Unit test for {@link OggSeeker}.
|
||||
*/
|
||||
public final class OggSeekerTest extends TestCase {
|
||||
|
||||
private OggSeeker oggSeeker;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
oggSeeker = new OggSeeker();
|
||||
oggSeeker.setup(1, 1);
|
||||
}
|
||||
|
||||
public void testSetupUnboundAudioLength() {
|
||||
try {
|
||||
new OggSeeker().setup(C.LENGTH_UNBOUNDED, 1000);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
public void testSetupZeroOrNegativeTotalSamples() {
|
||||
try {
|
||||
new OggSeeker().setup(1000, 0);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// ignored
|
||||
}
|
||||
try {
|
||||
new OggSeeker().setup(1000, -1000);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
public void testGetNextSeekPositionSetupNotCalled() throws IOException, InterruptedException {
|
||||
try {
|
||||
new OggSeeker().getNextSeekPosition(1000, TestData.createInput(new byte[0], false));
|
||||
fail();
|
||||
} catch (IllegalStateException e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
public void testGetNextSeekPositionMatch() throws IOException, InterruptedException {
|
||||
long targetGranule = 100000;
|
||||
long headerGranule = 52001;
|
||||
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x00, headerGranule, 22, 2),
|
||||
TestUtil.createByteArray(54, 55) // laces
|
||||
), false);
|
||||
long expectedPosition = -1;
|
||||
assertGetNextSeekPosition(expectedPosition, targetGranule, input);
|
||||
}
|
||||
|
||||
public void testGetNextSeekPositionTooHigh() throws IOException, InterruptedException {
|
||||
long targetGranule = 100000;
|
||||
long headerGranule = 200000;
|
||||
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x00, headerGranule, 22, 2),
|
||||
TestUtil.createByteArray(54, 55) // laces
|
||||
), false);
|
||||
long doublePageSize = 2 * (input.getLength() + 54 + 55);
|
||||
long expectedPosition = -doublePageSize + (targetGranule - headerGranule);
|
||||
assertGetNextSeekPosition(expectedPosition, targetGranule, input);
|
||||
}
|
||||
|
||||
public void testGetNextSeekPositionTooHighDistanceLower48000()
|
||||
throws IOException, InterruptedException {
|
||||
long targetGranule = 199999;
|
||||
long headerGranule = 200000;
|
||||
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x00, headerGranule, 22, 2),
|
||||
TestUtil.createByteArray(54, 55) // laces
|
||||
), false);
|
||||
long doublePageSize = 2 * (input.getLength() + 54 + 55);
|
||||
long expectedPosition = -doublePageSize - 1;
|
||||
assertGetNextSeekPosition(expectedPosition, targetGranule, input);
|
||||
}
|
||||
|
||||
public void testGetNextSeekPositionTooLow() throws IOException, InterruptedException {
|
||||
long headerGranule = 200000;
|
||||
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x00, headerGranule, 22, 2),
|
||||
TestUtil.createByteArray(54, 55) // laces
|
||||
), false);
|
||||
long targetGranule = 300000;
|
||||
long expectedPosition = -(27 + 2 + 54 + 55) + (targetGranule - headerGranule);
|
||||
assertGetNextSeekPosition(expectedPosition, targetGranule, input);
|
||||
}
|
||||
|
||||
private void assertGetNextSeekPosition(long expectedPosition, long targetGranule,
|
||||
FakeExtractorInput input) throws IOException, InterruptedException {
|
||||
while (true) {
|
||||
try {
|
||||
assertEquals(expectedPosition, oggSeeker.getNextSeekPosition(targetGranule, input));
|
||||
break;
|
||||
} catch (FakeExtractorInput.SimulatedIOException e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor.ogg;
|
||||
|
||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException;
|
||||
import com.google.android.exoplayer.testutil.TestUtil;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Unit test for {@link OggUtil}.
|
||||
*/
|
||||
public final class OggUtilTest extends TestCase {
|
||||
|
||||
private Random random = new Random(0);
|
||||
|
||||
public void testReadBits() throws Exception {
|
||||
assertEquals(0, OggUtil.readBits((byte) 0x00, 2, 2));
|
||||
assertEquals(1, OggUtil.readBits((byte) 0x02, 1, 1));
|
||||
assertEquals(15, OggUtil.readBits((byte) 0xF0, 4, 4));
|
||||
assertEquals(1, OggUtil.readBits((byte) 0x80, 1, 7));
|
||||
}
|
||||
|
||||
public void testPopulatePageHeader() throws IOException, InterruptedException {
|
||||
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x01, 123456, 4, 2),
|
||||
TestUtil.createByteArray(2, 2)
|
||||
), true);
|
||||
OggUtil.PageHeader header = new OggUtil.PageHeader();
|
||||
ParsableByteArray byteArray = new ParsableByteArray(27 + 2);
|
||||
populatePageHeader(input, header, byteArray, false);
|
||||
|
||||
assertEquals(0x01, header.type);
|
||||
assertEquals(27 + 2, header.headerSize);
|
||||
assertEquals(4, header.bodySize);
|
||||
assertEquals(2, header.pageSegmentCount);
|
||||
assertEquals(123456, header.granulePosition);
|
||||
assertEquals(4, header.pageSequenceNumber);
|
||||
assertEquals(0x1000, header.streamSerialNumber);
|
||||
assertEquals(0x100000, header.pageChecksum);
|
||||
assertEquals(0, header.revision);
|
||||
}
|
||||
|
||||
public void testPopulatePageHeaderQuiteOnExceptionLessThan27Bytes()
|
||||
throws IOException, InterruptedException {
|
||||
FakeExtractorInput input = TestData.createInput(TestUtil.createByteArray(2, 2), false);
|
||||
OggUtil.PageHeader header = new OggUtil.PageHeader();
|
||||
ParsableByteArray byteArray = new ParsableByteArray(27 + 2);
|
||||
assertFalse(populatePageHeader(input, header, byteArray, true));
|
||||
}
|
||||
|
||||
public void testPopulatePageHeaderQuiteOnExceptionNotOgg()
|
||||
throws IOException, InterruptedException {
|
||||
byte[] headerBytes = TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x01, 123456, 4, 2),
|
||||
TestUtil.createByteArray(2, 2)
|
||||
);
|
||||
// change from 'O' to 'o'
|
||||
headerBytes[0] = 'o';
|
||||
FakeExtractorInput input = TestData.createInput(headerBytes, false);
|
||||
OggUtil.PageHeader header = new OggUtil.PageHeader();
|
||||
ParsableByteArray byteArray = new ParsableByteArray(27 + 2);
|
||||
assertFalse(populatePageHeader(input, header, byteArray, true));
|
||||
}
|
||||
|
||||
public void testPopulatePageHeaderQuiteOnExceptionWrongRevision()
|
||||
throws IOException, InterruptedException {
|
||||
byte[] headerBytes = TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x01, 123456, 4, 2),
|
||||
TestUtil.createByteArray(2, 2)
|
||||
);
|
||||
// change revision from 0 to 1
|
||||
headerBytes[4] = 0x01;
|
||||
FakeExtractorInput input = TestData.createInput(headerBytes, false);
|
||||
OggUtil.PageHeader header = new OggUtil.PageHeader();
|
||||
ParsableByteArray byteArray = new ParsableByteArray(27 + 2);
|
||||
assertFalse(populatePageHeader(input, header, byteArray, true));
|
||||
}
|
||||
|
||||
private boolean populatePageHeader(FakeExtractorInput input, OggUtil.PageHeader header,
|
||||
ParsableByteArray byteArray, boolean quite) throws IOException, InterruptedException {
|
||||
while (true) {
|
||||
try {
|
||||
return OggUtil.populatePageHeader(input, header, byteArray, quite);
|
||||
} catch (SimulatedIOException e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testSkipToNextPage() throws Exception {
|
||||
FakeExtractorInput extractorInput = createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestUtil.buildTestData(4000, random),
|
||||
new byte[]{'O', 'g', 'g', 'S'},
|
||||
TestUtil.buildTestData(4000, random)
|
||||
), false);
|
||||
skipToNextPage(extractorInput);
|
||||
assertEquals(4000, extractorInput.getPosition());
|
||||
}
|
||||
|
||||
public void testSkipToNextPageUnbounded() throws Exception {
|
||||
FakeExtractorInput extractorInput = createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestUtil.buildTestData(4000, random),
|
||||
new byte[]{'O', 'g', 'g', 'S'},
|
||||
TestUtil.buildTestData(4000, random)
|
||||
), true);
|
||||
skipToNextPage(extractorInput);
|
||||
assertEquals(4000, extractorInput.getPosition());
|
||||
}
|
||||
|
||||
public void testSkipToNextPageOverlap() throws Exception {
|
||||
FakeExtractorInput extractorInput = createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestUtil.buildTestData(2046, random),
|
||||
new byte[]{'O', 'g', 'g', 'S'},
|
||||
TestUtil.buildTestData(4000, random)
|
||||
), false);
|
||||
skipToNextPage(extractorInput);
|
||||
assertEquals(2046, extractorInput.getPosition());
|
||||
}
|
||||
|
||||
public void testSkipToNextPageOverlapUnbounded() throws Exception {
|
||||
FakeExtractorInput extractorInput = createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestUtil.buildTestData(2046, random),
|
||||
new byte[]{'O', 'g', 'g', 'S'},
|
||||
TestUtil.buildTestData(4000, random)
|
||||
), true);
|
||||
skipToNextPage(extractorInput);
|
||||
assertEquals(2046, extractorInput.getPosition());
|
||||
}
|
||||
|
||||
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
|
||||
FakeExtractorInput extractorInput = createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
new byte[]{'x', 'O', 'g', 'g', 'S'}
|
||||
), false);
|
||||
skipToNextPage(extractorInput);
|
||||
assertEquals(1, extractorInput.getPosition());
|
||||
}
|
||||
|
||||
public void testSkipToNextPageNoMatch() throws Exception {
|
||||
FakeExtractorInput extractorInput = createInput(new byte[]{'g', 'g', 'S', 'O', 'g', 'g'},
|
||||
false);
|
||||
try {
|
||||
skipToNextPage(extractorInput);
|
||||
fail();
|
||||
} catch (EOFException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
private static void skipToNextPage(ExtractorInput extractorInput)
|
||||
throws IOException, InterruptedException {
|
||||
while (true) {
|
||||
try {
|
||||
OggUtil.skipToNextPage(extractorInput);
|
||||
break;
|
||||
} catch (SimulatedIOException e) { /* ignored */ }
|
||||
}
|
||||
}
|
||||
|
||||
private static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) {
|
||||
return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true)
|
||||
.setSimulateUnknownLength(simulateUnknownLength).setSimulatePartialReads(true).build();
|
||||
}
|
||||
}
|
||||
|
@ -43,15 +43,36 @@ public final class OggVorbisExtractorTest extends TestCase {
|
||||
public void testSniff() throws Exception {
|
||||
byte[] data = TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
|
||||
TestUtil.createByteArray(120, 120)); // Laces
|
||||
TestUtil.createByteArray(120, 120), // Laces
|
||||
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'});
|
||||
assertTrue(sniff(createInput(data)));
|
||||
}
|
||||
|
||||
public void testSniffFails() throws Exception {
|
||||
public void testSniffFailsOpusFile() throws Exception {
|
||||
byte[] data = TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x02, 0, 1000, 0x00),
|
||||
new byte[]{'O', 'p', 'u', 's'});
|
||||
assertFalse(sniff(createInput(data)));
|
||||
}
|
||||
|
||||
public void testSniffFailsInvalidOggHeader() throws Exception {
|
||||
byte[] data = TestData.buildOggHeader(0x00, 0, 1000, 0x00);
|
||||
assertFalse(sniff(createInput(data)));
|
||||
}
|
||||
|
||||
public void testSniffInvalidVorbisHeader() throws Exception {
|
||||
byte[] data = TestUtil.joinByteArrays(
|
||||
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
|
||||
TestUtil.createByteArray(120, 120), // Laces
|
||||
new byte[]{0x01, 'X', 'o', 'r', 'b', 'i', 's'});
|
||||
assertFalse(sniff(createInput(data)));
|
||||
}
|
||||
|
||||
public void testSniffFailsEOF() throws Exception {
|
||||
byte[] data = TestData.buildOggHeader(0x02, 0, 1000, 0x00);
|
||||
assertFalse(sniff(createInput(data)));
|
||||
}
|
||||
|
||||
public void testAppendNumberOfSamples() throws Exception {
|
||||
ParsableByteArray buffer = new ParsableByteArray(4);
|
||||
buffer.setLimit(0);
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor.ogg;
|
||||
|
||||
import com.google.android.exoplayer.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer.testutil.TestUtil;
|
||||
|
||||
/**
|
||||
@ -22,6 +23,11 @@ import com.google.android.exoplayer.testutil.TestUtil;
|
||||
*/
|
||||
/* package */ final class TestData {
|
||||
|
||||
/* package */ static FakeExtractorInput createInput(byte[] data, boolean simulateUnkownLength) {
|
||||
return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true)
|
||||
.setSimulateUnknownLength(simulateUnkownLength).setSimulatePartialReads(true).build();
|
||||
}
|
||||
|
||||
public static byte[] buildOggHeader(int headerType, long granule, int pageSequenceCounter,
|
||||
int pageSegmentCount) {
|
||||
return TestUtil.createByteArray(
|
||||
@ -46,7 +52,7 @@ import com.google.android.exoplayer.testutil.TestUtil;
|
||||
(pageSequenceCounter >> 24) & 0xFF,
|
||||
0x00, // LSB of page checksum.
|
||||
0x00,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00, // MSB of page checksum.
|
||||
pageSegmentCount);
|
||||
}
|
||||
|
@ -38,13 +38,6 @@ public final class VorbisUtilTest extends TestCase {
|
||||
assertEquals(0, VorbisUtil.iLog(-122));
|
||||
}
|
||||
|
||||
public void testReadBits() throws Exception {
|
||||
assertEquals(0, VorbisUtil.readBits((byte) 0x00, 2, 2));
|
||||
assertEquals(1, VorbisUtil.readBits((byte) 0x02, 1, 1));
|
||||
assertEquals(15, VorbisUtil.readBits((byte) 0xF0, 4, 4));
|
||||
assertEquals(1, VorbisUtil.readBits((byte) 0x80, 1, 7));
|
||||
}
|
||||
|
||||
public void testReadIdHeader() throws Exception {
|
||||
byte[] data = TestData.getIdentificationHeaderData();
|
||||
ParsableByteArray headerData = new ParsableByteArray(data, data.length);
|
||||
@ -95,4 +88,45 @@ public final class VorbisUtilTest extends TestCase {
|
||||
assertEquals(0, modes[1].windowType);
|
||||
}
|
||||
|
||||
public void testVerifyVorbisHeaderCapturePattern() throws ParserException {
|
||||
ParsableByteArray header = new ParsableByteArray(
|
||||
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'});
|
||||
assertEquals(true, VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false));
|
||||
}
|
||||
|
||||
public void testVerifyVorbisHeaderCapturePatternInvalidHeader() throws ParserException {
|
||||
ParsableByteArray header = new ParsableByteArray(
|
||||
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'});
|
||||
try {
|
||||
VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, false);
|
||||
fail();
|
||||
} catch (ParserException e) {
|
||||
assertEquals("expected header type 99", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void testVerifyVorbisHeaderCapturePatternInvalidHeaderQuite() throws ParserException {
|
||||
ParsableByteArray header = new ParsableByteArray(
|
||||
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'});
|
||||
assertFalse(VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, true));
|
||||
}
|
||||
|
||||
public void testVerifyVorbisHeaderCapturePatternInvalidPattern() throws ParserException {
|
||||
ParsableByteArray header = new ParsableByteArray(
|
||||
new byte[]{0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'});
|
||||
try {
|
||||
VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false);
|
||||
fail();
|
||||
} catch (ParserException e) {
|
||||
assertEquals("expected characters 'vorbis'", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void testVerifyVorbisHeaderCapturePatternQuiteInvalidPatternQuite()
|
||||
throws ParserException {
|
||||
ParsableByteArray header = new ParsableByteArray(
|
||||
new byte[]{0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'});
|
||||
assertFalse(VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, true));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,11 +15,12 @@
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor.ogg;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer.extractor.ogg.OggUtil.PacketInfoHolder;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@ -28,12 +29,14 @@ import java.io.IOException;
|
||||
*/
|
||||
/* package */ final class OggReader {
|
||||
|
||||
private static final String CAPTURE_PATTERN_PAGE = "OggS";
|
||||
public static final int OGG_MAX_SEGMENT_SIZE = 255;
|
||||
|
||||
private final PageHeader pageHeader = new PageHeader();
|
||||
private final OggUtil.PageHeader pageHeader = new OggUtil.PageHeader();
|
||||
private final ParsableByteArray headerArray = new ParsableByteArray(27 + 255);
|
||||
private final PacketInfoHolder holder = new PacketInfoHolder();
|
||||
|
||||
private int currentSegmentIndex = -1;
|
||||
private long elapsedSamples;
|
||||
|
||||
/**
|
||||
* Resets this reader.
|
||||
@ -65,37 +68,99 @@ import java.io.IOException;
|
||||
while (!packetComplete) {
|
||||
if (currentSegmentIndex < 0) {
|
||||
// We're at the start of a page.
|
||||
if (!populatePageHeader(input, pageHeader, headerArray, false)) {
|
||||
if (!OggUtil.populatePageHeader(input, pageHeader, headerArray, true)) {
|
||||
return false;
|
||||
}
|
||||
currentSegmentIndex = 0;
|
||||
}
|
||||
|
||||
int packetSize = 0;
|
||||
int segmentIndex = currentSegmentIndex;
|
||||
// add up packetSize from laces
|
||||
while (segmentIndex < pageHeader.pageSegmentCount) {
|
||||
int segmentLength = pageHeader.laces[segmentIndex++];
|
||||
packetSize += segmentLength;
|
||||
if (segmentLength != 255) {
|
||||
// packets end at first lace < 255
|
||||
break;
|
||||
int segmentIndex = 0;
|
||||
int bytesToSkip = pageHeader.headerSize;
|
||||
if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) {
|
||||
// After seeking, the first packet may be the remainder
|
||||
// part of a continued packet which has to be discarded.
|
||||
OggUtil.calculatePacketSize(pageHeader, segmentIndex, holder);
|
||||
segmentIndex += holder.segmentCount;
|
||||
bytesToSkip += holder.size;
|
||||
}
|
||||
input.skipFully(bytesToSkip);
|
||||
currentSegmentIndex = segmentIndex;
|
||||
}
|
||||
|
||||
if (packetSize > 0) {
|
||||
input.readFully(packetArray.data, packetArray.limit(), packetSize);
|
||||
packetArray.setLimit(packetArray.limit() + packetSize);
|
||||
OggUtil.calculatePacketSize(pageHeader, currentSegmentIndex, holder);
|
||||
int segmentIndex = currentSegmentIndex + holder.segmentCount;
|
||||
if (holder.size > 0) {
|
||||
input.readFully(packetArray.data, packetArray.limit(), holder.size);
|
||||
packetArray.setLimit(packetArray.limit() + holder.size);
|
||||
packetComplete = pageHeader.laces[segmentIndex - 1] != 255;
|
||||
}
|
||||
// advance now since we are sure reading didn't throw an exception
|
||||
currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? -1 : segmentIndex;
|
||||
currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? -1
|
||||
: segmentIndex;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OggReader.PageHeader} of the current page. The header might not have been
|
||||
* Skips to the last Ogg page in the stream and reads the header's granule field which is the
|
||||
* total number of samples per channel.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} to read from.
|
||||
* @return the total number of samples of this input.
|
||||
* @throws IOException thrown if reading from the input fails.
|
||||
* @throws InterruptedException thrown if interrupted while reading from the input.
|
||||
*/
|
||||
public long readGranuleOfLastPage(ExtractorInput input)
|
||||
throws IOException, InterruptedException {
|
||||
Assertions.checkArgument(input.getLength() != C.LENGTH_UNBOUNDED); // never read forever!
|
||||
OggUtil.skipToNextPage(input);
|
||||
pageHeader.reset();
|
||||
while ((pageHeader.type & 0x04) != 0x04) {
|
||||
if (pageHeader.bodySize > 0) {
|
||||
input.skipFully(pageHeader.bodySize);
|
||||
}
|
||||
OggUtil.populatePageHeader(input, pageHeader, headerArray, false);
|
||||
input.skipFully(pageHeader.headerSize);
|
||||
}
|
||||
return pageHeader.granulePosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips to the position of the start of the page containing the {@code targetGranule} and
|
||||
* returns the elapsed samples which is the granule of the page previous to the target page.
|
||||
* <p>
|
||||
* Note that the position of the {@code input} must be before the start of the page previous to
|
||||
* the page containing the targetGranule to get the correct number of elapsed samples.
|
||||
* Which is in short like: {@code pos(input) <= pos(targetPage.pageSequence - 1)}.
|
||||
*
|
||||
* @param input the {@link ExtractorInput} to read from.
|
||||
* @param targetGranule the target granule (number of frames per channel).
|
||||
* @return the number of elapsed samples at the start of the target page.
|
||||
* @throws ParserException thrown if populating the page header fails.
|
||||
* @throws IOException thrown if reading from the input fails.
|
||||
* @throws InterruptedException thrown if interrupted while reading from the input.
|
||||
*/
|
||||
public long skipToPageOfGranule(ExtractorInput input, long targetGranule)
|
||||
throws IOException, InterruptedException {
|
||||
OggUtil.skipToNextPage(input);
|
||||
OggUtil.populatePageHeader(input, pageHeader, headerArray, false);
|
||||
while (pageHeader.granulePosition < targetGranule) {
|
||||
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
|
||||
// Store in a member field to be able to resume after IOExceptions.
|
||||
elapsedSamples = pageHeader.granulePosition;
|
||||
// Peek next header.
|
||||
OggUtil.populatePageHeader(input, pageHeader, headerArray, false);
|
||||
}
|
||||
if (elapsedSamples == 0) {
|
||||
throw new ParserException();
|
||||
}
|
||||
input.resetPeekPosition();
|
||||
long returnValue = elapsedSamples;
|
||||
// Reset member state.
|
||||
elapsedSamples = 0;
|
||||
currentSegmentIndex = -1;
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OggUtil.PageHeader} of the current page. The header might not have been
|
||||
* populated if the first packet has yet to be read.
|
||||
* <p>
|
||||
* Note that there is only a single instance of {@code OggReader.PageHeader} which is mutable.
|
||||
@ -104,93 +169,8 @@ import java.io.IOException;
|
||||
*
|
||||
* @return the {@code PageHeader} of the current page or {@code null}.
|
||||
*/
|
||||
public PageHeader getPageHeader() {
|
||||
public OggUtil.PageHeader getPageHeader() {
|
||||
return pageHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads/peeks an Ogg page header and stores the data in the {@code header} object passed
|
||||
* as argument.
|
||||
*
|
||||
* @param input the {@link ExtractorInput} to read from.
|
||||
* @param header the {@link PageHeader} to read from.
|
||||
* @param scratch a scratch array temporary use.
|
||||
* @param peek pass {@code true} if data should only be peeked from current peek position.
|
||||
* @return {@code true} if the read was successful. {@code false} if the end of the
|
||||
* input was encountered having read no data.
|
||||
* @throws IOException thrown if reading data fails or the stream is invalid.
|
||||
* @throws InterruptedException thrown if thread is interrupted when reading/peeking.
|
||||
*/
|
||||
public static boolean populatePageHeader(ExtractorInput input, PageHeader header,
|
||||
ParsableByteArray scratch, boolean peek) throws IOException, InterruptedException {
|
||||
|
||||
scratch.reset();
|
||||
header.reset();
|
||||
if (!input.peekFully(scratch.data, 0, 27, true)) {
|
||||
return false;
|
||||
}
|
||||
if (scratch.readUnsignedInt() != Util.getIntegerCodeForString(CAPTURE_PATTERN_PAGE)) {
|
||||
throw new ParserException("expected OggS capture pattern at begin of page");
|
||||
}
|
||||
|
||||
header.revision = scratch.readUnsignedByte();
|
||||
if (header.revision != 0x00) {
|
||||
throw new ParserException("unsupported bit stream revision");
|
||||
}
|
||||
header.type = scratch.readUnsignedByte();
|
||||
|
||||
header.granulePosition = scratch.readLittleEndianLong();
|
||||
header.streamSerialNumber = scratch.readLittleEndianUnsignedInt();
|
||||
header.pageSequenceNumber = scratch.readLittleEndianUnsignedInt();
|
||||
header.pageChecksum = scratch.readLittleEndianUnsignedInt();
|
||||
header.pageSegmentCount = scratch.readUnsignedByte();
|
||||
|
||||
scratch.reset();
|
||||
// calculate total size of header including laces
|
||||
header.headerSize = 27 + header.pageSegmentCount;
|
||||
input.peekFully(scratch.data, 0, header.pageSegmentCount);
|
||||
for (int i = 0; i < header.pageSegmentCount; i++) {
|
||||
header.laces[i] = scratch.readUnsignedByte();
|
||||
header.bodySize += header.laces[i];
|
||||
}
|
||||
if (!peek) {
|
||||
input.skipFully(header.headerSize);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object to store header information. Be aware that {@code laces.length} is always 255.
|
||||
* Instead use {@code pageSegmentCount} to iterate.
|
||||
*/
|
||||
public static final class PageHeader {
|
||||
|
||||
public int revision;
|
||||
public int type;
|
||||
public long granulePosition;
|
||||
public long streamSerialNumber;
|
||||
public long pageSequenceNumber;
|
||||
public long pageChecksum;
|
||||
public int pageSegmentCount;
|
||||
public int headerSize;
|
||||
public int bodySize;
|
||||
public int[] laces = new int[255];
|
||||
|
||||
/**
|
||||
* Resets all primitive member fields to zero.
|
||||
*/
|
||||
public void reset() {
|
||||
revision = 0;
|
||||
type = 0;
|
||||
granulePosition = 0;
|
||||
streamSerialNumber = 0;
|
||||
pageSequenceNumber = 0;
|
||||
pageChecksum = 0;
|
||||
pageSegmentCount = 0;
|
||||
headerSize = 0;
|
||||
bodySize = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor.ogg;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Used to seek in an Ogg stream.
|
||||
*/
|
||||
/* package */ final class OggSeeker {
|
||||
|
||||
private static final int MATCH_RANGE = 72000;
|
||||
|
||||
private final OggUtil.PageHeader pageHeader = new OggUtil.PageHeader();
|
||||
private final ParsableByteArray headerArray = new ParsableByteArray(27 + 255);
|
||||
private long audioDataLength = C.LENGTH_UNBOUNDED;
|
||||
private long totalSamples;
|
||||
|
||||
/**
|
||||
* Setup the seeker with the data it needs to to an educated guess of seeking positions.
|
||||
*
|
||||
* @param audioDataLength the length of the audio data (total bytes - header bytes).
|
||||
* @param totalSamples the total number of samples of audio data.
|
||||
*/
|
||||
public void setup(long audioDataLength, long totalSamples) {
|
||||
Assertions.checkArgument(audioDataLength > 0 && totalSamples > 0);
|
||||
this.audioDataLength = audioDataLength;
|
||||
this.totalSamples = totalSamples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this {@code OggSeeker}.
|
||||
*/
|
||||
public void reset() {
|
||||
pageHeader.reset();
|
||||
headerArray.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput}
|
||||
* has to seek and then be passed for another call until -1 is return. If -1 is returned the
|
||||
* input is at a position which is before the start of the page before the target page and at
|
||||
* which it is sensible to just skip pages to the target granule and pre-roll instead of doing
|
||||
* another seek request.
|
||||
*
|
||||
* @param targetGranule the target granule position to seek to.
|
||||
* @param input the {@link ExtractorInput} to read from.
|
||||
* @return the position to seek the {@link ExtractorInput} to for a next call or -1 if it's close
|
||||
* enough to skip to the target page.
|
||||
* @throws IOException thrown if reading from the input fails.
|
||||
* @throws InterruptedException thrown if interrupted while reading from the input.
|
||||
*/
|
||||
public long getNextSeekPosition(long targetGranule, ExtractorInput input)
|
||||
throws IOException, InterruptedException {
|
||||
Assertions.checkState(audioDataLength != C.LENGTH_UNBOUNDED && totalSamples != 0);
|
||||
OggUtil.populatePageHeader(input, pageHeader, headerArray, false);
|
||||
long granuleDistance = targetGranule - pageHeader.granulePosition;
|
||||
if (granuleDistance <= 0 || granuleDistance > MATCH_RANGE) {
|
||||
// estimated position too high or too low
|
||||
long offset = (pageHeader.bodySize + pageHeader.headerSize)
|
||||
* (granuleDistance <= 0 ? 2 : 1);
|
||||
return input.getPosition() - offset + (granuleDistance * audioDataLength / totalSamples);
|
||||
}
|
||||
// position accepted (below target granule and within MATCH_RANGE)
|
||||
input.resetPeekPosition();
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor.ogg;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Utility methods for reading ogg streams.
|
||||
*/
|
||||
/* package */ final class OggUtil {
|
||||
|
||||
private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS");
|
||||
|
||||
/**
|
||||
* Reads an int of {@code length} bits from {@code src} starting at
|
||||
* {@code leastSignificantBitIndex}.
|
||||
*
|
||||
* @param src the {@code byte} to read from.
|
||||
* @param length the length in bits of the int to read.
|
||||
* @param leastSignificantBitIndex the index of the least significant bit of the int to read.
|
||||
* @return the int value read.
|
||||
*/
|
||||
public static int readBits(byte src, int length, int leastSignificantBitIndex) {
|
||||
return (src >> leastSignificantBitIndex) & (255 >>> (8 - length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips to the next page.
|
||||
*
|
||||
* @param input The {@code ExtractorInput} to skip to the next page.
|
||||
* @throws IOException thrown if peeking/reading from the input fails.
|
||||
* @throws InterruptedException thrown if interrupted while peeking/reading from the input.
|
||||
*/
|
||||
public static void skipToNextPage(ExtractorInput input)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
byte[] buffer = new byte[2048];
|
||||
int peekLength = buffer.length;
|
||||
while (true) {
|
||||
if (input.getLength() != C.LENGTH_UNBOUNDED
|
||||
&& input.getPosition() + peekLength > input.getLength()) {
|
||||
// Make sure to not peek beyond the end of the input.
|
||||
peekLength = (int) (input.getLength() - input.getPosition());
|
||||
if (peekLength < 4) {
|
||||
// Not found until eof.
|
||||
throw new EOFException();
|
||||
}
|
||||
}
|
||||
input.peekFully(buffer, 0, peekLength, false);
|
||||
for (int i = 0; i < peekLength - 3; i++) {
|
||||
if (buffer[i] == 'O' && buffer[i + 1] == 'g' && buffer[i + 2] == 'g'
|
||||
&& buffer[i + 3] == 'S') {
|
||||
// Match! Skip to the start of the pattern.
|
||||
input.skipFully(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Overlap by not skipping the entire peekLength.
|
||||
input.skipFully(peekLength - 3);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Peeks an Ogg page header and stores the data in the {@code header} object passed
|
||||
* as argument.
|
||||
*
|
||||
* @param input the {@link ExtractorInput} to read from.
|
||||
* @param header the {@link PageHeader} to read from.
|
||||
* @param scratch a scratch array temporary use.
|
||||
* @param quite if {@code true} no Exceptions are thrown but {@code false} is return if something
|
||||
* goes wrong.
|
||||
* @return {@code true} if the read was successful. {@code false} if the end of the
|
||||
* input was encountered having read no data.
|
||||
* @throws IOException thrown if reading data fails or the stream is invalid.
|
||||
* @throws InterruptedException thrown if thread is interrupted when reading/peeking.
|
||||
*/
|
||||
public static boolean populatePageHeader(ExtractorInput input, PageHeader header,
|
||||
ParsableByteArray scratch, boolean quite) throws IOException, InterruptedException {
|
||||
|
||||
scratch.reset();
|
||||
header.reset();
|
||||
boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNBOUNDED
|
||||
|| input.getLength() - input.getPeekPosition() >= 27;
|
||||
if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, 27, true)) {
|
||||
if (quite) {
|
||||
return false;
|
||||
} else {
|
||||
throw new EOFException();
|
||||
}
|
||||
}
|
||||
if (scratch.readUnsignedInt() != TYPE_OGGS) {
|
||||
if (quite) {
|
||||
return false;
|
||||
} else {
|
||||
throw new ParserException("expected OggS capture pattern at begin of page");
|
||||
}
|
||||
}
|
||||
|
||||
header.revision = scratch.readUnsignedByte();
|
||||
if (header.revision != 0x00) {
|
||||
if (quite) {
|
||||
return false;
|
||||
} else {
|
||||
throw new ParserException("unsupported bit stream revision");
|
||||
}
|
||||
}
|
||||
header.type = scratch.readUnsignedByte();
|
||||
|
||||
header.granulePosition = scratch.readLittleEndianLong();
|
||||
header.streamSerialNumber = scratch.readLittleEndianUnsignedInt();
|
||||
header.pageSequenceNumber = scratch.readLittleEndianUnsignedInt();
|
||||
header.pageChecksum = scratch.readLittleEndianUnsignedInt();
|
||||
header.pageSegmentCount = scratch.readUnsignedByte();
|
||||
|
||||
scratch.reset();
|
||||
// calculate total size of header including laces
|
||||
header.headerSize = 27 + header.pageSegmentCount;
|
||||
input.peekFully(scratch.data, 0, header.pageSegmentCount);
|
||||
for (int i = 0; i < header.pageSegmentCount; i++) {
|
||||
header.laces[i] = scratch.readUnsignedByte();
|
||||
header.bodySize += header.laces[i];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the size of the packet starting from {@code startSegmentIndex}.
|
||||
*
|
||||
* @param header the {@link PageHeader} with laces.
|
||||
* @param startSegmentIndex the index of the first segment of the packet.
|
||||
* @param holder a position holder to store the resulting size value.
|
||||
*/
|
||||
public static void calculatePacketSize(PageHeader header, int startSegmentIndex,
|
||||
PacketInfoHolder holder) {
|
||||
holder.segmentCount = 0;
|
||||
holder.size = 0;
|
||||
while (startSegmentIndex + holder.segmentCount < header.pageSegmentCount) {
|
||||
int segmentLength = header.laces[startSegmentIndex + holder.segmentCount++];
|
||||
holder.size += segmentLength;
|
||||
if (segmentLength != 255) {
|
||||
// packets end at first lace < 255
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object to store header information. Be aware that {@code laces.length} is always 255.
|
||||
* Instead use {@code pageSegmentCount} to iterate.
|
||||
*/
|
||||
public static final class PageHeader {
|
||||
|
||||
public int revision;
|
||||
public int type;
|
||||
public long granulePosition;
|
||||
public long streamSerialNumber;
|
||||
public long pageSequenceNumber;
|
||||
public long pageChecksum;
|
||||
public int pageSegmentCount;
|
||||
public int headerSize;
|
||||
public int bodySize;
|
||||
public final int[] laces = new int[255];
|
||||
|
||||
/**
|
||||
* Resets all primitive member fields to zero.
|
||||
*/
|
||||
public void reset() {
|
||||
revision = 0;
|
||||
type = 0;
|
||||
granulePosition = 0;
|
||||
streamSerialNumber = 0;
|
||||
pageSequenceNumber = 0;
|
||||
pageChecksum = 0;
|
||||
pageSegmentCount = 0;
|
||||
headerSize = 0;
|
||||
bodySize = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds size and number of segments of a packet.
|
||||
*/
|
||||
public static class PacketInfoHolder {
|
||||
public int size;
|
||||
public int segmentCount;
|
||||
}
|
||||
|
||||
}
|
@ -25,27 +25,21 @@ import com.google.android.exoplayer.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer.extractor.SeekMap;
|
||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer.extractor.ogg.VorbisUtil.Mode;
|
||||
import com.google.android.exoplayer.extractor.ogg.VorbisUtil.VorbisIdHeader;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* {@link Extractor} to extract Vorbis data out of Ogg byte stream.
|
||||
*/
|
||||
public final class OggVorbisExtractor implements Extractor {
|
||||
|
||||
private static final String TAG = "OggVorbisExtractor";
|
||||
|
||||
private static final int OGG_MAX_SEGMENT_SIZE = 255;
|
||||
public final class OggVorbisExtractor implements Extractor, SeekMap {
|
||||
|
||||
private final ParsableByteArray scratch = new ParsableByteArray(
|
||||
new byte[OGG_MAX_SEGMENT_SIZE * 255], 0);
|
||||
private final OggReader oggReader = new OggReader();
|
||||
new byte[OggReader.OGG_MAX_SEGMENT_SIZE * 255], 0);
|
||||
|
||||
private final OggReader oggReader = new OggReader();
|
||||
|
||||
private TrackOutput trackOutput;
|
||||
private VorbisSetup vorbisSetup;
|
||||
@ -53,36 +47,48 @@ public final class OggVorbisExtractor implements Extractor {
|
||||
private long elapsedSamples;
|
||||
private boolean seenFirstAudioPacket;
|
||||
|
||||
private final OggSeeker oggSeeker = new OggSeeker();
|
||||
private long targetGranule = -1;
|
||||
|
||||
private ExtractorOutput extractorOutput;
|
||||
private VorbisUtil.VorbisIdHeader vorbisIdHeader;
|
||||
private VorbisUtil.CommentHeader commentHeader;
|
||||
private long inputLength;
|
||||
private long audioStartPosition;
|
||||
private long totalSamples;
|
||||
private long durationUs;
|
||||
|
||||
@Override
|
||||
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
|
||||
try {
|
||||
OggReader.PageHeader header = new OggReader.PageHeader();
|
||||
OggReader.populatePageHeader(input, header, scratch, true);
|
||||
if ((header.type & 0x02) != 0x02) {
|
||||
throw new ParserException("expected page to be first page of a logical stream");
|
||||
OggUtil.PageHeader header = new OggUtil.PageHeader();
|
||||
if (!OggUtil.populatePageHeader(input, header, scratch, true)
|
||||
|| (header.type & 0x02) != 0x02 || header.bodySize < 7) {
|
||||
return false;
|
||||
}
|
||||
input.resetPeekPosition();
|
||||
scratch.reset();
|
||||
input.peekFully(scratch.data, 0, 7);
|
||||
return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, scratch, true);
|
||||
} catch (ParserException e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
return false;
|
||||
// does not happen
|
||||
} finally {
|
||||
input.resetPeekPosition();
|
||||
scratch.reset();
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput output) {
|
||||
trackOutput = output.track(0);
|
||||
output.endTracks();
|
||||
output.seekMap(new SeekMap.Unseekable(C.UNKNOWN_TIME_US));
|
||||
extractorOutput = output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek() {
|
||||
oggReader.reset();
|
||||
previousPacketBlockSize = -1;
|
||||
previousPacketBlockSize = 0;
|
||||
elapsedSamples = 0;
|
||||
seenFirstAudioPacket = false;
|
||||
scratch.reset();
|
||||
@ -92,35 +98,77 @@ public final class OggVorbisExtractor implements Extractor {
|
||||
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
if (vorbisSetup == null) {
|
||||
vorbisSetup = readSetupHeaders(input, scratch);
|
||||
VorbisIdHeader idHeader = vorbisSetup.idHeader;
|
||||
ArrayList<byte[]> codecInitializationData = new ArrayList<>();
|
||||
codecInitializationData.clear();
|
||||
codecInitializationData.add(idHeader.data);
|
||||
codecInitializationData.add(vorbisSetup.setupHeaderData);
|
||||
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS,
|
||||
idHeader.bitrateNominal, OGG_MAX_SEGMENT_SIZE * 255, idHeader.channels,
|
||||
idHeader.sampleRate, codecInitializationData, null));
|
||||
// Setup.
|
||||
if (totalSamples == 0) {
|
||||
if (vorbisSetup == null) {
|
||||
inputLength = input.getLength();
|
||||
vorbisSetup = readSetupHeaders(input, scratch);
|
||||
audioStartPosition = input.getPosition();
|
||||
// Output the format.
|
||||
ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
|
||||
codecInitialisationData.add(vorbisSetup.idHeader.data);
|
||||
codecInitialisationData.add(vorbisSetup.setupHeaderData);
|
||||
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS,
|
||||
vorbisSetup.idHeader.bitrateNominal, OggReader.OGG_MAX_SEGMENT_SIZE * 255,
|
||||
vorbisSetup.idHeader.channels, (int) vorbisSetup.idHeader.sampleRate,
|
||||
codecInitialisationData, null));
|
||||
if (inputLength == C.LENGTH_UNBOUNDED) {
|
||||
// If the length is unbounded, we cannot determine the duration or seek.
|
||||
totalSamples = -1;
|
||||
durationUs = C.LENGTH_UNBOUNDED;
|
||||
extractorOutput.seekMap(this);
|
||||
return RESULT_CONTINUE;
|
||||
}
|
||||
// Seek to just before the last page of stream to get the duration.
|
||||
seekPosition.position = input.getLength() - 8000;
|
||||
return RESULT_SEEK;
|
||||
}
|
||||
|
||||
totalSamples = oggReader.readGranuleOfLastPage(input);
|
||||
durationUs = totalSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
|
||||
oggSeeker.setup(inputLength - audioStartPosition, totalSamples);
|
||||
extractorOutput.seekMap(this);
|
||||
// Seek back to resume from where we finished reading vorbis headers.
|
||||
seekPosition.position = audioStartPosition;
|
||||
return RESULT_SEEK;
|
||||
}
|
||||
|
||||
// Seeking requested.
|
||||
if (!seenFirstAudioPacket && targetGranule > -1) {
|
||||
OggUtil.skipToNextPage(input);
|
||||
long position = oggSeeker.getNextSeekPosition(targetGranule, input);
|
||||
if (position != -1) {
|
||||
seekPosition.position = position;
|
||||
return RESULT_SEEK;
|
||||
} else {
|
||||
elapsedSamples = oggReader.skipToPageOfGranule(input, targetGranule);
|
||||
previousPacketBlockSize = vorbisIdHeader.blockSize0;
|
||||
// We're never at the first packet after seeking.
|
||||
seenFirstAudioPacket = true;
|
||||
oggSeeker.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Playback.
|
||||
if (oggReader.readPacket(input, scratch)) {
|
||||
// if this is an audio packet...
|
||||
// If this is an audio packet...
|
||||
if ((scratch.data[0] & 0x01) != 1) {
|
||||
// ... we need to decode the block size
|
||||
// ... Then we need to decode the block size
|
||||
int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup);
|
||||
// a packet contains samples produced from overlapping the previous and current frame data
|
||||
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
|
||||
int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4
|
||||
: 0;
|
||||
// codec expects the number of samples appended to audio data
|
||||
appendNumberOfSamples(scratch, samplesInPacket);
|
||||
|
||||
// calculate time and send audio data to codec
|
||||
long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
|
||||
trackOutput.sampleData(scratch, scratch.limit());
|
||||
trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null);
|
||||
|
||||
// update state in members for next iteration
|
||||
// A packet contains samples produced from overlapping the previous and current frame data
|
||||
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2).
|
||||
int samplesInPacket = seenFirstAudioPacket
|
||||
? ((packetBlockSize + previousPacketBlockSize) / 4) : 0;
|
||||
if (elapsedSamples + samplesInPacket >= targetGranule) {
|
||||
// Codec expects the number of samples appended to audio data.
|
||||
appendNumberOfSamples(scratch, samplesInPacket);
|
||||
// Calculate time and send audio data to codec.
|
||||
long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
|
||||
trackOutput.sampleData(scratch, scratch.limit());
|
||||
trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null);
|
||||
targetGranule = -1;
|
||||
}
|
||||
// Update state in members for next iteration.
|
||||
seenFirstAudioPacket = true;
|
||||
elapsedSamples += samplesInPacket;
|
||||
previousPacketBlockSize = packetBlockSize;
|
||||
@ -176,7 +224,7 @@ public final class OggVorbisExtractor implements Extractor {
|
||||
|
||||
private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) {
|
||||
// read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1)
|
||||
int modeNumber = VorbisUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);
|
||||
int modeNumber = OggUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);
|
||||
int currentBlockSize;
|
||||
if (!vorbisSetup.modes[modeNumber].blockFlag) {
|
||||
currentBlockSize = vorbisSetup.idHeader.blockSize0;
|
||||
@ -186,6 +234,31 @@ public final class OggVorbisExtractor implements Extractor {
|
||||
return currentBlockSize;
|
||||
}
|
||||
|
||||
// SeekMap implementation.
|
||||
|
||||
@Override
|
||||
public boolean isSeekable() {
|
||||
return inputLength != C.LENGTH_UNBOUNDED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDurationUs() {
|
||||
return durationUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPosition(long timeUs) {
|
||||
if (timeUs == 0) {
|
||||
targetGranule = -1;
|
||||
return audioStartPosition;
|
||||
}
|
||||
targetGranule = vorbisSetup.idHeader.sampleRate * timeUs / C.MICROS_PER_SECOND;
|
||||
return Math.max(audioStartPosition, ((inputLength - audioStartPosition) * timeUs
|
||||
/ durationUs) - 4000);
|
||||
}
|
||||
|
||||
// Internal classes.
|
||||
|
||||
/**
|
||||
* Class to hold all data read from Vorbis setup headers.
|
||||
*/
|
||||
|
@ -46,19 +46,6 @@ import java.util.Arrays;
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an int of {@code length} bits from {@code src} starting at
|
||||
* {@code leastSignificantBitIndex}.
|
||||
*
|
||||
* @param src the {@code byte} to read from.
|
||||
* @param length the length in bits of the int to read.
|
||||
* @param leastSignificantBitIndex the index of the least significant bit of the int to read.
|
||||
* @return the int value read.
|
||||
*/
|
||||
public static int readBits(byte src, int length, int leastSignificantBitIndex) {
|
||||
return (src >> leastSignificantBitIndex) & (255 >>> (8 - length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a vorbis identification header from {@code headerData}.
|
||||
*
|
||||
@ -71,11 +58,11 @@ import java.util.Arrays;
|
||||
public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData)
|
||||
throws ParserException {
|
||||
|
||||
captureVorbisHeader(0x01, headerData);
|
||||
verifyVorbisHeaderCapturePattern(0x01, headerData, false);
|
||||
|
||||
long version = headerData.readLittleEndianUnsignedInt();
|
||||
int channels = headerData.readUnsignedByte();
|
||||
int sampleRate = (int) headerData.readLittleEndianUnsignedInt();
|
||||
long sampleRate = headerData.readLittleEndianUnsignedInt();
|
||||
int bitrateMax = headerData.readLittleEndianInt();
|
||||
int bitrateNominal = headerData.readLittleEndianInt();
|
||||
int bitrateMin = headerData.readLittleEndianInt();
|
||||
@ -104,7 +91,8 @@ import java.util.Arrays;
|
||||
public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData)
|
||||
throws ParserException {
|
||||
|
||||
int length = captureVorbisHeader(0x03, headerData);
|
||||
verifyVorbisHeaderCapturePattern(0x03, headerData, false);
|
||||
int length = 7;
|
||||
|
||||
int len = (int) headerData.readLittleEndianUnsignedInt();
|
||||
length += 4;
|
||||
@ -127,21 +115,40 @@ import java.util.Arrays;
|
||||
return new CommentHeader(vendor, comments, length);
|
||||
}
|
||||
|
||||
private static int captureVorbisHeader(int headerType, ParsableByteArray idHeader)
|
||||
/**
|
||||
* Verifies whether the next bytes in {@code header} are a vorbis header of the given
|
||||
* {@code headerType}.
|
||||
*
|
||||
* @param headerType the type of the header expected.
|
||||
* @param header the alleged header bytes.
|
||||
* @param quite if {@code true} no exceptions are thrown. Instead {@code false} is returned.
|
||||
* @return the number of bytes read.
|
||||
* @throws ParserException thrown if header type or capture pattern is not as expected.
|
||||
*/
|
||||
public static boolean verifyVorbisHeaderCapturePattern(int headerType, ParsableByteArray header,
|
||||
boolean quite)
|
||||
throws ParserException {
|
||||
if (idHeader.readUnsignedByte() != headerType) {
|
||||
throw new ParserException("expected header type " + Integer.toHexString(headerType));
|
||||
if (header.readUnsignedByte() != headerType) {
|
||||
if (quite) {
|
||||
return false;
|
||||
} else {
|
||||
throw new ParserException("expected header type " + Integer.toHexString(headerType));
|
||||
}
|
||||
}
|
||||
|
||||
if (!(idHeader.readUnsignedByte() == 'v'
|
||||
&& idHeader.readUnsignedByte() == 'o'
|
||||
&& idHeader.readUnsignedByte() == 'r'
|
||||
&& idHeader.readUnsignedByte() == 'b'
|
||||
&& idHeader.readUnsignedByte() == 'i'
|
||||
&& idHeader.readUnsignedByte() == 's')) {
|
||||
throw new ParserException("expected characters 'vorbis'");
|
||||
if (!(header.readUnsignedByte() == 'v'
|
||||
&& header.readUnsignedByte() == 'o'
|
||||
&& header.readUnsignedByte() == 'r'
|
||||
&& header.readUnsignedByte() == 'b'
|
||||
&& header.readUnsignedByte() == 'i'
|
||||
&& header.readUnsignedByte() == 's')) {
|
||||
if (quite) {
|
||||
return false;
|
||||
} else {
|
||||
throw new ParserException("expected characters 'vorbis'");
|
||||
}
|
||||
}
|
||||
return 7; // bytes read
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -159,7 +166,7 @@ import java.util.Arrays;
|
||||
public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels)
|
||||
throws ParserException {
|
||||
|
||||
captureVorbisHeader(0x05, headerData);
|
||||
verifyVorbisHeaderCapturePattern(0x05, headerData, false);
|
||||
|
||||
int numberOfBooks = headerData.readUnsignedByte() + 1;
|
||||
|
||||
@ -433,7 +440,7 @@ import java.util.Arrays;
|
||||
|
||||
public final long version;
|
||||
public final int channels;
|
||||
public final int sampleRate;
|
||||
public final long sampleRate;
|
||||
public final int bitrateMax;
|
||||
public final int bitrateNominal;
|
||||
public final int bitrateMin;
|
||||
@ -442,7 +449,7 @@ import java.util.Arrays;
|
||||
public final boolean framingFlag;
|
||||
public final byte[] data;
|
||||
|
||||
public VorbisIdHeader(long version, int channels, int sampleRate, int bitrateMax,
|
||||
public VorbisIdHeader(long version, int channels, long sampleRate, int bitrateMax,
|
||||
int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag,
|
||||
byte[] data) {
|
||||
this.version = version;
|
||||
|
Loading…
x
Reference in New Issue
Block a user