Commit 7638bea0 by olly Committed by Oliver Woodman

Bring V2 ogg extractor up to date.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=120332721
parent 6f32636f
Showing with 664 additions and 201 deletions
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer.ext.flac; package com.google.android.exoplayer.ext.flac;
import com.google.android.exoplayer.DecoderInputBuffer; 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.SimpleDecoder;
import com.google.android.exoplayer.util.extensions.SimpleOutputBuffer; import com.google.android.exoplayer.util.extensions.SimpleOutputBuffer;
......
...@@ -23,6 +23,7 @@ import com.google.android.exoplayer.extractor.ExtractorOutput; ...@@ -23,6 +23,7 @@ import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.PositionHolder; import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.FlacStreamInfo;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.ext.flac; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.ext.flac;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.util.FlacStreamInfo;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
......
/*
* 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.
}
}
}
}
...@@ -31,19 +31,19 @@ import java.util.Arrays; ...@@ -31,19 +31,19 @@ import java.util.Arrays;
import java.util.Random; 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 Random random;
private OggReader oggReader; private OggParser oggParser;
private ParsableByteArray scratch; private ParsableByteArray scratch;
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
random = new Random(0); random = new Random(0);
oggReader = new OggReader(); oggParser = new OggParser();
scratch = new ParsableByteArray(new byte[255 * 255], 0); scratch = new ParsableByteArray(new byte[255 * 255], 0);
} }
...@@ -72,37 +72,37 @@ public final class OggReaderTest extends TestCase { ...@@ -72,37 +72,37 @@ public final class OggReaderTest extends TestCase {
fourthPacket), true); fourthPacket), true);
assertReadPacket(input, firstPacket); assertReadPacket(input, firstPacket);
assertTrue((oggReader.getPageHeader().type & 0x02) == 0x02); assertTrue((oggParser.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggReader.getPageHeader().type & 0x04) == 0x04); assertFalse((oggParser.getPageHeader().type & 0x04) == 0x04);
assertEquals(0x02, oggReader.getPageHeader().type); assertEquals(0x02, oggParser.getPageHeader().type);
assertEquals(27 + 1, oggReader.getPageHeader().headerSize); assertEquals(27 + 1, oggParser.getPageHeader().headerSize);
assertEquals(8, oggReader.getPageHeader().bodySize); assertEquals(8, oggParser.getPageHeader().bodySize);
assertEquals(0x00, oggReader.getPageHeader().revision); assertEquals(0x00, oggParser.getPageHeader().revision);
assertEquals(1, oggReader.getPageHeader().pageSegmentCount); assertEquals(1, oggParser.getPageHeader().pageSegmentCount);
assertEquals(1000, oggReader.getPageHeader().pageSequenceNumber); assertEquals(1000, oggParser.getPageHeader().pageSequenceNumber);
assertEquals(4096, oggReader.getPageHeader().streamSerialNumber); assertEquals(4096, oggParser.getPageHeader().streamSerialNumber);
assertEquals(0, oggReader.getPageHeader().granulePosition); assertEquals(0, oggParser.getPageHeader().granulePosition);
assertReadPacket(input, secondPacket); assertReadPacket(input, secondPacket);
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02); assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggReader.getPageHeader().type & 0x04) == 0x04); assertFalse((oggParser.getPageHeader().type & 0x04) == 0x04);
assertEquals(0, oggReader.getPageHeader().type); assertEquals(0, oggParser.getPageHeader().type);
assertEquals(27 + 2, oggReader.getPageHeader().headerSize); assertEquals(27 + 2, oggParser.getPageHeader().headerSize);
assertEquals(255 + 17, oggReader.getPageHeader().bodySize); assertEquals(255 + 17, oggParser.getPageHeader().bodySize);
assertEquals(2, oggReader.getPageHeader().pageSegmentCount); assertEquals(2, oggParser.getPageHeader().pageSegmentCount);
assertEquals(1001, oggReader.getPageHeader().pageSequenceNumber); assertEquals(1001, oggParser.getPageHeader().pageSequenceNumber);
assertEquals(16, oggReader.getPageHeader().granulePosition); assertEquals(16, oggParser.getPageHeader().granulePosition);
assertReadPacket(input, thirdPacket); assertReadPacket(input, thirdPacket);
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02); assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04); assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04);
assertEquals(4, oggReader.getPageHeader().type); assertEquals(4, oggParser.getPageHeader().type);
assertEquals(27 + 4, oggReader.getPageHeader().headerSize); assertEquals(27 + 4, oggParser.getPageHeader().headerSize);
assertEquals(255 + 1 + 255 + 16, oggReader.getPageHeader().bodySize); assertEquals(255 + 1 + 255 + 16, oggParser.getPageHeader().bodySize);
assertEquals(4, oggReader.getPageHeader().pageSegmentCount); assertEquals(4, oggParser.getPageHeader().pageSegmentCount);
// Page 1002 is empty, so current page is 1003. // Page 1002 is empty, so current page is 1003.
assertEquals(1003, oggReader.getPageHeader().pageSequenceNumber); assertEquals(1003, oggParser.getPageHeader().pageSequenceNumber);
assertEquals(128, oggReader.getPageHeader().granulePosition); assertEquals(128, oggParser.getPageHeader().granulePosition);
assertReadPacket(input, fourthPacket); assertReadPacket(input, fourthPacket);
...@@ -140,9 +140,9 @@ public final class OggReaderTest extends TestCase { ...@@ -140,9 +140,9 @@ public final class OggReaderTest extends TestCase {
Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true); Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true);
assertReadPacket(input, firstPacket); assertReadPacket(input, firstPacket);
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04); assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02); assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertEquals(1001, oggReader.getPageHeader().pageSequenceNumber); assertEquals(1001, oggParser.getPageHeader().pageSequenceNumber);
assertReadEof(input); assertReadEof(input);
} }
...@@ -170,9 +170,9 @@ public final class OggReaderTest extends TestCase { ...@@ -170,9 +170,9 @@ public final class OggReaderTest extends TestCase {
Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true); Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true);
assertReadPacket(input, firstPacket); assertReadPacket(input, firstPacket);
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04); assertTrue((oggParser.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02); assertFalse((oggParser.getPageHeader().type & 0x02) == 0x02);
assertEquals(1003, oggReader.getPageHeader().pageSequenceNumber); assertEquals(1003, oggParser.getPageHeader().pageSequenceNumber);
assertReadEof(input); assertReadEof(input);
} }
...@@ -281,7 +281,7 @@ public final class OggReaderTest extends TestCase { ...@@ -281,7 +281,7 @@ public final class OggReaderTest extends TestCase {
long elapsedSamplesExpected) throws IOException, InterruptedException { long elapsedSamplesExpected) throws IOException, InterruptedException {
while (true) { while (true) {
try { try {
assertEquals(elapsedSamplesExpected, oggReader.skipToPageOfGranule(input, granule)); assertEquals(elapsedSamplesExpected, oggParser.skipToPageOfGranule(input, granule));
return; return;
} catch (FakeExtractorInput.SimulatedIOException e) { } catch (FakeExtractorInput.SimulatedIOException e) {
input.resetPeekPosition(); input.resetPeekPosition();
...@@ -330,7 +330,7 @@ public final class OggReaderTest extends TestCase { ...@@ -330,7 +330,7 @@ public final class OggReaderTest extends TestCase {
throws IOException, InterruptedException { throws IOException, InterruptedException {
while (true) { while (true) {
try { try {
assertEquals(expected, oggReader.readGranuleOfLastPage(input)); assertEquals(expected, oggParser.readGranuleOfLastPage(input));
break; break;
} catch (FakeExtractorInput.SimulatedIOException e) { } catch (FakeExtractorInput.SimulatedIOException e) {
// ignored // ignored
...@@ -355,7 +355,7 @@ public final class OggReaderTest extends TestCase { ...@@ -355,7 +355,7 @@ public final class OggReaderTest extends TestCase {
throws InterruptedException, IOException { throws InterruptedException, IOException {
while (true) { while (true) {
try { try {
return oggReader.readPacket(input, scratch); return oggParser.readPacket(input, scratch);
} catch (FakeExtractorInput.SimulatedIOException e) { } catch (FakeExtractorInput.SimulatedIOException e) {
// Ignore. // Ignore.
} }
......
...@@ -32,7 +32,7 @@ import java.util.Random; ...@@ -32,7 +32,7 @@ import java.util.Random;
*/ */
public final class OggUtilTest extends TestCase { public final class OggUtilTest extends TestCase {
private final Random random = new Random(0); private Random random = new Random(0);
public void testReadBits() throws Exception { public void testReadBits() throws Exception {
assertEquals(0, OggUtil.readBits((byte) 0x00, 2, 2)); assertEquals(0, OggUtil.readBits((byte) 0x00, 2, 2));
......
...@@ -34,7 +34,7 @@ import com.google.android.exoplayer.testutil.TestUtil; ...@@ -34,7 +34,7 @@ import com.google.android.exoplayer.testutil.TestUtil;
0x4F, 0x67, 0x67, 0x53, // Oggs. 0x4F, 0x67, 0x67, 0x53, // Oggs.
0x00, // Stream revision. 0x00, // Stream revision.
headerType, headerType,
(int) (granule) & 0xFF, (int) (granule >> 0) & 0xFF,
(int) (granule >> 8) & 0xFF, (int) (granule >> 8) & 0xFF,
(int) (granule >> 16) & 0xFF, (int) (granule >> 16) & 0xFF,
(int) (granule >> 24) & 0xFF, (int) (granule >> 24) & 0xFF,
...@@ -46,7 +46,7 @@ import com.google.android.exoplayer.testutil.TestUtil; ...@@ -46,7 +46,7 @@ import com.google.android.exoplayer.testutil.TestUtil;
0x10, 0x10,
0x00, 0x00,
0x00, // MSB of data serial number. 0x00, // MSB of data serial number.
(pageSequenceCounter) & 0xFF, (pageSequenceCounter >> 0) & 0xFF,
(pageSequenceCounter >> 8) & 0xFF, (pageSequenceCounter >> 8) & 0xFF,
(pageSequenceCounter >> 16) & 0xFF, (pageSequenceCounter >> 16) & 0xFF,
(pageSequenceCounter >> 24) & 0xFF, (pageSequenceCounter >> 24) & 0xFF,
......
...@@ -15,10 +15,9 @@ ...@@ -15,10 +15,9 @@
*/ */
package com.google.android.exoplayer.extractor.ogg; 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;
import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException;
import com.google.android.exoplayer.testutil.TestUtil;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import junit.framework.TestCase; import junit.framework.TestCase;
...@@ -26,57 +25,24 @@ import junit.framework.TestCase; ...@@ -26,57 +25,24 @@ import junit.framework.TestCase;
import java.io.IOException; 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; private ParsableByteArray scratch;
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
extractor = new OggVorbisExtractor(); extractor = new VorbisReader();
scratch = new ParsableByteArray(new byte[255 * 255], 0); 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 { public void testAppendNumberOfSamples() throws Exception {
ParsableByteArray buffer = new ParsableByteArray(4); ParsableByteArray buffer = new ParsableByteArray(4);
buffer.setLimit(0); buffer.setLimit(0);
OggVorbisExtractor.appendNumberOfSamples(buffer, 0x01234567); VorbisReader.appendNumberOfSamples(buffer, 0x01234567);
assertEquals(4, buffer.limit()); assertEquals(4, buffer.limit());
assertEquals(0x67, buffer.data[0]); assertEquals(0x67, buffer.data[0]);
assertEquals(0x45, buffer.data[1]); assertEquals(0x45, buffer.data[1]);
...@@ -86,7 +52,7 @@ public final class OggVorbisExtractorTest extends TestCase { ...@@ -86,7 +52,7 @@ public final class OggVorbisExtractorTest extends TestCase {
public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException { public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException {
byte[] data = TestData.getVorbisHeaderPages(); byte[] data = TestData.getVorbisHeaderPages();
OggVorbisExtractor.VorbisSetup vorbisSetup = readSetupHeaders(createInput(data)); VorbisReader.VorbisSetup vorbisSetup = readSetupHeaders(createInput(data));
assertNotNull(vorbisSetup.idHeader); assertNotNull(vorbisSetup.idHeader);
assertNotNull(vorbisSetup.commentHeader); assertNotNull(vorbisSetup.commentHeader);
...@@ -122,16 +88,6 @@ public final class OggVorbisExtractorTest extends TestCase { ...@@ -122,16 +88,6 @@ public final class OggVorbisExtractorTest extends TestCase {
.setSimulateUnknownLength(true).setSimulatePartialReads(true).build(); .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) private VorbisSetup readSetupHeaders(FakeExtractorInput input)
throws IOException, InterruptedException { throws IOException, InterruptedException {
while (true) { while (true) {
......
/*
* 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;
}
}
/*
* 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);
}
}
...@@ -27,7 +27,7 @@ import java.io.IOException; ...@@ -27,7 +27,7 @@ import java.io.IOException;
/** /**
* Reads OGG packets from an {@link ExtractorInput}. * Reads OGG packets from an {@link ExtractorInput}.
*/ */
/* package */ final class OggReader { /* package */ final class OggParser {
public static final int OGG_MAX_SEGMENT_SIZE = 255; public static final int OGG_MAX_SEGMENT_SIZE = 255;
...@@ -163,7 +163,7 @@ import java.io.IOException; ...@@ -163,7 +163,7 @@ import java.io.IOException;
* Returns the {@link OggUtil.PageHeader} of the current page. The header might not have been * 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. * populated if the first packet has yet to be read.
* <p> * <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 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). * the next page is read (which implies reading and populating the next header).
* *
......
...@@ -29,6 +29,8 @@ import java.io.IOException; ...@@ -29,6 +29,8 @@ import java.io.IOException;
*/ */
/* package */ final class OggUtil { /* package */ final class OggUtil {
public static final int PAGE_HEADER_SIZE = 27;
private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS"); private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS");
/** /**
...@@ -86,7 +88,7 @@ import java.io.IOException; ...@@ -86,7 +88,7 @@ import java.io.IOException;
* *
* @param input the {@link ExtractorInput} to read from. * @param input the {@link ExtractorInput} to read from.
* @param header the {@link PageHeader} 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 * @param quite if {@code true} no Exceptions are thrown but {@code false} is return if something
* goes wrong. * goes wrong.
* @return {@code true} if the read was successful. {@code false} if the end of the * @return {@code true} if the read was successful. {@code false} if the end of the
...@@ -100,8 +102,8 @@ import java.io.IOException; ...@@ -100,8 +102,8 @@ import java.io.IOException;
scratch.reset(); scratch.reset();
header.reset(); header.reset();
boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNBOUNDED boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNBOUNDED
|| input.getLength() - input.getPeekPosition() >= 27; || input.getLength() - input.getPeekPosition() >= PAGE_HEADER_SIZE;
if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, 27, true)) { if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, PAGE_HEADER_SIZE, true)) {
if (quite) { if (quite) {
return false; return false;
} else { } else {
...@@ -134,7 +136,7 @@ import java.io.IOException; ...@@ -134,7 +136,7 @@ import java.io.IOException;
scratch.reset(); scratch.reset();
// calculate total size of header including laces // 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); input.peekFully(scratch.data, 0, header.pageSegmentCount);
for (int i = 0; i < header.pageSegmentCount; i++) { for (int i = 0; i < header.pageSegmentCount; i++) {
header.laces[i] = scratch.readUnsignedByte(); header.laces[i] = scratch.readUnsignedByte();
......
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;
}
...@@ -26,9 +26,7 @@ import com.google.android.exoplayer.util.Assertions; ...@@ -26,9 +26,7 @@ import com.google.android.exoplayer.util.Assertions;
/* package */ final class VorbisBitArray { /* package */ final class VorbisBitArray {
public final byte[] data; public final byte[] data;
private int limit;
private final int limit;
private int byteOffset; private int byteOffset;
private int bitOffset; private int bitOffset;
......
...@@ -174,7 +174,7 @@ import java.util.Arrays; ...@@ -174,7 +174,7 @@ import java.util.Arrays;
bitArray.skipBits(headerData.getPosition() * 8); bitArray.skipBits(headerData.getPosition() * 8);
for (int i = 0; i < numberOfBooks; i++) { for (int i = 0; i < numberOfBooks; i++) {
skipBook(bitArray); readBook(bitArray);
} }
int timeCount = bitArray.readBits(6) + 1; int timeCount = bitArray.readBits(6) + 1;
...@@ -336,7 +336,7 @@ import java.util.Arrays; ...@@ -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) { if (bitArray.readBits(24) != 0x564342) {
throw new ParserException("expected code book to start with [0x56, 0x43, 0x42] at " throw new ParserException("expected code book to start with [0x56, 0x43, 0x42] at "
+ bitArray.getPosition()); + bitArray.getPosition());
...@@ -393,6 +393,7 @@ import java.util.Arrays; ...@@ -393,6 +393,7 @@ import java.util.Arrays;
// discard (no decoding required yet) // discard (no decoding required yet)
bitArray.skipBits((int) (lookupValuesCount * valueBits)); bitArray.skipBits((int) (lookupValuesCount * valueBits));
} }
return new CodeBook(dimensions, entries, lengthMap, lookupType, isOrdered);
} }
/** /**
...@@ -402,6 +403,25 @@ import java.util.Arrays; ...@@ -402,6 +403,25 @@ import java.util.Arrays;
return (long) Math.floor(Math.pow(entries, 1.d / dimension)); 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 static final class CommentHeader {
public final String vendor; public final String vendor;
......
/*
* 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;
}
};
}
}
...@@ -13,12 +13,13 @@ ...@@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * 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 minBlockSize;
public final int maxBlockSize; public final int maxBlockSize;
public final int minFrameSize; public final int minFrameSize;
...@@ -28,6 +29,28 @@ package com.google.android.exoplayer.ext.flac; ...@@ -28,6 +29,28 @@ package com.google.android.exoplayer.ext.flac;
public final int bitsPerSample; public final int bitsPerSample;
public final long totalSamples; 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, public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize,
int sampleRate, int channels, int bitsPerSample, long totalSamples) { int sampleRate, int channels, int bitsPerSample, long totalSamples) {
this.minBlockSize = minBlockSize; this.minBlockSize = minBlockSize;
...@@ -51,4 +74,5 @@ package com.google.android.exoplayer.ext.flac; ...@@ -51,4 +74,5 @@ package com.google.android.exoplayer.ext.flac;
public long durationUs() { public long durationUs() {
return (totalSamples * 1000000L) / sampleRate; return (totalSamples * 1000000L) / sampleRate;
} }
} }
/*
* 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;
}
}
...@@ -391,4 +391,38 @@ public final class ParsableByteArray { ...@@ -391,4 +391,38 @@ public final class ParsableByteArray {
return line; 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;
}
} }
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