Bring V2 ogg extractor up to date.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=120332721
This commit is contained in:
olly 2016-04-20 06:57:41 -07:00 committed by Oliver Woodman
parent 6f32636f40
commit 7638bea016
20 changed files with 663 additions and 200 deletions

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer.ext.flac;
import com.google.android.exoplayer.DecoderInputBuffer;
import com.google.android.exoplayer.util.FlacStreamInfo;
import com.google.android.exoplayer.util.extensions.SimpleDecoder;
import com.google.android.exoplayer.util.extensions.SimpleOutputBuffer;

View file

@ -23,6 +23,7 @@ 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.util.FlacStreamInfo;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer.ext.flac;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.util.FlacStreamInfo;
import java.io.IOException;
import java.nio.ByteBuffer;

View file

@ -0,0 +1,95 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException;
import com.google.android.exoplayer.testutil.TestUtil;
import junit.framework.TestCase;
import java.io.IOException;
/**
* Unit test for {@link OggExtractor}.
*/
public final class OggExtractorTest extends TestCase {
private OggExtractor extractor;
@Override
public void setUp() throws Exception {
super.setUp();
extractor = new OggExtractor();
}
public void testSniffVorbis() throws Exception {
byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(120, 120), // Laces
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'});
assertTrue(sniff(createInput(data)));
}
public void testSniffFlac() throws Exception {
byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(120, 120), // Laces
new byte[]{0x7F, 'F', 'L', 'A', 'C', ' ', ' '});
assertTrue(sniff(createInput(data)));
}
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 testSniffInvalidHeader() throws Exception {
byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(120, 120), // Laces
new byte[]{0x7F, '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)));
}
private static FakeExtractorInput createInput(byte[] data) {
return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true)
.setSimulateUnknownLength(true).setSimulatePartialReads(true).build();
}
private boolean sniff(FakeExtractorInput input) throws InterruptedException, IOException {
while (true) {
try {
return extractor.sniff(input);
} catch (SimulatedIOException e) {
// Ignore.
}
}
}
}

View file

@ -31,19 +31,19 @@ import java.util.Arrays;
import java.util.Random;
/**
* Unit test for {@link OggReader}.
* Unit test for {@link OggParser}.
*/
public final class OggReaderTest extends TestCase {
public final class OggParserTest extends TestCase {
private Random random;
private OggReader oggReader;
private OggParser oggParser;
private ParsableByteArray scratch;
@Override
public void setUp() throws Exception {
super.setUp();
random = new Random(0);
oggReader = new OggReader();
oggParser = new OggParser();
scratch = new ParsableByteArray(new byte[255 * 255], 0);
}
@ -72,37 +72,37 @@ public final class OggReaderTest extends TestCase {
fourthPacket), true);
assertReadPacket(input, firstPacket);
assertTrue((oggReader.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggReader.getPageHeader().type & 0x04) == 0x04);
assertEquals(0x02, oggReader.getPageHeader().type);
assertEquals(27 + 1, oggReader.getPageHeader().headerSize);
assertEquals(8, oggReader.getPageHeader().bodySize);
assertEquals(0x00, oggReader.getPageHeader().revision);
assertEquals(1, oggReader.getPageHeader().pageSegmentCount);
assertEquals(1000, oggReader.getPageHeader().pageSequenceNumber);
assertEquals(4096, oggReader.getPageHeader().streamSerialNumber);
assertEquals(0, oggReader.getPageHeader().granulePosition);
assertTrue((oggParser.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggParser.getPageHeader().type & 0x04) == 0x04);
assertEquals(0x02, oggParser.getPageHeader().type);
assertEquals(27 + 1, oggParser.getPageHeader().headerSize);
assertEquals(8, oggParser.getPageHeader().bodySize);
assertEquals(0x00, oggParser.getPageHeader().revision);
assertEquals(1, oggParser.getPageHeader().pageSegmentCount);
assertEquals(1000, oggParser.getPageHeader().pageSequenceNumber);
assertEquals(4096, oggParser.getPageHeader().streamSerialNumber);
assertEquals(0, oggParser.getPageHeader().granulePosition);
assertReadPacket(input, secondPacket);
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggReader.getPageHeader().type & 0x04) == 0x04);
assertEquals(0, oggReader.getPageHeader().type);
assertEquals(27 + 2, oggReader.getPageHeader().headerSize);
assertEquals(255 + 17, oggReader.getPageHeader().bodySize);
assertEquals(2, oggReader.getPageHeader().pageSegmentCount);
assertEquals(1001, oggReader.getPageHeader().pageSequenceNumber);
assertEquals(16, oggReader.getPageHeader().granulePosition);
assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggParser.getPageHeader().type & 0x04) == 0x04);
assertEquals(0, oggParser.getPageHeader().type);
assertEquals(27 + 2, oggParser.getPageHeader().headerSize);
assertEquals(255 + 17, oggParser.getPageHeader().bodySize);
assertEquals(2, oggParser.getPageHeader().pageSegmentCount);
assertEquals(1001, oggParser.getPageHeader().pageSequenceNumber);
assertEquals(16, oggParser.getPageHeader().granulePosition);
assertReadPacket(input, thirdPacket);
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02);
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04);
assertEquals(4, oggReader.getPageHeader().type);
assertEquals(27 + 4, oggReader.getPageHeader().headerSize);
assertEquals(255 + 1 + 255 + 16, oggReader.getPageHeader().bodySize);
assertEquals(4, oggReader.getPageHeader().pageSegmentCount);
assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04);
assertEquals(4, oggParser.getPageHeader().type);
assertEquals(27 + 4, oggParser.getPageHeader().headerSize);
assertEquals(255 + 1 + 255 + 16, oggParser.getPageHeader().bodySize);
assertEquals(4, oggParser.getPageHeader().pageSegmentCount);
// Page 1002 is empty, so current page is 1003.
assertEquals(1003, oggReader.getPageHeader().pageSequenceNumber);
assertEquals(128, oggReader.getPageHeader().granulePosition);
assertEquals(1003, oggParser.getPageHeader().pageSequenceNumber);
assertEquals(128, oggParser.getPageHeader().granulePosition);
assertReadPacket(input, fourthPacket);
@ -140,9 +140,9 @@ public final class OggReaderTest extends TestCase {
Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true);
assertReadPacket(input, firstPacket);
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02);
assertEquals(1001, oggReader.getPageHeader().pageSequenceNumber);
assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertEquals(1001, oggParser.getPageHeader().pageSequenceNumber);
assertReadEof(input);
}
@ -170,9 +170,9 @@ public final class OggReaderTest extends TestCase {
Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true);
assertReadPacket(input, firstPacket);
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02);
assertEquals(1003, oggReader.getPageHeader().pageSequenceNumber);
assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertEquals(1003, oggParser.getPageHeader().pageSequenceNumber);
assertReadEof(input);
}
@ -281,7 +281,7 @@ public final class OggReaderTest extends TestCase {
long elapsedSamplesExpected) throws IOException, InterruptedException {
while (true) {
try {
assertEquals(elapsedSamplesExpected, oggReader.skipToPageOfGranule(input, granule));
assertEquals(elapsedSamplesExpected, oggParser.skipToPageOfGranule(input, granule));
return;
} catch (FakeExtractorInput.SimulatedIOException e) {
input.resetPeekPosition();
@ -330,7 +330,7 @@ public final class OggReaderTest extends TestCase {
throws IOException, InterruptedException {
while (true) {
try {
assertEquals(expected, oggReader.readGranuleOfLastPage(input));
assertEquals(expected, oggParser.readGranuleOfLastPage(input));
break;
} catch (FakeExtractorInput.SimulatedIOException e) {
// ignored
@ -355,7 +355,7 @@ public final class OggReaderTest extends TestCase {
throws InterruptedException, IOException {
while (true) {
try {
return oggReader.readPacket(input, scratch);
return oggParser.readPacket(input, scratch);
} catch (FakeExtractorInput.SimulatedIOException e) {
// Ignore.
}

View file

@ -32,7 +32,7 @@ import java.util.Random;
*/
public final class OggUtilTest extends TestCase {
private final Random random = new Random(0);
private Random random = new Random(0);
public void testReadBits() throws Exception {
assertEquals(0, OggUtil.readBits((byte) 0x00, 2, 2));

View file

@ -34,7 +34,7 @@ import com.google.android.exoplayer.testutil.TestUtil;
0x4F, 0x67, 0x67, 0x53, // Oggs.
0x00, // Stream revision.
headerType,
(int) (granule) & 0xFF,
(int) (granule >> 0) & 0xFF,
(int) (granule >> 8) & 0xFF,
(int) (granule >> 16) & 0xFF,
(int) (granule >> 24) & 0xFF,
@ -46,7 +46,7 @@ import com.google.android.exoplayer.testutil.TestUtil;
0x10,
0x00,
0x00, // MSB of data serial number.
(pageSequenceCounter) & 0xFF,
(pageSequenceCounter >> 0) & 0xFF,
(pageSequenceCounter >> 8) & 0xFF,
(pageSequenceCounter >> 16) & 0xFF,
(pageSequenceCounter >> 24) & 0xFF,

View file

@ -15,10 +15,9 @@
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.extractor.ogg.OggVorbisExtractor.VorbisSetup;
import com.google.android.exoplayer.extractor.ogg.VorbisReader.VorbisSetup;
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;
@ -26,57 +25,24 @@ import junit.framework.TestCase;
import java.io.IOException;
/**
* Unit test for {@link OggVorbisExtractor}.
* Unit test for {@link VorbisReader}.
*/
public final class OggVorbisExtractorTest extends TestCase {
public final class VorbisReaderTest extends TestCase {
private OggVorbisExtractor extractor;
private VorbisReader extractor;
private ParsableByteArray scratch;
@Override
public void setUp() throws Exception {
super.setUp();
extractor = new OggVorbisExtractor();
extractor = new VorbisReader();
scratch = new ParsableByteArray(new byte[255 * 255], 0);
}
public void testSniff() throws Exception {
byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(120, 120), // Laces
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'});
assertTrue(sniff(createInput(data)));
}
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);
OggVorbisExtractor.appendNumberOfSamples(buffer, 0x01234567);
VorbisReader.appendNumberOfSamples(buffer, 0x01234567);
assertEquals(4, buffer.limit());
assertEquals(0x67, buffer.data[0]);
assertEquals(0x45, buffer.data[1]);
@ -86,7 +52,7 @@ public final class OggVorbisExtractorTest extends TestCase {
public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException {
byte[] data = TestData.getVorbisHeaderPages();
OggVorbisExtractor.VorbisSetup vorbisSetup = readSetupHeaders(createInput(data));
VorbisReader.VorbisSetup vorbisSetup = readSetupHeaders(createInput(data));
assertNotNull(vorbisSetup.idHeader);
assertNotNull(vorbisSetup.commentHeader);
@ -122,16 +88,6 @@ public final class OggVorbisExtractorTest extends TestCase {
.setSimulateUnknownLength(true).setSimulatePartialReads(true).build();
}
private boolean sniff(FakeExtractorInput input) throws InterruptedException, IOException {
while (true) {
try {
return extractor.sniff(input);
} catch (SimulatedIOException e) {
// Ignore.
}
}
}
private VorbisSetup readSetupHeaders(FakeExtractorInput input)
throws IOException, InterruptedException {
while (true) {

View file

@ -0,0 +1,97 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.util.FlacSeekTable;
import com.google.android.exoplayer.util.FlacStreamInfo;
import com.google.android.exoplayer.util.FlacUtil;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* {@link StreamReader} to extract Flac data out of Ogg byte stream.
*/
/* package */ final class FlacReader extends StreamReader {
private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF;
private static final byte SEEKTABLE_PACKET_TYPE = 0x03;
private FlacStreamInfo streamInfo;
private FlacSeekTable seekTable;
private boolean firstAudioPacketProcessed;
/* package */ static boolean verifyBitstreamType(ParsableByteArray data) {
return data.readUnsignedByte() == 0x7F && // packet type
data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC"
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
long position = input.getPosition();
if (!oggParser.readPacket(input, scratch)) {
return Extractor.RESULT_END_OF_INPUT;
}
byte[] data = scratch.data;
if (streamInfo == null) {
streamInfo = new FlacStreamInfo(data, 17);
byte[] metadata = Arrays.copyOfRange(data, 9, scratch.limit());
metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
List<byte[]> initializationData = Collections.singletonList(metadata);
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC,
Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate,
initializationData, null));
} else if (data[0] == AUDIO_PACKET_TYPE) {
if (!firstAudioPacketProcessed) {
if (seekTable != null) {
extractorOutput.seekMap(seekTable.createSeekMap(position, streamInfo.sampleRate,
streamInfo.durationUs()));
seekTable = null;
} else {
extractorOutput.seekMap(new SeekMap.Unseekable(streamInfo.durationUs()));
}
firstAudioPacketProcessed = true;
}
trackOutput.sampleData(scratch, scratch.limit());
scratch.setPosition(0);
long timeUs = FlacUtil.extractSampleTimestamp(streamInfo, scratch);
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, scratch.limit(), 0, null);
} else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE && seekTable == null) {
seekTable = FlacSeekTable.parseSeekTable(scratch);
}
scratch.reset();
return Extractor.RESULT_CONTINUE;
}
}

View file

@ -0,0 +1,86 @@
/*
* 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.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.TrackOutput;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* Ogg {@link Extractor}.
*/
public class OggExtractor implements Extractor {
private StreamReader streamReader;
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
try {
ParsableByteArray scratch = new ParsableByteArray(new byte[OggUtil.PAGE_HEADER_SIZE], 0);
OggUtil.PageHeader header = new OggUtil.PageHeader();
if (!OggUtil.populatePageHeader(input, header, scratch, true)
|| (header.type & 0x02) != 0x02 || header.bodySize < 7) {
return false;
}
scratch.reset();
input.peekFully(scratch.data, 0, 7);
if (FlacReader.verifyBitstreamType(scratch)) {
streamReader = new FlacReader();
} else {
scratch.reset();
if (VorbisReader.verifyBitstreamType(scratch)) {
streamReader = new VorbisReader();
} else {
return false;
}
}
return true;
} catch (ParserException e) {
// does not happen
} finally {
}
return false;
}
@Override
public void init(ExtractorOutput output) {
TrackOutput trackOutput = output.track(0);
output.endTracks();
streamReader.init(output, trackOutput);
}
@Override
public void seek() {
streamReader.seek();
}
@Override
public void release() {
// Do nothing
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
return streamReader.read(input, seekPosition);
}
}

View file

@ -27,7 +27,7 @@ import java.io.IOException;
/**
* Reads OGG packets from an {@link ExtractorInput}.
*/
/* package */ final class OggReader {
/* package */ final class OggParser {
public static final int OGG_MAX_SEGMENT_SIZE = 255;
@ -163,7 +163,7 @@ import java.io.IOException;
* Returns the {@link OggUtil.PageHeader} of the current page. The header might not have been
* populated if the first packet has yet to be read.
* <p>
* Note that there is only a single instance of {@code OggReader.PageHeader} which is mutable.
* Note that there is only a single instance of {@code OggParser.PageHeader} which is mutable.
* The value of the fields might be changed by the reader when reading the stream advances and
* the next page is read (which implies reading and populating the next header).
*

View file

@ -29,6 +29,8 @@ import java.io.IOException;
*/
/* package */ final class OggUtil {
public static final int PAGE_HEADER_SIZE = 27;
private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS");
/**
@ -86,7 +88,7 @@ import java.io.IOException;
*
* @param input the {@link ExtractorInput} to read from.
* @param header the {@link PageHeader} to read from.
* @param scratch a scratch array temporary use.
* @param scratch a scratch array temporary use. Its size should be at least PAGE_HEADER_SIZE
* @param quite if {@code true} no Exceptions are thrown but {@code false} is return if something
* goes wrong.
* @return {@code true} if the read was successful. {@code false} if the end of the
@ -100,8 +102,8 @@ import java.io.IOException;
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)) {
|| input.getLength() - input.getPeekPosition() >= PAGE_HEADER_SIZE;
if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, PAGE_HEADER_SIZE, true)) {
if (quite) {
return false;
} else {
@ -134,7 +136,7 @@ import java.io.IOException;
scratch.reset();
// calculate total size of header including laces
header.headerSize = 27 + header.pageSegmentCount;
header.headerSize = PAGE_HEADER_SIZE + header.pageSegmentCount;
input.peekFully(scratch.data, 0, header.pageSegmentCount);
for (int i = 0; i < header.pageSegmentCount; i++) {
header.laces[i] = scratch.readUnsignedByte();

View file

@ -0,0 +1,44 @@
package com.google.android.exoplayer.extractor.ogg;
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.TrackOutput;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* StreamReader abstract class.
*/
/* package */ abstract class StreamReader {
protected final ParsableByteArray scratch = new ParsableByteArray(
new byte[OggParser.OGG_MAX_SEGMENT_SIZE * 255], 0);
protected final OggParser oggParser = new OggParser();
protected TrackOutput trackOutput;
protected ExtractorOutput extractorOutput;
void init(ExtractorOutput output, TrackOutput trackOutput) {
this.extractorOutput = output;
this.trackOutput = trackOutput;
}
/**
* @see Extractor#seek()
*/
void seek() {
oggParser.reset();
scratch.reset();
}
/**
* @see Extractor#read(ExtractorInput, PositionHolder)
*/
abstract int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException;
}

View file

@ -26,9 +26,7 @@ import com.google.android.exoplayer.util.Assertions;
/* package */ final class VorbisBitArray {
public final byte[] data;
private final int limit;
private int limit;
private int byteOffset;
private int bitOffset;

View file

@ -20,10 +20,8 @@ import com.google.android.exoplayer.Format;
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;
@ -32,16 +30,10 @@ import java.io.IOException;
import java.util.ArrayList;
/**
* {@link Extractor} to extract Vorbis data out of Ogg byte stream.
* {@link StreamReader} to extract Vorbis data out of Ogg byte stream.
*/
public final class OggVorbisExtractor implements Extractor, SeekMap {
/* package */ final class VorbisReader extends StreamReader implements SeekMap {
private final ParsableByteArray scratch = new ParsableByteArray(
new byte[OggReader.OGG_MAX_SEGMENT_SIZE * 255], 0);
private final OggReader oggReader = new OggReader();
private TrackOutput trackOutput;
private VorbisSetup vorbisSetup;
private int previousPacketBlockSize;
private long elapsedSamples;
@ -50,7 +42,6 @@ public final class OggVorbisExtractor implements Extractor, SeekMap {
private final OggSeeker oggSeeker = new OggSeeker();
private long targetGranule = -1;
private ExtractorOutput extractorOutput;
private VorbisUtil.VorbisIdHeader vorbisIdHeader;
private VorbisUtil.CommentHeader commentHeader;
private long inputLength;
@ -58,129 +49,105 @@ public final class OggVorbisExtractor implements Extractor, SeekMap {
private long totalSamples;
private long durationUs;
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
/* package */ static boolean verifyBitstreamType(ParsableByteArray data) {
try {
OggUtil.PageHeader header = new OggUtil.PageHeader();
if (!OggUtil.populatePageHeader(input, header, scratch, true)
|| (header.type & 0x02) != 0x02 || header.bodySize < 7) {
return false;
}
scratch.reset();
input.peekFully(scratch.data, 0, 7);
return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, scratch, true);
return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true);
} catch (ParserException e) {
// does not happen
} finally {
scratch.reset();
return false;
}
return false;
}
@Override
public void init(ExtractorOutput output) {
trackOutput = output.track(0);
output.endTracks();
extractorOutput = output;
}
@Override
public void seek() {
oggReader.reset();
super.seek();
previousPacketBlockSize = 0;
elapsedSamples = 0;
seenFirstAudioPacket = false;
scratch.reset();
}
@Override
public void release() {
// Do nothing
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
// Setup.
// 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;
extractorOutput.seekMap(this);
if (inputLength != C.LENGTH_UNBOUNDED) {
// seek to the end just before the last page of stream to get the duration
seekPosition.position = input.getLength() - 8000;
return Extractor.RESULT_SEEK;
}
// Seek to just before the last page of stream to get the duration.
seekPosition.position = input.getLength() - 8000;
return RESULT_SEEK;
}
totalSamples = inputLength == C.LENGTH_UNBOUNDED ? -1
: oggParser.readGranuleOfLastPage(input);
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;
ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
codecInitialisationData.add(vorbisSetup.idHeader.data);
codecInitialisationData.add(vorbisSetup.setupHeaderData);
durationUs = inputLength == C.LENGTH_UNBOUNDED ? C.UNSET_TIME_US
: (totalSamples * C.MICROS_PER_SECOND) / vorbisSetup.idHeader.sampleRate;
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS,
this.vorbisSetup.idHeader.bitrateNominal, OggParser.OGG_MAX_SEGMENT_SIZE * 255,
this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
codecInitialisationData, null));
if (inputLength != C.LENGTH_UNBOUNDED) {
oggSeeker.setup(inputLength - audioStartPosition, totalSamples);
// seek back to resume from where we finished reading vorbis headers
seekPosition.position = audioStartPosition;
return Extractor.RESULT_SEEK;
}
}
// Seeking requested.
// seeking requested
if (!seenFirstAudioPacket && targetGranule > -1) {
OggUtil.skipToNextPage(input);
long position = oggSeeker.getNextSeekPosition(targetGranule, input);
if (position != -1) {
seekPosition.position = position;
return RESULT_SEEK;
return Extractor.RESULT_SEEK;
} else {
elapsedSamples = oggReader.skipToPageOfGranule(input, targetGranule);
elapsedSamples = oggParser.skipToPageOfGranule(input, targetGranule);
previousPacketBlockSize = vorbisIdHeader.blockSize0;
// We're never at the first packet after seeking.
// 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...
// playback
if (oggParser.readPacket(input, scratch)) {
// if this is an audio packet...
if ((scratch.data[0] & 0x01) != 1) {
// ... Then we need to decode the block size
// ... 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;
// 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.
// 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;
trackOutput.sampleData(scratch, scratch.limit());
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, scratch.limit(), 0, null);
targetGranule = -1;
}
// Update state in members for next iteration.
// update state in members for next iteration
seenFirstAudioPacket = true;
elapsedSamples += samplesInPacket;
previousPacketBlockSize = packetBlockSize;
}
scratch.reset();
return RESULT_CONTINUE;
return Extractor.RESULT_CONTINUE;
}
return RESULT_END_OF_INPUT;
return Extractor.RESULT_END_OF_INPUT;
}
//@VisibleForTesting
@ -188,18 +155,18 @@ public final class OggVorbisExtractor implements Extractor, SeekMap {
throws IOException, InterruptedException {
if (vorbisIdHeader == null) {
oggReader.readPacket(input, scratch);
oggParser.readPacket(input, scratch);
vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch);
scratch.reset();
}
if (commentHeader == null) {
oggReader.readPacket(input, scratch);
oggParser.readPacket(input, scratch);
commentHeader = VorbisUtil.readVorbisCommentHeader(scratch);
scratch.reset();
}
oggReader.readPacket(input, scratch);
oggParser.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
@ -238,16 +205,9 @@ public final class OggVorbisExtractor implements Extractor, SeekMap {
return currentBlockSize;
}
// SeekMap implementation.
@Override
public boolean isSeekable() {
return inputLength != C.LENGTH_UNBOUNDED;
}
@Override
public long getDurationUs() {
return durationUs;
return vorbisSetup != null && inputLength != C.LENGTH_UNBOUNDED;
}
@Override
@ -261,7 +221,10 @@ public final class OggVorbisExtractor implements Extractor, SeekMap {
/ durationUs) - 4000);
}
// Internal classes.
@Override
public long getDurationUs() {
return durationUs;
}
/**
* Class to hold all data read from Vorbis setup headers.

View file

@ -174,7 +174,7 @@ import java.util.Arrays;
bitArray.skipBits(headerData.getPosition() * 8);
for (int i = 0; i < numberOfBooks; i++) {
skipBook(bitArray);
readBook(bitArray);
}
int timeCount = bitArray.readBits(6) + 1;
@ -336,7 +336,7 @@ import java.util.Arrays;
}
}
private static void skipBook(VorbisBitArray bitArray) throws ParserException {
private static CodeBook readBook(VorbisBitArray bitArray) throws ParserException {
if (bitArray.readBits(24) != 0x564342) {
throw new ParserException("expected code book to start with [0x56, 0x43, 0x42] at "
+ bitArray.getPosition());
@ -393,6 +393,7 @@ import java.util.Arrays;
// discard (no decoding required yet)
bitArray.skipBits((int) (lookupValuesCount * valueBits));
}
return new CodeBook(dimensions, entries, lengthMap, lookupType, isOrdered);
}
/**
@ -402,6 +403,25 @@ import java.util.Arrays;
return (long) Math.floor(Math.pow(entries, 1.d / dimension));
}
public static final class CodeBook {
public final int dimensions;
public final int entries;
public final long[] lengthMap;
public final int lookupType;
public final boolean isOrdered;
public CodeBook(int dimensions, int entries, long[] lengthMap, int lookupType,
boolean isOrdered) {
this.dimensions = dimensions;
this.entries = entries;
this.lengthMap = lengthMap;
this.lookupType = lookupType;
this.isOrdered = isOrdered;
}
}
public static final class CommentHeader {
public final String vendor;

View file

@ -0,0 +1,91 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.util;
import com.google.android.exoplayer.extractor.SeekMap;
/**
* FLAC seek table class
*/
public final class FlacSeekTable {
private static final int METADATA_LENGTH_OFFSET = 1;
private static final int SEEK_POINT_SIZE = 18;
private final long[] sampleNumbers;
private final long[] offsets;
/**
* Parses a FLAC file seek table metadata structure and creates a FlacSeekTable instance.
*
* @param data A ParsableByteArray including whole seek table metadata block. Its position should
* be set to the beginning of the block.
* @return A FlacSeekTable instance keeping seek table data
* @see <a href="https://xiph.org/flac/format.html#metadata_block_seektable">FLAC format
* METADATA_BLOCK_SEEKTABLE</a>
*/
public static FlacSeekTable parseSeekTable(ParsableByteArray data) {
data.skipBytes(METADATA_LENGTH_OFFSET);
int length = data.readUnsignedInt24();
int numberOfSeekPoints = length / SEEK_POINT_SIZE;
long[] sampleNumbers = new long[numberOfSeekPoints];
long[] offsets = new long[numberOfSeekPoints];
for (int i = 0; i < numberOfSeekPoints; i++) {
sampleNumbers[i] = data.readLong();
offsets[i] = data.readLong();
data.skipBytes(2); // Skip "Number of samples in the target frame."
}
return new FlacSeekTable(sampleNumbers, offsets);
}
private FlacSeekTable(long[] sampleNumbers, long[] offsets) {
this.sampleNumbers = sampleNumbers;
this.offsets = offsets;
}
/**
* Creates a {@link SeekMap} wrapper for this FlacSeekTable.
*
* @param firstFrameOffset Offset of the first FLAC frame
* @param sampleRate Sample rate of the FLAC file.
* @return A SeekMap wrapper for this FlacSeekTable.
*/
public SeekMap createSeekMap(final long firstFrameOffset, final long sampleRate,
final long durationUs) {
return new SeekMap() {
@Override
public boolean isSeekable() {
return true;
}
@Override
public long getPosition(long timeUs) {
long sample = (timeUs * sampleRate) / 1000000L;
int index = Util.binarySearchFloor(sampleNumbers, sample, true, true);
return firstFrameOffset + offsets[index];
}
@Override
public long getDurationUs() {
return durationUs;
}
};
}
}

View file

@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.ext.flac;
package com.google.android.exoplayer.util;
/**
* Holder for flac stream info.
* Holder for FLAC stream info.
*/
/* package */ final class FlacStreamInfo {
public final class FlacStreamInfo {
public final int minBlockSize;
public final int maxBlockSize;
public final int minFrameSize;
@ -28,6 +29,28 @@ package com.google.android.exoplayer.ext.flac;
public final int bitsPerSample;
public final long totalSamples;
/**
* Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure.
*
* @param data An array holding FLAC stream info metadata structure
* @param offset Offset of the structure in the array
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a>
*/
public FlacStreamInfo(byte[] data, int offset) {
ParsableBitArray scratch = new ParsableBitArray(data);
scratch.setPosition(offset * 8);
this.minBlockSize = scratch.readBits(16);
this.maxBlockSize = scratch.readBits(16);
this.minFrameSize = scratch.readBits(24);
this.maxFrameSize = scratch.readBits(24);
this.sampleRate = scratch.readBits(20);
this.channels = scratch.readBits(3) + 1;
this.bitsPerSample = scratch.readBits(5) + 1;
this.totalSamples = scratch.readBits(36);
// Remaining 16 bytes is md5 value
}
public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize,
int sampleRate, int channels, int bitsPerSample, long totalSamples) {
this.minBlockSize = minBlockSize;
@ -51,4 +74,5 @@ package com.google.android.exoplayer.ext.flac;
public long durationUs() {
return (totalSamples * 1000000L) / sampleRate;
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.util;
/**
* Utility functions for FLAC
*/
public final class FlacUtil {
private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
/**
* Prevents initialization.
*/
private FlacUtil() {}
/**
* Extracts sample timestamp from the given binary FLAC frame header data structure.
*
* @param streamInfo A {@link FlacStreamInfo} instance
* @param frameData A {@link ParsableByteArray} including binary FLAC frame header data structure.
* Its position should be set to the beginning of the structure.
* @return Sample timestamp
* @see <a href="https://xiph.org/flac/format.html#frame_header">FLAC format FRAME_HEADER</a>
*/
public static long extractSampleTimestamp(FlacStreamInfo streamInfo,
ParsableByteArray frameData) {
frameData.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);
long sampleNumber = frameData.readUtf8EncodedLong();
if (streamInfo.minBlockSize == streamInfo.maxBlockSize) {
// if fixed block size then sampleNumber is frame number
sampleNumber *= streamInfo.minBlockSize;
}
return (sampleNumber * 1000000L) / streamInfo.sampleRate;
}
}

View file

@ -391,4 +391,38 @@ public final class ParsableByteArray {
return line;
}
/**
* Reads a long value encoded by UTF-8 encoding
* @throws NumberFormatException if there is a problem with decoding
* @return Decoded long value
*/
public long readUtf8EncodedLong() {
int length = 0;
long value = data[position];
// find the high most 0 bit
for (int j = 7; j >= 0; j--) {
if ((value & (1 << j)) == 0) {
if (j < 6) {
value &= (1 << j) - 1;
length = 7 - j;
} else if (j == 7) {
length = 1;
}
break;
}
}
if (length == 0) {
throw new NumberFormatException("Invalid UTF-8 sequence first byte: " + value);
}
for (int i = 1; i < length; i++) {
int x = data[position + i];
if ((x & 0xC0) != 0x80) { // if the high most 0 bit not 7th
throw new NumberFormatException("Invalid UTF-8 sequence continuation byte: " + value);
}
value = (value << 6) | (x & 0x3F);
}
position += length;
return value;
}
}