diff --git a/library/src/androidTest/assets/ts/sample.ac3 b/library/src/androidTest/assets/ts/sample.ac3 new file mode 100644 index 0000000000..0e39f0ec98 Binary files /dev/null and b/library/src/androidTest/assets/ts/sample.ac3 differ diff --git a/library/src/androidTest/assets/ts/sample.ac3.0.dump b/library/src/androidTest/assets/ts/sample.ac3.0.dump new file mode 100644 index 0000000000..c5f241950b --- /dev/null +++ b/library/src/androidTest/assets/ts/sample.ac3.0.dump @@ -0,0 +1,61 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = 0 +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/ac3 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = -1 + pixelWidthHeightRatio = -1.0 + channelCount = 6 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = -1 + encoderPadding = -1 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + sample count = 8 + sample 0: + time = 0 + flags = 1 + data = length 1536, hash 7108D5C2 + sample 1: + time = 32000 + flags = 1 + data = length 1536, hash 80BF3B34 + sample 2: + time = 64000 + flags = 1 + data = length 1536, hash 5D09685 + sample 3: + time = 96000 + flags = 1 + data = length 1536, hash A9A24E44 + sample 4: + time = 128000 + flags = 1 + data = length 1536, hash 6F856273 + sample 5: + time = 160000 + flags = 1 + data = length 1536, hash B1737D3C + sample 6: + time = 192000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 7: + time = 224000 + flags = 1 + data = length 1536, hash 99B9B943 +tracksEnded = true diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java new file mode 100644 index 0000000000..ab44e3aed3 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java @@ -0,0 +1,36 @@ +/* + * 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.exoplayer2.extractor.ts; + +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.testutil.TestUtil; + +/** + * Unit test for {@link Ac3Extractor}. + */ +public final class Ac3ExtractorTest extends InstrumentationTestCase { + + public void testSample() throws Exception { + TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + @Override + public Extractor create() { + return new Ac3Extractor(); + } + }, "ts/sample.ac3", getInstrumentation()); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 2f43f900f7..29fad1fbde 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -87,6 +87,13 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { } catch (ClassNotFoundException e) { // Extractor not found. } + try { + extractorClasses.add( + Class.forName("com.google.android.exoplayer2.extractor.ts.Ac3Extractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } try { extractorClasses.add( Class.forName("com.google.android.exoplayer2.extractor.ts.TsExtractor") diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java new file mode 100644 index 0000000000..2bb2c93a0d --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -0,0 +1,156 @@ +/* + * 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.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.Ac3Util; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; + +import java.io.IOException; + +/** + * Facilitates the extraction of AC-3 samples from elementary audio files formatted as AC-3 + * bitstreams. + */ +public final class Ac3Extractor implements Extractor { + + /** + * Factory for {@link Ac3Extractor} instances. + */ + public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { + + @Override + public Extractor[] createExtractors() { + return new Extractor[] {new Ac3Extractor()}; + } + + }; + + /** + * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving + * up. + */ + private static final int MAX_SNIFF_BYTES = 8 * 1024; + private static final int AC3_SYNC_WORD = 0x0B77; + private static final int MAX_SYNC_FRAME_SIZE = 2786; + private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + + private final long firstSampleTimestampUs; + private final ParsableByteArray sampleData; + + private Ac3Reader reader; + private boolean startedPacket; + + public Ac3Extractor() { + this(0); + } + + public Ac3Extractor(long firstSampleTimestampUs) { + this.firstSampleTimestampUs = firstSampleTimestampUs; + sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + // Skip any ID3 headers. + ParsableByteArray scratch = new ParsableByteArray(10); + int startPosition = 0; + while (true) { + input.peekFully(scratch.data, 0, 10); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + break; + } + scratch.skipBytes(3); + int length = scratch.readSynchSafeInt(); + startPosition += 10 + length; + input.advancePeekPosition(length); + } + input.resetPeekPosition(); + input.advancePeekPosition(startPosition); + + int headerPosition = startPosition; + int validFramesCount = 0; + while (true) { + input.peekFully(scratch.data, 0, 5); + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (syncBytes != AC3_SYNC_WORD) { + validFramesCount = 0; + input.resetPeekPosition(); + if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) { + return false; + } + input.advancePeekPosition(headerPosition); + } else { + if (++validFramesCount >= 4) { + return true; + } + int frameSize = Ac3Util.parseAc3SyncframeSize(scratch.data); + input.advancePeekPosition(frameSize - 5); + } + } + } + + @Override + public void init(ExtractorOutput output) { + reader = new Ac3Reader(output.track(0), false); // TODO: Add support for embedded ID3. + output.endTracks(); + output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + } + + @Override + public void seek(long position) { + startedPacket = false; + reader.seek(); + } + + @Override + public void release() { + // Do nothing. + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, + InterruptedException { + int bytesRead = input.read(sampleData.data, 0, MAX_SYNC_FRAME_SIZE); + if (bytesRead == C.RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + + // Feed whatever data we have to the reader, regardless of whether the read finished or not. + sampleData.setPosition(0); + sampleData.setLimit(bytesRead); + + if (!startedPacket) { + // Pass data to the reader as though it's contained within a single infinitely long packet. + reader.packetStarted(firstSampleTimestampUs, true); + startedPacket = true; + } + // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes + // unnecessary to copy the data through packetBuffer. + reader.consume(sampleData); + return RESULT_CONTINUE; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 09d2d1ec61..b64c56ab07 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -51,6 +51,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; // Used when reading the samples. private long timeUs; + // TODO: Remove the isEac3 parameter by reading the BSID field. /** * Constructs a new reader for (E-)AC-3 elementary streams. * diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 7556623021..f131d8997b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -57,7 +57,7 @@ public final class AdtsExtractor implements Extractor { private final ParsableByteArray packetBuffer; // Accessed only by the loading thread. - private AdtsReader adtsReader; + private AdtsReader reader; private boolean startedPacket; public AdtsExtractor() { @@ -81,8 +81,8 @@ public final class AdtsExtractor implements Extractor { if (scratch.readUnsignedInt24() != ID3_TAG) { break; } - int length = (scratch.data[6] & 0x7F) << 21 | ((scratch.data[7] & 0x7F) << 14) - | ((scratch.data[8] & 0x7F) << 7) | (scratch.data[9] & 0x7F); + scratch.skipBytes(3); + int length = scratch.readSynchSafeInt(); startPosition += 10 + length; input.advancePeekPosition(length); } @@ -126,7 +126,7 @@ public final class AdtsExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - adtsReader = new AdtsReader(output.track(0), output.track(1)); + reader = new AdtsReader(output.track(0), output.track(1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } @@ -134,7 +134,7 @@ public final class AdtsExtractor implements Extractor { @Override public void seek(long position) { startedPacket = false; - adtsReader.seek(); + reader.seek(); } @Override @@ -154,14 +154,14 @@ public final class AdtsExtractor implements Extractor { packetBuffer.setPosition(0); packetBuffer.setLimit(bytesRead); - // TODO: Make it possible for adtsReader to consume the dataSource directly, so that it becomes - // unnecessary to copy the data through packetBuffer. if (!startedPacket) { // Pass data to the reader as though it's contained within a single infinitely long packet. - adtsReader.packetStarted(firstSampleTimestampUs, true); + reader.packetStarted(firstSampleTimestampUs, true); startedPacket = true; } - adtsReader.consume(packetBuffer); + // TODO: Make it possible for reader to consume the dataSource directly, so that it becomes + // unnecessary to copy the data through packetBuffer. + reader.consume(packetBuffer); return RESULT_CONTINUE; }