Commit adc7ecec by eguven Committed by Oliver Woodman

Support MPEG-TS streams that start/end with an incomplete TS packet or lost sync.

Issue: #1332
Issue: #1101
Issue: #1083
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=125659191
parent 762ec41f
...@@ -20,11 +20,18 @@ import com.google.android.exoplayer.testutil.TestUtil; ...@@ -20,11 +20,18 @@ import com.google.android.exoplayer.testutil.TestUtil;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;
/** /**
* Unit test for {@link TsExtractor}. * Unit test for {@link TsExtractor}.
*/ */
public final class TsExtractorTest extends InstrumentationTestCase { public final class TsExtractorTest extends InstrumentationTestCase {
private static final int TS_PACKET_SIZE = 188;
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
@Override @Override
...@@ -34,4 +41,36 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -34,4 +41,36 @@ public final class TsExtractorTest extends InstrumentationTestCase {
}, "ts/sample.ts", getInstrumentation()); }, "ts/sample.ts", getInstrumentation());
} }
public void testIncompleteSample() throws Exception {
Random random = new Random(0);
byte[] fileData = TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts");
ByteArrayOutputStream out = new ByteArrayOutputStream(fileData.length * 2);
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);
out.write(fileData, 0, TS_PACKET_SIZE * 5);
for (int i = TS_PACKET_SIZE * 5; i < fileData.length; i += TS_PACKET_SIZE) {
writeJunkData(out, random.nextInt(TS_PACKET_SIZE));
out.write(fileData, i, TS_PACKET_SIZE);
}
out.write(TS_SYNC_BYTE);
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);
fileData = out.toByteArray();
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
@Override
public Extractor create() {
return new TsExtractor();
}
}, "ts/sample.ts", fileData, getInstrumentation());
}
private static void writeJunkData(ByteArrayOutputStream out, int length) throws IOException {
for (int i = 0; i < length; i++) {
if (((byte) i) == TS_SYNC_BYTE) {
out.write(0);
} else {
out.write(i);
}
}
}
} }
...@@ -22,6 +22,7 @@ import com.google.android.exoplayer.extractor.ExtractorInput; ...@@ -22,6 +22,7 @@ import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.PositionHolder; import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
...@@ -65,6 +66,9 @@ public final class TsExtractor implements Extractor { ...@@ -65,6 +66,9 @@ public final class TsExtractor implements Extractor {
private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3"); private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3");
private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC"); private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC");
private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2
private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT;
private final PtsTimestampAdjuster ptsTimestampAdjuster; private final PtsTimestampAdjuster ptsTimestampAdjuster;
private final int workaroundFlags; private final int workaroundFlags;
private final ParsableByteArray tsPacketBuffer; private final ParsableByteArray tsPacketBuffer;
...@@ -87,7 +91,7 @@ public final class TsExtractor implements Extractor { ...@@ -87,7 +91,7 @@ public final class TsExtractor implements Extractor {
public TsExtractor(PtsTimestampAdjuster ptsTimestampAdjuster, int workaroundFlags) { public TsExtractor(PtsTimestampAdjuster ptsTimestampAdjuster, int workaroundFlags) {
this.ptsTimestampAdjuster = ptsTimestampAdjuster; this.ptsTimestampAdjuster = ptsTimestampAdjuster;
this.workaroundFlags = workaroundFlags; this.workaroundFlags = workaroundFlags;
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE);
tsScratch = new ParsableBitArray(new byte[3]); tsScratch = new ParsableBitArray(new byte[3]);
tsPayloadReaders = new SparseArray<>(); tsPayloadReaders = new SparseArray<>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader()); tsPayloadReaders.put(TS_PAT_PID, new PatReader());
...@@ -98,15 +102,20 @@ public final class TsExtractor implements Extractor { ...@@ -98,15 +102,20 @@ public final class TsExtractor implements Extractor {
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
byte[] scratch = new byte[1]; byte[] buffer = tsPacketBuffer.data;
for (int i = 0; i < 5; i++) { input.peekFully(buffer, 0, BUFFER_SIZE);
input.peekFully(scratch, 0, 1); for (int j = 0; j < TS_PACKET_SIZE; j++) {
if ((scratch[0] & 0xFF) != 0x47) { for (int i = 0; true; i++) {
return false; if (i == BUFFER_PACKET_COUNT) {
input.skipFully(j);
return true;
}
if (buffer[j + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) {
break;
}
} }
input.advancePeekPosition(TS_PACKET_SIZE - 1);
} }
return true; return false;
} }
@Override @Override
...@@ -121,6 +130,7 @@ public final class TsExtractor implements Extractor { ...@@ -121,6 +130,7 @@ public final class TsExtractor implements Extractor {
for (int i = 0; i < tsPayloadReaders.size(); i++) { for (int i = 0; i < tsPayloadReaders.size(); i++) {
tsPayloadReaders.valueAt(i).seek(); tsPayloadReaders.valueAt(i).seek();
} }
tsPacketBuffer.reset();
} }
@Override @Override
...@@ -131,19 +141,40 @@ public final class TsExtractor implements Extractor { ...@@ -131,19 +141,40 @@ public final class TsExtractor implements Extractor {
@Override @Override
public int read(ExtractorInput input, PositionHolder seekPosition) public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) { byte[] data = tsPacketBuffer.data;
return RESULT_END_OF_INPUT; // Shift bytes to the start of the buffer if there isn't enough space left at the end
if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) {
int bytesLeft = tsPacketBuffer.bytesLeft();
if (bytesLeft > 0) {
System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft);
}
tsPacketBuffer.reset(data, bytesLeft);
}
// Read more bytes until there is at least one packet size
while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) {
int limit = tsPacketBuffer.limit();
int read = input.read(data, limit, BUFFER_SIZE - limit);
if (read == C.RESULT_END_OF_INPUT) {
return RESULT_END_OF_INPUT;
}
tsPacketBuffer.setLimit(limit + read);
} }
// Note: see ISO/IEC 13818-1, section 2.4.3.2 for detailed information on the format of // Note: see ISO/IEC 13818-1, section 2.4.3.2 for detailed information on the format of
// the header. // the header.
tsPacketBuffer.setPosition(0); final int limit = tsPacketBuffer.limit();
tsPacketBuffer.setLimit(TS_PACKET_SIZE); int position = tsPacketBuffer.getPosition();
int syncByte = tsPacketBuffer.readUnsignedByte(); while (position < limit && data[position] != TS_SYNC_BYTE) {
if (syncByte != TS_SYNC_BYTE) { position++;
}
tsPacketBuffer.setPosition(position);
int endOfPacket = position + TS_PACKET_SIZE;
if (endOfPacket > limit) {
return RESULT_CONTINUE; return RESULT_CONTINUE;
} }
tsPacketBuffer.skipBytes(1);
tsPacketBuffer.readBytes(tsScratch, 3); tsPacketBuffer.readBytes(tsScratch, 3);
tsScratch.skipBits(1); // transport_error_indicator tsScratch.skipBits(1); // transport_error_indicator
boolean payloadUnitStartIndicator = tsScratch.readBit(); boolean payloadUnitStartIndicator = tsScratch.readBit();
...@@ -164,10 +195,14 @@ public final class TsExtractor implements Extractor { ...@@ -164,10 +195,14 @@ public final class TsExtractor implements Extractor {
if (payloadExists) { if (payloadExists) {
TsPayloadReader payloadReader = tsPayloadReaders.get(pid); TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
if (payloadReader != null) { if (payloadReader != null) {
tsPacketBuffer.setLimit(endOfPacket);
payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator, output); payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator, output);
Assertions.checkState(tsPacketBuffer.getPosition() <= endOfPacket);
tsPacketBuffer.setLimit(limit);
} }
} }
tsPacketBuffer.setPosition(endOfPacket);
return RESULT_CONTINUE; return RESULT_CONTINUE;
} }
......
...@@ -87,6 +87,7 @@ public class TestUtil { ...@@ -87,6 +87,7 @@ public class TestUtil {
private static void consumeTestData(Extractor extractor, FakeExtractorInput input, private static void consumeTestData(Extractor extractor, FakeExtractorInput input,
FakeExtractorOutput output, boolean retryFromStartIfLive) FakeExtractorOutput output, boolean retryFromStartIfLive)
throws IOException, InterruptedException { throws IOException, InterruptedException {
extractor.seek(input.getPosition());
PositionHolder seekPositionHolder = new PositionHolder(); PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE; int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) { while (readResult != Extractor.RESULT_END_OF_INPUT) {
...@@ -193,8 +194,8 @@ public class TestUtil { ...@@ -193,8 +194,8 @@ public class TestUtil {
} }
/** /**
* Calls {@link #assertOutput(Extractor, String, Instrumentation, boolean, boolean, boolean)} with * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean,
* all possible combinations of "simulate" parameters. * boolean)} with all possible combinations of "simulate" parameters.
* *
* @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
* class which is to be tested. * class which is to be tested.
...@@ -202,18 +203,37 @@ public class TestUtil { ...@@ -202,18 +203,37 @@ public class TestUtil {
* @param instrumentation To be used to load the sample file. * @param instrumentation To be used to load the sample file.
* @throws IOException If reading from the input fails. * @throws IOException If reading from the input fails.
* @throws InterruptedException If interrupted while reading from the input. * @throws InterruptedException If interrupted while reading from the input.
* @see #assertOutput(Extractor, String, Instrumentation, boolean, boolean, boolean) * @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean)
*/ */
public static void assertOutput(ExtractorFactory factory, String sampleFile, public static void assertOutput(ExtractorFactory factory, String sampleFile,
Instrumentation instrumentation) throws IOException, InterruptedException { Instrumentation instrumentation) throws IOException, InterruptedException {
assertOutput(factory.create(), sampleFile, instrumentation, false, false, false); byte[] fileData = getByteArray(instrumentation, sampleFile);
assertOutput(factory.create(), sampleFile, instrumentation, true, false, false); assertOutput(factory, sampleFile, fileData, instrumentation);
assertOutput(factory.create(), sampleFile, instrumentation, false, true, false); }
assertOutput(factory.create(), sampleFile, instrumentation, true, true, false);
assertOutput(factory.create(), sampleFile, instrumentation, false, false, true); /**
assertOutput(factory.create(), sampleFile, instrumentation, true, false, true); * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean,
assertOutput(factory.create(), sampleFile, instrumentation, false, true, true); * boolean)} with all possible combinations of "simulate" parameters.
assertOutput(factory.create(), sampleFile, instrumentation, true, true, true); *
* @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
* class which is to be tested.
* @param sampleFile The path to the input sample.
* @param fileData Content of the input file.
* @param instrumentation To be used to load the sample file.
* @throws IOException If reading from the input fails.
* @throws InterruptedException If interrupted while reading from the input.
* @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean)
*/
public static void assertOutput(ExtractorFactory factory, String sampleFile, byte[] fileData,
Instrumentation instrumentation) throws IOException, InterruptedException {
assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, false);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, false);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, false);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, false);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, true);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, true);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, true);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, true);
} }
/** /**
...@@ -224,6 +244,7 @@ public class TestUtil { ...@@ -224,6 +244,7 @@ public class TestUtil {
* *
* @param extractor The {@link Extractor} to be tested. * @param extractor The {@link Extractor} to be tested.
* @param sampleFile The path to the input sample. * @param sampleFile The path to the input sample.
* @param fileData Content of the input file.
* @param instrumentation To be used to load the sample file. * @param instrumentation To be used to load the sample file.
* @param simulateIOErrors If true simulates IOErrors. * @param simulateIOErrors If true simulates IOErrors.
* @param simulateUnknownLength If true simulates unknown input length. * @param simulateUnknownLength If true simulates unknown input length.
...@@ -233,9 +254,9 @@ public class TestUtil { ...@@ -233,9 +254,9 @@ public class TestUtil {
* @throws InterruptedException If interrupted while reading from the input. * @throws InterruptedException If interrupted while reading from the input.
*/ */
public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile, public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile,
Instrumentation instrumentation, boolean simulateIOErrors, boolean simulateUnknownLength, byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors,
boolean simulateUnknownLength,
boolean simulatePartialReads) throws IOException, InterruptedException { boolean simulatePartialReads) throws IOException, InterruptedException {
byte[] fileData = getByteArray(instrumentation, sampleFile);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData)
.setSimulateIOErrors(simulateIOErrors) .setSimulateIOErrors(simulateIOErrors)
.setSimulateUnknownLength(simulateUnknownLength) .setSimulateUnknownLength(simulateUnknownLength)
...@@ -262,7 +283,6 @@ public class TestUtil { ...@@ -262,7 +283,6 @@ public class TestUtil {
for (int i = 0; i < extractorOutput.numberOfTracks; i++) { for (int i = 0; i < extractorOutput.numberOfTracks; i++) {
extractorOutput.trackOutputs.valueAt(i).clear(); extractorOutput.trackOutputs.valueAt(i).clear();
} }
extractor.seek(position);
consumeTestData(extractor, input, extractorOutput, false); consumeTestData(extractor, input, extractorOutput, false);
extractorOutput.assertOutput(instrumentation, sampleFile + '.' + j + DUMP_EXTENSION); extractorOutput.assertOutput(instrumentation, sampleFile + '.' + j + DUMP_EXTENSION);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment