diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoder.java index d7468e5a8f..a8c93d08a6 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoder.java @@ -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; diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacExtractor.java index dc4ca2e508..3f7a3beb8b 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacExtractor.java @@ -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; diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacJni.java index 1c26909c26..c7b066869c 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacJni.java @@ -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; diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorTest.java new file mode 100644 index 0000000000..f75ffc08cf --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorTest.java @@ -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. + } + } + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggParserTest.java similarity index 83% rename from library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggReaderTest.java rename to library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggParserTest.java index f1270aeb2f..ebacfee144 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggParserTest.java @@ -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. } diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggUtilTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggUtilTest.java index 97a5343029..b5c4ae08e8 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggUtilTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggUtilTest.java @@ -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)); diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/TestData.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/TestData.java index 411cbbf08e..014438ee1f 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/TestData.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/TestData.java @@ -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, diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/VorbisReaderTest.java similarity index 63% rename from library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractorTest.java rename to library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/VorbisReaderTest.java index 46f59200c3..a085372d2b 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/VorbisReaderTest.java @@ -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) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/FlacReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/FlacReader.java new file mode 100644 index 0000000000..1b539e542f --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/FlacReader.java @@ -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 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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggExtractor.java new file mode 100644 index 0000000000..f41aa0bbc8 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggExtractor.java @@ -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); + } +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggParser.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggReader.java rename to library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggParser.java index 3fb2ed473a..8bef5ab61e 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggParser.java @@ -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. *

- * 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). * diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java index d62ba6ef42..c17d4b61f3 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java @@ -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(); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/StreamReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/StreamReader.java new file mode 100644 index 0000000000..af251cfd52 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/StreamReader.java @@ -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; +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisBitArray.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisBitArray.java index 462c3790b3..95ae0eb789 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisBitArray.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisBitArray.java @@ -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; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisReader.java similarity index 65% rename from library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractor.java rename to library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisReader.java index 80d9125757..70c25d8942 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggVorbisExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisReader.java @@ -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 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 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. diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisUtil.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisUtil.java index cc419f7055..a325e4e03e 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisUtil.java @@ -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; diff --git a/library/src/main/java/com/google/android/exoplayer/util/FlacSeekTable.java b/library/src/main/java/com/google/android/exoplayer/util/FlacSeekTable.java new file mode 100644 index 0000000000..879d33cc70 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/FlacSeekTable.java @@ -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 FLAC format + * METADATA_BLOCK_SEEKTABLE + */ + 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; + } + }; + } +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacStreamInfo.java b/library/src/main/java/com/google/android/exoplayer/util/FlacStreamInfo.java similarity index 60% rename from extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacStreamInfo.java rename to library/src/main/java/com/google/android/exoplayer/util/FlacStreamInfo.java index 8de58f7993..0f75c8032e 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacStreamInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/util/FlacStreamInfo.java @@ -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 FLAC format + * METADATA_BLOCK_STREAMINFO + */ + 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; } + } diff --git a/library/src/main/java/com/google/android/exoplayer/util/FlacUtil.java b/library/src/main/java/com/google/android/exoplayer/util/FlacUtil.java new file mode 100644 index 0000000000..ab070ae194 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/FlacUtil.java @@ -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 FLAC format FRAME_HEADER + */ + 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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java index f54de76066..4aaf5d807e 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java @@ -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; + } + }