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 {
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));
}
}
/*
* 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;
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.
*/
......
......@@ -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}.
*
* @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;
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;
......
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