Add support for header stripping in Matroska streams.

Issue: #589
This commit is contained in:
Oliver Woodman 2015-07-15 18:53:32 +01:00
parent 6c2b3c875d
commit 79db618ba6
3 changed files with 181 additions and 72 deletions

View file

@ -113,7 +113,9 @@ public final class WebmExtractor implements Extractor {
private static final int ID_CONTENT_ENCODING = 0x6240;
private static final int ID_CONTENT_ENCODING_ORDER = 0x5031;
private static final int ID_CONTENT_ENCODING_SCOPE = 0x5032;
private static final int ID_CONTENT_ENCODING_TYPE = 0x5033;
private static final int ID_CONTENT_COMPRESSION = 0x5034;
private static final int ID_CONTENT_COMPRESSION_ALGORITHM = 0x4254;
private static final int ID_CONTENT_COMPRESSION_SETTINGS = 0x4255;
private static final int ID_CONTENT_ENCRYPTION = 0x5035;
private static final int ID_CONTENT_ENCRYPTION_ALGORITHM = 0x47E1;
private static final int ID_CONTENT_ENCRYPTION_KEY_ID = 0x47E2;
@ -139,6 +141,7 @@ public final class WebmExtractor implements Extractor {
private final ParsableByteArray scratch;
private final ParsableByteArray vorbisNumPageSamples;
private final ParsableByteArray seekEntryIdBytes;
private final ParsableByteArray sampleStrippedBytes;
private long segmentContentPosition = UNKNOWN;
private long segmentContentSize = UNKNOWN;
@ -178,7 +181,7 @@ public final class WebmExtractor implements Extractor {
// Sample reading state.
private int sampleBytesRead;
private boolean sampleEncryptionDataRead;
private boolean sampleEncodingHandled;
private int sampleCurrentNalBytesRemaining;
private int sampleBytesWritten;
private boolean sampleRead;
@ -200,6 +203,7 @@ public final class WebmExtractor implements Extractor {
seekEntryIdBytes = new ParsableByteArray(4);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
sampleStrippedBytes = new ParsableByteArray();
}
@Override
@ -213,10 +217,7 @@ public final class WebmExtractor implements Extractor {
blockState = BLOCK_STATE_START;
reader.reset();
varintReader.reset();
sampleCurrentNalBytesRemaining = 0;
sampleBytesRead = 0;
sampleBytesWritten = 0;
sampleEncryptionDataRead = false;
resetSample();
}
@Override
@ -247,6 +248,7 @@ public final class WebmExtractor implements Extractor {
case ID_VIDEO:
case ID_CONTENT_ENCODINGS:
case ID_CONTENT_ENCODING:
case ID_CONTENT_COMPRESSION:
case ID_CONTENT_ENCRYPTION:
case ID_CONTENT_ENCRYPTION_AES_SETTINGS:
case ID_CUES:
@ -269,7 +271,7 @@ public final class WebmExtractor implements Extractor {
case ID_CHANNELS:
case ID_CONTENT_ENCODING_ORDER:
case ID_CONTENT_ENCODING_SCOPE:
case ID_CONTENT_ENCODING_TYPE:
case ID_CONTENT_COMPRESSION_ALGORITHM:
case ID_CONTENT_ENCRYPTION_ALGORITHM:
case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
case ID_CUE_TIME:
@ -280,6 +282,7 @@ public final class WebmExtractor implements Extractor {
case ID_CODEC_ID:
return EbmlReader.TYPE_STRING;
case ID_SEEK_ID:
case ID_CONTENT_COMPRESSION_SETTINGS:
case ID_CONTENT_ENCRYPTION_KEY_ID:
case ID_SIMPLE_BLOCK:
case ID_BLOCK:
@ -371,17 +374,20 @@ public final class WebmExtractor implements Extractor {
blockState = BLOCK_STATE_START;
return;
case ID_CONTENT_ENCODING:
if (!trackFormat.hasContentEncryption) {
// We found a ContentEncoding other than Encryption.
throw new ParserException("Found an unsupported ContentEncoding");
if (trackFormat.hasContentEncryption) {
if (trackFormat.encryptionKeyId == null) {
throw new ParserException("Encrypted Track found but ContentEncKeyID was not found");
}
if (!sentDrmInitData) {
extractorOutput.drmInitData(
new DrmInitData.Universal(MimeTypes.VIDEO_WEBM, trackFormat.encryptionKeyId));
sentDrmInitData = true;
}
}
if (trackFormat.encryptionKeyId == null) {
throw new ParserException("Encrypted Track found but ContentEncKeyID was not found");
}
if (!sentDrmInitData) {
extractorOutput.drmInitData(
new DrmInitData.Universal(MimeTypes.VIDEO_WEBM, trackFormat.encryptionKeyId));
sentDrmInitData = true;
return;
case ID_CONTENT_ENCODINGS:
if (trackFormat.hasContentEncryption && trackFormat.sampleStrippedBytes != null) {
throw new ParserException("Combining encryption and compression is not supported");
}
return;
case ID_TRACK_ENTRY:
@ -474,16 +480,15 @@ public final class WebmExtractor implements Extractor {
}
return;
case ID_CONTENT_ENCODING_SCOPE:
// This extractor only supports the scope of all frames (since that's the only scope used
// for Encryption).
// This extractor only supports the scope of all frames.
if (value != 1) {
throw new ParserException("ContentEncodingScope " + value + " not supported");
}
return;
case ID_CONTENT_ENCODING_TYPE:
// This extractor only supports Encrypted ContentEncodingType.
if (value != 1) {
throw new ParserException("ContentEncodingType " + value + " not supported");
case ID_CONTENT_COMPRESSION_ALGORITHM:
// This extractor only supports header stripping.
if (value != 3) {
throw new ParserException("ContentCompAlgo " + value + " not supported");
}
return;
case ID_CONTENT_ENCRYPTION_ALGORITHM:
@ -560,6 +565,11 @@ public final class WebmExtractor implements Extractor {
trackFormat.codecPrivate = new byte[contentSize];
input.readFully(trackFormat.codecPrivate, 0, contentSize);
return;
case ID_CONTENT_COMPRESSION_SETTINGS:
// This extractor only supports header stripping, so the payload is the stripped bytes.
trackFormat.sampleStrippedBytes = new byte[contentSize];
input.readFully(trackFormat.sampleStrippedBytes, 0, contentSize);
return;
case ID_CONTENT_ENCRYPTION_KEY_ID:
trackFormat.encryptionKeyId = new byte[contentSize];
input.readFully(trackFormat.encryptionKeyId, 0, contentSize);
@ -714,9 +724,15 @@ public final class WebmExtractor implements Extractor {
private void outputSampleMetadata(TrackOutput trackOutput, long timeUs) {
trackOutput.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, blockEncryptionKeyId);
sampleRead = true;
resetSample();
}
private void resetSample() {
sampleBytesRead = 0;
sampleBytesWritten = 0;
sampleEncryptionDataRead = false;
sampleCurrentNalBytesRemaining = 0;
sampleEncodingHandled = false;
sampleStrippedBytes.reset();
}
/**
@ -738,26 +754,30 @@ public final class WebmExtractor implements Extractor {
private void writeSampleData(ExtractorInput input, TrackOutput output, TrackFormat format,
int size) throws IOException, InterruptedException {
// Read the sample's encryption signal byte and set the IV size if necessary.
if (format.hasContentEncryption && !sampleEncryptionDataRead) {
// Clear the encrypted flag.
blockFlags &= ~C.SAMPLE_FLAG_ENCRYPTED;
input.readFully(scratch.data, 0, 1);
sampleBytesRead++;
if ((scratch.data[0] & 0x80) == 0x80) {
throw new ParserException("Extension bit is set in signal byte");
}
sampleEncryptionDataRead = true;
// If the sample is encrypted, write the IV size instead of the signal byte, and set the flag.
if ((scratch.data[0] & 0x01) == 0x01) {
scratch.data[0] = (byte) ENCRYPTION_IV_SIZE;
scratch.setPosition(0);
output.sampleData(scratch, 1);
sampleBytesWritten++;
blockFlags |= C.SAMPLE_FLAG_ENCRYPTED;
if (!sampleEncodingHandled) {
if (format.hasContentEncryption) {
// If the sample is encrypted, read its encryption signal byte and set the IV size.
// Clear the encrypted flag.
blockFlags &= ~C.SAMPLE_FLAG_ENCRYPTED;
input.readFully(scratch.data, 0, 1);
sampleBytesRead++;
if ((scratch.data[0] & 0x80) == 0x80) {
throw new ParserException("Extension bit is set in signal byte");
}
if ((scratch.data[0] & 0x01) == 0x01) {
scratch.data[0] = (byte) ENCRYPTION_IV_SIZE;
scratch.setPosition(0);
output.sampleData(scratch, 1);
sampleBytesWritten++;
blockFlags |= C.SAMPLE_FLAG_ENCRYPTED;
}
} else if (format.sampleStrippedBytes != null) {
// If the sample has header stripping, prepare to read/output the stripped bytes first.
sampleStrippedBytes.reset(format.sampleStrippedBytes, format.sampleStrippedBytes.length);
}
sampleEncodingHandled = true;
}
size += sampleStrippedBytes.limit();
if (CODEC_ID_H264.equals(format.codecId)) {
// TODO: Deduplicate with Mp4Extractor.
@ -776,28 +796,23 @@ public final class WebmExtractor implements Extractor {
while (sampleBytesRead < size) {
if (sampleCurrentNalBytesRemaining == 0) {
// Read the NAL length so that we know where we find the next one.
input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff,
readToTarget(input, nalLengthData, nalUnitLengthFieldLengthDiff,
nalUnitLengthFieldLength);
nalLength.setPosition(0);
sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
// Write a start code for the current NAL unit.
nalStartCode.setPosition(0);
output.sampleData(nalStartCode, 4);
sampleBytesRead += nalUnitLengthFieldLength;
sampleBytesWritten += 4;
} else {
// Write the payload of the NAL unit.
int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining);
sampleCurrentNalBytesRemaining -= writtenBytes;
sampleBytesRead += writtenBytes;
sampleBytesWritten += writtenBytes;
sampleCurrentNalBytesRemaining -=
readToOutput(input, output, sampleCurrentNalBytesRemaining);
}
}
} else {
while (sampleBytesRead < size) {
int writtenBytes = output.sampleData(input, size - sampleBytesRead);
sampleBytesRead += writtenBytes;
sampleBytesWritten += writtenBytes;
readToOutput(input, output, size - sampleBytesRead);
}
}
@ -814,6 +829,39 @@ public final class WebmExtractor implements Extractor {
}
}
/**
* Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of
* pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}.
*/
private void readToTarget(ExtractorInput input, byte[] target, int offset, int length)
throws IOException, InterruptedException {
int pendingStrippedBytes = Math.min(length, sampleStrippedBytes.bytesLeft());
input.readFully(target, offset + pendingStrippedBytes, length - pendingStrippedBytes);
if (pendingStrippedBytes > 0) {
sampleStrippedBytes.readBytes(target, offset, pendingStrippedBytes);
}
sampleBytesRead += length;
}
/**
* Outputs up to {@code length} bytes of sample data to {@code output}, consisting of either
* {@link #sampleStrippedBytes} or data read from {@code input}.
*/
private int readToOutput(ExtractorInput input, TrackOutput output, int length)
throws IOException, InterruptedException {
int bytesRead;
int strippedBytesLeft = sampleStrippedBytes.bytesLeft();
if (strippedBytesLeft > 0) {
bytesRead = Math.min(length, strippedBytesLeft);
output.sampleData(sampleStrippedBytes, bytesRead);
} else {
bytesRead = output.sampleData(input, length);
}
sampleBytesRead += bytesRead;
sampleBytesWritten += bytesRead;
return bytesRead;
}
/**
* Builds a {@link ChunkIndex} containing recently gathered Cues information.
*
@ -958,6 +1006,7 @@ public final class WebmExtractor implements Extractor {
public int type = UNKNOWN;
public int defaultSampleDurationNs = UNKNOWN;
public boolean hasContentEncryption;
public byte[] sampleStrippedBytes;
public byte[] encryptionKeyId;
public byte[] codecPrivate;

View file

@ -28,7 +28,7 @@ import java.util.List;
*/
/* package */ final class StreamBuilder {
/** Used by {@link #addVp9Track} to create a Track header with Encryption. */
/** Used by {@link #addVp9Track} to create a track header with encryption/compression. */
public static final class ContentEncodingSettings {
private final int order;
@ -36,14 +36,24 @@ import java.util.List;
private final int type;
private final int algorithm;
private final int aesCipherMode;
private final byte[] strippedBytes;
public ContentEncodingSettings(int order, int scope, int type, int algorithm,
int aesCipherMode) {
public ContentEncodingSettings(int order, int scope, int algorithm, int aesCipherMode) {
this.order = order;
this.scope = scope;
this.type = type;
this.type = 1; // Encryption
this.algorithm = algorithm;
this.aesCipherMode = aesCipherMode;
this.strippedBytes = null;
}
public ContentEncodingSettings(int order, int scope, int algorithm, byte[] strippedBytes) {
this.order = order;
this.scope = scope;
this.type = 0; // Compression
this.algorithm = algorithm;
this.aesCipherMode = 0;
this.strippedBytes = strippedBytes;
}
}
@ -225,6 +235,23 @@ import java.util.List;
byte[] heightBytes = getIntegerBytes(pixelHeight);
EbmlElement contentEncodingSettingsElement;
if (contentEncodingSettings != null) {
EbmlElement encryptionOrCompressionElement;
if (contentEncodingSettings.type == 0) {
encryptionOrCompressionElement = element(0x5034, // ContentCompression
element(0x4254, (byte) (contentEncodingSettings.algorithm & 0xFF)), // ContentCompAlgo
element(0x4255, contentEncodingSettings.strippedBytes)); // ContentCompSettings
} else if (contentEncodingSettings.type == 1) {
encryptionOrCompressionElement = element(0x5035, // ContentEncryption
// ContentEncAlgo
element(0x47E1, (byte) (contentEncodingSettings.algorithm & 0xFF)),
element(0x47E2, TEST_ENCRYPTION_KEY_ID), // ContentEncKeyID
element(0x47E7, // ContentEncAESSettings
// AESSettingsCipherMode
element(0x47E8, (byte) (contentEncodingSettings.aesCipherMode & 0xFF))));
} else {
throw new IllegalArgumentException("Unexpected encoding type.");
}
contentEncodingSettingsElement =
element(0x6D80, // ContentEncodings
element(0x6240, // ContentEncoding
@ -234,13 +261,7 @@ import java.util.List;
element(0x5032, (byte) (contentEncodingSettings.scope & 0xFF)),
// ContentEncodingType
element(0x5033, (byte) (contentEncodingSettings.type & 0xFF)),
element(0x5035, // ContentEncryption
// ContentEncAlgo
element(0x47E1, (byte) (contentEncodingSettings.algorithm & 0xFF)),
element(0x47E2, TEST_ENCRYPTION_KEY_ID), // ContentEncKeyID
element(0x47E7, // ContentEncAESSettings
// AESSettingsCipherMode
element(0x47E8, (byte) (contentEncodingSettings.aesCipherMode & 0xFF))))));
encryptionOrCompressionElement));
} else {
contentEncodingSettingsElement = empty();
}

View file

@ -205,7 +205,7 @@ public final class WebmExtractorTest extends InstrumentationTestCase {
}
public void testPrepareContentEncodingEncryption() throws IOException, InterruptedException {
ContentEncodingSettings settings = new StreamBuilder.ContentEncodingSettings(0, 1, 1, 5, 1);
ContentEncodingSettings settings = new StreamBuilder.ContentEncodingSettings(0, 1, 5, 1);
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
@ -305,7 +305,7 @@ public final class WebmExtractorTest extends InstrumentationTestCase {
}
public void testPrepareInvalidContentEncodingOrder() throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(1, 1, 1, 5, 1);
ContentEncodingSettings settings = new ContentEncodingSettings(1, 1, 5, 1);
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
@ -320,7 +320,7 @@ public final class WebmExtractorTest extends InstrumentationTestCase {
}
public void testPrepareInvalidContentEncodingScope() throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 0, 1, 5, 1);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 0, 5, 1);
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
@ -334,8 +334,9 @@ public final class WebmExtractorTest extends InstrumentationTestCase {
}
}
public void testPrepareInvalidContentEncodingType() throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 0, 5, 1);
public void testPrepareInvalidContentCompAlgo()
throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 0, new byte[0]);
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
@ -345,12 +346,12 @@ public final class WebmExtractorTest extends InstrumentationTestCase {
TestUtil.consumeTestData(extractor, data);
fail();
} catch (ParserException exception) {
assertEquals("ContentEncodingType 0 not supported", exception.getMessage());
assertEquals("ContentCompAlgo 0 not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncAlgo() throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 4, 1);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 4, 1);
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
@ -365,7 +366,7 @@ public final class WebmExtractorTest extends InstrumentationTestCase {
}
public void testPrepareInvalidAESSettingsCipherMode() throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 0);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 5, 0);
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
@ -395,6 +396,44 @@ public final class WebmExtractorTest extends InstrumentationTestCase {
assertSample(0, media, 0, true, false, null, getVideoOutput());
}
public void testReadSampleKeyframeStripped() throws IOException, InterruptedException {
byte[] strippedBytes = new byte[] {-1, -1};
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 3, strippedBytes);
byte[] sampleBytes = createFrameData(100);
byte[] unstrippedSampleBytes = TestUtil.joinByteArrays(strippedBytes, sampleBytes);
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
.addVp9Track(TEST_WIDTH, TEST_HEIGHT, settings)
.addSimpleBlockMedia(1 /* trackNumber */, 0 /* clusterTimecode */, 0 /* blockTimecode */,
true /* keyframe */, false /* invisible */, sampleBytes)
.build(1);
TestUtil.consumeTestData(extractor, data);
assertVp9VideoFormat();
assertSample(0, unstrippedSampleBytes, 0, true, false, null, getVideoOutput());
}
public void testReadSampleKeyframeManyBytesStripped() throws IOException, InterruptedException {
byte[] strippedBytes = createFrameData(100);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 3, strippedBytes);
byte[] sampleBytes = createFrameData(5);
byte[] unstrippedSampleBytes = TestUtil.joinByteArrays(strippedBytes, sampleBytes);
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
.addVp9Track(TEST_WIDTH, TEST_HEIGHT, settings)
.addSimpleBlockMedia(1 /* trackNumber */, 0 /* clusterTimecode */, 0 /* blockTimecode */,
true /* keyframe */, false /* invisible */, sampleBytes)
.build(1);
TestUtil.consumeTestData(extractor, data);
assertVp9VideoFormat();
assertSample(0, unstrippedSampleBytes, 0, true, false, null, getVideoOutput());
}
public void testReadTwoTrackSamples() throws IOException, InterruptedException {
byte[] media = createFrameData(100);
byte[] data = new StreamBuilder()
@ -479,7 +518,7 @@ public final class WebmExtractorTest extends InstrumentationTestCase {
public void testReadEncryptedFrame() throws IOException, InterruptedException {
byte[] media = createFrameData(100);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 5, 1);
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
@ -498,7 +537,7 @@ public final class WebmExtractorTest extends InstrumentationTestCase {
public void testReadEncryptedFrameWithInvalidSignalByte()
throws IOException, InterruptedException {
byte[] media = createFrameData(100);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 5, 1);
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)