mirror of
https://github.com/samsonjs/media.git
synced 2026-04-03 10:55:48 +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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue