diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ts/TsExtractorTest.java index 85f4cc1a91..30175a6b85 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ts/TsExtractorTest.java @@ -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); + } + } + } + } 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 4dcc9605c7..87b973355f 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 @@ -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; } diff --git a/testutils/src/main/java/com/google/android/exoplayer/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer/testutil/TestUtil.java index 6d0449ad5a..3ae823a2a5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer/testutil/TestUtil.java @@ -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);