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
This commit is contained in:
eguven 2016-06-23 03:00:14 -07:00 committed by Oliver Woodman
parent 762ec41f95
commit adc7ecec09
3 changed files with 122 additions and 28 deletions

View file

@ -20,11 +20,18 @@ import com.google.android.exoplayer.testutil.TestUtil;
import android.test.InstrumentationTestCase;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;
/**
* Unit test for {@link TsExtractor}.
*/
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 {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
@Override
@ -34,4 +41,36 @@ public final class TsExtractorTest extends InstrumentationTestCase {
}, "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);
}
}
}
}

View file

@ -22,6 +22,7 @@ import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.PositionHolder;
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.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
@ -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 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 int workaroundFlags;
private final ParsableByteArray tsPacketBuffer;
@ -87,7 +91,7 @@ public final class TsExtractor implements Extractor {
public TsExtractor(PtsTimestampAdjuster ptsTimestampAdjuster, int workaroundFlags) {
this.ptsTimestampAdjuster = ptsTimestampAdjuster;
this.workaroundFlags = workaroundFlags;
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE);
tsScratch = new ParsableBitArray(new byte[3]);
tsPayloadReaders = new SparseArray<>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
@ -98,15 +102,20 @@ public final class TsExtractor implements Extractor {
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
byte[] scratch = new byte[1];
for (int i = 0; i < 5; i++) {
input.peekFully(scratch, 0, 1);
if ((scratch[0] & 0xFF) != 0x47) {
return false;
byte[] buffer = tsPacketBuffer.data;
input.peekFully(buffer, 0, BUFFER_SIZE);
for (int j = 0; j < TS_PACKET_SIZE; j++) {
for (int i = 0; true; i++) {
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
@ -121,6 +130,7 @@ public final class TsExtractor implements Extractor {
for (int i = 0; i < tsPayloadReaders.size(); i++) {
tsPayloadReaders.valueAt(i).seek();
}
tsPacketBuffer.reset();
}
@Override
@ -131,19 +141,40 @@ public final class TsExtractor implements Extractor {
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) {
return RESULT_END_OF_INPUT;
byte[] data = tsPacketBuffer.data;
// 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
// the header.
tsPacketBuffer.setPosition(0);
tsPacketBuffer.setLimit(TS_PACKET_SIZE);
int syncByte = tsPacketBuffer.readUnsignedByte();
if (syncByte != TS_SYNC_BYTE) {
final int limit = tsPacketBuffer.limit();
int position = tsPacketBuffer.getPosition();
while (position < limit && data[position] != TS_SYNC_BYTE) {
position++;
}
tsPacketBuffer.setPosition(position);
int endOfPacket = position + TS_PACKET_SIZE;
if (endOfPacket > limit) {
return RESULT_CONTINUE;
}
tsPacketBuffer.skipBytes(1);
tsPacketBuffer.readBytes(tsScratch, 3);
tsScratch.skipBits(1); // transport_error_indicator
boolean payloadUnitStartIndicator = tsScratch.readBit();
@ -164,10 +195,14 @@ public final class TsExtractor implements Extractor {
if (payloadExists) {
TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
if (payloadReader != null) {
tsPacketBuffer.setLimit(endOfPacket);
payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator, output);
Assertions.checkState(tsPacketBuffer.getPosition() <= endOfPacket);
tsPacketBuffer.setLimit(limit);
}
}
tsPacketBuffer.setPosition(endOfPacket);
return RESULT_CONTINUE;
}

View file

@ -87,6 +87,7 @@ public class TestUtil {
private static void consumeTestData(Extractor extractor, FakeExtractorInput input,
FakeExtractorOutput output, boolean retryFromStartIfLive)
throws IOException, InterruptedException {
extractor.seek(input.getPosition());
PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) {
@ -193,8 +194,8 @@ public class TestUtil {
}
/**
* Calls {@link #assertOutput(Extractor, String, Instrumentation, boolean, boolean, boolean)} with
* all possible combinations of "simulate" parameters.
* Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean,
* boolean)} with all possible combinations of "simulate" parameters.
*
* @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
* class which is to be tested.
@ -202,18 +203,37 @@ public class TestUtil {
* @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, Instrumentation, boolean, boolean, boolean)
* @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean)
*/
public static void assertOutput(ExtractorFactory factory, String sampleFile,
Instrumentation instrumentation) throws IOException, InterruptedException {
assertOutput(factory.create(), sampleFile, instrumentation, false, false, false);
assertOutput(factory.create(), sampleFile, instrumentation, true, false, false);
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);
assertOutput(factory.create(), sampleFile, instrumentation, false, true, true);
assertOutput(factory.create(), sampleFile, instrumentation, true, true, true);
byte[] fileData = getByteArray(instrumentation, sampleFile);
assertOutput(factory, sampleFile, fileData, instrumentation);
}
/**
* Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean,
* boolean)} with all possible combinations of "simulate" parameters.
*
* @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 {
*
* @param extractor The {@link Extractor} 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.
* @param simulateIOErrors If true simulates IOErrors.
* @param simulateUnknownLength If true simulates unknown input length.
@ -233,9 +254,9 @@ public class TestUtil {
* @throws InterruptedException If interrupted while reading from the input.
*/
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 {
byte[] fileData = getByteArray(instrumentation, sampleFile);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData)
.setSimulateIOErrors(simulateIOErrors)
.setSimulateUnknownLength(simulateUnknownLength)
@ -262,7 +283,6 @@ public class TestUtil {
for (int i = 0; i < extractorOutput.numberOfTracks; i++) {
extractorOutput.trackOutputs.valueAt(i).clear();
}
extractor.seek(position);
consumeTestData(extractor, input, extractorOutput, false);
extractorOutput.assertOutput(instrumentation, sampleFile + '.' + j + DUMP_EXTENSION);