mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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:
parent
762ec41f95
commit
adc7ecec09
3 changed files with 122 additions and 28 deletions
|
|
@ -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 {
|
||||||
}, "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;
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
@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 {
|
||||||
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 {
|
||||||
@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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
* @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 {
|
||||||
*
|
*
|
||||||
* @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 {
|
||||||
* @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 {
|
||||||
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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue