Commit a067bd09 by olly Committed by Oliver Woodman

Make OggVorbisExtractor seekable.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=117252304
parent 4ffa3556
/*
* 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
}
}
}
}
/*
* 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 { ...@@ -43,15 +43,36 @@ public final class OggVorbisExtractorTest extends TestCase {
public void testSniff() throws Exception { public void testSniff() throws Exception {
byte[] data = TestUtil.joinByteArrays( byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x02), TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(120, 120)); // Laces TestUtil.createByteArray(120, 120), // Laces
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'});
assertTrue(sniff(createInput(data))); assertTrue(sniff(createInput(data)));
} }
public void testSniffFails() throws Exception { public void testSniffFailsOpusFile() throws Exception {
byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x00),
new byte[]{'O', 'p', 'u', 's'});
assertFalse(sniff(createInput(data)));
}
public void testSniffFailsInvalidOggHeader() throws Exception {
byte[] data = TestData.buildOggHeader(0x00, 0, 1000, 0x00); byte[] data = TestData.buildOggHeader(0x00, 0, 1000, 0x00);
assertFalse(sniff(createInput(data))); assertFalse(sniff(createInput(data)));
} }
public void testSniffInvalidVorbisHeader() throws Exception {
byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(120, 120), // Laces
new byte[]{0x01, 'X', 'o', 'r', 'b', 'i', 's'});
assertFalse(sniff(createInput(data)));
}
public void testSniffFailsEOF() throws Exception {
byte[] data = TestData.buildOggHeader(0x02, 0, 1000, 0x00);
assertFalse(sniff(createInput(data)));
}
public void testAppendNumberOfSamples() throws Exception { public void testAppendNumberOfSamples() throws Exception {
ParsableByteArray buffer = new ParsableByteArray(4); ParsableByteArray buffer = new ParsableByteArray(4);
buffer.setLimit(0); buffer.setLimit(0);
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.extractor.ogg; package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.testutil.TestUtil; import com.google.android.exoplayer.testutil.TestUtil;
/** /**
...@@ -22,6 +23,11 @@ import com.google.android.exoplayer.testutil.TestUtil; ...@@ -22,6 +23,11 @@ import com.google.android.exoplayer.testutil.TestUtil;
*/ */
/* package */ final class TestData { /* package */ final class TestData {
/* package */ static FakeExtractorInput createInput(byte[] data, boolean simulateUnkownLength) {
return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true)
.setSimulateUnknownLength(simulateUnkownLength).setSimulatePartialReads(true).build();
}
public static byte[] buildOggHeader(int headerType, long granule, int pageSequenceCounter, public static byte[] buildOggHeader(int headerType, long granule, int pageSequenceCounter,
int pageSegmentCount) { int pageSegmentCount) {
return TestUtil.createByteArray( return TestUtil.createByteArray(
...@@ -46,7 +52,7 @@ import com.google.android.exoplayer.testutil.TestUtil; ...@@ -46,7 +52,7 @@ import com.google.android.exoplayer.testutil.TestUtil;
(pageSequenceCounter >> 24) & 0xFF, (pageSequenceCounter >> 24) & 0xFF,
0x00, // LSB of page checksum. 0x00, // LSB of page checksum.
0x00, 0x00,
0x00, 0x10,
0x00, // MSB of page checksum. 0x00, // MSB of page checksum.
pageSegmentCount); pageSegmentCount);
} }
......
...@@ -38,13 +38,6 @@ public final class VorbisUtilTest extends TestCase { ...@@ -38,13 +38,6 @@ public final class VorbisUtilTest extends TestCase {
assertEquals(0, VorbisUtil.iLog(-122)); assertEquals(0, VorbisUtil.iLog(-122));
} }
public void testReadBits() throws Exception {
assertEquals(0, VorbisUtil.readBits((byte) 0x00, 2, 2));
assertEquals(1, VorbisUtil.readBits((byte) 0x02, 1, 1));
assertEquals(15, VorbisUtil.readBits((byte) 0xF0, 4, 4));
assertEquals(1, VorbisUtil.readBits((byte) 0x80, 1, 7));
}
public void testReadIdHeader() throws Exception { public void testReadIdHeader() throws Exception {
byte[] data = TestData.getIdentificationHeaderData(); byte[] data = TestData.getIdentificationHeaderData();
ParsableByteArray headerData = new ParsableByteArray(data, data.length); ParsableByteArray headerData = new ParsableByteArray(data, data.length);
...@@ -95,4 +88,45 @@ public final class VorbisUtilTest extends TestCase { ...@@ -95,4 +88,45 @@ public final class VorbisUtilTest extends TestCase {
assertEquals(0, modes[1].windowType); assertEquals(0, modes[1].windowType);
} }
public void testVerifyVorbisHeaderCapturePattern() throws ParserException {
ParsableByteArray header = new ParsableByteArray(
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'});
assertEquals(true, VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false));
}
public void testVerifyVorbisHeaderCapturePatternInvalidHeader() throws ParserException {
ParsableByteArray header = new ParsableByteArray(
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'});
try {
VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, false);
fail();
} catch (ParserException e) {
assertEquals("expected header type 99", e.getMessage());
}
}
public void testVerifyVorbisHeaderCapturePatternInvalidHeaderQuite() throws ParserException {
ParsableByteArray header = new ParsableByteArray(
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'});
assertFalse(VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, true));
}
public void testVerifyVorbisHeaderCapturePatternInvalidPattern() throws ParserException {
ParsableByteArray header = new ParsableByteArray(
new byte[]{0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'});
try {
VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false);
fail();
} catch (ParserException e) {
assertEquals("expected characters 'vorbis'", e.getMessage());
}
}
public void testVerifyVorbisHeaderCapturePatternQuiteInvalidPatternQuite()
throws ParserException {
ParsableByteArray header = new ParsableByteArray(
new byte[]{0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'});
assertFalse(VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, true));
}
} }
/*
* 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;
}
}
/*
* 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; ...@@ -25,27 +25,21 @@ import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.extractor.ogg.VorbisUtil.Mode; import com.google.android.exoplayer.extractor.ogg.VorbisUtil.Mode;
import com.google.android.exoplayer.extractor.ogg.VorbisUtil.VorbisIdHeader;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Log;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
/** /**
* {@link Extractor} to extract Vorbis data out of Ogg byte stream. * {@link Extractor} to extract Vorbis data out of Ogg byte stream.
*/ */
public final class OggVorbisExtractor implements Extractor { public final class OggVorbisExtractor implements Extractor, SeekMap {
private static final String TAG = "OggVorbisExtractor";
private static final int OGG_MAX_SEGMENT_SIZE = 255;
private final ParsableByteArray scratch = new ParsableByteArray( private final ParsableByteArray scratch = new ParsableByteArray(
new byte[OGG_MAX_SEGMENT_SIZE * 255], 0); new byte[OggReader.OGG_MAX_SEGMENT_SIZE * 255], 0);
private final OggReader oggReader = new OggReader();
private final OggReader oggReader = new OggReader();
private TrackOutput trackOutput; private TrackOutput trackOutput;
private VorbisSetup vorbisSetup; private VorbisSetup vorbisSetup;
...@@ -53,36 +47,48 @@ public final class OggVorbisExtractor implements Extractor { ...@@ -53,36 +47,48 @@ public final class OggVorbisExtractor implements Extractor {
private long elapsedSamples; private long elapsedSamples;
private boolean seenFirstAudioPacket; private boolean seenFirstAudioPacket;
private final OggSeeker oggSeeker = new OggSeeker();
private long targetGranule = -1;
private ExtractorOutput extractorOutput;
private VorbisUtil.VorbisIdHeader vorbisIdHeader; private VorbisUtil.VorbisIdHeader vorbisIdHeader;
private VorbisUtil.CommentHeader commentHeader; private VorbisUtil.CommentHeader commentHeader;
private long inputLength;
private long audioStartPosition;
private long totalSamples;
private long durationUs;
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
try { try {
OggReader.PageHeader header = new OggReader.PageHeader(); OggUtil.PageHeader header = new OggUtil.PageHeader();
OggReader.populatePageHeader(input, header, scratch, true); if (!OggUtil.populatePageHeader(input, header, scratch, true)
if ((header.type & 0x02) != 0x02) { || (header.type & 0x02) != 0x02 || header.bodySize < 7) {
throw new ParserException("expected page to be first page of a logical stream"); return false;
} }
input.resetPeekPosition(); scratch.reset();
input.peekFully(scratch.data, 0, 7);
return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, scratch, true);
} catch (ParserException e) { } catch (ParserException e) {
Log.e(TAG, e.getMessage()); // does not happen
return false; } finally {
input.resetPeekPosition();
scratch.reset();
} }
return true; return false;
} }
@Override @Override
public void init(ExtractorOutput output) { public void init(ExtractorOutput output) {
trackOutput = output.track(0); trackOutput = output.track(0);
output.endTracks(); output.endTracks();
output.seekMap(new SeekMap.Unseekable(C.UNKNOWN_TIME_US)); extractorOutput = output;
} }
@Override @Override
public void seek() { public void seek() {
oggReader.reset(); oggReader.reset();
previousPacketBlockSize = -1; previousPacketBlockSize = 0;
elapsedSamples = 0; elapsedSamples = 0;
seenFirstAudioPacket = false; seenFirstAudioPacket = false;
scratch.reset(); scratch.reset();
...@@ -92,35 +98,77 @@ public final class OggVorbisExtractor implements Extractor { ...@@ -92,35 +98,77 @@ public final class OggVorbisExtractor implements Extractor {
public int read(ExtractorInput input, PositionHolder seekPosition) public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (vorbisSetup == null) { // Setup.
vorbisSetup = readSetupHeaders(input, scratch); if (totalSamples == 0) {
VorbisIdHeader idHeader = vorbisSetup.idHeader; if (vorbisSetup == null) {
ArrayList<byte[]> codecInitializationData = new ArrayList<>(); inputLength = input.getLength();
codecInitializationData.clear(); vorbisSetup = readSetupHeaders(input, scratch);
codecInitializationData.add(idHeader.data); audioStartPosition = input.getPosition();
codecInitializationData.add(vorbisSetup.setupHeaderData); // Output the format.
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
idHeader.bitrateNominal, OGG_MAX_SEGMENT_SIZE * 255, idHeader.channels, codecInitialisationData.add(vorbisSetup.idHeader.data);
idHeader.sampleRate, codecInitializationData, null)); codecInitialisationData.add(vorbisSetup.setupHeaderData);
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS,
vorbisSetup.idHeader.bitrateNominal, OggReader.OGG_MAX_SEGMENT_SIZE * 255,
vorbisSetup.idHeader.channels, (int) vorbisSetup.idHeader.sampleRate,
codecInitialisationData, null));
if (inputLength == C.LENGTH_UNBOUNDED) {
// If the length is unbounded, we cannot determine the duration or seek.
totalSamples = -1;
durationUs = C.LENGTH_UNBOUNDED;
extractorOutput.seekMap(this);
return RESULT_CONTINUE;
}
// Seek to just before the last page of stream to get the duration.
seekPosition.position = input.getLength() - 8000;
return RESULT_SEEK;
}
totalSamples = oggReader.readGranuleOfLastPage(input);
durationUs = totalSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
oggSeeker.setup(inputLength - audioStartPosition, totalSamples);
extractorOutput.seekMap(this);
// Seek back to resume from where we finished reading vorbis headers.
seekPosition.position = audioStartPosition;
return RESULT_SEEK;
}
// Seeking requested.
if (!seenFirstAudioPacket && targetGranule > -1) {
OggUtil.skipToNextPage(input);
long position = oggSeeker.getNextSeekPosition(targetGranule, input);
if (position != -1) {
seekPosition.position = position;
return RESULT_SEEK;
} else {
elapsedSamples = oggReader.skipToPageOfGranule(input, targetGranule);
previousPacketBlockSize = vorbisIdHeader.blockSize0;
// We're never at the first packet after seeking.
seenFirstAudioPacket = true;
oggSeeker.reset();
}
} }
// Playback.
if (oggReader.readPacket(input, scratch)) { if (oggReader.readPacket(input, scratch)) {
// if this is an audio packet... // If this is an audio packet...
if ((scratch.data[0] & 0x01) != 1) { if ((scratch.data[0] & 0x01) != 1) {
// ... we need to decode the block size // ... Then we need to decode the block size
int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup); int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup);
// a packet contains samples produced from overlapping the previous and current frame data // A packet contains samples produced from overlapping the previous and current frame data
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2) // (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2).
int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4 int samplesInPacket = seenFirstAudioPacket
: 0; ? ((packetBlockSize + previousPacketBlockSize) / 4) : 0;
// codec expects the number of samples appended to audio data if (elapsedSamples + samplesInPacket >= targetGranule) {
appendNumberOfSamples(scratch, samplesInPacket); // Codec expects the number of samples appended to audio data.
appendNumberOfSamples(scratch, samplesInPacket);
// calculate time and send audio data to codec // Calculate time and send audio data to codec.
long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate; long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
trackOutput.sampleData(scratch, scratch.limit()); trackOutput.sampleData(scratch, scratch.limit());
trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null); trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null);
targetGranule = -1;
// update state in members for next iteration }
// Update state in members for next iteration.
seenFirstAudioPacket = true; seenFirstAudioPacket = true;
elapsedSamples += samplesInPacket; elapsedSamples += samplesInPacket;
previousPacketBlockSize = packetBlockSize; previousPacketBlockSize = packetBlockSize;
...@@ -176,7 +224,7 @@ public final class OggVorbisExtractor implements Extractor { ...@@ -176,7 +224,7 @@ public final class OggVorbisExtractor implements Extractor {
private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) { private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) {
// read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1) // read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1)
int modeNumber = VorbisUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1); int modeNumber = OggUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);
int currentBlockSize; int currentBlockSize;
if (!vorbisSetup.modes[modeNumber].blockFlag) { if (!vorbisSetup.modes[modeNumber].blockFlag) {
currentBlockSize = vorbisSetup.idHeader.blockSize0; currentBlockSize = vorbisSetup.idHeader.blockSize0;
...@@ -186,6 +234,31 @@ public final class OggVorbisExtractor implements Extractor { ...@@ -186,6 +234,31 @@ public final class OggVorbisExtractor implements Extractor {
return currentBlockSize; return currentBlockSize;
} }
// SeekMap implementation.
@Override
public boolean isSeekable() {
return inputLength != C.LENGTH_UNBOUNDED;
}
@Override
public long getDurationUs() {
return durationUs;
}
@Override
public long getPosition(long timeUs) {
if (timeUs == 0) {
targetGranule = -1;
return audioStartPosition;
}
targetGranule = vorbisSetup.idHeader.sampleRate * timeUs / C.MICROS_PER_SECOND;
return Math.max(audioStartPosition, ((inputLength - audioStartPosition) * timeUs
/ durationUs) - 4000);
}
// Internal classes.
/** /**
* Class to hold all data read from Vorbis setup headers. * Class to hold all data read from Vorbis setup headers.
*/ */
......
...@@ -47,19 +47,6 @@ import java.util.Arrays; ...@@ -47,19 +47,6 @@ import java.util.Arrays;
} }
/** /**
* Reads an int of {@code length} bits from {@code src} starting at
* {@code leastSignificantBitIndex}.
*
* @param src the {@code byte} to read from.
* @param length the length in bits of the int to read.
* @param leastSignificantBitIndex the index of the least significant bit of the int to read.
* @return the int value read.
*/
public static int readBits(byte src, int length, int leastSignificantBitIndex) {
return (src >> leastSignificantBitIndex) & (255 >>> (8 - length));
}
/**
* Reads a vorbis identification header from {@code headerData}. * Reads a vorbis identification header from {@code headerData}.
* *
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis
...@@ -71,11 +58,11 @@ import java.util.Arrays; ...@@ -71,11 +58,11 @@ import java.util.Arrays;
public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData) public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData)
throws ParserException { throws ParserException {
captureVorbisHeader(0x01, headerData); verifyVorbisHeaderCapturePattern(0x01, headerData, false);
long version = headerData.readLittleEndianUnsignedInt(); long version = headerData.readLittleEndianUnsignedInt();
int channels = headerData.readUnsignedByte(); int channels = headerData.readUnsignedByte();
int sampleRate = (int) headerData.readLittleEndianUnsignedInt(); long sampleRate = headerData.readLittleEndianUnsignedInt();
int bitrateMax = headerData.readLittleEndianInt(); int bitrateMax = headerData.readLittleEndianInt();
int bitrateNominal = headerData.readLittleEndianInt(); int bitrateNominal = headerData.readLittleEndianInt();
int bitrateMin = headerData.readLittleEndianInt(); int bitrateMin = headerData.readLittleEndianInt();
...@@ -104,7 +91,8 @@ import java.util.Arrays; ...@@ -104,7 +91,8 @@ import java.util.Arrays;
public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData) public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData)
throws ParserException { throws ParserException {
int length = captureVorbisHeader(0x03, headerData); verifyVorbisHeaderCapturePattern(0x03, headerData, false);
int length = 7;
int len = (int) headerData.readLittleEndianUnsignedInt(); int len = (int) headerData.readLittleEndianUnsignedInt();
length += 4; length += 4;
...@@ -127,21 +115,40 @@ import java.util.Arrays; ...@@ -127,21 +115,40 @@ import java.util.Arrays;
return new CommentHeader(vendor, comments, length); return new CommentHeader(vendor, comments, length);
} }
private static int captureVorbisHeader(int headerType, ParsableByteArray idHeader) /**
* Verifies whether the next bytes in {@code header} are a vorbis header of the given
* {@code headerType}.
*
* @param headerType the type of the header expected.
* @param header the alleged header bytes.
* @param quite if {@code true} no exceptions are thrown. Instead {@code false} is returned.
* @return the number of bytes read.
* @throws ParserException thrown if header type or capture pattern is not as expected.
*/
public static boolean verifyVorbisHeaderCapturePattern(int headerType, ParsableByteArray header,
boolean quite)
throws ParserException { throws ParserException {
if (idHeader.readUnsignedByte() != headerType) { if (header.readUnsignedByte() != headerType) {
throw new ParserException("expected header type " + Integer.toHexString(headerType)); if (quite) {
return false;
} else {
throw new ParserException("expected header type " + Integer.toHexString(headerType));
}
} }
if (!(idHeader.readUnsignedByte() == 'v' if (!(header.readUnsignedByte() == 'v'
&& idHeader.readUnsignedByte() == 'o' && header.readUnsignedByte() == 'o'
&& idHeader.readUnsignedByte() == 'r' && header.readUnsignedByte() == 'r'
&& idHeader.readUnsignedByte() == 'b' && header.readUnsignedByte() == 'b'
&& idHeader.readUnsignedByte() == 'i' && header.readUnsignedByte() == 'i'
&& idHeader.readUnsignedByte() == 's')) { && header.readUnsignedByte() == 's')) {
throw new ParserException("expected characters 'vorbis'"); if (quite) {
return false;
} else {
throw new ParserException("expected characters 'vorbis'");
}
} }
return 7; // bytes read return true;
} }
/** /**
...@@ -159,7 +166,7 @@ import java.util.Arrays; ...@@ -159,7 +166,7 @@ import java.util.Arrays;
public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels) public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels)
throws ParserException { throws ParserException {
captureVorbisHeader(0x05, headerData); verifyVorbisHeaderCapturePattern(0x05, headerData, false);
int numberOfBooks = headerData.readUnsignedByte() + 1; int numberOfBooks = headerData.readUnsignedByte() + 1;
...@@ -433,7 +440,7 @@ import java.util.Arrays; ...@@ -433,7 +440,7 @@ import java.util.Arrays;
public final long version; public final long version;
public final int channels; public final int channels;
public final int sampleRate; public final long sampleRate;
public final int bitrateMax; public final int bitrateMax;
public final int bitrateNominal; public final int bitrateNominal;
public final int bitrateMin; public final int bitrateMin;
...@@ -442,7 +449,7 @@ import java.util.Arrays; ...@@ -442,7 +449,7 @@ import java.util.Arrays;
public final boolean framingFlag; public final boolean framingFlag;
public final byte[] data; public final byte[] data;
public VorbisIdHeader(long version, int channels, int sampleRate, int bitrateMax, public VorbisIdHeader(long version, int channels, long sampleRate, int bitrateMax,
int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag, int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag,
byte[] data) { byte[] data) {
this.version = version; this.version = version;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment