mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +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;
|
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;
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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;
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
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;
|
||||||
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 @@
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
.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) {
|
||||||
|
|
@ -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}.
|
* 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;
|
||||||
* 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;
|
||||||
*/
|
*/
|
||||||
/* 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;
|
||||||
*
|
*
|
||||||
* @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;
|
||||||
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;
|
||||||
|
|
||||||
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();
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
/* 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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,8 @@ import com.google.android.exoplayer.Format;
|
||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.extractor.Extractor;
|
import com.google.android.exoplayer.extractor.Extractor;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
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.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.ogg.VorbisUtil.Mode;
|
import com.google.android.exoplayer.extractor.ogg.VorbisUtil.Mode;
|
||||||
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;
|
||||||
|
|
@ -32,16 +30,10 @@ import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
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 VorbisSetup vorbisSetup;
|
||||||
private int previousPacketBlockSize;
|
private int previousPacketBlockSize;
|
||||||
private long elapsedSamples;
|
private long elapsedSamples;
|
||||||
|
|
@ -50,7 +42,6 @@ public final class OggVorbisExtractor implements Extractor, SeekMap {
|
||||||
private final OggSeeker oggSeeker = new OggSeeker();
|
private final OggSeeker oggSeeker = new OggSeeker();
|
||||||
private long targetGranule = -1;
|
private long targetGranule = -1;
|
||||||
|
|
||||||
private ExtractorOutput extractorOutput;
|
|
||||||
private VorbisUtil.VorbisIdHeader vorbisIdHeader;
|
private VorbisUtil.VorbisIdHeader vorbisIdHeader;
|
||||||
private VorbisUtil.CommentHeader commentHeader;
|
private VorbisUtil.CommentHeader commentHeader;
|
||||||
private long inputLength;
|
private long inputLength;
|
||||||
|
|
@ -58,129 +49,105 @@ public final class OggVorbisExtractor implements Extractor, SeekMap {
|
||||||
private long totalSamples;
|
private long totalSamples;
|
||||||
private long durationUs;
|
private long durationUs;
|
||||||
|
|
||||||
@Override
|
/* package */ static boolean verifyBitstreamType(ParsableByteArray data) {
|
||||||
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
|
|
||||||
try {
|
try {
|
||||||
OggUtil.PageHeader header = new OggUtil.PageHeader();
|
return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true);
|
||||||
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);
|
|
||||||
} catch (ParserException e) {
|
} catch (ParserException e) {
|
||||||
// does not happen
|
return false;
|
||||||
} finally {
|
|
||||||
scratch.reset();
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(ExtractorOutput output) {
|
|
||||||
trackOutput = output.track(0);
|
|
||||||
output.endTracks();
|
|
||||||
extractorOutput = output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void seek() {
|
public void seek() {
|
||||||
oggReader.reset();
|
super.seek();
|
||||||
previousPacketBlockSize = 0;
|
previousPacketBlockSize = 0;
|
||||||
elapsedSamples = 0;
|
elapsedSamples = 0;
|
||||||
seenFirstAudioPacket = false;
|
seenFirstAudioPacket = false;
|
||||||
scratch.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void release() {
|
|
||||||
// Do nothing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read(ExtractorInput input, PositionHolder seekPosition)
|
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
|
||||||
// Setup.
|
// setup
|
||||||
if (totalSamples == 0) {
|
if (totalSamples == 0) {
|
||||||
if (vorbisSetup == null) {
|
if (vorbisSetup == null) {
|
||||||
inputLength = input.getLength();
|
inputLength = input.getLength();
|
||||||
vorbisSetup = readSetupHeaders(input, scratch);
|
vorbisSetup = readSetupHeaders(input, scratch);
|
||||||
audioStartPosition = input.getPosition();
|
audioStartPosition = input.getPosition();
|
||||||
// Output the format.
|
extractorOutput.seekMap(this);
|
||||||
ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
|
if (inputLength != C.LENGTH_UNBOUNDED) {
|
||||||
codecInitialisationData.add(vorbisSetup.idHeader.data);
|
// seek to the end just before the last page of stream to get the duration
|
||||||
codecInitialisationData.add(vorbisSetup.setupHeaderData);
|
seekPosition.position = input.getLength() - 8000;
|
||||||
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS,
|
return Extractor.RESULT_SEEK;
|
||||||
vorbisSetup.idHeader.bitrateNominal, OggReader.OGG_MAX_SEGMENT_SIZE * 255,
|
|
||||||
vorbisSetup.idHeader.channels, (int) vorbisSetup.idHeader.sampleRate,
|
|
||||||
codecInitialisationData, null));
|
|
||||||
if (inputLength == C.LENGTH_UNBOUNDED) {
|
|
||||||
// If the length is unbounded, we cannot determine the duration or seek.
|
|
||||||
totalSamples = -1;
|
|
||||||
durationUs = C.LENGTH_UNBOUNDED;
|
|
||||||
extractorOutput.seekMap(this);
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
}
|
||||||
// Seek to just before the last page of stream to get the duration.
|
|
||||||
seekPosition.position = input.getLength() - 8000;
|
|
||||||
return RESULT_SEEK;
|
|
||||||
}
|
}
|
||||||
|
totalSamples = inputLength == C.LENGTH_UNBOUNDED ? -1
|
||||||
|
: oggParser.readGranuleOfLastPage(input);
|
||||||
|
|
||||||
totalSamples = oggReader.readGranuleOfLastPage(input);
|
ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
|
||||||
durationUs = totalSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
|
codecInitialisationData.add(vorbisSetup.idHeader.data);
|
||||||
oggSeeker.setup(inputLength - audioStartPosition, totalSamples);
|
codecInitialisationData.add(vorbisSetup.setupHeaderData);
|
||||||
extractorOutput.seekMap(this);
|
|
||||||
// Seek back to resume from where we finished reading vorbis headers.
|
durationUs = inputLength == C.LENGTH_UNBOUNDED ? C.UNSET_TIME_US
|
||||||
seekPosition.position = audioStartPosition;
|
: (totalSamples * C.MICROS_PER_SECOND) / vorbisSetup.idHeader.sampleRate;
|
||||||
return RESULT_SEEK;
|
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) {
|
if (!seenFirstAudioPacket && targetGranule > -1) {
|
||||||
OggUtil.skipToNextPage(input);
|
OggUtil.skipToNextPage(input);
|
||||||
long position = oggSeeker.getNextSeekPosition(targetGranule, input);
|
long position = oggSeeker.getNextSeekPosition(targetGranule, input);
|
||||||
if (position != -1) {
|
if (position != -1) {
|
||||||
seekPosition.position = position;
|
seekPosition.position = position;
|
||||||
return RESULT_SEEK;
|
return Extractor.RESULT_SEEK;
|
||||||
} else {
|
} else {
|
||||||
elapsedSamples = oggReader.skipToPageOfGranule(input, targetGranule);
|
elapsedSamples = oggParser.skipToPageOfGranule(input, targetGranule);
|
||||||
previousPacketBlockSize = vorbisIdHeader.blockSize0;
|
previousPacketBlockSize = vorbisIdHeader.blockSize0;
|
||||||
// We're never at the first packet after seeking.
|
// we're never at the first packet after seeking
|
||||||
seenFirstAudioPacket = true;
|
seenFirstAudioPacket = true;
|
||||||
oggSeeker.reset();
|
oggSeeker.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Playback.
|
// playback
|
||||||
if (oggReader.readPacket(input, scratch)) {
|
if (oggParser.readPacket(input, scratch)) {
|
||||||
// If this is an audio packet...
|
// if this is an audio packet...
|
||||||
if ((scratch.data[0] & 0x01) != 1) {
|
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);
|
int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup);
|
||||||
// A packet contains samples produced from overlapping the previous and current frame data
|
// 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).
|
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
|
||||||
int samplesInPacket = seenFirstAudioPacket
|
int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4
|
||||||
? ((packetBlockSize + previousPacketBlockSize) / 4) : 0;
|
: 0;
|
||||||
if (elapsedSamples + samplesInPacket >= targetGranule) {
|
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);
|
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;
|
long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
|
||||||
trackOutput.sampleData(scratch, scratch.limit());
|
trackOutput.sampleData(scratch, scratch.limit());
|
||||||
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, scratch.limit(), 0, null);
|
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, scratch.limit(), 0, null);
|
||||||
targetGranule = -1;
|
targetGranule = -1;
|
||||||
}
|
}
|
||||||
// Update state in members for next iteration.
|
// update state in members for next iteration
|
||||||
seenFirstAudioPacket = true;
|
seenFirstAudioPacket = true;
|
||||||
elapsedSamples += samplesInPacket;
|
elapsedSamples += samplesInPacket;
|
||||||
previousPacketBlockSize = packetBlockSize;
|
previousPacketBlockSize = packetBlockSize;
|
||||||
}
|
}
|
||||||
scratch.reset();
|
scratch.reset();
|
||||||
return RESULT_CONTINUE;
|
return Extractor.RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
return RESULT_END_OF_INPUT;
|
return Extractor.RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
//@VisibleForTesting
|
//@VisibleForTesting
|
||||||
|
|
@ -188,18 +155,18 @@ public final class OggVorbisExtractor implements Extractor, SeekMap {
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
|
||||||
if (vorbisIdHeader == null) {
|
if (vorbisIdHeader == null) {
|
||||||
oggReader.readPacket(input, scratch);
|
oggParser.readPacket(input, scratch);
|
||||||
vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch);
|
vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch);
|
||||||
scratch.reset();
|
scratch.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commentHeader == null) {
|
if (commentHeader == null) {
|
||||||
oggReader.readPacket(input, scratch);
|
oggParser.readPacket(input, scratch);
|
||||||
commentHeader = VorbisUtil.readVorbisCommentHeader(scratch);
|
commentHeader = VorbisUtil.readVorbisCommentHeader(scratch);
|
||||||
scratch.reset();
|
scratch.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
oggReader.readPacket(input, scratch);
|
oggParser.readPacket(input, scratch);
|
||||||
// the third packet contains the setup header
|
// the third packet contains the setup header
|
||||||
byte[] setupHeaderData = new byte[scratch.limit()];
|
byte[] setupHeaderData = new byte[scratch.limit()];
|
||||||
// raw data of vorbis setup header has to be passed to decoder as CSD buffer #2
|
// 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;
|
return currentBlockSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SeekMap implementation.
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSeekable() {
|
public boolean isSeekable() {
|
||||||
return inputLength != C.LENGTH_UNBOUNDED;
|
return vorbisSetup != null && inputLength != C.LENGTH_UNBOUNDED;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getDurationUs() {
|
|
||||||
return durationUs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -261,7 +221,10 @@ public final class OggVorbisExtractor implements Extractor, SeekMap {
|
||||||
/ durationUs) - 4000);
|
/ durationUs) - 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal classes.
|
@Override
|
||||||
|
public long getDurationUs() {
|
||||||
|
return durationUs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to hold all data read from Vorbis setup headers.
|
* Class to hold all data read from Vorbis setup headers.
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
// 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;
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
* 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;
|
||||||
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;
|
||||||
public long durationUs() {
|
public long durationUs() {
|
||||||
return (totalSamples * 1000000L) / sampleRate;
|
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;
|
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