Commit 8c98c588 by Oliver Woodman

Add support for fixed-size lacing in Matroska streams.

parent 4c4782c7
......@@ -52,9 +52,10 @@ public interface Extractor {
/**
* Extracts data read from a provided {@link ExtractorInput}.
* <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>
* 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
* 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
......
......@@ -52,9 +52,9 @@ import java.util.concurrent.TimeUnit;
*/
public final class WebmExtractor implements Extractor {
private static final int SAMPLE_STATE_START = 0;
private static final int SAMPLE_STATE_HEADER = 1;
private static final int SAMPLE_STATE_DATA = 2;
private static final int BLOCK_STATE_START = 0;
private static final int BLOCK_STATE_HEADER = 1;
private static final int BLOCK_STATE_DATA = 2;
private static final int CUES_STATE_NOT_BUILT = 0;
private static final int CUES_STATE_BUILDING = 1;
......@@ -98,6 +98,7 @@ public final class WebmExtractor implements Extractor {
private static final int ID_TRACK_ENTRY = 0xAE;
private static final int ID_TRACK_NUMBER = 0xD7;
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_PRIVATE = 0x63A2;
private static final int ID_CODEC_DELAY = 0x56AA;
......@@ -125,6 +126,7 @@ public final class WebmExtractor implements Extractor {
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
private static final int LACING_NONE = 0;
private static final int LACING_FIXED_SIZE = 2;
private final EbmlReader reader;
private final VarintReader varintReader;
......@@ -132,7 +134,7 @@ public final class WebmExtractor implements Extractor {
// Temporary arrays.
private final ParsableByteArray nalStartCode;
private final ParsableByteArray nalLength;
private final ParsableByteArray sampleHeaderScratch;
private final ParsableByteArray scratch;
private final ParsableByteArray vorbisNumPageSamples;
private final ParsableByteArray seekEntryIdBytes;
......@@ -161,15 +163,22 @@ public final class WebmExtractor implements Extractor {
private LongArray cueClusterPositions;
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.
private int blockBytesRead;
private int sampleState;
private int sampleSize;
private int sampleBytesRead;
private boolean sampleEncryptionDataRead;
private int sampleCurrentNalBytesRemaining;
private int sampleTrackNumber;
private int sampleFlags;
private byte[] sampleEncryptionKeyId;
private long sampleTimeUs;
private int sampleBytesWritten;
private boolean sampleRead;
private boolean sampleSeenReferenceBlock;
......@@ -184,7 +193,7 @@ public final class WebmExtractor implements Extractor {
this.reader = reader;
this.reader.init(new InnerEbmlReaderOutput());
varintReader = new VarintReader();
sampleHeaderScratch = new ParsableByteArray(4);
scratch = new ParsableByteArray(4);
vorbisNumPageSamples = new ParsableByteArray(ByteBuffer.allocate(4).putInt(-1).array());
seekEntryIdBytes = new ParsableByteArray(4);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
......@@ -199,10 +208,13 @@ public final class WebmExtractor implements Extractor {
@Override
public void seek() {
clusterTimecodeUs = UNKNOWN;
sampleState = SAMPLE_STATE_START;
blockState = BLOCK_STATE_START;
reader.reset();
varintReader.reset();
sampleCurrentNalBytesRemaining = 0;
sampleBytesRead = 0;
sampleBytesWritten = 0;
sampleEncryptionDataRead = false;
}
@Override
......@@ -249,6 +261,7 @@ public final class WebmExtractor implements Extractor {
case ID_PIXEL_HEIGHT:
case ID_TRACK_NUMBER:
case ID_TRACK_TYPE:
case ID_DEFAULT_DURATION:
case ID_CODEC_DELAY:
case ID_SEEK_PRE_ROLL:
case ID_CHANNELS:
......@@ -342,17 +355,18 @@ public final class WebmExtractor implements Extractor {
}
return;
case ID_BLOCK_GROUP:
if (sampleState != SAMPLE_STATE_DATA) {
// We've skipped this sample (due to incompatible track number).
if (blockState != BLOCK_STATE_DATA) {
// We've skipped this block (due to incompatible track number).
return;
}
// If the ReferenceBlock element was not found for this sample, then it is a keyframe.
if (!sampleSeenReferenceBlock) {
sampleFlags |= C.SAMPLE_FLAG_SYNC;
blockFlags |= C.SAMPLE_FLAG_SYNC;
}
outputSampleMetadata(
(audioTrackFormat != null && sampleTrackNumber == audioTrackFormat.number)
? audioTrackFormat.trackOutput : videoTrackFormat.trackOutput);
(audioTrackFormat != null && blockTrackNumber == audioTrackFormat.number)
? audioTrackFormat.trackOutput : videoTrackFormat.trackOutput, blockTimeUs);
blockState = BLOCK_STATE_START;
return;
case ID_CONTENT_ENCODING:
if (!trackFormat.hasContentEncryption) {
......@@ -436,6 +450,9 @@ public final class WebmExtractor implements Extractor {
case ID_TRACK_TYPE:
trackFormat.type = (int) value;
return;
case ID_DEFAULT_DURATION:
trackFormat.defaultSampleDurationNs = (int) value;
break;
case ID_CODEC_DELAY:
trackFormat.codecDelayNs = value;
return;
......@@ -552,76 +569,130 @@ public final class WebmExtractor implements Extractor {
// for info about how data is organized in SimpleBlock and Block elements respectively. They
// differ only in the way flags are specified.
if (sampleState == SAMPLE_STATE_START) {
sampleTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true);
blockBytesRead = varintReader.getLastLength();
sampleState = SAMPLE_STATE_HEADER;
if (blockState == BLOCK_STATE_START) {
blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true);
blockTrackNumberLength = varintReader.getLastLength();
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
&& audioTrackFormat.number != sampleTrackNumber
&& videoTrackFormat.number != sampleTrackNumber)
&& audioTrackFormat.number != blockTrackNumber
&& videoTrackFormat.number != blockTrackNumber)
|| (audioTrackFormat != null && videoTrackFormat == null
&& audioTrackFormat.number != sampleTrackNumber)
&& audioTrackFormat.number != blockTrackNumber)
|| (audioTrackFormat == null && videoTrackFormat != null
&& videoTrackFormat.number != sampleTrackNumber)) {
input.skipFully(contentSize - blockBytesRead);
sampleState = SAMPLE_STATE_START;
&& videoTrackFormat.number != blockTrackNumber)) {
input.skipFully(contentSize - blockTrackNumberLength);
blockState = BLOCK_STATE_START;
return;
}
TrackFormat sampleTrackFormat =
(audioTrackFormat != null && sampleTrackNumber == audioTrackFormat.number)
(audioTrackFormat != null && blockTrackNumber == audioTrackFormat.number)
? audioTrackFormat : videoTrackFormat;
TrackOutput trackOutput = sampleTrackFormat.trackOutput;
if (sampleState == SAMPLE_STATE_HEADER) {
byte[] sampleHeaderScratchData = sampleHeaderScratch.data;
// Next 3 bytes have timecode and flags. If encrypted, the 4th byte is a signal byte.
int remainingHeaderLength = sampleTrackFormat.hasContentEncryption ? 4 : 3;
input.readFully(sampleHeaderScratchData, 0, remainingHeaderLength);
blockBytesRead += remainingHeaderLength;
// First two bytes are the relative timecode.
int timecode = (sampleHeaderScratchData[0] << 8)
| (sampleHeaderScratchData[1] & 0xFF);
sampleTimeUs = clusterTimecodeUs + scaleTimecodeToUs(timecode);
// Third byte contains the lacing value and some flags.
int lacing = (sampleHeaderScratchData[2] & 0x06) >> 1;
if (lacing != LACING_NONE) {
if (blockState == BLOCK_STATE_HEADER) {
// Read the relative timecode (2 bytes) and flags (1 byte).
readScratch(input, 3);
int lacing = (scratch.data[2] & 0x06) >> 1;
if (lacing == LACING_NONE) {
blockLacingSampleCount = 1;
blockLacingSampleSize = contentSize - blockTrackNumberLength - 3;
} else if (lacing == LACING_FIXED_SIZE) {
if (id != ID_SIMPLE_BLOCK) {
throw new ParserException("Lacing only supported in SimpleBlocks.");
}
// Read the sample count (1 byte).
readScratch(input, 4);
blockLacingSampleCount = (scratch.data[3] & 0xFF) + 1;
blockLacingSampleSize =
(contentSize - blockTrackNumberLength - 4) / blockLacingSampleCount;
} else {
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");
int timecode = (scratch.data[0] << 8) | (scratch.data[1] & 0xFF);
blockTimeUs = clusterTimecodeUs + scaleTimecodeToUs(timecode);
boolean isInvisible = (scratch.data[2] & 0x08) == 0x08;
boolean isKeyframe = (id == ID_SIMPLE_BLOCK && (scratch.data[2] & 0x80) == 0x80);
blockFlags = (isKeyframe ? C.SAMPLE_FLAG_SYNC : 0)
| (isInvisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0);
blockEncryptionKeyId = sampleTrackFormat.encryptionKeyId;
blockState = BLOCK_STATE_DATA;
blockLacingSampleIndex = 0;
}
if (id == ID_SIMPLE_BLOCK) {
// 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;
default:
throw new ParserException("Unexpected id: " + id);
}
}
private void outputSampleMetadata(TrackOutput trackOutput, long timeUs) {
trackOutput.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, blockEncryptionKeyId);
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;
}
isEncrypted = (sampleHeaderScratchData[3] & 0x01) == 0x01;
input.readFully(scratch.data, scratch.limit(), requiredLength - scratch.limit());
scratch.setLimit(requiredLength);
}
sampleFlags = (isKeyframe ? C.SAMPLE_FLAG_SYNC : 0)
| (isInvisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0)
| (isEncrypted ? C.SAMPLE_FLAG_ENCRYPTED : 0);
sampleEncryptionKeyId = sampleTrackFormat.encryptionKeyId;
sampleSize = contentSize - blockBytesRead;
if (isEncrypted) {
// Write the vector size.
sampleHeaderScratch.data[0] = (byte) ENCRYPTION_IV_SIZE;
sampleHeaderScratch.setPosition(0);
trackOutput.sampleData(sampleHeaderScratch, 1);
sampleSize++;
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;
}
sampleState = SAMPLE_STATE_DATA;
}
if (CODEC_ID_H264.equals(sampleTrackFormat.codecId)) {
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
......@@ -630,64 +701,50 @@ public final class WebmExtractor implements Extractor {
nalLengthData[0] = 0;
nalLengthData[1] = 0;
nalLengthData[2] = 0;
int nalUnitLengthFieldLength = sampleTrackFormat.nalUnitLengthFieldLength;
int nalUnitLengthFieldLengthDiff = 4 - sampleTrackFormat.nalUnitLengthFieldLength;
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 (blockBytesRead < contentSize) {
// 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);
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;
output.sampleData(nalStartCode, 4);
sampleBytesRead += nalUnitLengthFieldLength;
sampleBytesWritten += 4;
} else {
// Write the payload of the NAL unit.
int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining);
blockBytesRead += writtenBytes;
int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining);
sampleCurrentNalBytesRemaining -= writtenBytes;
sampleBytesRead += writtenBytes;
sampleBytesWritten += writtenBytes;
}
}
} else {
while (blockBytesRead < contentSize) {
blockBytesRead += trackOutput.sampleData(input, contentSize - blockBytesRead);
while (sampleBytesRead < size) {
int writtenBytes = output.sampleData(input, size - sampleBytesRead);
sampleBytesRead += writtenBytes;
sampleBytesWritten += writtenBytes;
}
}
if (CODEC_ID_VORBIS.equals(sampleTrackFormat.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.
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);
trackOutput.sampleData(vorbisNumPageSamples, 4);
sampleSize += 4;
output.sampleData(vorbisNumPageSamples, 4);
sampleBytesWritten += 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) {
outputSampleMetadata(trackOutput);
}
return;
default:
throw new IllegalStateException("Unexpected id: " + id);
}
}
private void outputSampleMetadata(TrackOutput trackOutput) {
trackOutput.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, sampleEncryptionKeyId);
sampleState = SAMPLE_STATE_START;
sampleRead = true;
}
/**
......@@ -817,6 +874,7 @@ public final class WebmExtractor implements Extractor {
public String codecId;
public int number = UNKNOWN;
public int type = UNKNOWN;
public int defaultSampleDurationNs = UNKNOWN;
public boolean hasContentEncryption;
public byte[] encryptionKeyId;
public byte[] codecPrivate;
......
......@@ -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) {
int length = 0;
for (byte[] byteArray : byteArrays) {
......@@ -94,16 +102,28 @@ import java.util.List;
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,
int seekPreRoll, byte[] 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;
}
public StreamBuilder addVorbisTrack(int channelCount, int sampleRate, byte[] codecPrivate) {
trackEntries.add(createAudioTrackEntry("A_VORBIS", channelCount, sampleRate, codecPrivate,
NO_VALUE, NO_VALUE));
NO_VALUE, NO_VALUE, NO_VALUE));
return this;
}
......@@ -120,7 +140,7 @@ import java.util.List;
byte[] data) {
byte flags = (byte) ((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00));
EbmlElement simpleBlockElement = createSimpleBlock(trackNumber, blockTimecode, flags,
true, validSignalByte, data);
true, validSignalByte, 1, data);
mediaSegments.add(createCluster(clusterTimecode, simpleBlockElement));
return this;
}
......@@ -130,7 +150,15 @@ import java.util.List;
int blockTimecode, boolean keyframe, boolean invisible, byte[] data) {
byte flags = (byte) ((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00));
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));
return this;
}
......@@ -248,7 +276,7 @@ import java.util.List;
}
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[] sampleRateDoubleBytes = getLongBytes(Double.doubleToLongBits(sampleRate));
return element(0xAE, // TrackEntry
......@@ -262,6 +290,9 @@ import java.util.List;
element(0xE1, // Audio
element(0x9F, channelCountByte), // Channels
element(0xB5, sampleRateDoubleBytes)), // SamplingFrequency
// DefaultDuration
defaultDurationNs != NO_VALUE ? element(0x23E383, getIntegerBytes(defaultDurationNs))
: empty(),
element(0x63A2, codecPrivate)); // CodecPrivate
}
......@@ -272,12 +303,20 @@ import java.util.List;
}
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[] 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
timeBytes[2], timeBytes[3], flags); // Timecode and flags
}
if (encrypted) {
simpleBlockBytes = joinByteArrays(
simpleBlockBytes, createByteArray(validSignalByte ? 0x01 : 0x80),
......@@ -302,14 +341,6 @@ import java.util.List;
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) {
return createByteArray(
(value & 0xFF000000) >> 24,
......
......@@ -37,9 +37,13 @@ import com.google.android.exoplayer.util.ParsableByteArray;
import android.net.Uri;
import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
/**
......@@ -59,9 +63,16 @@ public class WebmExtractorTest extends InstrumentationTestCase {
private static final int TEST_VORBIS_INFO_SIZE = 30;
private static final int TEST_VORBIS_BOOKS_SIZE = 4140;
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 ZERO_UUID = new UUID(0, 0);
private static final String WEBM_DOC_TYPE = "webm";
private static final String MATROSKA_DOC_TYPE = "matroska";
private WebmExtractor extractor;
private TestExtractorOutput extractorOutput;
......@@ -125,6 +136,19 @@ public class WebmExtractorTest extends InstrumentationTestCase {
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 {
byte[] data = new StreamBuilder()
.setHeader(WEBM_DOC_TYPE)
......@@ -257,6 +281,17 @@ public class WebmExtractorTest extends InstrumentationTestCase {
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 {
byte[] data = new StreamBuilder()
.setHeader("webB")
......@@ -359,7 +394,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data);
assertVp9VideoFormat();
assertSample(media, 0, true, false, false, videoOutput);
assertSample(media, 0, true, false, null, videoOutput);
}
public void testReadTwoTrackSamples() throws IOException, InterruptedException {
......@@ -381,8 +416,8 @@ public class WebmExtractorTest extends InstrumentationTestCase {
assertEquals(2, extractorOutput.numberOfTracks);
assertVp9VideoFormat();
assertAudioFormat(MimeTypes.AUDIO_OPUS);
assertSample(media, 0, true, false, false, videoOutput);
assertSample(media, 0, true, false, false, audioOutput);
assertSample(media, 0, true, false, null, videoOutput);
assertSample(media, 0, true, false, null, audioOutput);
}
public void testReadTwoTrackSamplesWithSkippedTrack() throws IOException, InterruptedException {
......@@ -407,8 +442,8 @@ public class WebmExtractorTest extends InstrumentationTestCase {
assertEquals(2, extractorOutput.numberOfTracks);
assertVp9VideoFormat();
assertAudioFormat(MimeTypes.AUDIO_OPUS);
assertSample(media, 0, true, false, false, videoOutput);
assertSample(media, 0, true, false, false, audioOutput);
assertSample(media, 0, true, false, null, videoOutput);
assertSample(media, 0, true, false, null, audioOutput);
}
public void testReadBlock() throws IOException, InterruptedException {
......@@ -425,7 +460,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data);
assertAudioFormat(MimeTypes.AUDIO_OPUS);
assertSample(media, 0, true, false, false, audioOutput);
assertSample(media, 0, true, false, null, audioOutput);
}
public void testReadBlockNonKeyframe() throws IOException, InterruptedException {
......@@ -441,7 +476,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data);
assertVp9VideoFormat();
assertSample(media, 0, false, false, false, videoOutput);
assertSample(media, 0, false, false, null, videoOutput);
}
public void testReadEncryptedFrame() throws IOException, InterruptedException {
......@@ -459,7 +494,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data);
assertVp9VideoFormat();
assertSample(media, 0, true, false, true, videoOutput);
assertSample(media, 0, true, false, TEST_ENCRYPTION_KEY_ID, videoOutput);
}
public void testReadEncryptedFrameWithInvalidSignalByte()
......@@ -496,7 +531,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data);
assertVp9VideoFormat();
assertSample(media, 25000, false, true, false, videoOutput);
assertSample(media, 25000, false, true, null, videoOutput);
}
public void testReadSampleCustomTimescale() throws IOException, InterruptedException {
......@@ -512,7 +547,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data);
assertVp9VideoFormat();
assertSample(media, 25, false, false, false, videoOutput);
assertSample(media, 25, false, false, null, videoOutput);
}
public void testReadSampleNegativeSimpleBlockTimecode() throws IOException, InterruptedException {
......@@ -528,7 +563,28 @@ public class WebmExtractorTest extends InstrumentationTestCase {
consume(data);
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 {
......@@ -554,6 +610,13 @@ public class WebmExtractorTest extends InstrumentationTestCase {
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) {
MediaFormat format = audioOutput.format;
assertEquals(TEST_CHANNEL_COUNT, format.channelCount);
......@@ -583,18 +646,18 @@ public class WebmExtractorTest extends InstrumentationTestCase {
}
}
private void assertSample(byte[] expectedMedia, int timeUs, boolean keyframe,
boolean invisible, boolean encrypted, TestTrackOutput output) {
if (encrypted) {
private void assertSample(byte[] expectedMedia, long timeUs, boolean keyframe, boolean invisible,
byte[] encryptionKey, TestTrackOutput output) {
if (encryptionKey != null) {
expectedMedia = StreamBuilder.joinByteArrays(
new byte[] {(byte) StreamBuilder.TEST_INITIALIZATION_VECTOR.length},
StreamBuilder.TEST_INITIALIZATION_VECTOR, expectedMedia);
}
android.test.MoreAsserts.assertEquals(expectedMedia, output.sampleData);
assertEquals(timeUs, output.sampleTimeUs);
assertEquals(keyframe, (output.sampleFlags & C.SAMPLE_FLAG_SYNC) != 0);
assertEquals(invisible, (output.sampleFlags & C.SAMPLE_FLAG_DECODE_ONLY) != 0);
assertEquals(encrypted, (output.sampleFlags & C.SAMPLE_FLAG_ENCRYPTED) != 0);
int flags = 0;
flags |= keyframe ? C.SAMPLE_FLAG_SYNC : 0;
flags |= invisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
flags |= encryptionKey != null ? C.SAMPLE_FLAG_ENCRYPTED : 0;
output.assertNextSample(expectedMedia, timeUs, flags, encryptionKey);
}
private byte[] getVorbisCodecPrivate() {
......@@ -666,10 +729,21 @@ public class WebmExtractorTest extends InstrumentationTestCase {
/** Implements {@link TrackOutput} for test purposes. */
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;
private long sampleTimeUs;
private int sampleFlags;
private byte[] sampleData;
private byte[] currentSampleData;
public TestTrackOutput() {
sampleData = new LinkedList<byte[]>();
sampleTimesUs = new LinkedList<Long>();
sampleFlags = new LinkedList<Integer>();
sampleSizes = new LinkedList<Integer>();
sampleEncryptionKeys = new LinkedList<byte[]>();
}
@Override
public void format(MediaFormat format) {
......@@ -681,8 +755,8 @@ public class WebmExtractorTest extends InstrumentationTestCase {
InterruptedException {
byte[] newData = new byte[length];
input.readFully(newData, 0, length);
sampleData =
sampleData == null ? newData : StreamBuilder.joinByteArrays(sampleData, newData);
currentSampleData = currentSampleData == null
? newData : StreamBuilder.joinByteArrays(currentSampleData, newData);
return length;
}
......@@ -690,14 +764,31 @@ public class WebmExtractorTest extends InstrumentationTestCase {
public void sampleData(ParsableByteArray data, int length) {
byte[] newData = new byte[length];
data.readBytes(newData, 0, length);
sampleData =
sampleData == null ? newData : StreamBuilder.joinByteArrays(sampleData, newData);
currentSampleData = currentSampleData == null
? newData : StreamBuilder.joinByteArrays(currentSampleData, newData);
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
this.sampleTimeUs = timeUs;
this.sampleFlags = flags;
sampleData.add(currentSampleData);
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