diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ts/AdtsReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ts/AdtsReaderTest.java new file mode 100644 index 0000000000..78f0dd5ce9 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ts/AdtsReaderTest.java @@ -0,0 +1,175 @@ +/* + * 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.ts; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.testutil.FakeTrackOutput; +import com.google.android.exoplayer.testutil.TestUtil; +import com.google.android.exoplayer.util.ParsableByteArray; + +import junit.framework.TestCase; + +import java.util.Arrays; + +/** + * Test for {@link AdtsReader}. + */ +public class AdtsReaderTest extends TestCase { + + public static final byte[] ID3_DATA_1 = TestUtil.createByteArray( + 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3d, 0x54, 0x58, + 0x58, 0x58, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x03, 0x00, 0x20, 0x2a, + 0x2a, 0x2a, 0x20, 0x54, 0x48, 0x49, 0x53, 0x20, 0x49, 0x53, 0x20, 0x54, + 0x69, 0x6d, 0x65, 0x64, 0x20, 0x4d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, + 0x61, 0x20, 0x40, 0x20, 0x2d, 0x2d, 0x20, 0x30, 0x30, 0x3a, 0x30, 0x30, + 0x3a, 0x30, 0x30, 0x2e, 0x30, 0x20, 0x2a, 0x2a, 0x2a, 0x20, 0x00); + + public static final byte[] ID3_DATA_2 = TestUtil.createByteArray( + 0x49, + 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x50, 0x52, 0x49, + 0x56, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, + 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, + 0x6e, 0x67, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0xbb, 0xa0); + + public static final byte[] ADTS_HEADER = TestUtil.createByteArray( + 0xff, 0xf1, 0x50, 0x80, 0x01, 0xdf, 0xfc); + + public static final byte[] ADTS_CONTENT = TestUtil.createByteArray( + 0x20, 0x00, 0x20, 0x00, 0x00, 0x80, 0x0e); + + private static final byte TEST_DATA[] = TestUtil.joinByteArrays( + ID3_DATA_1, + ID3_DATA_2, + ADTS_HEADER, + ADTS_CONTENT); + + private static final long ADTS_SAMPLE_DURATION = 23219L; + + private ParsableByteArray data; + + private AdtsReader adtsReader; + private FakeTrackOutput adtsOutput; + private FakeTrackOutput id3Output; + + @Override + protected void setUp() throws Exception { + adtsOutput = new FakeTrackOutput(); + id3Output = new FakeTrackOutput(); + adtsReader = new AdtsReader(adtsOutput, id3Output); + data = new ParsableByteArray(TEST_DATA); + } + + public void testSkipToNextSample() throws Exception { + for (int i = 1; i <= ID3_DATA_1.length + ID3_DATA_2.length; i++) { + data.setPosition(i); + feed(); + // Once the data position set to ID3_DATA_1.length, no more id3 samples are read + int id3SampleCount = Math.min(i, ID3_DATA_1.length); + assertSampleCounts(id3SampleCount, i); + } + } + + public void testSkipToNextSampleResetsState() throws Exception { + data = new ParsableByteArray(TestUtil.joinByteArrays( + ADTS_HEADER, + ADTS_CONTENT, + // Adts sample missing the first sync byte + Arrays.copyOfRange(ADTS_HEADER, 1, ADTS_HEADER.length), + ADTS_CONTENT)); + feed(); + assertSampleCounts(0, 1); + adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.SAMPLE_FLAG_SYNC, null); + } + + public void testNoData() throws Exception { + feedLimited(0); + assertSampleCounts(0, 0); + } + + public void testNotEnoughDataForIdentifier() throws Exception { + feedLimited(3 - 1); + assertSampleCounts(0, 0); + } + + public void testNotEnoughDataForHeader() throws Exception { + feedLimited(10 - 1); + assertSampleCounts(0, 0); + } + + public void testNotEnoughDataForWholeId3Packet() throws Exception { + feedLimited(ID3_DATA_1.length - 1); + assertSampleCounts(0, 0); + } + + public void testConsumeWholeId3Packet() throws Exception { + feedLimited(ID3_DATA_1.length); + assertSampleCounts(1, 0); + id3Output.assertSample(0, ID3_DATA_1, 0, C.SAMPLE_FLAG_SYNC, null); + } + + public void testMultiId3Packet() throws Exception { + feedLimited(ID3_DATA_1.length + ID3_DATA_2.length - 1); + assertSampleCounts(1, 0); + id3Output.assertSample(0, ID3_DATA_1, 0, C.SAMPLE_FLAG_SYNC, null); + } + + public void testMultiId3PacketConsumed() throws Exception { + feedLimited(ID3_DATA_1.length + ID3_DATA_2.length); + assertSampleCounts(2, 0); + id3Output.assertSample(0, ID3_DATA_1, 0, C.SAMPLE_FLAG_SYNC, null); + id3Output.assertSample(1, ID3_DATA_2, 0, C.SAMPLE_FLAG_SYNC, null); + } + + public void testMultiPacketConsumed() throws Exception { + for (int i = 0; i < 10; i++) { + data.setPosition(0); + adtsReader.consume(data, 0, i == 0); + + long timeUs = ADTS_SAMPLE_DURATION * i; + int j = i * 2; + assertSampleCounts(j + 2, i + 1); + + id3Output.assertSample(j, ID3_DATA_1, timeUs, C.SAMPLE_FLAG_SYNC, null); + id3Output.assertSample(j + 1, ID3_DATA_2, timeUs, C.SAMPLE_FLAG_SYNC, null); + adtsOutput.assertSample(i, ADTS_CONTENT, timeUs, C.SAMPLE_FLAG_SYNC, null); + } + } + + public void testAdtsDataOnly() throws Exception { + data.setPosition(ID3_DATA_1.length + ID3_DATA_2.length); + feed(); + assertSampleCounts(0, 1); + adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.SAMPLE_FLAG_SYNC, null); + } + + private void feedLimited(int limit) { + data.setLimit(limit); + feed(); + } + + private void feed() { + adtsReader.consume(data, 0, true); + } + + private void assertSampleCounts(int id3SampleCount, int adtsSampleCount) { + id3Output.assertSampleCount(id3SampleCount); + adtsOutput.assertSampleCount(adtsSampleCount); + } + +} + diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index 23d7985236..d1233d00a5 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; import android.annotation.SuppressLint; @@ -180,6 +181,11 @@ public final class MediaFormat { NO_VALUE); } + public static MediaFormat createId3Format() { + return createFormatForMimeType(null, MimeTypes.APPLICATION_ID3, MediaFormat.NO_VALUE, + C.UNKNOWN_TIME_US); + } + /* package */ MediaFormat(String trackId, String mimeType, int bitrate, int maxInputSize, long durationUs, int width, int height, int rotationDegrees, float pixelWidthHeightRatio, int channelCount, int sampleRate, String language, long subsampleOffsetUs, diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/DummyTrackOutput.java b/library/src/main/java/com/google/android/exoplayer/extractor/DummyTrackOutput.java new file mode 100644 index 0000000000..330630c89e --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/DummyTrackOutput.java @@ -0,0 +1,47 @@ +/* + * 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; + +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * A dummy {@link TrackOutput} implementation. + */ +public class DummyTrackOutput implements TrackOutput { + @Override + public void format(MediaFormat format) { + // Do nothing. + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return input.skip(length); + } + + @Override + public void sampleData(ParsableByteArray data, int length) { + data.skipBytes(length); + } + + @Override + public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { + // Do nothing. + } +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java index 24b578b028..d83e9a1561 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java @@ -110,7 +110,7 @@ public final class AdtsExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - adtsReader = new AdtsReader(output.track(0)); + adtsReader = new AdtsReader(output.track(0), output.track(1)); output.endTracks(); output.seekMap(SeekMap.UNSEEKABLE); } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsReader.java index f4976e8ce0..c4578f363a 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsReader.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; import android.util.Pair; +import java.util.Arrays; import java.util.Collections; /** @@ -32,20 +33,34 @@ import java.util.Collections; */ /* package */ final class AdtsReader extends ElementaryStreamReader { - private static final int STATE_FINDING_SYNC = 0; - private static final int STATE_READING_HEADER = 1; - private static final int STATE_READING_SAMPLE = 2; + private static final int STATE_FINDING_SAMPLE = 0; + private static final int STATE_READING_ID3_HEADER = 1; + private static final int STATE_READING_ADTS_HEADER = 2; + private static final int STATE_READING_SAMPLE = 3; private static final int HEADER_SIZE = 5; private static final int CRC_SIZE = 2; + // Match states used while looking for the next sample + private static final int MATCH_STATE_VALUE_SHIFT = 8; + private static final int MATCH_STATE_START = 1 << MATCH_STATE_VALUE_SHIFT; + private static final int MATCH_STATE_FF = 2 << MATCH_STATE_VALUE_SHIFT; + private static final int MATCH_STATE_I = 3 << MATCH_STATE_VALUE_SHIFT; + private static final int MATCH_STATE_ID = 4 << MATCH_STATE_VALUE_SHIFT; + + private static final int ID3_HEADER_SIZE = 10; + private static final int ID3_SIZE_OFFSET = 6; + private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'}; + private final ParsableBitArray adtsScratch; + private final ParsableByteArray id3HeaderBuffer; + private final TrackOutput id3Output; private int state; private int bytesRead; - // Used to find the header. - private boolean lastByteWasFF; + private int matchState; + private boolean hasCrc; // Used when parsing the header. @@ -56,17 +71,25 @@ import java.util.Collections; // Used when reading the samples. private long timeUs; - public AdtsReader(TrackOutput output) { + private TrackOutput currentOutput; + private long currentSampleDuration; + + /** + * @param output A {@link TrackOutput} to which AAC samples should be written. + * @param id3Output A {@link TrackOutput} to which ID3 samples should be written. + */ + public AdtsReader(TrackOutput output, TrackOutput id3Output) { super(output); + this.id3Output = id3Output; + id3Output.format(MediaFormat.createId3Format()); adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); - state = STATE_FINDING_SYNC; + id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); + setFindingSampleState(); } @Override public void seek() { - state = STATE_FINDING_SYNC; - bytesRead = 0; - lastByteWasFF = false; + setFindingSampleState(); } @Override @@ -76,30 +99,22 @@ import java.util.Collections; } while (data.bytesLeft() > 0) { switch (state) { - case STATE_FINDING_SYNC: - if (skipToNextSync(data)) { - bytesRead = 0; - state = STATE_READING_HEADER; + case STATE_FINDING_SAMPLE: + findNextSample(data); + break; + case STATE_READING_ID3_HEADER: + if (continueRead(data, id3HeaderBuffer.data, ID3_HEADER_SIZE)) { + parseId3Header(); } break; - case STATE_READING_HEADER: + case STATE_READING_ADTS_HEADER: int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; if (continueRead(data, adtsScratch.data, targetLength)) { - parseHeader(); - bytesRead = 0; - state = STATE_READING_SAMPLE; + parseAdtsHeader(); } break; case STATE_READING_SAMPLE: - int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); - output.sampleData(data, bytesToRead); - bytesRead += bytesToRead; - if (bytesRead == sampleSize) { - output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null); - timeUs += sampleDurationUs; - bytesRead = 0; - state = STATE_FINDING_SYNC; - } + readSample(data); break; } } @@ -127,36 +142,109 @@ import java.util.Collections; } /** - * Locates the next sync word, advancing the position to the byte that immediately follows it. - * If a sync word was not located, the position is advanced to the limit. + * Sets the state to STATE_FINDING_SAMPLE. + */ + private void setFindingSampleState() { + state = STATE_FINDING_SAMPLE; + bytesRead = 0; + matchState = MATCH_STATE_START; + } + + /** + * Sets the state to STATE_READING_ID3_HEADER and resets the fields required for + * {@link #parseId3Header()}. + */ + private void setReadingId3HeaderState() { + state = STATE_READING_ID3_HEADER; + bytesRead = ID3_IDENTIFIER.length; + sampleSize = 0; + id3HeaderBuffer.setPosition(0); + } + + /** + * Sets the state to STATE_READING_SAMPLE. + * + * @param outputToUse TrackOutput object to write the sample to + * @param currentSampleDuration Duration of the sample to be read + * @param priorReadBytes Size of prior read bytes + * @param sampleSize Size of the sample + */ + private void setReadingSampleState(TrackOutput outputToUse, long currentSampleDuration, + int priorReadBytes, int sampleSize) { + state = STATE_READING_SAMPLE; + bytesRead = priorReadBytes; + this.currentOutput = outputToUse; + this.currentSampleDuration = currentSampleDuration; + this.sampleSize = sampleSize; + } + + /** + * Sets the state to STATE_READING_ADTS_HEADER. + */ + private void setReadingAdtsHeaderState() { + state = STATE_READING_ADTS_HEADER; + bytesRead = 0; + } + + /** + * Locates the next sample start, advancing the position to the byte that immediately follows + * identifier. If a sample was not located, the position is advanced to the limit. * * @param pesBuffer The buffer whose position should be advanced. - * @return True if a sync word position was found. False otherwise. */ - private boolean skipToNextSync(ParsableByteArray pesBuffer) { + private void findNextSample(ParsableByteArray pesBuffer) { byte[] adtsData = pesBuffer.data; - int startOffset = pesBuffer.getPosition(); + int position = pesBuffer.getPosition(); int endOffset = pesBuffer.limit(); - for (int i = startOffset; i < endOffset; i++) { - boolean byteIsFF = (adtsData[i] & 0xFF) == 0xFF; - boolean found = lastByteWasFF && !byteIsFF && (adtsData[i] & 0xF0) == 0xF0; - lastByteWasFF = byteIsFF; - if (found) { - hasCrc = (adtsData[i] & 0x1) == 0; - pesBuffer.setPosition(i + 1); - // Reset lastByteWasFF for next time. - lastByteWasFF = false; - return true; + while (position < endOffset) { + int data = adtsData[position++] & 0xFF; + if (matchState == MATCH_STATE_FF && data >= 0xF0 && data != 0xFF) { + hasCrc = (data & 0x1) == 0; + setReadingAdtsHeaderState(); + pesBuffer.setPosition(position); + return; + } + switch (matchState | data) { + case MATCH_STATE_START | 0xFF: + matchState = MATCH_STATE_FF; + break; + case MATCH_STATE_START | 'I': + matchState = MATCH_STATE_I; + break; + case MATCH_STATE_I | 'D': + matchState = MATCH_STATE_ID; + break; + case MATCH_STATE_ID | '3': + setReadingId3HeaderState(); + pesBuffer.setPosition(position); + return; + default: + if (matchState != MATCH_STATE_START) { + // If matching fails in a later state, revert to MATCH_STATE_START and + // check this byte again + matchState = MATCH_STATE_START; + position--; + } + break; } } - pesBuffer.setPosition(endOffset); - return false; + pesBuffer.setPosition(position); + } + + /** + * Parses the Id3 header. + */ + private void parseId3Header() { + id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE); + id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET); + setReadingSampleState(id3Output, 0, ID3_HEADER_SIZE, + id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE); } /** * Parses the sample header. */ - private void parseHeader() { + private void parseAdtsHeader() { adtsScratch.setPosition(0); if (!hasOutputFormat) { @@ -183,10 +271,26 @@ import java.util.Collections; } adtsScratch.skipBits(4); - sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE; + int sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE; if (hasCrc) { sampleSize -= CRC_SIZE; } + + setReadingSampleState(output, sampleDurationUs, 0, sampleSize); + } + + /** + * Reads the rest of the sample + */ + private void readSample(ParsableByteArray data) { + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + currentOutput.sampleData(data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + currentOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null); + timeUs += currentSampleDuration; + setFindingSampleState(); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java index c7ec2f7dc4..569ae6acc4 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer.extractor.ts; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.extractor.TrackOutput; -import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableByteArray; /** @@ -35,8 +34,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; public Id3Reader(TrackOutput output) { super(output); - output.format(MediaFormat.createFormatForMimeType(null, MimeTypes.APPLICATION_ID3, - MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US)); + output.format(MediaFormat.createId3Format()); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java index 728197c6ab..8a6e9dfe68 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.extractor.ts; +import com.google.android.exoplayer.extractor.DummyTrackOutput; import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorOutput; @@ -334,7 +335,8 @@ public final class TsExtractor implements Extractor { pesPayloadReader = new MpegAudioReader(output.track(TS_STREAM_TYPE_MPA_LSF)); break; case TS_STREAM_TYPE_AAC: - pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC)); + pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC), + new DummyTrackOutput()); break; case TS_STREAM_TYPE_AC3: pesPayloadReader = new Ac3Reader(output.track(TS_STREAM_TYPE_AC3), false);