mirror of
https://github.com/samsonjs/media.git
synced 2026-04-10 12:05:47 +00:00
Bring V2 ogg extractor up to date.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=120332721
This commit is contained in:
parent
6f32636f40
commit
7638bea016
20 changed files with 663 additions and 200 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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).
|
||||
*
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue