Commit 8c98c588 by Oliver Woodman

Add support for fixed-size lacing in Matroska streams.

parent 4c4782c7
...@@ -52,9 +52,10 @@ public interface Extractor { ...@@ -52,9 +52,10 @@ public interface Extractor {
/** /**
* Extracts data read from a provided {@link ExtractorInput}. * Extracts data read from a provided {@link ExtractorInput}.
* <p> * <p>
* Each read will extract at most one sample from the stream before returning. * A single call to this method will block until some progress has been made, but will not block
* for longer than this. Hence each call will consume only a small amount of input data.
* <p> * <p>
* In the common case, {@link #RESULT_CONTINUE} is returned to indicate that * In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the
* {@link ExtractorInput} passed to the next read is required to provide data continuing from the * {@link ExtractorInput} passed to the next read is required to provide data continuing from the
* position in the stream reached by the returning call. If the extractor requires data to be * position in the stream reached by the returning call. If the extractor requires data to be
* provided from a different position, then that position is set in {@code seekPosition} and * provided from a different position, then that position is set in {@code seekPosition} and
......
...@@ -52,9 +52,9 @@ import java.util.concurrent.TimeUnit; ...@@ -52,9 +52,9 @@ import java.util.concurrent.TimeUnit;
*/ */
public final class WebmExtractor implements Extractor { public final class WebmExtractor implements Extractor {
private static final int SAMPLE_STATE_START = 0; private static final int BLOCK_STATE_START = 0;
private static final int SAMPLE_STATE_HEADER = 1; private static final int BLOCK_STATE_HEADER = 1;
private static final int SAMPLE_STATE_DATA = 2; private static final int BLOCK_STATE_DATA = 2;
private static final int CUES_STATE_NOT_BUILT = 0; private static final int CUES_STATE_NOT_BUILT = 0;
private static final int CUES_STATE_BUILDING = 1; private static final int CUES_STATE_BUILDING = 1;
...@@ -98,6 +98,7 @@ public final class WebmExtractor implements Extractor { ...@@ -98,6 +98,7 @@ public final class WebmExtractor implements Extractor {
private static final int ID_TRACK_ENTRY = 0xAE; private static final int ID_TRACK_ENTRY = 0xAE;
private static final int ID_TRACK_NUMBER = 0xD7; private static final int ID_TRACK_NUMBER = 0xD7;
private static final int ID_TRACK_TYPE = 0x83; private static final int ID_TRACK_TYPE = 0x83;
private static final int ID_DEFAULT_DURATION = 0x23E383;
private static final int ID_CODEC_ID = 0x86; private static final int ID_CODEC_ID = 0x86;
private static final int ID_CODEC_PRIVATE = 0x63A2; private static final int ID_CODEC_PRIVATE = 0x63A2;
private static final int ID_CODEC_DELAY = 0x56AA; private static final int ID_CODEC_DELAY = 0x56AA;
...@@ -125,6 +126,7 @@ public final class WebmExtractor implements Extractor { ...@@ -125,6 +126,7 @@ public final class WebmExtractor implements Extractor {
private static final int ID_CUE_CLUSTER_POSITION = 0xF1; private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
private static final int LACING_NONE = 0; private static final int LACING_NONE = 0;
private static final int LACING_FIXED_SIZE = 2;
private final EbmlReader reader; private final EbmlReader reader;
private final VarintReader varintReader; private final VarintReader varintReader;
...@@ -132,7 +134,7 @@ public final class WebmExtractor implements Extractor { ...@@ -132,7 +134,7 @@ public final class WebmExtractor implements Extractor {
// Temporary arrays. // Temporary arrays.
private final ParsableByteArray nalStartCode; private final ParsableByteArray nalStartCode;
private final ParsableByteArray nalLength; private final ParsableByteArray nalLength;
private final ParsableByteArray sampleHeaderScratch; private final ParsableByteArray scratch;
private final ParsableByteArray vorbisNumPageSamples; private final ParsableByteArray vorbisNumPageSamples;
private final ParsableByteArray seekEntryIdBytes; private final ParsableByteArray seekEntryIdBytes;
...@@ -161,15 +163,22 @@ public final class WebmExtractor implements Extractor { ...@@ -161,15 +163,22 @@ public final class WebmExtractor implements Extractor {
private LongArray cueClusterPositions; private LongArray cueClusterPositions;
private boolean seenClusterPositionForCurrentCuePoint; private boolean seenClusterPositionForCurrentCuePoint;
// Block reading state.
private int blockState;
private long blockTimeUs;
private int blockLacingSampleIndex;
private int blockLacingSampleCount;
private int blockLacingSampleSize;
private int blockTrackNumber;
private int blockTrackNumberLength;
private int blockFlags;
private byte[] blockEncryptionKeyId;
// Sample reading state. // Sample reading state.
private int blockBytesRead; private int sampleBytesRead;
private int sampleState; private boolean sampleEncryptionDataRead;
private int sampleSize;
private int sampleCurrentNalBytesRemaining; private int sampleCurrentNalBytesRemaining;
private int sampleTrackNumber; private int sampleBytesWritten;
private int sampleFlags;
private byte[] sampleEncryptionKeyId;
private long sampleTimeUs;
private boolean sampleRead; private boolean sampleRead;
private boolean sampleSeenReferenceBlock; private boolean sampleSeenReferenceBlock;
...@@ -184,7 +193,7 @@ public final class WebmExtractor implements Extractor { ...@@ -184,7 +193,7 @@ public final class WebmExtractor implements Extractor {
this.reader = reader; this.reader = reader;
this.reader.init(new InnerEbmlReaderOutput()); this.reader.init(new InnerEbmlReaderOutput());
varintReader = new VarintReader(); varintReader = new VarintReader();
sampleHeaderScratch = new ParsableByteArray(4); scratch = new ParsableByteArray(4);
vorbisNumPageSamples = new ParsableByteArray(ByteBuffer.allocate(4).putInt(-1).array()); vorbisNumPageSamples = new ParsableByteArray(ByteBuffer.allocate(4).putInt(-1).array());
seekEntryIdBytes = new ParsableByteArray(4); seekEntryIdBytes = new ParsableByteArray(4);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
...@@ -199,10 +208,13 @@ public final class WebmExtractor implements Extractor { ...@@ -199,10 +208,13 @@ public final class WebmExtractor implements Extractor {
@Override @Override
public void seek() { public void seek() {
clusterTimecodeUs = UNKNOWN; clusterTimecodeUs = UNKNOWN;
sampleState = SAMPLE_STATE_START; blockState = BLOCK_STATE_START;
reader.reset(); reader.reset();
varintReader.reset(); varintReader.reset();
sampleCurrentNalBytesRemaining = 0; sampleCurrentNalBytesRemaining = 0;
sampleBytesRead = 0;
sampleBytesWritten = 0;
sampleEncryptionDataRead = false;
} }
@Override @Override
...@@ -249,6 +261,7 @@ public final class WebmExtractor implements Extractor { ...@@ -249,6 +261,7 @@ public final class WebmExtractor implements Extractor {
case ID_PIXEL_HEIGHT: case ID_PIXEL_HEIGHT:
case ID_TRACK_NUMBER: case ID_TRACK_NUMBER:
case ID_TRACK_TYPE: case ID_TRACK_TYPE:
case ID_DEFAULT_DURATION:
case ID_CODEC_DELAY: case ID_CODEC_DELAY:
case ID_SEEK_PRE_ROLL: case ID_SEEK_PRE_ROLL:
case ID_CHANNELS: case ID_CHANNELS:
...@@ -342,17 +355,18 @@ public final class WebmExtractor implements Extractor { ...@@ -342,17 +355,18 @@ public final class WebmExtractor implements Extractor {
} }
return; return;
case ID_BLOCK_GROUP: case ID_BLOCK_GROUP:
if (sampleState != SAMPLE_STATE_DATA) { if (blockState != BLOCK_STATE_DATA) {
// We've skipped this sample (due to incompatible track number). // We've skipped this block (due to incompatible track number).
return; return;
} }
// If the ReferenceBlock element was not found for this sample, then it is a keyframe. // If the ReferenceBlock element was not found for this sample, then it is a keyframe.
if (!sampleSeenReferenceBlock) { if (!sampleSeenReferenceBlock) {
sampleFlags |= C.SAMPLE_FLAG_SYNC; blockFlags |= C.SAMPLE_FLAG_SYNC;
} }
outputSampleMetadata( outputSampleMetadata(
(audioTrackFormat != null && sampleTrackNumber == audioTrackFormat.number) (audioTrackFormat != null && blockTrackNumber == audioTrackFormat.number)
? audioTrackFormat.trackOutput : videoTrackFormat.trackOutput); ? audioTrackFormat.trackOutput : videoTrackFormat.trackOutput, blockTimeUs);
blockState = BLOCK_STATE_START;
return; return;
case ID_CONTENT_ENCODING: case ID_CONTENT_ENCODING:
if (!trackFormat.hasContentEncryption) { if (!trackFormat.hasContentEncryption) {
...@@ -436,6 +450,9 @@ public final class WebmExtractor implements Extractor { ...@@ -436,6 +450,9 @@ public final class WebmExtractor implements Extractor {
case ID_TRACK_TYPE: case ID_TRACK_TYPE:
trackFormat.type = (int) value; trackFormat.type = (int) value;
return; return;
case ID_DEFAULT_DURATION:
trackFormat.defaultSampleDurationNs = (int) value;
break;
case ID_CODEC_DELAY: case ID_CODEC_DELAY:
trackFormat.codecDelayNs = value; trackFormat.codecDelayNs = value;
return; return;
...@@ -552,142 +569,182 @@ public final class WebmExtractor implements Extractor { ...@@ -552,142 +569,182 @@ public final class WebmExtractor implements Extractor {
// for info about how data is organized in SimpleBlock and Block elements respectively. They // for info about how data is organized in SimpleBlock and Block elements respectively. They
// differ only in the way flags are specified. // differ only in the way flags are specified.
if (sampleState == SAMPLE_STATE_START) { if (blockState == BLOCK_STATE_START) {
sampleTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true); blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true);
blockBytesRead = varintReader.getLastLength(); blockTrackNumberLength = varintReader.getLastLength();
sampleState = SAMPLE_STATE_HEADER; blockState = BLOCK_STATE_HEADER;
scratch.reset();
} }
// Ignore the frame if the track number equals neither the audio track nor the video track. // Ignore the block if the track number equals neither the audio track nor the video track.
if ((audioTrackFormat != null && videoTrackFormat != null if ((audioTrackFormat != null && videoTrackFormat != null
&& audioTrackFormat.number != sampleTrackNumber && audioTrackFormat.number != blockTrackNumber
&& videoTrackFormat.number != sampleTrackNumber) && videoTrackFormat.number != blockTrackNumber)
|| (audioTrackFormat != null && videoTrackFormat == null || (audioTrackFormat != null && videoTrackFormat == null
&& audioTrackFormat.number != sampleTrackNumber) && audioTrackFormat.number != blockTrackNumber)
|| (audioTrackFormat == null && videoTrackFormat != null || (audioTrackFormat == null && videoTrackFormat != null
&& videoTrackFormat.number != sampleTrackNumber)) { && videoTrackFormat.number != blockTrackNumber)) {
input.skipFully(contentSize - blockBytesRead); input.skipFully(contentSize - blockTrackNumberLength);
sampleState = SAMPLE_STATE_START; blockState = BLOCK_STATE_START;
return; return;
} }
TrackFormat sampleTrackFormat = TrackFormat sampleTrackFormat =
(audioTrackFormat != null && sampleTrackNumber == audioTrackFormat.number) (audioTrackFormat != null && blockTrackNumber == audioTrackFormat.number)
? audioTrackFormat : videoTrackFormat; ? audioTrackFormat : videoTrackFormat;
TrackOutput trackOutput = sampleTrackFormat.trackOutput; TrackOutput trackOutput = sampleTrackFormat.trackOutput;
if (sampleState == SAMPLE_STATE_HEADER) { if (blockState == BLOCK_STATE_HEADER) {
byte[] sampleHeaderScratchData = sampleHeaderScratch.data; // Read the relative timecode (2 bytes) and flags (1 byte).
// Next 3 bytes have timecode and flags. If encrypted, the 4th byte is a signal byte. readScratch(input, 3);
int remainingHeaderLength = sampleTrackFormat.hasContentEncryption ? 4 : 3; int lacing = (scratch.data[2] & 0x06) >> 1;
input.readFully(sampleHeaderScratchData, 0, remainingHeaderLength); if (lacing == LACING_NONE) {
blockBytesRead += remainingHeaderLength; blockLacingSampleCount = 1;
blockLacingSampleSize = contentSize - blockTrackNumberLength - 3;
// First two bytes are the relative timecode. } else if (lacing == LACING_FIXED_SIZE) {
int timecode = (sampleHeaderScratchData[0] << 8) if (id != ID_SIMPLE_BLOCK) {
| (sampleHeaderScratchData[1] & 0xFF); throw new ParserException("Lacing only supported in SimpleBlocks.");
sampleTimeUs = clusterTimecodeUs + scaleTimecodeToUs(timecode);
// Third byte contains the lacing value and some flags.
int lacing = (sampleHeaderScratchData[2] & 0x06) >> 1;
if (lacing != LACING_NONE) {
throw new ParserException("Lacing mode not supported: " + lacing);
}
boolean isInvisible = (sampleHeaderScratchData[2] & 0x08) == 0x08;
boolean isKeyframe =
(id == ID_SIMPLE_BLOCK && (sampleHeaderScratchData[2] & 0x80) == 0x80);
boolean isEncrypted = false;
// If encrypted, the fourth byte is an encryption signal byte.
if (sampleTrackFormat.hasContentEncryption) {
if ((sampleHeaderScratchData[3] & 0x80) == 0x80) {
throw new ParserException("Extension bit is set in signal byte");
} }
isEncrypted = (sampleHeaderScratchData[3] & 0x01) == 0x01;
}
sampleFlags = (isKeyframe ? C.SAMPLE_FLAG_SYNC : 0) // Read the sample count (1 byte).
| (isInvisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0) readScratch(input, 4);
| (isEncrypted ? C.SAMPLE_FLAG_ENCRYPTED : 0); blockLacingSampleCount = (scratch.data[3] & 0xFF) + 1;
sampleEncryptionKeyId = sampleTrackFormat.encryptionKeyId; blockLacingSampleSize =
sampleSize = contentSize - blockBytesRead; (contentSize - blockTrackNumberLength - 4) / blockLacingSampleCount;
if (isEncrypted) { } else {
// Write the vector size. throw new ParserException("Lacing mode not supported: " + lacing);
sampleHeaderScratch.data[0] = (byte) ENCRYPTION_IV_SIZE;
sampleHeaderScratch.setPosition(0);
trackOutput.sampleData(sampleHeaderScratch, 1);
sampleSize++;
}
sampleState = SAMPLE_STATE_DATA;
}
if (CODEC_ID_H264.equals(sampleTrackFormat.codecId)) {
// TODO: Deduplicate with Mp4Extractor.
// Zero the top three bytes of the array that we'll use to parse nal unit lengths, in case
// they're only 1 or 2 bytes long.
byte[] nalLengthData = nalLength.data;
nalLengthData[0] = 0;
nalLengthData[1] = 0;
nalLengthData[2] = 0;
int nalUnitLengthFieldLength = sampleTrackFormat.nalUnitLengthFieldLength;
int nalUnitLengthFieldLengthDiff = 4 - sampleTrackFormat.nalUnitLengthFieldLength;
// NAL units are length delimited, but the decoder requires start code delimited units.
// Loop until we've written the sample to the track output, replacing length delimiters
// with start codes as we encounter them.
while (blockBytesRead < contentSize) {
if (sampleCurrentNalBytesRemaining == 0) {
// Read the NAL length so that we know where we find the next one.
input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff,
nalUnitLengthFieldLength);
blockBytesRead += nalUnitLengthFieldLength;
nalLength.setPosition(0);
sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
// Write a start code for the current NAL unit.
nalStartCode.setPosition(0);
trackOutput.sampleData(nalStartCode, 4);
sampleSize += nalUnitLengthFieldLengthDiff;
} else {
// Write the payload of the NAL unit.
int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining);
blockBytesRead += writtenBytes;
sampleCurrentNalBytesRemaining -= writtenBytes;
}
}
} else {
while (blockBytesRead < contentSize) {
blockBytesRead += trackOutput.sampleData(input, contentSize - blockBytesRead);
} }
}
if (CODEC_ID_VORBIS.equals(sampleTrackFormat.codecId)) { int timecode = (scratch.data[0] << 8) | (scratch.data[1] & 0xFF);
// Vorbis decoder in android MediaCodec [1] expects the last 4 bytes of the sample to be blockTimeUs = clusterTimecodeUs + scaleTimecodeToUs(timecode);
// the number of samples in the current page. This definition holds good only for Ogg and boolean isInvisible = (scratch.data[2] & 0x08) == 0x08;
// irrelevant for WebM. So we always set this to -1 (the decoder will ignore this value if boolean isKeyframe = (id == ID_SIMPLE_BLOCK && (scratch.data[2] & 0x80) == 0x80);
// we set it to -1). The android platform media extractor [2] does the same. blockFlags = (isKeyframe ? C.SAMPLE_FLAG_SYNC : 0)
// [1] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/codecs/vorbis/dec/SoftVorbis.cpp#314 | (isInvisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0);
// [2] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/NuMediaExtractor.cpp#474 blockEncryptionKeyId = sampleTrackFormat.encryptionKeyId;
vorbisNumPageSamples.setPosition(0); blockState = BLOCK_STATE_DATA;
trackOutput.sampleData(vorbisNumPageSamples, 4); blockLacingSampleIndex = 0;
sampleSize += 4;
} }
// For SimpleBlock, we send the metadata here as we have all the information. For Block, we
// send the metadata at the end of the BlockGroup element since we'll know if the frame is a
// keyframe or not only at that point.
if (id == ID_SIMPLE_BLOCK) { if (id == ID_SIMPLE_BLOCK) {
outputSampleMetadata(trackOutput); // For SimpleBlock, we have metadata for each sample here.
while (blockLacingSampleIndex < blockLacingSampleCount) {
writeSampleData(input, trackOutput, sampleTrackFormat, blockLacingSampleSize);
long sampleTimeUs = this.blockTimeUs
+ (blockLacingSampleIndex * sampleTrackFormat.defaultSampleDurationNs) / 1000;
outputSampleMetadata(trackOutput, sampleTimeUs);
blockLacingSampleIndex++;
}
blockState = BLOCK_STATE_START;
} else {
// For Block, we send the metadata at the end of the BlockGroup element since we'll know
// if the sample is a keyframe or not only at that point.
writeSampleData(input, trackOutput, sampleTrackFormat, blockLacingSampleSize);
} }
return; return;
default: default:
throw new IllegalStateException("Unexpected id: " + id); throw new ParserException("Unexpected id: " + id);
} }
} }
private void outputSampleMetadata(TrackOutput trackOutput) { private void outputSampleMetadata(TrackOutput trackOutput, long timeUs) {
trackOutput.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, sampleEncryptionKeyId); trackOutput.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, blockEncryptionKeyId);
sampleState = SAMPLE_STATE_START;
sampleRead = true; sampleRead = true;
sampleBytesRead = 0;
sampleBytesWritten = 0;
sampleEncryptionDataRead = false;
}
/**
* Ensures {@link #scratch} contains at least {@code requiredLength} bytes of data, reading from
* the extractor input if necessary.
*/
private void readScratch(ExtractorInput input, int requiredLength)
throws IOException, InterruptedException {
if (scratch.limit() >= requiredLength) {
return;
}
input.readFully(scratch.data, scratch.limit(), requiredLength - scratch.limit());
scratch.setLimit(requiredLength);
}
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 (CODEC_ID_H264.equals(format.codecId)) {
// TODO: Deduplicate with Mp4Extractor.
// Zero the top three bytes of the array that we'll use to parse nal unit lengths, in case
// they're only 1 or 2 bytes long.
byte[] nalLengthData = nalLength.data;
nalLengthData[0] = 0;
nalLengthData[1] = 0;
nalLengthData[2] = 0;
int nalUnitLengthFieldLength = format.nalUnitLengthFieldLength;
int nalUnitLengthFieldLengthDiff = 4 - format.nalUnitLengthFieldLength;
// NAL units are length delimited, but the decoder requires start code delimited units.
// Loop until we've written the sample to the track output, replacing length delimiters with
// start codes as we encounter them.
while (sampleBytesRead < size) {
if (sampleCurrentNalBytesRemaining == 0) {
// Read the NAL length so that we know where we find the next one.
input.readFully(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;
}
}
} else {
while (sampleBytesRead < size) {
int writtenBytes = output.sampleData(input, size - sampleBytesRead);
sampleBytesRead += writtenBytes;
sampleBytesWritten += writtenBytes;
}
}
if (CODEC_ID_VORBIS.equals(format.codecId)) {
// Vorbis decoder in android MediaCodec [1] expects the last 4 bytes of the sample to be the
// number of samples in the current page. This definition holds good only for Ogg and
// irrelevant for WebM. So we always set this to -1 (the decoder will ignore this value if we
// set it to -1). The android platform media extractor [2] does the same.
// [1] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/codecs/vorbis/dec/SoftVorbis.cpp#314
// [2] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/NuMediaExtractor.cpp#474
vorbisNumPageSamples.setPosition(0);
output.sampleData(vorbisNumPageSamples, 4);
sampleBytesWritten += 4;
}
} }
/** /**
...@@ -817,6 +874,7 @@ public final class WebmExtractor implements Extractor { ...@@ -817,6 +874,7 @@ public final class WebmExtractor implements Extractor {
public String codecId; public String codecId;
public int number = UNKNOWN; public int number = UNKNOWN;
public int type = UNKNOWN; public int type = UNKNOWN;
public int defaultSampleDurationNs = UNKNOWN;
public boolean hasContentEncryption; public boolean hasContentEncryption;
public byte[] encryptionKeyId; public byte[] encryptionKeyId;
public byte[] codecPrivate; public byte[] codecPrivate;
......
...@@ -47,6 +47,14 @@ import java.util.List; ...@@ -47,6 +47,14 @@ import java.util.List;
} }
public static byte[] createByteArray(int... intArray) {
byte[] byteArray = new byte[intArray.length];
for (int i = 0; i < byteArray.length; i++) {
byteArray[i] = (byte) intArray[i];
}
return byteArray;
}
public static byte[] joinByteArrays(byte[]... byteArrays) { public static byte[] joinByteArrays(byte[]... byteArrays) {
int length = 0; int length = 0;
for (byte[] byteArray : byteArrays) { for (byte[] byteArray : byteArrays) {
...@@ -94,16 +102,28 @@ import java.util.List; ...@@ -94,16 +102,28 @@ import java.util.List;
return this; return this;
} }
public StreamBuilder addH264Track(int width, int height, byte[] codecPrivate) {
trackEntries.add(createVideoTrackEntry("V_MPEG4/ISO/AVC", width, height, null, codecPrivate));
return this;
}
public StreamBuilder addOpusTrack(int channelCount, int sampleRate, int codecDelay, public StreamBuilder addOpusTrack(int channelCount, int sampleRate, int codecDelay,
int seekPreRoll, byte[] codecPrivate) { int seekPreRoll, byte[] codecPrivate) {
trackEntries.add(createAudioTrackEntry("A_OPUS", channelCount, sampleRate, codecPrivate, trackEntries.add(createAudioTrackEntry("A_OPUS", channelCount, sampleRate, codecPrivate,
codecDelay, seekPreRoll)); codecDelay, seekPreRoll, NO_VALUE));
return this;
}
public StreamBuilder addOpusTrack(int channelCount, int sampleRate, int codecDelay,
int seekPreRoll, byte[] codecPrivate, int defaultDurationNs) {
trackEntries.add(createAudioTrackEntry("A_OPUS", channelCount, sampleRate, codecPrivate,
codecDelay, seekPreRoll, defaultDurationNs));
return this; return this;
} }
public StreamBuilder addVorbisTrack(int channelCount, int sampleRate, byte[] codecPrivate) { public StreamBuilder addVorbisTrack(int channelCount, int sampleRate, byte[] codecPrivate) {
trackEntries.add(createAudioTrackEntry("A_VORBIS", channelCount, sampleRate, codecPrivate, trackEntries.add(createAudioTrackEntry("A_VORBIS", channelCount, sampleRate, codecPrivate,
NO_VALUE, NO_VALUE)); NO_VALUE, NO_VALUE, NO_VALUE));
return this; return this;
} }
...@@ -120,7 +140,7 @@ import java.util.List; ...@@ -120,7 +140,7 @@ import java.util.List;
byte[] data) { byte[] data) {
byte flags = (byte) ((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00)); byte flags = (byte) ((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00));
EbmlElement simpleBlockElement = createSimpleBlock(trackNumber, blockTimecode, flags, EbmlElement simpleBlockElement = createSimpleBlock(trackNumber, blockTimecode, flags,
true, validSignalByte, data); true, validSignalByte, 1, data);
mediaSegments.add(createCluster(clusterTimecode, simpleBlockElement)); mediaSegments.add(createCluster(clusterTimecode, simpleBlockElement));
return this; return this;
} }
...@@ -130,7 +150,15 @@ import java.util.List; ...@@ -130,7 +150,15 @@ import java.util.List;
int blockTimecode, boolean keyframe, boolean invisible, byte[] data) { int blockTimecode, boolean keyframe, boolean invisible, byte[] data) {
byte flags = (byte) ((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00)); byte flags = (byte) ((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00));
EbmlElement simpleBlockElement = createSimpleBlock(trackNumber, blockTimecode, flags, EbmlElement simpleBlockElement = createSimpleBlock(trackNumber, blockTimecode, flags,
false, true, data); false, true, 1, data);
mediaSegments.add(createCluster(clusterTimecode, simpleBlockElement));
return this;
}
public StreamBuilder addSimpleBlockMediaWithFixedSizeLacing(int trackNumber, int clusterTimecode,
int blockTimecode, int lacingFrameCount, byte[] data) {
EbmlElement simpleBlockElement = createSimpleBlock(trackNumber, blockTimecode,
0x80 /* flags = keyframe */, false, true, lacingFrameCount, data);
mediaSegments.add(createCluster(clusterTimecode, simpleBlockElement)); mediaSegments.add(createCluster(clusterTimecode, simpleBlockElement));
return this; return this;
} }
...@@ -248,7 +276,7 @@ import java.util.List; ...@@ -248,7 +276,7 @@ import java.util.List;
} }
private static EbmlElement createAudioTrackEntry(String codecId, int channelCount, int sampleRate, private static EbmlElement createAudioTrackEntry(String codecId, int channelCount, int sampleRate,
byte[] codecPrivate, int codecDelay, int seekPreRoll) { byte[] codecPrivate, int codecDelay, int seekPreRoll, int defaultDurationNs) {
byte channelCountByte = (byte) (channelCount & 0xFF); byte channelCountByte = (byte) (channelCount & 0xFF);
byte[] sampleRateDoubleBytes = getLongBytes(Double.doubleToLongBits(sampleRate)); byte[] sampleRateDoubleBytes = getLongBytes(Double.doubleToLongBits(sampleRate));
return element(0xAE, // TrackEntry return element(0xAE, // TrackEntry
...@@ -262,6 +290,9 @@ import java.util.List; ...@@ -262,6 +290,9 @@ import java.util.List;
element(0xE1, // Audio element(0xE1, // Audio
element(0x9F, channelCountByte), // Channels element(0x9F, channelCountByte), // Channels
element(0xB5, sampleRateDoubleBytes)), // SamplingFrequency element(0xB5, sampleRateDoubleBytes)), // SamplingFrequency
// DefaultDuration
defaultDurationNs != NO_VALUE ? element(0x23E383, getIntegerBytes(defaultDurationNs))
: empty(),
element(0x63A2, codecPrivate)); // CodecPrivate element(0x63A2, codecPrivate)); // CodecPrivate
} }
...@@ -272,12 +303,20 @@ import java.util.List; ...@@ -272,12 +303,20 @@ import java.util.List;
} }
private static EbmlElement createSimpleBlock(int trackNumber, int timecode, int flags, private static EbmlElement createSimpleBlock(int trackNumber, int timecode, int flags,
boolean encrypted, boolean validSignalByte, byte[] data) { boolean encrypted, boolean validSignalByte, int lacingFrameCount, byte[] data) {
byte[] trackNumberBytes = getIntegerBytes(trackNumber); byte[] trackNumberBytes = getIntegerBytes(trackNumber);
byte[] timeBytes = getIntegerBytes(timecode); byte[] timeBytes = getIntegerBytes(timecode);
byte[] simpleBlockBytes = createByteArray( byte[] simpleBlockBytes;
if (lacingFrameCount > 1) {
flags |= 0x04; // Fixed-size lacing
simpleBlockBytes = createByteArray(
0x40, trackNumberBytes[3], // Track number size=2
timeBytes[2], timeBytes[3], flags, lacingFrameCount - 1); // Timecode, flags and lacing.
} else {
simpleBlockBytes = createByteArray(
0x40, trackNumberBytes[3], // Track number size=2 0x40, trackNumberBytes[3], // Track number size=2
timeBytes[2], timeBytes[3], flags); // Timecode and flags timeBytes[2], timeBytes[3], flags); // Timecode and flags
}
if (encrypted) { if (encrypted) {
simpleBlockBytes = joinByteArrays( simpleBlockBytes = joinByteArrays(
simpleBlockBytes, createByteArray(validSignalByte ? 0x01 : 0x80), simpleBlockBytes, createByteArray(validSignalByte ? 0x01 : 0x80),
...@@ -302,14 +341,6 @@ import java.util.List; ...@@ -302,14 +341,6 @@ import java.util.List;
block); block);
} }
private static byte[] createByteArray(int... intArray) {
byte[] byteArray = new byte[intArray.length];
for (int i = 0; i < byteArray.length; i++) {
byteArray[i] = (byte) intArray[i];
}
return byteArray;
}
private static byte[] getIntegerBytes(int value) { private static byte[] getIntegerBytes(int value) {
return createByteArray( return createByteArray(
(value & 0xFF000000) >> 24, (value & 0xFF000000) >> 24,
......
...@@ -37,9 +37,13 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -37,9 +37,13 @@ import com.google.android.exoplayer.util.ParsableByteArray;
import android.net.Uri; import android.net.Uri;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID; import java.util.UUID;
/** /**
...@@ -59,9 +63,16 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -59,9 +63,16 @@ public class WebmExtractorTest extends InstrumentationTestCase {
private static final int TEST_VORBIS_INFO_SIZE = 30; private static final int TEST_VORBIS_INFO_SIZE = 30;
private static final int TEST_VORBIS_BOOKS_SIZE = 4140; private static final int TEST_VORBIS_BOOKS_SIZE = 4140;
private static final byte[] TEST_OPUS_CODEC_PRIVATE = new byte[] {0, 0}; private static final byte[] TEST_OPUS_CODEC_PRIVATE = new byte[] {0, 0};
private static final int TEST_DEFAULT_DURATION_NS = 33 * 1000 * 1000;
private static final byte[] TEST_H264_CODEC_PRIVATE = StreamBuilder.createByteArray(0x01, 0x4D,
0x40, 0x1E, 0xFF, 0xE1, 0x00, 0x17, 0x67, 0x4D, 0x40, 0x1E, 0xE8, 0x80, 0x50, 0x17, 0xFC,
0xB8, 0x08, 0x80, 0x00, 0x01, 0xF4, 0x80, 0x00, 0x75, 0x30, 0x07, 0x8B, 0x16, 0x89, 0x01,
0x00, 0x04, 0x68, 0xEB, 0xEF, 0x20);
private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
private static final UUID ZERO_UUID = new UUID(0, 0); private static final UUID ZERO_UUID = new UUID(0, 0);
private static final String WEBM_DOC_TYPE = "webm"; private static final String WEBM_DOC_TYPE = "webm";
private static final String MATROSKA_DOC_TYPE = "matroska";
private WebmExtractor extractor; private WebmExtractor extractor;
private TestExtractorOutput extractorOutput; private TestExtractorOutput extractorOutput;
...@@ -125,6 +136,19 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -125,6 +136,19 @@ public class WebmExtractorTest extends InstrumentationTestCase {
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US)); assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
} }
public void testPrepareH264() throws IOException, InterruptedException {
byte[] data = new StreamBuilder()
.setHeader(MATROSKA_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
.addH264Track(TEST_WIDTH, TEST_HEIGHT, TEST_H264_CODEC_PRIVATE)
.build(1);
consume(data);
assertH264VideoFormat();
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
}
public void testPrepareTwoTracks() throws IOException, InterruptedException { public void testPrepareTwoTracks() throws IOException, InterruptedException {
byte[] data = new StreamBuilder() byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE) .setHeader(WEBM_DOC_TYPE)
...@@ -257,6 +281,17 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -257,6 +281,17 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data); consume(data);
} }
public void testAcceptsMatroskaDocType() throws IOException, InterruptedException {
byte[] data = new StreamBuilder()
.setHeader(MATROSKA_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
.addVp9Track(TEST_WIDTH, TEST_HEIGHT, null)
.build(1);
// No exception is thrown.
consume(data);
}
public void testPrepareInvalidDocType() throws IOException, InterruptedException { public void testPrepareInvalidDocType() throws IOException, InterruptedException {
byte[] data = new StreamBuilder() byte[] data = new StreamBuilder()
.setHeader("webB") .setHeader("webB")
...@@ -359,7 +394,7 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -359,7 +394,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data); consume(data);
assertVp9VideoFormat(); assertVp9VideoFormat();
assertSample(media, 0, true, false, false, videoOutput); assertSample(media, 0, true, false, null, videoOutput);
} }
public void testReadTwoTrackSamples() throws IOException, InterruptedException { public void testReadTwoTrackSamples() throws IOException, InterruptedException {
...@@ -381,8 +416,8 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -381,8 +416,8 @@ public class WebmExtractorTest extends InstrumentationTestCase {
assertEquals(2, extractorOutput.numberOfTracks); assertEquals(2, extractorOutput.numberOfTracks);
assertVp9VideoFormat(); assertVp9VideoFormat();
assertAudioFormat(MimeTypes.AUDIO_OPUS); assertAudioFormat(MimeTypes.AUDIO_OPUS);
assertSample(media, 0, true, false, false, videoOutput); assertSample(media, 0, true, false, null, videoOutput);
assertSample(media, 0, true, false, false, audioOutput); assertSample(media, 0, true, false, null, audioOutput);
} }
public void testReadTwoTrackSamplesWithSkippedTrack() throws IOException, InterruptedException { public void testReadTwoTrackSamplesWithSkippedTrack() throws IOException, InterruptedException {
...@@ -407,8 +442,8 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -407,8 +442,8 @@ public class WebmExtractorTest extends InstrumentationTestCase {
assertEquals(2, extractorOutput.numberOfTracks); assertEquals(2, extractorOutput.numberOfTracks);
assertVp9VideoFormat(); assertVp9VideoFormat();
assertAudioFormat(MimeTypes.AUDIO_OPUS); assertAudioFormat(MimeTypes.AUDIO_OPUS);
assertSample(media, 0, true, false, false, videoOutput); assertSample(media, 0, true, false, null, videoOutput);
assertSample(media, 0, true, false, false, audioOutput); assertSample(media, 0, true, false, null, audioOutput);
} }
public void testReadBlock() throws IOException, InterruptedException { public void testReadBlock() throws IOException, InterruptedException {
...@@ -425,7 +460,7 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -425,7 +460,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data); consume(data);
assertAudioFormat(MimeTypes.AUDIO_OPUS); assertAudioFormat(MimeTypes.AUDIO_OPUS);
assertSample(media, 0, true, false, false, audioOutput); assertSample(media, 0, true, false, null, audioOutput);
} }
public void testReadBlockNonKeyframe() throws IOException, InterruptedException { public void testReadBlockNonKeyframe() throws IOException, InterruptedException {
...@@ -441,7 +476,7 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -441,7 +476,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data); consume(data);
assertVp9VideoFormat(); assertVp9VideoFormat();
assertSample(media, 0, false, false, false, videoOutput); assertSample(media, 0, false, false, null, videoOutput);
} }
public void testReadEncryptedFrame() throws IOException, InterruptedException { public void testReadEncryptedFrame() throws IOException, InterruptedException {
...@@ -459,7 +494,7 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -459,7 +494,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data); consume(data);
assertVp9VideoFormat(); assertVp9VideoFormat();
assertSample(media, 0, true, false, true, videoOutput); assertSample(media, 0, true, false, TEST_ENCRYPTION_KEY_ID, videoOutput);
} }
public void testReadEncryptedFrameWithInvalidSignalByte() public void testReadEncryptedFrameWithInvalidSignalByte()
...@@ -496,7 +531,7 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -496,7 +531,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data); consume(data);
assertVp9VideoFormat(); assertVp9VideoFormat();
assertSample(media, 25000, false, true, false, videoOutput); assertSample(media, 25000, false, true, null, videoOutput);
} }
public void testReadSampleCustomTimescale() throws IOException, InterruptedException { public void testReadSampleCustomTimescale() throws IOException, InterruptedException {
...@@ -512,7 +547,7 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -512,7 +547,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data); consume(data);
assertVp9VideoFormat(); assertVp9VideoFormat();
assertSample(media, 25, false, false, false, videoOutput); assertSample(media, 25, false, false, null, videoOutput);
} }
public void testReadSampleNegativeSimpleBlockTimecode() throws IOException, InterruptedException { public void testReadSampleNegativeSimpleBlockTimecode() throws IOException, InterruptedException {
...@@ -528,7 +563,28 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -528,7 +563,28 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data); consume(data);
assertVp9VideoFormat(); assertVp9VideoFormat();
assertSample(media, 1000, true, true, false, videoOutput); assertSample(media, 1000, true, true, null, videoOutput);
}
public void testReadSampleWithLacing() throws IOException, InterruptedException {
byte[] media = createFrameData(100);
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
.setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US)
.addOpusTrack(TEST_CHANNEL_COUNT, TEST_SAMPLE_RATE, TEST_CODEC_DELAY, TEST_SEEK_PRE_ROLL,
TEST_OPUS_CODEC_PRIVATE, TEST_DEFAULT_DURATION_NS)
.addSimpleBlockMediaWithFixedSizeLacing(2 /* trackNumber */, 0 /* clusterTimecode */,
0 /* blockTimecode */, 20, media)
.build(1);
consume(data);
assertAudioFormat(MimeTypes.AUDIO_OPUS);
for (int i = 0; i < 20; i++) {
long expectedTimeUs = i * TEST_DEFAULT_DURATION_NS / 1000;
assertSample(Arrays.copyOfRange(media, i * 5, i * 5 + 5), expectedTimeUs, true, false, null,
audioOutput);
}
} }
private void consume(byte[] data) throws IOException, InterruptedException { private void consume(byte[] data) throws IOException, InterruptedException {
...@@ -554,6 +610,13 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -554,6 +610,13 @@ public class WebmExtractorTest extends InstrumentationTestCase {
assertEquals(MimeTypes.VIDEO_VP9, format.mimeType); assertEquals(MimeTypes.VIDEO_VP9, format.mimeType);
} }
private void assertH264VideoFormat() {
MediaFormat format = videoOutput.format;
assertEquals(TEST_WIDTH, format.width);
assertEquals(TEST_HEIGHT, format.height);
assertEquals(MimeTypes.VIDEO_H264, format.mimeType);
}
private void assertAudioFormat(String expectedMimeType) { private void assertAudioFormat(String expectedMimeType) {
MediaFormat format = audioOutput.format; MediaFormat format = audioOutput.format;
assertEquals(TEST_CHANNEL_COUNT, format.channelCount); assertEquals(TEST_CHANNEL_COUNT, format.channelCount);
...@@ -583,18 +646,18 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -583,18 +646,18 @@ public class WebmExtractorTest extends InstrumentationTestCase {
} }
} }
private void assertSample(byte[] expectedMedia, int timeUs, boolean keyframe, private void assertSample(byte[] expectedMedia, long timeUs, boolean keyframe, boolean invisible,
boolean invisible, boolean encrypted, TestTrackOutput output) { byte[] encryptionKey, TestTrackOutput output) {
if (encrypted) { if (encryptionKey != null) {
expectedMedia = StreamBuilder.joinByteArrays( expectedMedia = StreamBuilder.joinByteArrays(
new byte[] {(byte) StreamBuilder.TEST_INITIALIZATION_VECTOR.length}, new byte[] {(byte) StreamBuilder.TEST_INITIALIZATION_VECTOR.length},
StreamBuilder.TEST_INITIALIZATION_VECTOR, expectedMedia); StreamBuilder.TEST_INITIALIZATION_VECTOR, expectedMedia);
} }
android.test.MoreAsserts.assertEquals(expectedMedia, output.sampleData); int flags = 0;
assertEquals(timeUs, output.sampleTimeUs); flags |= keyframe ? C.SAMPLE_FLAG_SYNC : 0;
assertEquals(keyframe, (output.sampleFlags & C.SAMPLE_FLAG_SYNC) != 0); flags |= invisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
assertEquals(invisible, (output.sampleFlags & C.SAMPLE_FLAG_DECODE_ONLY) != 0); flags |= encryptionKey != null ? C.SAMPLE_FLAG_ENCRYPTED : 0;
assertEquals(encrypted, (output.sampleFlags & C.SAMPLE_FLAG_ENCRYPTED) != 0); output.assertNextSample(expectedMedia, timeUs, flags, encryptionKey);
} }
private byte[] getVorbisCodecPrivate() { private byte[] getVorbisCodecPrivate() {
...@@ -666,10 +729,21 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -666,10 +729,21 @@ public class WebmExtractorTest extends InstrumentationTestCase {
/** Implements {@link TrackOutput} for test purposes. */ /** Implements {@link TrackOutput} for test purposes. */
public static class TestTrackOutput implements TrackOutput { public static class TestTrackOutput implements TrackOutput {
private final Queue<byte[]> sampleData;
private final Queue<Long> sampleTimesUs;
private final Queue<Integer> sampleFlags;
private final Queue<Integer> sampleSizes;
private final Queue<byte[]> sampleEncryptionKeys;
public MediaFormat format; public MediaFormat format;
private long sampleTimeUs; private byte[] currentSampleData;
private int sampleFlags;
private byte[] sampleData; public TestTrackOutput() {
sampleData = new LinkedList<byte[]>();
sampleTimesUs = new LinkedList<Long>();
sampleFlags = new LinkedList<Integer>();
sampleSizes = new LinkedList<Integer>();
sampleEncryptionKeys = new LinkedList<byte[]>();
}
@Override @Override
public void format(MediaFormat format) { public void format(MediaFormat format) {
...@@ -681,8 +755,8 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -681,8 +755,8 @@ public class WebmExtractorTest extends InstrumentationTestCase {
InterruptedException { InterruptedException {
byte[] newData = new byte[length]; byte[] newData = new byte[length];
input.readFully(newData, 0, length); input.readFully(newData, 0, length);
sampleData = currentSampleData = currentSampleData == null
sampleData == null ? newData : StreamBuilder.joinByteArrays(sampleData, newData); ? newData : StreamBuilder.joinByteArrays(currentSampleData, newData);
return length; return length;
} }
...@@ -690,14 +764,31 @@ public class WebmExtractorTest extends InstrumentationTestCase { ...@@ -690,14 +764,31 @@ public class WebmExtractorTest extends InstrumentationTestCase {
public void sampleData(ParsableByteArray data, int length) { public void sampleData(ParsableByteArray data, int length) {
byte[] newData = new byte[length]; byte[] newData = new byte[length];
data.readBytes(newData, 0, length); data.readBytes(newData, 0, length);
sampleData = currentSampleData = currentSampleData == null
sampleData == null ? newData : StreamBuilder.joinByteArrays(sampleData, newData); ? newData : StreamBuilder.joinByteArrays(currentSampleData, newData);
} }
@Override @Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
this.sampleTimeUs = timeUs; sampleData.add(currentSampleData);
this.sampleFlags = flags; sampleTimesUs.add(timeUs);
sampleFlags.add(flags);
sampleSizes.add(size);
sampleEncryptionKeys.add(encryptionKey);
currentSampleData = null;
}
public void assertNextSample(byte[] data, Long timeUs, Integer flags, byte[] encryptionKey) {
assertEquals((Integer) data.length, sampleSizes.poll());
MoreAsserts.assertEquals(data, sampleData.poll());
assertEquals(timeUs, sampleTimesUs.poll());
assertEquals(flags, sampleFlags.poll());
byte[] sampleEncryptionKey = sampleEncryptionKeys.poll();
if (encryptionKey == null) {
assertEquals(null, sampleEncryptionKey);
} else {
MoreAsserts.assertEquals(encryptionKey, sampleEncryptionKey);
}
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment