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 index 78f0dd5ce9..e75b63011b 100644 --- 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 @@ -60,11 +60,11 @@ public class AdtsReaderTest extends TestCase { private static final long ADTS_SAMPLE_DURATION = 23219L; - private ParsableByteArray data; - - private AdtsReader adtsReader; private FakeTrackOutput adtsOutput; private FakeTrackOutput id3Output; + private AdtsReader adtsReader; + private ParsableByteArray data; + private boolean firstFeed; @Override protected void setUp() throws Exception { @@ -72,6 +72,7 @@ public class AdtsReaderTest extends TestCase { id3Output = new FakeTrackOutput(); adtsReader = new AdtsReader(adtsOutput, id3Output); data = new ParsableByteArray(TEST_DATA); + firstFeed = true; } public void testSkipToNextSample() throws Exception { @@ -138,7 +139,7 @@ public class AdtsReaderTest extends TestCase { public void testMultiPacketConsumed() throws Exception { for (int i = 0; i < 10; i++) { data.setPosition(0); - adtsReader.consume(data, 0, i == 0); + feed(); long timeUs = ADTS_SAMPLE_DURATION * i; int j = i * 2; @@ -158,12 +159,21 @@ public class AdtsReaderTest extends TestCase { } private void feedLimited(int limit) { + maybeStartPacket(); data.setLimit(limit); feed(); } private void feed() { - adtsReader.consume(data, 0, true); + maybeStartPacket(); + adtsReader.consume(data); + } + + private void maybeStartPacket() { + if (firstFeed) { + adtsReader.packetStarted(0, true); + firstFeed = false; + } } private void assertSampleCounts(int id3SampleCount, int adtsSampleCount) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Ac3Reader.java index 217a3786c3..cdb7c71b9b 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Ac3Reader.java @@ -74,10 +74,12 @@ import com.google.android.exoplayer.util.ParsableByteArray; } @Override - public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { - if (startOfPacket) { - timeUs = pesTimeUs; - } + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: 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 f86dce9ddd..cf754b1bc4 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 @@ -46,7 +46,7 @@ public final class AdtsExtractor implements Extractor { // Accessed only by the loading thread. private AdtsReader adtsReader; - private boolean firstPacket; + private boolean startedPacket; public AdtsExtractor() { this(0); @@ -55,7 +55,6 @@ public final class AdtsExtractor implements Extractor { public AdtsExtractor(long firstSampleTimestampUs) { this.firstSampleTimestampUs = firstSampleTimestampUs; packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); - firstPacket = true; } @Override @@ -118,7 +117,7 @@ public final class AdtsExtractor implements Extractor { @Override public void seek() { - firstPacket = true; + startedPacket = false; adtsReader.seek(); } @@ -136,8 +135,12 @@ public final class AdtsExtractor implements Extractor { // TODO: Make it possible for adtsReader to consume the dataSource directly, so that it becomes // unnecessary to copy the data through packetBuffer. - adtsReader.consume(packetBuffer, firstSampleTimestampUs, firstPacket); - firstPacket = false; + if (!startedPacket) { + // Pass data to the reader as though it's contained within a single infinitely long packet. + adtsReader.packetStarted(firstSampleTimestampUs, true); + startedPacket = true; + } + adtsReader.consume(packetBuffer); return RESULT_CONTINUE; } 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 31456f2c94..57e65d291b 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 @@ -93,10 +93,12 @@ import java.util.Collections; } @Override - public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { - if (startOfPacket) { - timeUs = pesTimeUs; - } + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SAMPLE: diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/DtsReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/DtsReader.java index 59220e33ef..f7c56f9ca5 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/DtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/DtsReader.java @@ -73,10 +73,12 @@ import com.google.android.exoplayer.util.ParsableByteArray; } @Override - public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { - if (startOfPacket) { - timeUs = pesTimeUs; - } + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/ElementaryStreamReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/ElementaryStreamReader.java index 7bcdd1775a..3cd3fe6984 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/ElementaryStreamReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/ElementaryStreamReader.java @@ -34,27 +34,26 @@ import com.google.android.exoplayer.util.ParsableByteArray; /** * Notifies the reader that a seek has occurred. - *
- * Following a call to this method, the data passed to the next invocation of - * {@link #consume(ParsableByteArray, long, boolean)} will not be a continuation of the data that - * was previously passed. Hence the reader should reset any internal state. */ public abstract void seek(); /** - * Consumes (possibly partial) payload data. + * Invoked when a packet starts. * - * @param data The payload data to consume. - * @param pesTimeUs The timestamp associated with the payload. - * @param startOfPacket True if this is the first time this method is being called for the - * current packet. False otherwise. + * @param pesTimeUs The timestamp associated with the packet. + * @param dataAlignmentIndicator The data alignment indicator associated with the packet. */ - public abstract void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket); + public abstract void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator); /** - * Invoked once all of the payload data for a packet has been passed to - * {@link #consume(ParsableByteArray, long, boolean)}. The next call to - * {@link #consume(ParsableByteArray, long, boolean)} will have {@code startOfPacket == true}. + * Consumes (possibly partial) data from the current packet. + * + * @param data The data to consume. + */ + public abstract void consume(ParsableByteArray data); + + /** + * Invoked when a packet ends. */ public abstract void packetFinished(); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H262Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H262Reader.java index 25bccb3b9f..c3c41c90e8 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H262Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H262Reader.java @@ -48,10 +48,13 @@ import java.util.Collections; // State that should be reset on seek. private final boolean[] prefixFlags; private final CsdBuffer csdBuffer; - private boolean foundFirstFrameInPacket; private boolean foundFirstFrameInGroup; private long totalBytesWritten; + // Per packet state that gets reset at the start of each packet. + private long pesTimeUs; + private boolean foundFirstFrameInPacket; + // Per sample state that gets reset at the start of each frame. private boolean isKeyframe; private long framePosition; @@ -73,10 +76,13 @@ import java.util.Collections; } @Override - public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { - if (startOfPacket) { - foundFirstFrameInPacket = false; - } + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + this.pesTimeUs = pesTimeUs; + foundFirstFrameInPacket = false; + } + + @Override + public void consume(ParsableByteArray data) { while (data.bytesLeft() > 0) { int offset = data.getPosition(); int limit = data.limit(); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java index fc00e26549..1261bf70b2 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java @@ -57,6 +57,9 @@ import java.util.List; private boolean foundFirstSample; private long totalBytesWritten; + // Per packet state that gets reset at the start of each packet. + private long pesTimeUs; + // Per sample state that gets reset at the start of each sample. private boolean isKeyframe; private long samplePosition; @@ -78,7 +81,6 @@ import java.util.List; @Override public void seek() { - seiReader.seek(); NalUnitUtil.clearPrefixFlags(prefixFlags); sps.reset(); pps.reset(); @@ -91,7 +93,12 @@ import java.util.List; } @Override - public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + this.pesTimeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { while (data.bytesLeft() > 0) { int offset = data.getPosition(); int limit = data.limit(); @@ -194,7 +201,7 @@ import java.util.List; int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength); seiWrapper.reset(sei.nalData, unescapedLength); seiWrapper.setPosition(4); // NAL prefix and nal_unit() header. - seiReader.consume(seiWrapper, pesTimeUs, true); + seiReader.consume(pesTimeUs, seiWrapper); } } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java index f7172b9a16..79ad60ad1d 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java @@ -58,6 +58,9 @@ import java.util.Collections; private final SampleReader sampleReader; private long totalBytesWritten; + // Per packet state that gets reset at the start of each packet. + private long pesTimeUs; + // Scratch variables to avoid allocations. private final ParsableByteArray seiWrapper; @@ -76,7 +79,6 @@ import java.util.Collections; @Override public void seek() { - seiReader.seek(); NalUnitUtil.clearPrefixFlags(prefixFlags); vps.reset(); sps.reset(); @@ -88,7 +90,12 @@ import java.util.Collections; } @Override - public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + this.pesTimeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { while (data.bytesLeft() > 0) { int offset = data.getPosition(); int limit = data.limit(); @@ -179,7 +186,7 @@ import java.util.Collections; // Skip the NAL prefix and type. seiWrapper.skipBytes(5); - seiReader.consume(seiWrapper, pesTimeUs, true); + seiReader.consume(pesTimeUs, seiWrapper); } if (suffixSei.endNalUnit(discardPadding)) { int unescapedLength = NalUnitUtil.unescapeStream(suffixSei.nalData, suffixSei.nalLength); @@ -187,7 +194,7 @@ import java.util.Collections; // Skip the NAL prefix and type. seiWrapper.skipBytes(5); - seiReader.consume(seiWrapper, pesTimeUs, true); + seiReader.consume(pesTimeUs, seiWrapper); } } 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 db3bbe0e69..38f9762c45 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 @@ -26,16 +26,22 @@ import com.google.android.exoplayer.util.ParsableByteArray; */ /* package */ final class Id3Reader extends ElementaryStreamReader { + private static final int ID3_HEADER_SIZE = 10; + + private final ParsableByteArray id3Header; + // State that should be reset on seek. private boolean writingSample; // Per sample state that gets reset at the start of each sample. private long sampleTimeUs; private int sampleSize; + private int sampleBytesRead; public Id3Reader(TrackOutput output) { super(output); output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, Format.NO_VALUE)); + id3Header = new ParsableByteArray(ID3_HEADER_SIZE); } @Override @@ -44,20 +50,43 @@ import com.google.android.exoplayer.util.ParsableByteArray; } @Override - public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { - if (startOfPacket) { - writingSample = true; - sampleTimeUs = pesTimeUs; - sampleSize = 0; + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + if (!dataAlignmentIndicator) { + return; } - if (writingSample) { - sampleSize += data.bytesLeft(); - output.sampleData(data, data.bytesLeft()); + writingSample = true; + sampleTimeUs = pesTimeUs; + sampleSize = 0; + sampleBytesRead = 0; + } + + @Override + public void consume(ParsableByteArray data) { + if (!writingSample) { + return; } + int bytesAvailable = data.bytesLeft(); + if (sampleBytesRead < ID3_HEADER_SIZE) { + // We're still reading the ID3 header. + int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_SIZE - sampleBytesRead); + System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead, + headerBytesAvailable); + if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_SIZE) { + // We've finished reading the ID3 header. Extract the sample size. + id3Header.setPosition(6); // 'ID3' (3) + version (2) + flags (1) + sampleSize = ID3_HEADER_SIZE + id3Header.readSynchSafeInt(); + } + } + // Write data to the output. + output.sampleData(data, bytesAvailable); + sampleBytesRead += bytesAvailable; } @Override public void packetFinished() { + if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) { + return; + } output.sampleMetadata(sampleTimeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null); writingSample = false; } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/MpegAudioReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/MpegAudioReader.java index 7a11201116..c951828dcf 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/MpegAudioReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/MpegAudioReader.java @@ -66,10 +66,12 @@ import com.google.android.exoplayer.util.ParsableByteArray; } @Override - public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { - if (startOfPacket) { - timeUs = pesTimeUs; - } + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_HEADER: diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java index 8509bd103f..a61708bb3c 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java @@ -23,26 +23,21 @@ import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableByteArray; /** - * Parses a SEI data from H.264 frames and extracts samples with closed captions data. - * - * TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that - * a sample with an earlier timestamp won't be added to it. + * Consumes SEI buffers, outputting contained EIA608 messages to a {@link TrackOutput}. */ -/* package */ final class SeiReader extends ElementaryStreamReader { +// TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that +// a sample with an earlier timestamp won't be added to it. +/* package */ final class SeiReader { + + private final TrackOutput output; public SeiReader(TrackOutput output) { - super(output); + this.output = output; output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_EIA608, Format.NO_VALUE, null)); } - @Override - public void seek() { - // Do nothing. - } - - @Override - public void consume(ParsableByteArray seiBuffer, long pesTimeUs, boolean startOfPacket) { + public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { int b; while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { // Parse payload type. @@ -57,7 +52,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; b = seiBuffer.readUnsignedByte(); payloadSize += b; } while (b == 0xFF); - // Process the payload. We only support EIA-608 payloads currently. + // Process the payload. if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) { output.sampleData(seiBuffer, payloadSize); output.sampleMetadata(pesTimeUs, C.SAMPLE_FLAG_SYNC, payloadSize, 0, null); @@ -67,9 +62,4 @@ import com.google.android.exoplayer.util.ParsableByteArray; } } - @Override - public void packetFinished() { - // Do nothing. - } - } 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 c15256332f..823e5afc9b 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 @@ -439,13 +439,13 @@ public final class TsExtractor implements Extractor { private int state; private int bytesRead; - private boolean bodyStarted; private boolean ptsFlag; private boolean dtsFlag; private boolean seenFirstDts; private int extendedHeaderLength; private int payloadSize; + private boolean dataAlignmentIndicator; private long timeUs; public PesReader(ElementaryStreamReader pesPayloadReader) { @@ -458,7 +458,6 @@ public final class TsExtractor implements Extractor { public void seek() { state = STATE_FINDING_HEADER; bytesRead = 0; - bodyStarted = false; seenFirstDts = false; pesPayloadReader.seek(); } @@ -483,10 +482,8 @@ public final class TsExtractor implements Extractor { if (payloadSize != -1) { Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); } - // Either way, if the body was started, notify the reader that it has now finished. - if (bodyStarted) { - pesPayloadReader.packetFinished(); - } + // Either way, notify the reader that it has now finished. + pesPayloadReader.packetFinished(); break; } setState(STATE_READING_HEADER); @@ -508,7 +505,7 @@ public final class TsExtractor implements Extractor { if (continueRead(data, pesScratch.data, readLength) && continueRead(data, null, extendedHeaderLength)) { parseHeaderExtension(); - bodyStarted = false; + pesPayloadReader.packetStarted(timeUs, dataAlignmentIndicator); setState(STATE_READING_BODY); } break; @@ -519,8 +516,7 @@ public final class TsExtractor implements Extractor { readLength -= padding; data.setLimit(data.getPosition() + readLength); } - pesPayloadReader.consume(data, timeUs, !bodyStarted); - bodyStarted = true; + pesPayloadReader.consume(data); if (payloadSize != -1) { payloadSize -= readLength; if (payloadSize == 0) { @@ -573,9 +569,9 @@ public final class TsExtractor implements Extractor { pesScratch.skipBits(8); // stream_id. int packetLength = pesScratch.readBits(16); - // First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1), - // data_alignment_indicator (1), copyright (1), original_or_copy (1) - pesScratch.skipBits(8); + pesScratch.skipBits(5); // '10' (2), PES_scrambling_control (2), PES_priority (1) + dataAlignmentIndicator = pesScratch.readBit(); + pesScratch.skipBits(2); // copyright (1), original_or_copy (1) ptsFlag = pesScratch.readBit(); dtsFlag = pesScratch.readBit(); // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1),