Fix handling of extended ID3 tags in MPEG-TS/HLS.

Issue #1181
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=117136370
This commit is contained in:
olly 2016-03-14 08:43:50 -07:00 committed by Oliver Woodman
parent b76db7acd2
commit 028ce2582c
13 changed files with 145 additions and 90 deletions

View File

@ -60,11 +60,11 @@ public class AdtsReaderTest extends TestCase {
private static final long ADTS_SAMPLE_DURATION = 23219L; private static final long ADTS_SAMPLE_DURATION = 23219L;
private ParsableByteArray data;
private AdtsReader adtsReader;
private FakeTrackOutput adtsOutput; private FakeTrackOutput adtsOutput;
private FakeTrackOutput id3Output; private FakeTrackOutput id3Output;
private AdtsReader adtsReader;
private ParsableByteArray data;
private boolean firstFeed;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
@ -72,6 +72,7 @@ public class AdtsReaderTest extends TestCase {
id3Output = new FakeTrackOutput(); id3Output = new FakeTrackOutput();
adtsReader = new AdtsReader(adtsOutput, id3Output); adtsReader = new AdtsReader(adtsOutput, id3Output);
data = new ParsableByteArray(TEST_DATA); data = new ParsableByteArray(TEST_DATA);
firstFeed = true;
} }
public void testSkipToNextSample() throws Exception { public void testSkipToNextSample() throws Exception {
@ -138,7 +139,7 @@ public class AdtsReaderTest extends TestCase {
public void testMultiPacketConsumed() throws Exception { public void testMultiPacketConsumed() throws Exception {
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
data.setPosition(0); data.setPosition(0);
adtsReader.consume(data, 0, i == 0); feed();
long timeUs = ADTS_SAMPLE_DURATION * i; long timeUs = ADTS_SAMPLE_DURATION * i;
int j = i * 2; int j = i * 2;
@ -158,12 +159,21 @@ public class AdtsReaderTest extends TestCase {
} }
private void feedLimited(int limit) { private void feedLimited(int limit) {
maybeStartPacket();
data.setLimit(limit); data.setLimit(limit);
feed(); feed();
} }
private void 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) { private void assertSampleCounts(int id3SampleCount, int adtsSampleCount) {

View File

@ -74,10 +74,12 @@ import com.google.android.exoplayer.util.ParsableByteArray;
} }
@Override @Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (startOfPacket) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
@Override
public void consume(ParsableByteArray data) {
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
case STATE_FINDING_SYNC: case STATE_FINDING_SYNC:

View File

@ -46,7 +46,7 @@ public final class AdtsExtractor implements Extractor {
// Accessed only by the loading thread. // Accessed only by the loading thread.
private AdtsReader adtsReader; private AdtsReader adtsReader;
private boolean firstPacket; private boolean startedPacket;
public AdtsExtractor() { public AdtsExtractor() {
this(0); this(0);
@ -55,7 +55,6 @@ public final class AdtsExtractor implements Extractor {
public AdtsExtractor(long firstSampleTimestampUs) { public AdtsExtractor(long firstSampleTimestampUs) {
this.firstSampleTimestampUs = firstSampleTimestampUs; this.firstSampleTimestampUs = firstSampleTimestampUs;
packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
firstPacket = true;
} }
@Override @Override
@ -118,7 +117,7 @@ public final class AdtsExtractor implements Extractor {
@Override @Override
public void seek() { public void seek() {
firstPacket = true; startedPacket = false;
adtsReader.seek(); 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 // TODO: Make it possible for adtsReader to consume the dataSource directly, so that it becomes
// unnecessary to copy the data through packetBuffer. // unnecessary to copy the data through packetBuffer.
adtsReader.consume(packetBuffer, firstSampleTimestampUs, firstPacket); if (!startedPacket) {
firstPacket = false; // 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; return RESULT_CONTINUE;
} }

View File

@ -93,10 +93,12 @@ import java.util.Collections;
} }
@Override @Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (startOfPacket) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
@Override
public void consume(ParsableByteArray data) {
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
case STATE_FINDING_SAMPLE: case STATE_FINDING_SAMPLE:

View File

@ -73,10 +73,12 @@ import com.google.android.exoplayer.util.ParsableByteArray;
} }
@Override @Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (startOfPacket) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
@Override
public void consume(ParsableByteArray data) {
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
case STATE_FINDING_SYNC: case STATE_FINDING_SYNC:

View File

@ -34,27 +34,26 @@ import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
* Notifies the reader that a seek has occurred. * Notifies the reader that a seek has occurred.
* <p>
* 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(); 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 packet.
* @param pesTimeUs The timestamp associated with the payload. * @param dataAlignmentIndicator The data alignment indicator associated with the packet.
* @param startOfPacket True if this is the first time this method is being called for the
* current packet. False otherwise.
*/ */
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 * Consumes (possibly partial) data from the current packet.
* {@link #consume(ParsableByteArray, long, boolean)}. The next call to *
* {@link #consume(ParsableByteArray, long, boolean)} will have {@code startOfPacket == true}. * @param data The data to consume.
*/
public abstract void consume(ParsableByteArray data);
/**
* Invoked when a packet ends.
*/ */
public abstract void packetFinished(); public abstract void packetFinished();

View File

@ -48,10 +48,13 @@ import java.util.Collections;
// State that should be reset on seek. // State that should be reset on seek.
private final boolean[] prefixFlags; private final boolean[] prefixFlags;
private final CsdBuffer csdBuffer; private final CsdBuffer csdBuffer;
private boolean foundFirstFrameInPacket;
private boolean foundFirstFrameInGroup; private boolean foundFirstFrameInGroup;
private long totalBytesWritten; 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. // Per sample state that gets reset at the start of each frame.
private boolean isKeyframe; private boolean isKeyframe;
private long framePosition; private long framePosition;
@ -73,10 +76,13 @@ import java.util.Collections;
} }
@Override @Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (startOfPacket) { this.pesTimeUs = pesTimeUs;
foundFirstFrameInPacket = false; foundFirstFrameInPacket = false;
} }
@Override
public void consume(ParsableByteArray data) {
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
int offset = data.getPosition(); int offset = data.getPosition();
int limit = data.limit(); int limit = data.limit();

View File

@ -57,6 +57,9 @@ import java.util.List;
private boolean foundFirstSample; private boolean foundFirstSample;
private long totalBytesWritten; 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. // Per sample state that gets reset at the start of each sample.
private boolean isKeyframe; private boolean isKeyframe;
private long samplePosition; private long samplePosition;
@ -78,7 +81,6 @@ import java.util.List;
@Override @Override
public void seek() { public void seek() {
seiReader.seek();
NalUnitUtil.clearPrefixFlags(prefixFlags); NalUnitUtil.clearPrefixFlags(prefixFlags);
sps.reset(); sps.reset();
pps.reset(); pps.reset();
@ -91,7 +93,12 @@ import java.util.List;
} }
@Override @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) { while (data.bytesLeft() > 0) {
int offset = data.getPosition(); int offset = data.getPosition();
int limit = data.limit(); int limit = data.limit();
@ -194,7 +201,7 @@ import java.util.List;
int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength); int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength);
seiWrapper.reset(sei.nalData, unescapedLength); seiWrapper.reset(sei.nalData, unescapedLength);
seiWrapper.setPosition(4); // NAL prefix and nal_unit() header. seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.
seiReader.consume(seiWrapper, pesTimeUs, true); seiReader.consume(pesTimeUs, seiWrapper);
} }
} }

View File

@ -58,6 +58,9 @@ import java.util.Collections;
private final SampleReader sampleReader; private final SampleReader sampleReader;
private long totalBytesWritten; private long totalBytesWritten;
// Per packet state that gets reset at the start of each packet.
private long pesTimeUs;
// Scratch variables to avoid allocations. // Scratch variables to avoid allocations.
private final ParsableByteArray seiWrapper; private final ParsableByteArray seiWrapper;
@ -76,7 +79,6 @@ import java.util.Collections;
@Override @Override
public void seek() { public void seek() {
seiReader.seek();
NalUnitUtil.clearPrefixFlags(prefixFlags); NalUnitUtil.clearPrefixFlags(prefixFlags);
vps.reset(); vps.reset();
sps.reset(); sps.reset();
@ -88,7 +90,12 @@ import java.util.Collections;
} }
@Override @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) { while (data.bytesLeft() > 0) {
int offset = data.getPosition(); int offset = data.getPosition();
int limit = data.limit(); int limit = data.limit();
@ -179,7 +186,7 @@ import java.util.Collections;
// Skip the NAL prefix and type. // Skip the NAL prefix and type.
seiWrapper.skipBytes(5); seiWrapper.skipBytes(5);
seiReader.consume(seiWrapper, pesTimeUs, true); seiReader.consume(pesTimeUs, seiWrapper);
} }
if (suffixSei.endNalUnit(discardPadding)) { if (suffixSei.endNalUnit(discardPadding)) {
int unescapedLength = NalUnitUtil.unescapeStream(suffixSei.nalData, suffixSei.nalLength); int unescapedLength = NalUnitUtil.unescapeStream(suffixSei.nalData, suffixSei.nalLength);
@ -187,7 +194,7 @@ import java.util.Collections;
// Skip the NAL prefix and type. // Skip the NAL prefix and type.
seiWrapper.skipBytes(5); seiWrapper.skipBytes(5);
seiReader.consume(seiWrapper, pesTimeUs, true); seiReader.consume(pesTimeUs, seiWrapper);
} }
} }

View File

@ -26,16 +26,22 @@ import com.google.android.exoplayer.util.ParsableByteArray;
*/ */
/* package */ final class Id3Reader extends ElementaryStreamReader { /* 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. // State that should be reset on seek.
private boolean writingSample; private boolean writingSample;
// Per sample state that gets reset at the start of each sample. // Per sample state that gets reset at the start of each sample.
private long sampleTimeUs; private long sampleTimeUs;
private int sampleSize; private int sampleSize;
private int sampleBytesRead;
public Id3Reader(TrackOutput output) { public Id3Reader(TrackOutput output) {
super(output); super(output);
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, Format.NO_VALUE)); output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, Format.NO_VALUE));
id3Header = new ParsableByteArray(ID3_HEADER_SIZE);
} }
@Override @Override
@ -44,20 +50,43 @@ import com.google.android.exoplayer.util.ParsableByteArray;
} }
@Override @Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (startOfPacket) { if (!dataAlignmentIndicator) {
return;
}
writingSample = true; writingSample = true;
sampleTimeUs = pesTimeUs; sampleTimeUs = pesTimeUs;
sampleSize = 0; sampleSize = 0;
sampleBytesRead = 0;
} }
if (writingSample) {
sampleSize += data.bytesLeft(); @Override
output.sampleData(data, data.bytesLeft()); 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 @Override
public void packetFinished() { public void packetFinished() {
if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) {
return;
}
output.sampleMetadata(sampleTimeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null); output.sampleMetadata(sampleTimeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null);
writingSample = false; writingSample = false;
} }

View File

@ -66,10 +66,12 @@ import com.google.android.exoplayer.util.ParsableByteArray;
} }
@Override @Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (startOfPacket) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
@Override
public void consume(ParsableByteArray data) {
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
case STATE_FINDING_HEADER: case STATE_FINDING_HEADER:

View File

@ -23,26 +23,21 @@ import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
* Parses a SEI data from H.264 frames and extracts samples with closed captions data. * Consumes SEI buffers, outputting contained EIA608 messages to a {@link TrackOutput}.
*
* 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 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) { public SeiReader(TrackOutput output) {
super(output); this.output = output;
output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_EIA608, Format.NO_VALUE, output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_EIA608, Format.NO_VALUE,
null)); null));
} }
@Override public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
public void seek() {
// Do nothing.
}
@Override
public void consume(ParsableByteArray seiBuffer, long pesTimeUs, boolean startOfPacket) {
int b; int b;
while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
// Parse payload type. // Parse payload type.
@ -57,7 +52,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
b = seiBuffer.readUnsignedByte(); b = seiBuffer.readUnsignedByte();
payloadSize += b; payloadSize += b;
} while (b == 0xFF); } while (b == 0xFF);
// Process the payload. We only support EIA-608 payloads currently. // Process the payload.
if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) { if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) {
output.sampleData(seiBuffer, payloadSize); output.sampleData(seiBuffer, payloadSize);
output.sampleMetadata(pesTimeUs, C.SAMPLE_FLAG_SYNC, payloadSize, 0, null); 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.
}
} }

View File

@ -439,13 +439,13 @@ public final class TsExtractor implements Extractor {
private int state; private int state;
private int bytesRead; private int bytesRead;
private boolean bodyStarted;
private boolean ptsFlag; private boolean ptsFlag;
private boolean dtsFlag; private boolean dtsFlag;
private boolean seenFirstDts; private boolean seenFirstDts;
private int extendedHeaderLength; private int extendedHeaderLength;
private int payloadSize; private int payloadSize;
private boolean dataAlignmentIndicator;
private long timeUs; private long timeUs;
public PesReader(ElementaryStreamReader pesPayloadReader) { public PesReader(ElementaryStreamReader pesPayloadReader) {
@ -458,7 +458,6 @@ public final class TsExtractor implements Extractor {
public void seek() { public void seek() {
state = STATE_FINDING_HEADER; state = STATE_FINDING_HEADER;
bytesRead = 0; bytesRead = 0;
bodyStarted = false;
seenFirstDts = false; seenFirstDts = false;
pesPayloadReader.seek(); pesPayloadReader.seek();
} }
@ -483,10 +482,8 @@ public final class TsExtractor implements Extractor {
if (payloadSize != -1) { if (payloadSize != -1) {
Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); 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. // Either way, notify the reader that it has now finished.
if (bodyStarted) {
pesPayloadReader.packetFinished(); pesPayloadReader.packetFinished();
}
break; break;
} }
setState(STATE_READING_HEADER); setState(STATE_READING_HEADER);
@ -508,7 +505,7 @@ public final class TsExtractor implements Extractor {
if (continueRead(data, pesScratch.data, readLength) if (continueRead(data, pesScratch.data, readLength)
&& continueRead(data, null, extendedHeaderLength)) { && continueRead(data, null, extendedHeaderLength)) {
parseHeaderExtension(); parseHeaderExtension();
bodyStarted = false; pesPayloadReader.packetStarted(timeUs, dataAlignmentIndicator);
setState(STATE_READING_BODY); setState(STATE_READING_BODY);
} }
break; break;
@ -519,8 +516,7 @@ public final class TsExtractor implements Extractor {
readLength -= padding; readLength -= padding;
data.setLimit(data.getPosition() + readLength); data.setLimit(data.getPosition() + readLength);
} }
pesPayloadReader.consume(data, timeUs, !bodyStarted); pesPayloadReader.consume(data);
bodyStarted = true;
if (payloadSize != -1) { if (payloadSize != -1) {
payloadSize -= readLength; payloadSize -= readLength;
if (payloadSize == 0) { if (payloadSize == 0) {
@ -573,9 +569,9 @@ public final class TsExtractor implements Extractor {
pesScratch.skipBits(8); // stream_id. pesScratch.skipBits(8); // stream_id.
int packetLength = pesScratch.readBits(16); int packetLength = pesScratch.readBits(16);
// First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1), pesScratch.skipBits(5); // '10' (2), PES_scrambling_control (2), PES_priority (1)
// data_alignment_indicator (1), copyright (1), original_or_copy (1) dataAlignmentIndicator = pesScratch.readBit();
pesScratch.skipBits(8); pesScratch.skipBits(2); // copyright (1), original_or_copy (1)
ptsFlag = pesScratch.readBit(); ptsFlag = pesScratch.readBit();
dtsFlag = pesScratch.readBit(); dtsFlag = pesScratch.readBit();
// ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1), // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1),