Commit 89ce1cce by Oliver Woodman

OggVorbisExtractor (WIP - Seeking not yet enabled)

parent 88fa1495
...@@ -242,6 +242,8 @@ import java.util.Locale; ...@@ -242,6 +242,8 @@ import java.util.Locale;
+ "&key=ik0", Util.TYPE_OTHER), + "&key=ik0", Util.TYPE_OTHER),
new Sample("Google Play (MP3 Audio)", new Sample("Google Play (MP3 Audio)",
"http://storage.googleapis.com/exoplayer-test-media-0/play.mp3", Util.TYPE_OTHER), "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3", Util.TYPE_OTHER),
new Sample("Google Play (Ogg/Vorbis Audio)",
"https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg", Util.TYPE_OTHER),
new Sample("Google Glass (WebM Video with Vorbis Audio)", new Sample("Google Glass (WebM Video with Vorbis Audio)",
"http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", Util.TYPE_OTHER), "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", Util.TYPE_OTHER),
new Sample("Big Buck Bunny (FLV Video)", new Sample("Big Buck Bunny (FLV Video)",
......
/*
* 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.util.ParsableByteArray;
import android.util.Log;
import junit.framework.TestCase;
import java.io.IOException;
/**
* Unit test for {@link OggVorbisExtractor}.
*/
public final class OggVorbisExtractorTest extends TestCase {
private static final String TAG = "OggVorbisExtractorTest";
private OggVorbisExtractor extractor;
private RecordableOggExtractorInput extractorInput;
@Override
public void setUp() throws Exception {
super.setUp();
extractorInput = new RecordableOggExtractorInput(1024 * 64);
extractor = new OggVorbisExtractor();
}
public void testSniff() throws Exception {
extractorInput.recordOggHeader((byte) 0x02, 0, (byte) 0x02);
extractorInput.recordOggLaces(new byte[]{120, 120});
assertTrue(extractor.sniff(extractorInput));
}
public void testSniffFails() throws Exception {
extractorInput.recordOggHeader((byte) 0x00, 0, (byte) 0);
assertFalse(extractor.sniff(extractorInput));
}
public void testAppendNumberOfSamples() throws Exception {
ParsableByteArray buffer = new ParsableByteArray(4);
buffer.setLimit(0);
OggVorbisExtractor.appendNumberOfSamples(buffer, 0x01234567);
assertEquals(4, buffer.limit());
assertEquals(0x67, buffer.data[0]);
assertEquals(0x45, buffer.data[1]);
assertEquals(0x23, buffer.data[2]);
assertEquals(0x01, buffer.data[3]);
}
public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException {
extractorInput.doThrowExceptionsAtRead(true);
extractorInput.doThrowExceptionsAtPeek(true);
byte[] data = TestData.getVorbisHeaderPages();
extractorInput.record(data);
int exceptionCount = 0;
int maxExceptions = 20;
OggVorbisExtractor.VorbisSetup vorbisSetup;
while (exceptionCount < maxExceptions) {
try {
vorbisSetup = extractor.readSetupHeaders(extractorInput,
new ParsableByteArray(new byte[255 * 255], 0));
assertNotNull(vorbisSetup.idHeader);
assertNotNull(vorbisSetup.commentHeader);
assertNotNull(vorbisSetup.setupHeaderData);
assertNotNull(vorbisSetup.modes);
assertEquals(45, vorbisSetup.commentHeader.length);
assertEquals(30, vorbisSetup.idHeader.data.length);
assertEquals(3597, vorbisSetup.setupHeaderData.length);
assertEquals(-1, vorbisSetup.idHeader.bitrateMax);
assertEquals(-1, vorbisSetup.idHeader.bitrateMin);
assertEquals(66666, vorbisSetup.idHeader.bitrateNominal);
assertEquals(512, vorbisSetup.idHeader.blockSize0);
assertEquals(1024, vorbisSetup.idHeader.blockSize1);
assertEquals(2, vorbisSetup.idHeader.channels);
assertTrue(vorbisSetup.idHeader.framingFlag);
assertEquals(22050, vorbisSetup.idHeader.sampleRate);
assertEquals(0, vorbisSetup.idHeader.version);
assertEquals("Xiph.Org libVorbis I 20030909", vorbisSetup.commentHeader.vendor);
assertEquals(1, vorbisSetup.iLogModes);
assertEquals(data[data.length - 1],
vorbisSetup.setupHeaderData[vorbisSetup.setupHeaderData.length - 1]);
assertFalse(vorbisSetup.modes[0].blockFlag);
assertTrue(vorbisSetup.modes[1].blockFlag);
break;
} catch (Throwable e) {
Log.e(TAG, e.getMessage(), e);
extractorInput.resetPeekPosition();
exceptionCount++;
}
}
if (exceptionCount >= maxExceptions) {
fail("more than " + maxExceptions + " exceptions thrown");
}
}
}
/*
* 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 java.io.EOFException;
import java.io.IOException;
/**
* Implementation of {@link ExtractorInput} for testing purpose.
*/
/* package */ class RecordableExtractorInput implements ExtractorInput {
protected static final byte STREAM_REVISION = 0x00;
private byte[] data;
private int readOffset;
private int writeOffset;
private int peekOffset;
private boolean throwExceptionsAtRead = false;
private boolean throwExceptionsAtPeek = false;
private int numberOfReadsUntilException = 1;
private int numberOfPeeksUntilException = 1;
private int readCounter;
private int peekCounter;
private int maxReadExceptions = Integer.MAX_VALUE;
private int maxPeekExceptions = Integer.MAX_VALUE;
private int readExceptionCounter;
private int peekExceptionCounter;
/**
* Constructs an instance with a initial array of bytes.
*
* @param data the initial data.
* @param writeOffset the {@code writeOffset} from where to start recording.
*/
public RecordableExtractorInput(byte[] data, int writeOffset) {
this.data = data;
this.writeOffset = writeOffset;
}
/**
* Constructs an instance with an empty data array with length {@code maxBytes}.
*
* @param maxBytes the maximal number of bytes this {@code ExtractorInput} can store.
*/
public RecordableExtractorInput(int maxBytes) {
this(new byte[maxBytes], 0);
}
@Override
public int read(byte[] target, int offset, int length) throws IOException, InterruptedException {
readFully(target, offset, length);
return isEOF() ? C.RESULT_END_OF_INPUT : length;
}
@Override
public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
readCounter++;
if (throwExceptionsAtRead
&& readExceptionCounter < maxReadExceptions
&& readCounter % numberOfReadsUntilException == 0) {
readCounter = 0;
numberOfReadsUntilException++;
readExceptionCounter++;
throw new IOException("deliberately thrown an exception for testing");
}
if (readOffset + length > writeOffset) {
if (!allowEndOfInput) {
throw new EOFException();
}
return false;
}
System.arraycopy(data, readOffset, target, offset, length);
readOffset += length;
peekOffset = readOffset;
return true;
}
@Override
public void readFully(byte[] target, int offset, int length)
throws IOException, InterruptedException {
readFully(target, offset, length, false);
}
@Override
public int skip(int length) throws IOException, InterruptedException {
skipFully(length);
return isEOF() ? C.RESULT_END_OF_INPUT : length;
}
private boolean isEOF() {
return readOffset == writeOffset;
}
@Override
public boolean skipFully(int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
if (readOffset + length >= writeOffset) {
if (!allowEndOfInput) {
throw new EOFException();
}
return false;
}
readOffset += length;
peekOffset = readOffset;
return true;
}
@Override
public void skipFully(int length) throws IOException, InterruptedException {
skipFully(length, false);
}
@Override
public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
peekCounter++;
if (throwExceptionsAtPeek
&& peekExceptionCounter < maxPeekExceptions
&& peekCounter % numberOfPeeksUntilException == 0) {
peekCounter = 0;
numberOfPeeksUntilException++;
peekExceptionCounter++;
throw new IOException("deliberately thrown an exception for testing");
}
if (peekOffset + length > writeOffset) {
if (!allowEndOfInput) {
throw new EOFException();
}
return false;
}
System.arraycopy(data, peekOffset, target, offset, length);
peekOffset += length;
return true;
}
@Override
public void peekFully(byte[] target, int offset, int length)
throws IOException, InterruptedException {
peekFully(target, offset, length, false);
}
@Override
public boolean advancePeekPosition(int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
if (peekOffset + length >= writeOffset) {
if (!allowEndOfInput) {
throw new EOFException();
}
return false;
}
peekOffset += length;
return true;
}
@Override
public void advancePeekPosition(int length) throws IOException, InterruptedException {
advancePeekPosition(length, false);
}
@Override
public void resetPeekPosition() {
peekOffset = readOffset;
}
@Override
public long getPosition() {
return readOffset;
}
@Override
public long getLength() {
return writeOffset;
}
/**
* Records the {@code bytes}.
*
* @param bytes the bytes to record.
*/
public void record(final byte[] bytes) {
System.arraycopy(bytes, 0, data, writeOffset, bytes.length);
writeOffset += bytes.length;
}
/** Records a single byte. **/
public void record(byte b) {
record(new byte[]{b});
}
/**
* Gets a byte array with length {@code length} with ascending values starting from 0 (zero).
*
* @param length the length of the array.
* @return an array of bytes with ascending values.
*/
public static byte[] getBytesGrowingValues(int length) {
return fillBytesGrowingValues(new byte[length], length, (byte) 0);
}
/**
* Gets a byte array with length {@code length} with ascending values starting
* from {@code startValue}.
*
* @param length the length of the array.
* @param startValue the value from which to start.
* @return an array of bytes with ascending values starting from {@code startValue}.
*/
public static byte[] getBytesGrowingValues(int length, byte startValue) {
return fillBytesGrowingValues(new byte[length], length, startValue);
}
/**
* Fills the byte array passed as argument with ascending values.
*
* @param bytes the byte array to fill with values.
* @param limit the number of bytes to set in the array.
* @param startValue the startValue from which the values in the array have to start.
*/
public static byte[] fillBytesGrowingValues(byte[] bytes, int limit, byte startValue) {
for (int i = 0; i < bytes.length; i++) {
if (i < limit) {
bytes[i] = (byte) ((i + startValue) % 255);
} else {
bytes[i] = 0;
}
}
return bytes;
}
public void setMaxReadExceptions(int maxReadExceptions) {
this.maxReadExceptions = maxReadExceptions;
}
public void setMaxPeekExceptions(int maxPeekExceptions) {
this.maxPeekExceptions = maxPeekExceptions;
}
public void setNumberOfReadsUntilException(int numberOfReadsUntilException) {
this.numberOfReadsUntilException = numberOfReadsUntilException;
}
public void setNumberOfPeeksUntilException(int numberOfPeeksUntilException) {
this.numberOfPeeksUntilException = numberOfPeeksUntilException;
}
public void doThrowExceptionsAtRead(boolean throwExceptionsAtRead) {
this.throwExceptionsAtRead = throwExceptionsAtRead;
}
public void doThrowExceptionsAtPeek(boolean throwExceptionsAtPeek) {
this.throwExceptionsAtPeek = throwExceptionsAtPeek;
}
}
/*
* 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;
/**
* A {@link RecordableOggExtractorInput} with convenient methods to record an OGG byte stream.
*/
/* package */ final class RecordableOggExtractorInput extends RecordableExtractorInput {
private long pageSequenceCounter;
public RecordableOggExtractorInput(byte[] data, int writeOffset) {
super(data, writeOffset);
pageSequenceCounter = 1000;
}
public RecordableOggExtractorInput(int maxBytes) {
this(new byte[maxBytes], 0);
}
/**
* Syntax sugar to make tests more readable.
*
* @param laces the laces to record to the data.
*/
protected void recordOggLaces(final byte[] laces) {
record(laces);
}
/**
* Syntax sugar to make tests more readable.
*
* @param packet the packet bytes to record to the data.
*/
protected void recordOggPacket(final byte[] packet) {
record(packet);
}
protected void recordOggHeader(final byte headerType, final long granule,
final byte pageSegmentCount) {
record((byte) 0x4F); // O
record((byte) 0x67); // g
record((byte) 0x67); // g
record((byte) 0x53); // S
record(STREAM_REVISION);
record(headerType);
recordGranulePosition(granule);
record((byte) 0x00); // LSB of data serial number
record((byte) 0x10);
record((byte) 0x00);
record((byte) 0x00); // MSB of data serial number
recordPageSequenceCounter();
record((byte) 0x00); // LSB of page checksum
record((byte) 0x00);
record((byte) 0x00);
record((byte) 0x00); // MSB of page checksum
record(pageSegmentCount); // 0 - 255
}
protected void recordGranulePosition(long granule) {
record((byte) (granule & 0xFF));
record((byte) ((granule >> 8) & 0xFF));
record((byte) ((granule >> 16) & 0xFF));
record((byte) ((granule >> 24) & 0xFF));
record((byte) ((granule >> 32) & 0xFF));
record((byte) ((granule >> 40) & 0xFF));
record((byte) ((granule >> 48) & 0xFF));
record((byte) ((granule >> 56) & 0xFF));
}
protected void recordPageSequenceCounter() {
record((byte) (pageSequenceCounter & 0xFF));
record((byte) ((pageSequenceCounter >> 8) & 0xFF));
record((byte) ((pageSequenceCounter >> 16) & 0xFF));
record((byte) ((pageSequenceCounter++ >> 24) & 0xFF));
}
}
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ParsableByteArray;
import junit.framework.TestCase;
/**
* Unit test for {@link VorbisUtil}.
*/
public final class VorbisUtilTest extends TestCase {
public void testILog() throws Exception {
assertEquals(0, VorbisUtil.iLog(0));
assertEquals(1, VorbisUtil.iLog(1));
assertEquals(2, VorbisUtil.iLog(2));
assertEquals(2, VorbisUtil.iLog(3));
assertEquals(3, VorbisUtil.iLog(4));
assertEquals(3, VorbisUtil.iLog(5));
assertEquals(4, VorbisUtil.iLog(8));
assertEquals(0, VorbisUtil.iLog(-1));
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);
VorbisUtil.VorbisIdHeader vorbisIdHeader =
VorbisUtil.readVorbisIdentificationHeader(headerData);
assertEquals(22050, vorbisIdHeader.sampleRate);
assertEquals(0, vorbisIdHeader.version);
assertTrue(vorbisIdHeader.framingFlag);
assertEquals(2, vorbisIdHeader.channels);
assertEquals(512, vorbisIdHeader.blockSize0);
assertEquals(1024, vorbisIdHeader.blockSize1);
assertEquals(-1, vorbisIdHeader.bitrateMax);
assertEquals(-1, vorbisIdHeader.bitrateMin);
assertEquals(66666, vorbisIdHeader.bitrateNominal);
assertEquals(66666, vorbisIdHeader.getApproximateBitrate());
}
public void testReadCommentHeader() throws ParserException {
byte[] data = TestData.getCommentHeaderDataUTF8();
ParsableByteArray headerData = new ParsableByteArray(data, data.length);
VorbisUtil.CommentHeader commentHeader = VorbisUtil.readVorbisCommentHeader(headerData);
assertEquals("Xiph.Org libVorbis I 20120203 (Omnipresent)", commentHeader.vendor);
assertEquals(3, commentHeader.comments.length);
assertEquals("ALBUM=äö", commentHeader.comments[0]);
assertEquals("TITLE=A sample song", commentHeader.comments[1]);
assertEquals("ARTIST=Google", commentHeader.comments[2]);
}
public void testReadVorbisModes() throws ParserException {
byte[] data = TestData.getSetupHeaderData();
ParsableByteArray headerData = new ParsableByteArray(data, data.length);
VorbisUtil.Mode[] modes = VorbisUtil.readVorbisModes(headerData, 2);
assertEquals(2, modes.length);
assertEquals(false, modes[0].blockFlag);
assertEquals(0, modes[0].mapping);
assertEquals(0, modes[0].transformType);
assertEquals(0, modes[0].windowType);
assertEquals(true, modes[1].blockFlag);
assertEquals(1, modes[1].mapping);
assertEquals(0, modes[1].transformType);
assertEquals(0, modes[1].windowType);
}
}
...@@ -154,6 +154,13 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -154,6 +154,13 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
// Extractor not found. // Extractor not found.
} }
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.ogg.OggVorbisExtractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
} }
private final ExtractorHolder extractorHolder; private final ExtractorHolder extractorHolder;
......
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import java.io.IOException;
/**
* Reads OGG packets from an {@link ExtractorInput}.
*/
/* package */ final class OggReader {
private static final String CAPTURE_PATTERN_PAGE = "OggS";
private final PageHeader pageHeader = new PageHeader();
private final ParsableByteArray headerArray = new ParsableByteArray(27 + 255);
private int currentSegmentIndex = -1;
/**
* Resets this reader.
*/
public void reset() {
pageHeader.reset();
headerArray.reset();
currentSegmentIndex = -1;
}
/**
* Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make
* sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader
* can resume properly from an error while reading a continued packet spanned across multiple
* pages.
*
* @param input the {@link ExtractorInput} to read data from.
* @param packetArray the {@link ParsableByteArray} to write the packet data into.
* @return {@code true} if the read was successful. {@code false} if the end of the input was
* encountered having read no data.
* @throws IOException thrown if reading from the input fails.
* @throws InterruptedException thrown if interrupted while reading from input.
*/
public boolean readPacket(ExtractorInput input, ParsableByteArray packetArray)
throws IOException, InterruptedException {
Assertions.checkState(input != null && packetArray != null);
boolean packetComplete = false;
while (!packetComplete) {
if (currentSegmentIndex < 0) {
// We're at the start of a page.
if (!populatePageHeader(input, pageHeader, headerArray, false)) {
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;
}
}
if (packetSize > 0) {
input.readFully(packetArray.data, packetArray.limit(), packetSize);
packetArray.setLimit(packetArray.limit() + packetSize);
packetComplete = pageHeader.laces[segmentIndex - 1] != 255;
}
// advance now since we are sure reading didn't throw an exception
currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? -1 : segmentIndex;
}
return true;
}
/**
* Returns the {@link OggReader.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.
* The value of the fields might be changed by the reader when reading the stream advances and
* the next page is read (which implies reading and populating the next header).
*
* @return the {@code PageHeader} of the current page or {@code null}.
*/
public 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;
}
}
}
/*
* 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.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
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.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;
private final ParsableByteArray scratch = new ParsableByteArray(
new byte[OGG_MAX_SEGMENT_SIZE * 255], 0);
private final OggReader oggReader = new OggReader();
private TrackOutput trackOutput;
private VorbisSetup vorbisSetup;
private int previousPacketBlockSize;
private long elapsedSamples;
private boolean seenFirstAudioPacket;
private VorbisUtil.VorbisIdHeader vorbisIdHeader;
private VorbisUtil.CommentHeader commentHeader;
@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");
}
input.resetPeekPosition();
} catch (ParserException e) {
Log.e(TAG, e.getMessage());
return false;
}
return true;
}
@Override
public void init(ExtractorOutput output) {
trackOutput = output.track(0);
output.endTracks();
output.seekMap(SeekMap.UNSEEKABLE);
}
@Override
public void seek() {
oggReader.reset();
previousPacketBlockSize = -1;
elapsedSamples = 0;
seenFirstAudioPacket = false;
scratch.reset();
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (vorbisSetup == null) {
vorbisSetup = readSetupHeaders(input, scratch);
ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
codecInitialisationData.clear();
codecInitialisationData.add(vorbisSetup.idHeader.data);
codecInitialisationData.add(vorbisSetup.setupHeaderData);
long duration = input.getLength() == C.LENGTH_UNBOUNDED ? C.UNKNOWN_TIME_US
: input.getLength() * 8000000 / vorbisSetup.idHeader.getApproximateBitrate();
trackOutput.format(MediaFormat.createAudioFormat(null, MimeTypes.AUDIO_VORBIS,
this.vorbisSetup.idHeader.bitrateNominal, OGG_MAX_SEGMENT_SIZE * 255, duration,
this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
codecInitialisationData, null));
}
if (oggReader.readPacket(input, scratch)) {
// if this is an audio packet...
if ((scratch.data[0] & 0x01) != 1) {
// ... we need to decode the block size
int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup);
// a packet contains samples produced from overlapping the previous and current frame data
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4
: 0;
// 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
seenFirstAudioPacket = true;
elapsedSamples += samplesInPacket;
previousPacketBlockSize = packetBlockSize;
}
scratch.reset();
return RESULT_CONTINUE;
}
return RESULT_END_OF_INPUT;
}
//@VisibleForTesting
/* package */ VorbisSetup readSetupHeaders(ExtractorInput input, ParsableByteArray scratch)
throws IOException, InterruptedException {
if (vorbisIdHeader == null) {
oggReader.readPacket(input, scratch);
vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch);
scratch.reset();
}
if (commentHeader == null) {
oggReader.readPacket(input, scratch);
commentHeader = VorbisUtil.readVorbisCommentHeader(scratch);
scratch.reset();
}
oggReader.readPacket(input, scratch);
// the third packet contains the setup header
byte[] setupHeaderData = new byte[scratch.limit()];
// raw data of vorbis setup header has to be passed to decoder as CSD buffer #2
System.arraycopy(scratch.data, 0, setupHeaderData, 0, scratch.limit());
// partially decode setup header to get the modes
Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels);
// we need the ilog of modes all the time when extracting, so we compute it once
int iLogModes = VorbisUtil.iLog(modes.length - 1);
scratch.reset();
return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes);
}
//@VisibleForTesting
/* package */ static void appendNumberOfSamples(ParsableByteArray buffer,
long packetSampleCount) {
buffer.setLimit(buffer.limit() + 4);
// The vorbis decoder expects the number of samples in the packet
// to be appended to the audio data as an int32
buffer.data[buffer.limit() - 4] = (byte) ((packetSampleCount) & 0xFF);
buffer.data[buffer.limit() - 3] = (byte) ((packetSampleCount >>> 8) & 0xFF);
buffer.data[buffer.limit() - 2] = (byte) ((packetSampleCount >>> 16) & 0xFF);
buffer.data[buffer.limit() - 1] = (byte) ((packetSampleCount >>> 24) & 0xFF);
}
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 currentBlockSize;
if (!vorbisSetup.modes[modeNumber].blockFlag) {
currentBlockSize = vorbisSetup.idHeader.blockSize0;
} else {
currentBlockSize = vorbisSetup.idHeader.blockSize1;
}
return currentBlockSize;
}
/**
* Class to hold all data read from Vorbis setup headers.
*/
/* package */ static final class VorbisSetup {
public final VorbisUtil.VorbisIdHeader idHeader;
public final VorbisUtil.CommentHeader commentHeader;
public final byte[] setupHeaderData;
public final Mode[] modes;
public final int iLogModes;
public VorbisSetup(VorbisUtil.VorbisIdHeader idHeader, VorbisUtil.CommentHeader
commentHeader, byte[] setupHeaderData, Mode[] modes, int iLogModes) {
this.idHeader = idHeader;
this.commentHeader = commentHeader;
this.setupHeaderData = setupHeaderData;
this.modes = modes;
this.iLogModes = iLogModes;
}
}
}
/*
* 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.util.Assertions;
/**
* Wraps a byte array, providing methods that allow it to be read as a vorbis bitstream.
*
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002">Vorbis bitpacking
* specification</a>
*/
/* package */ final class VorbisBitArray {
public final byte[] data;
private int limit;
private int byteOffset;
private int bitOffset;
/**
* Creates a new instance that wraps an existing array.
*
* @param data the array to wrap.
*/
public VorbisBitArray(byte[] data) {
this(data, data.length);
}
/**
* Creates a new instance that wraps an existing array.
*
* @param data the array to wrap.
* @param limit the limit in bytes.
*/
public VorbisBitArray(byte[] data, int limit) {
this.data = data;
this.limit = limit * 8;
}
/** Resets the reading position to zero. */
public void reset() {
byteOffset = 0;
bitOffset = 0;
}
/**
* Reads a single bit.
*
* @return {@code true} if the bit is set, {@code false} otherwise.
*/
public boolean readBit() {
return readBits(1) == 1;
}
/**
* Reads up to 32 bits.
*
* @param numBits The number of bits to read.
* @return An int whose bottom {@code numBits} bits hold the read data.
*/
public int readBits(int numBits) {
Assertions.checkState(getPosition() + numBits <= limit);
if (numBits == 0) {
return 0;
}
int result = 0;
int bitCount = 0;
if (bitOffset != 0) {
bitCount = Math.min(numBits, 8 - bitOffset);
int mask = 0xFF >>> (8 - bitCount);
result = (data[byteOffset] >>> bitOffset) & mask;
bitOffset += bitCount;
if (bitOffset == 8) {
byteOffset++;
bitOffset = 0;
}
}
if (numBits - bitCount > 7) {
int numBytes = (numBits - bitCount) / 8;
for (int i = 0; i < numBytes; i++) {
result |= (data[byteOffset++] & 0xFFL) << bitCount;
bitCount += 8;
}
}
if (numBits > bitCount) {
int bitsOnNextByte = numBits - bitCount;
int mask = 0xFF >>> (8 - bitsOnNextByte);
result |= (data[byteOffset] & mask) << bitCount;
bitOffset += bitsOnNextByte;
}
return result;
}
/**
* Skips {@code numberOfBits} bits.
*
* @param numberOfBits the number of bits to skip.
*/
public void skipBits(int numberOfBits) {
Assertions.checkState(getPosition() + numberOfBits <= limit);
byteOffset += numberOfBits / 8;
bitOffset += numberOfBits % 8;
if (bitOffset > 7) {
byteOffset++;
bitOffset -= 8;
}
}
/**
* Gets the current reading position in bits.
*
* @return the current reading position in bits.
*/
public int getPosition() {
return byteOffset * 8 + bitOffset;
}
/**
* Sets the index of the current reading position in bits.
*
* @param position the new reading position in bits.
*/
public void setPosition(int position) {
Assertions.checkArgument(position < limit && position >= 0);
byteOffset = position / 8;
bitOffset = position - (byteOffset * 8);
}
/**
* Gets the number of remaining bits.
*
* @return number of remaining bits.
*/
public int bitsLeft() {
return limit - getPosition();
}
/**
* Returns the limit in bits.
*
* @return the limit in bits.
**/
public int limit() {
return limit;
}
}
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