From eea57d0d76020dfcfec33ec964204a9e58b17707 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 15 Jun 2015 18:02:35 +0100 Subject: [PATCH] Enhance WebM extractor tests --- .../extractor/webm/StreamBuilder.java | 461 +++++++++ .../extractor/webm/WebmExtractorTest.java | 881 +++++++----------- 2 files changed, 782 insertions(+), 560 deletions(-) create mode 100644 library/src/test/java/com/google/android/exoplayer/extractor/webm/StreamBuilder.java diff --git a/library/src/test/java/com/google/android/exoplayer/extractor/webm/StreamBuilder.java b/library/src/test/java/com/google/android/exoplayer/extractor/webm/StreamBuilder.java new file mode 100644 index 0000000000..07b2564cf0 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/extractor/webm/StreamBuilder.java @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.extractor.webm; + +import com.google.android.exoplayer.util.Assertions; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * Provides byte arrays containing WebM data for {@link WebmExtractorTest}. + */ +/* package */ final class StreamBuilder { + + /** Used by {@link #addVp9Track} to create a Track header with Encryption. */ + public static final class ContentEncodingSettings { + + private final int order; + private final int scope; + private final int type; + private final int algorithm; + private final int aesCipherMode; + + public ContentEncodingSettings(int order, int scope, int type, int algorithm, + int aesCipherMode) { + this.order = order; + this.scope = scope; + this.type = type; + this.algorithm = algorithm; + this.aesCipherMode = aesCipherMode; + } + + } + + public static byte[] joinByteArrays(byte[]... byteArrays) { + int length = 0; + for (byte[] byteArray : byteArrays) { + length += byteArray.length; + } + byte[] joined = new byte[length]; + length = 0; + for (byte[] byteArray : byteArrays) { + System.arraycopy(byteArray, 0, joined, length, byteArray.length); + length += byteArray.length; + } + return joined; + } + + public static final byte[] TEST_ENCRYPTION_KEY_ID = { 0x00, 0x01, 0x02, 0x03 }; + public static final byte[] TEST_INITIALIZATION_VECTOR = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 + }; + + private static final int NO_VALUE = -1; + + private EbmlElement header; + private EbmlElement info; + private List trackEntries; + private List mediaSegments; + + public StreamBuilder() { + trackEntries = new LinkedList<>(); + mediaSegments = new LinkedList<>(); + } + + public StreamBuilder setHeader(String docType) { + header = createEbmlElement(1, docType, 2); + return this; + } + + public StreamBuilder setInfo(int timecodeScale, long durationUs) { + info = createInfoElement(timecodeScale, durationUs); + return this; + } + + public StreamBuilder addVp9Track(int width, int height, + ContentEncodingSettings contentEncodingSettings) { + trackEntries.add(createVideoTrackEntry("V_VP9", width, height, contentEncodingSettings, null)); + 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)); + return this; + } + + public StreamBuilder addVorbisTrack(int channelCount, int sampleRate, byte[] codecPrivate) { + trackEntries.add(createAudioTrackEntry("A_VORBIS", channelCount, sampleRate, codecPrivate, + NO_VALUE, NO_VALUE)); + return this; + } + + public StreamBuilder addUnsupportedTrack() { + trackEntries.add(element(0xAE, // TrackEntry + element(0x86, "D_WEBVTT/metadata".getBytes()), // CodecID + element(0xD7, (byte) 0x03), // TrackNumber + element(0x83, (byte) 0x11))); // TrackType + return this; + } + + public StreamBuilder addSimpleBlockEncryptedMedia(int trackNumber, int clusterTimecode, + int blockTimecode, boolean keyframe, boolean invisible, boolean validSignalByte, + byte[] data) { + byte flags = (byte) ((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00)); + EbmlElement simpleBlockElement = createSimpleBlock(trackNumber, blockTimecode, flags, + true, validSignalByte, data); + mediaSegments.add(createCluster(clusterTimecode, simpleBlockElement)); + return this; + } + + + public StreamBuilder addSimpleBlockMedia(int trackNumber, int clusterTimecode, + 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); + mediaSegments.add(createCluster(clusterTimecode, simpleBlockElement)); + return this; + } + + public StreamBuilder addBlockMedia(int trackNumber, int clusterTimecode, int blockTimecode, + boolean keyframe, boolean invisible, byte[] data) { + byte flags = (byte) (invisible ? 0x08 : 0x00); + EbmlElement blockElement = + createBlock(trackNumber, blockTimecode, keyframe, flags, data); + mediaSegments.add(createCluster(clusterTimecode, blockElement)); + return this; + } + + /** + * Serializes the constructed stream to a {@code byte[]} using the specified number of cue points. + */ + public byte[] build(int cuePointCount) { + Assertions.checkNotNull(header); + Assertions.checkNotNull(info); + + EbmlElement tracks = element(0x1654AE6B, trackEntries.toArray(new EbmlElement[0])); + + // Get the size of the initialization segment. + EbmlElement[] cuePointElements = new EbmlElement[cuePointCount]; + for (int i = 0; i < cuePointCount; i++) { + cuePointElements[i] = createCuePointElement(10 * i, 0); + } + EbmlElement cues = element(0x1C53BB6B, cuePointElements); // Cues + long initializationSegmentSize = info.getSize() + tracks.getSize() + cues.getSize(); + + // Recreate the initialization segment using its size as an offset. + for (int i = 0; i < cuePointCount; i++) { + cuePointElements[i] = createCuePointElement(10 * i, (int) initializationSegmentSize); + } + cues = element(0x1C53BB6B, cuePointElements); // Cues + + // Build the top-level segment element. + EbmlElement[] children = new EbmlElement[3 + mediaSegments.size()]; + System.arraycopy(mediaSegments.toArray(new EbmlElement[0]), 0, children, 3, + mediaSegments.size()); + children[0] = info; + children[1] = tracks; + children[2] = cues; + EbmlElement segmentElement = element(0x18538067, children); // Segment + + // Serialize the EBML header and the top-level segment element. + return EbmlElement.serialize(header, segmentElement); + } + + private static EbmlElement createCuePointElement(int cueTime, int cueClusterPosition) { + byte[] positionBytes = getLongBytes(cueClusterPosition); + return element(0xBB, // CuePoint + element(0xB3, (byte) (cueTime & 0xFF)), // CueTime + element(0xB7, // CueTrackPositions + element(0xF1, positionBytes))); // CueClusterPosition + } + + private static EbmlElement createEbmlElement(int ebmlReadVersion, String docType, + int docTypeReadVersion) { + return element(0x1A45DFA3, // EBML + element(0x42F7, (byte) (ebmlReadVersion & 0xFF)), // EBMLReadVersion + element(0x4282, docType.getBytes()), // DocType + element(0x4285, (byte) (docTypeReadVersion & 0xFF))); // DocTypeReadVersion + } + + private EbmlElement createInfoElement(int timecodeScale, long durationUs) { + byte[] timecodeScaleBytes = getIntegerBytes(timecodeScale); + byte[] durationBytes = getLongBytes(Double.doubleToLongBits(durationUs / 1000.0)); + return element(0x1549A966, // Info + element(0x2AD7B1, timecodeScaleBytes), // TimecodeScale + element(0x4489, durationBytes)); // Duration + } + + private static EbmlElement createVideoTrackEntry(String codecId, int pixelWidth, int pixelHeight, + ContentEncodingSettings contentEncodingSettings, byte[] codecPrivate) { + byte[] widthBytes = getIntegerBytes(pixelWidth); + byte[] heightBytes = getIntegerBytes(pixelHeight); + EbmlElement contentEncodingSettingsElement; + if (contentEncodingSettings != null) { + contentEncodingSettingsElement = + element(0x6D80, // ContentEncodings + element(0x6240, // ContentEncoding + // ContentEncodingOrder + element(0x5031, (byte) (contentEncodingSettings.order & 0xFF)), + // ContentEncodingScope + 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)))))); + } else { + contentEncodingSettingsElement = empty(); + } + EbmlElement codecPrivateElement; + if (codecPrivate != null) { + codecPrivateElement = element(0x63A2, codecPrivate); // CodecPrivate + } else { + codecPrivateElement = empty(); + } + + return element(0xAE, // TrackEntry + element(0x86, codecId.getBytes()), // CodecID + element(0xD7, (byte) 0x01), // TrackNumber + element(0x83, (byte) 0x01), // TrackType + contentEncodingSettingsElement, + element(0xE0, // Video + element(0xB0, widthBytes[2], widthBytes[3]), + element(0xBA, heightBytes[2], heightBytes[3])), + codecPrivateElement); + } + + private static EbmlElement createAudioTrackEntry(String codecId, int channelCount, int sampleRate, + byte[] codecPrivate, int codecDelay, int seekPreRoll) { + byte channelCountByte = (byte) (channelCount & 0xFF); + byte[] sampleRateDoubleBytes = getLongBytes(Double.doubleToLongBits(sampleRate)); + return element(0xAE, // TrackEntry + element(0x86, codecId.getBytes()), // CodecID + element(0xD7, (byte) 0x02), // TrackNumber + element(0x83, (byte) 0x02), // TrackType + // CodecDelay + codecDelay != NO_VALUE ? element(0x56AA, getIntegerBytes(codecDelay)) : empty(), + // SeekPreRoll + seekPreRoll != NO_VALUE ? element(0x56BB, getIntegerBytes(seekPreRoll)) : empty(), + element(0xE1, // Audio + element(0x9F, channelCountByte), // Channels + element(0xB5, sampleRateDoubleBytes)), // SamplingFrequency + element(0x63A2, codecPrivate)); // CodecPrivate + } + + private static EbmlElement createCluster(int timecode, EbmlElement blockGroupOrSimpleBlock) { + return element(0x1F43B675, // Cluster + element(0xE7, getIntegerBytes(timecode)), // Timecode + blockGroupOrSimpleBlock); + } + + private static EbmlElement createSimpleBlock(int trackNumber, int timecode, int flags, + boolean encrypted, boolean validSignalByte, byte[] data) { + byte[] trackNumberBytes = getIntegerBytes(trackNumber); + byte[] timeBytes = getIntegerBytes(timecode); + byte[] 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), + Arrays.copyOfRange(TEST_INITIALIZATION_VECTOR, 0, 8)); + } + return element(0xA3, // SimpleBlock + joinByteArrays(simpleBlockBytes, data)); + } + + private static EbmlElement createBlock(int trackNumber, int timecode, boolean keyframe, int flags, + byte[] data) { + byte[] trackNumberBytes = getIntegerBytes(trackNumber); + byte[] timeBytes = getIntegerBytes(timecode); + byte[] blockBytes = createByteArray( + 0x40, trackNumberBytes[3], // Track number size=2 + timeBytes[2], timeBytes[3], flags); // Timecode and flags + EbmlElement block = element(0xA1, // Block + joinByteArrays(blockBytes, data)); + EbmlElement referenceBlock = keyframe ? empty() : element(0xFB, (byte) 0x00); // ReferenceBlock + return element(0xA0, // BlockGroup + referenceBlock, + 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, + (value & 0x00FF0000) >> 16, + (value & 0x0000FF00) >> 8, + (value & 0x000000FF)); + } + + private static byte[] getLongBytes(long value) { + byte[] result = new byte[8]; + for (int i = 0; i < 8; i++) { + result[7 - i] = (byte) ((value >> (8 * i)) & 0xFF); + } + return result; + } + + /** @see EbmlElement#EbmlElement(int, EbmlElement...) */ + private static EbmlElement element(int type, EbmlElement... childElements) { + return new EbmlElement(type, childElements); + } + + /** @see EbmlElement#EbmlElement(int, byte...) */ + private static EbmlElement element(int type, byte... payload) { + return new EbmlElement(type, payload); + } + + /** @see EbmlElement#EbmlElement() */ + private static EbmlElement empty() { + return new EbmlElement(); + } + + /** Represents a WebM EBML element that can be serialized as a byte array. */ + private static final class EbmlElement { + + /** Returns a byte[] containing the concatenation of the data from all {@code elements}. */ + public static byte[] serialize(EbmlElement... elements) { + int size = 0; + for (EbmlElement element : elements) { + size += element.getSize(); + } + ByteBuffer buffer = ByteBuffer.allocate(size); + for (EbmlElement element : elements) { + element.getData(buffer); + } + return buffer.array(); + } + + private final int id; + private final EbmlElement[] childElements; + private final byte[] payload; + + /** Creates an element containing the specified {@code childElements}. */ + EbmlElement(int id, EbmlElement... childElements) { + this.id = id; + this.childElements = childElements; + payload = null; + } + + /** Creates an element containing the specified {@code payload}. */ + EbmlElement(int id, byte... payload) { + this.id = id; + this.payload = payload; + childElements = null; + } + + /** Creates a completely empty element that will contribute no bytes to the output. */ + EbmlElement() { + id = NO_VALUE; + payload = null; + childElements = null; + } + + private long getSize() { + if (id == NO_VALUE) { + return 0; + } + + long payloadSize = getPayloadSize(); + return getIdLength() + getVIntLength(payloadSize) + payloadSize; + } + + private long getPayloadSize() { + if (payload != null) { + return payload.length; + } + + int payloadSize = 0; + for (EbmlElement element : childElements) { + payloadSize += element.getSize(); + } + return payloadSize; + } + + private void getData(ByteBuffer byteBuffer) { + if (id == NO_VALUE) { + return; + } + + putId(byteBuffer); + putVInt(byteBuffer, getPayloadSize()); + if (payload != null) { + byteBuffer.put(payload); + } else { + for (EbmlElement atom : childElements) { + atom.getData(byteBuffer); + } + } + } + + private int getIdLength() { + if (id == NO_VALUE) { + return 0; + } + + for (int i = 0; i < 4; i++) { + if (id < 1 << (7 * i + 8)) { + return i + 1; + } + } + throw new IllegalArgumentException(); + } + + private static int getVIntLength(long vInt) { + for (int i = 1; i < 9; i++) { + if (vInt < 1 << (7 * i)) { + return i; + } + } + throw new IllegalArgumentException(); + } + + private void putId(ByteBuffer byteBuffer) { + int length = getIdLength(); + for (int i = length - 1; i >= 0; i--) { + byteBuffer.put((byte) ((id >> (i * 8)) & 0xFF)); + } + } + + private static void putVInt(ByteBuffer byteBuffer, long vInt) { + int vIntLength = getVIntLength(vInt); + vInt |= 1 << (vIntLength * 7); + for (int i = vIntLength - 1; i >= 0; i--) { + byteBuffer.put((byte) ((vInt >> (i * 8)) & 0xFF)); + } + } + + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java b/library/src/test/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java index 8d51e13f61..55dfe1ad44 100644 --- a/library/src/test/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java +++ b/library/src/test/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.extractor.webm; +import static com.google.android.exoplayer.extractor.webm.StreamBuilder.TEST_ENCRYPTION_KEY_ID; + import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.ParserException; @@ -26,6 +28,7 @@ import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.TrackOutput; +import com.google.android.exoplayer.extractor.webm.StreamBuilder.ContentEncodingSettings; import com.google.android.exoplayer.testutil.FakeDataSource; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; @@ -37,7 +40,6 @@ import android.test.InstrumentationTestCase; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.Arrays; import java.util.UUID; /** @@ -45,32 +47,21 @@ import java.util.UUID; */ public class WebmExtractorTest extends InstrumentationTestCase { - private static final int CUE_POINT_ELEMENT_BYTE_SIZE = 31; - private static final int DEFAULT_TIMECODE_SCALE = 1000000; - private static final long TEST_DURATION_US = 9920000L; private static final int TEST_WIDTH = 1280; private static final int TEST_HEIGHT = 720; private static final int TEST_CHANNEL_COUNT = 1; private static final int TEST_SAMPLE_RATE = 48000; - private static final long TEST_CODEC_DELAY = 6500000; - private static final long TEST_SEEK_PRE_ROLL = 80000000; - private static final int TEST_OPUS_CODEC_PRIVATE_SIZE = 2; + private static final int TEST_CODEC_DELAY = 6500000; + private static final int TEST_SEEK_PRE_ROLL = 80000000; private static final String TEST_VORBIS_CODEC_PRIVATE = "webm/vorbis_codec_private"; private static final int TEST_VORBIS_INFO_SIZE = 30; private static final int TEST_VORBIS_BOOKS_SIZE = 4140; - private static final byte[] TEST_ENCRYPTION_KEY_ID = { 0x00, 0x01, 0x02, 0x03 }; + private static final byte[] TEST_OPUS_CODEC_PRIVATE = new byte[] {0, 0}; private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); private static final UUID ZERO_UUID = new UUID(0, 0); - private static final byte[] TEST_INITIALIZATION_VECTOR = { - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 - }; - - private static final int ID_VP9 = 0; - private static final int ID_OPUS = 1; - private static final int ID_VORBIS = 2; - private static final int ID_DUMMY = 3; + private static final String WEBM_DOC_TYPE = "webm"; private WebmExtractor extractor; private TestExtractorOutput extractorOutput; @@ -95,68 +86,113 @@ public class WebmExtractorTest extends InstrumentationTestCase { } public void testReadInitializationSegment() throws IOException, InterruptedException { - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, null)); - assertVideoFormat(); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .build(1); + + consume(data); + + assertVp9VideoFormat(); assertIndex(new IndexPoint(0, 0, TEST_DURATION_US)); } public void testPrepareOpus() throws IOException, InterruptedException { - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_OPUS}, null)); - assertAudioFormat(ID_OPUS); + 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) + .build(1); + + consume(data); + + assertAudioFormat(MimeTypes.AUDIO_OPUS); assertIndex(new IndexPoint(0, 0, TEST_DURATION_US)); } public void testPrepareVorbis() throws IOException, InterruptedException { - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VORBIS}, null)); - assertAudioFormat(ID_VORBIS); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVorbisTrack(TEST_CHANNEL_COUNT, TEST_SAMPLE_RATE, getVorbisCodecPrivate()) + .build(1); + + consume(data); + + assertAudioFormat(MimeTypes.AUDIO_VORBIS); assertIndex(new IndexPoint(0, 0, TEST_DURATION_US)); } public void testPrepareTwoTracks() throws IOException, InterruptedException { - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9, ID_OPUS}, null)); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .addOpusTrack(TEST_CHANNEL_COUNT, TEST_SAMPLE_RATE, TEST_CODEC_DELAY, + TEST_SEEK_PRE_ROLL, TEST_OPUS_CODEC_PRIVATE) + .build(1); + + consume(data); + assertEquals(2, extractorOutput.numberOfTracks); - assertVideoFormat(); - assertAudioFormat(ID_OPUS); + assertVp9VideoFormat(); + assertAudioFormat(MimeTypes.AUDIO_OPUS); assertIndex(new IndexPoint(0, 0, TEST_DURATION_US)); } public void testPrepareThreeTracks() throws IOException, InterruptedException { - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9, ID_DUMMY, ID_OPUS}, null)); - // Eventhough the input stream has 3 tracks, only 2 of them are supported and will be reported. + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .addUnsupportedTrack() + .addOpusTrack(TEST_CHANNEL_COUNT, TEST_SAMPLE_RATE, TEST_CODEC_DELAY, + TEST_SEEK_PRE_ROLL, TEST_OPUS_CODEC_PRIVATE) + .build(1); + + consume(data); + + // Even though the input stream has 3 tracks, only 2 of them are supported and will be reported. assertEquals(2, extractorOutput.numberOfTracks); - assertVideoFormat(); - assertAudioFormat(ID_OPUS); + assertVp9VideoFormat(); + assertAudioFormat(MimeTypes.AUDIO_OPUS); assertIndex(new IndexPoint(0, 0, TEST_DURATION_US)); } public void testPrepareFourTracks() throws IOException, InterruptedException { - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9, ID_VORBIS, ID_VP9, ID_OPUS}, null)); - // Eventhough the input stream has 4 supported tracks, only the first video and audio track will - // be reported. + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .addVorbisTrack(TEST_CHANNEL_COUNT, TEST_SAMPLE_RATE, getVorbisCodecPrivate()) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .addOpusTrack(TEST_CHANNEL_COUNT, TEST_SAMPLE_RATE, TEST_CODEC_DELAY, + TEST_SEEK_PRE_ROLL, TEST_OPUS_CODEC_PRIVATE) + .build(1); + + consume(data); + + // Even though the input stream has 4 supported tracks, only the first video and audio track + // will be reported. assertEquals(2, extractorOutput.numberOfTracks); - assertVideoFormat(); - assertAudioFormat(ID_VORBIS); + assertVp9VideoFormat(); + assertAudioFormat(MimeTypes.AUDIO_VORBIS); assertIndex(new IndexPoint(0, 0, TEST_DURATION_US)); } public void testPrepareContentEncodingEncryption() throws IOException, InterruptedException { - ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1); - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, settings)); - assertVideoFormat(); + ContentEncodingSettings settings = new StreamBuilder.ContentEncodingSettings(0, 1, 1, 5, 1); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, settings) + .build(1); + + consume(data); + + assertVp9VideoFormat(); assertIndex(new IndexPoint(0, 0, TEST_DURATION_US)); DrmInitData drmInitData = extractorOutput.drmInitData; assertNotNull(drmInitData); @@ -165,10 +201,15 @@ public class WebmExtractorTest extends InstrumentationTestCase { } public void testPrepareThreeCuePoints() throws IOException, InterruptedException { - consume( - createInitializationSegment(3, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, null)); - assertVideoFormat(); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .build(3); + + consume(data); + + assertVp9VideoFormat(); assertIndex( new IndexPoint(0, 0, 10000), new IndexPoint(10000, 0, 10000), @@ -176,8 +217,15 @@ public class WebmExtractorTest extends InstrumentationTestCase { } public void testPrepareCustomTimecodeScale() throws IOException, InterruptedException { - consume(createInitializationSegment(3, 0, true, 1000, new int[] {ID_VP9}, null)); - assertVideoFormat(); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(1000, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .build(3); + + consume(data); + + assertVp9VideoFormat(); assertIndex( new IndexPoint(0, 0, 10), new IndexPoint(10, 0, 10), @@ -185,21 +233,38 @@ public class WebmExtractorTest extends InstrumentationTestCase { } public void testPrepareNoCuePoints() throws IOException, InterruptedException { + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .build(0); try { - consume( - createInitializationSegment(0, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, null)); + consume(data); fail(); } catch (ParserException exception) { assertEquals("Invalid/missing cue points", exception.getMessage()); } } + public void testAcceptsWebmDocType() throws IOException, InterruptedException { + byte[] data = new StreamBuilder() + .setHeader(WEBM_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") + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .build(1); try { - consume( - createInitializationSegment(1, 0, false, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, null)); + consume(data); fail(); } catch (ParserException exception) { assertEquals("DocType webB not supported", exception.getMessage()); @@ -208,10 +273,13 @@ public class WebmExtractorTest extends InstrumentationTestCase { public void testPrepareInvalidContentEncodingOrder() throws IOException, InterruptedException { ContentEncodingSettings settings = new ContentEncodingSettings(1, 1, 1, 5, 1); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, settings) + .build(1); try { - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, settings)); + consume(data); fail(); } catch (ParserException exception) { assertEquals("ContentEncodingOrder 1 not supported", exception.getMessage()); @@ -220,10 +288,13 @@ public class WebmExtractorTest extends InstrumentationTestCase { public void testPrepareInvalidContentEncodingScope() throws IOException, InterruptedException { ContentEncodingSettings settings = new ContentEncodingSettings(0, 0, 1, 5, 1); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, settings) + .build(1); try { - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, settings)); + consume(data); fail(); } catch (ParserException exception) { assertEquals("ContentEncodingScope 0 not supported", exception.getMessage()); @@ -232,10 +303,13 @@ public class WebmExtractorTest extends InstrumentationTestCase { public void testPrepareInvalidContentEncodingType() throws IOException, InterruptedException { ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 0, 5, 1); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, settings) + .build(1); try { - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, settings)); + consume(data); fail(); } catch (ParserException exception) { assertEquals("ContentEncodingType 0 not supported", exception.getMessage()); @@ -244,10 +318,13 @@ public class WebmExtractorTest extends InstrumentationTestCase { public void testPrepareInvalidContentEncAlgo() throws IOException, InterruptedException { ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 4, 1); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, settings) + .build(1); try { - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, settings)); + consume(data); fail(); } catch (ParserException exception) { assertEquals("ContentEncAlgo 4 not supported", exception.getMessage()); @@ -256,10 +333,13 @@ public class WebmExtractorTest extends InstrumentationTestCase { public void testPrepareInvalidAESSettingsCipherMode() throws IOException, InterruptedException { ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 0); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, settings) + .build(1); try { - consume( - createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, settings)); + consume(data); fail(); } catch (ParserException exception) { assertEquals("AESSettingsCipherMode 0 not supported", exception.getMessage()); @@ -267,110 +347,136 @@ public class WebmExtractorTest extends InstrumentationTestCase { } public void testReadSampleKeyframe() throws IOException, InterruptedException { - MediaSegment mediaSegment = - createMediaSegment(100, 0, 0, true, false, true, false, false, 1); - byte[] testInputData = joinByteArrays( - createInitializationSegment( - 1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, null), - mediaSegment.clusterBytes); - consume(testInputData); - assertVideoFormat(); - assertSample(mediaSegment, 0, true, false, false, videoOutput); + byte[] media = createFrameData(100); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .addSimpleBlockMedia(1 /* trackNumber */, 0 /* clusterTimecode */, 0 /* blockTimecode */, + true /* keyframe */, false /* invisible */, media) + .build(1); + + consume(data); + + assertVp9VideoFormat(); + assertSample(media, 0, true, false, false, videoOutput); } public void testReadTwoTrackSamples() throws IOException, InterruptedException { - MediaSegment mediaSegmentAudio = - createMediaSegment(100, 0, 0, true, false, true, false, false, 2); - MediaSegment mediaSegmentVideo = - createMediaSegment(100, 0, 0, true, false, true, false, false, 1); - byte[] testInputData = joinByteArrays( - createInitializationSegment( - 1, mediaSegmentAudio.clusterBytes.length + mediaSegmentVideo.clusterBytes.length, - true, DEFAULT_TIMECODE_SCALE, new int[] {ID_VP9, ID_OPUS}, null), - mediaSegmentVideo.clusterBytes, mediaSegmentAudio.clusterBytes); - consume(testInputData); + byte[] media = createFrameData(100); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .addOpusTrack(TEST_CHANNEL_COUNT, TEST_SAMPLE_RATE, TEST_CODEC_DELAY, + TEST_SEEK_PRE_ROLL, TEST_OPUS_CODEC_PRIVATE) + .addSimpleBlockMedia(1 /* trackNumber */, 0 /* clusterTimecode */, 0 /* blockTimecode */, + true /* keyframe */, false /* invisible */, media) + .addSimpleBlockMedia(2 /* trackNumber */, 0 /* clusterTimecode */, 0 /* blockTimecode */, + true /* keyframe */, false /* invisible */, media) + .build(1); + + consume(data); + assertEquals(2, extractorOutput.numberOfTracks); - assertVideoFormat(); - assertAudioFormat(ID_OPUS); - assertSample(mediaSegmentVideo, 0, true, false, false, videoOutput); - assertSample(mediaSegmentAudio, 0, true, false, false, audioOutput); + assertVp9VideoFormat(); + assertAudioFormat(MimeTypes.AUDIO_OPUS); + assertSample(media, 0, true, false, false, videoOutput); + assertSample(media, 0, true, false, false, audioOutput); } public void testReadTwoTrackSamplesWithSkippedTrack() throws IOException, InterruptedException { - MediaSegment mediaSegmentAudio = - createMediaSegment(100, 0, 0, true, false, true, false, false, 2); - MediaSegment mediaSegmentVideo = - createMediaSegment(100, 0, 0, true, false, true, false, false, 1); - MediaSegment mediaSegmentDummy = - createMediaSegment(100, 0, 0, true, false, true, false, false, 17); - byte[] testInputData = joinByteArrays( - createInitializationSegment( - 1, - mediaSegmentAudio.clusterBytes.length + mediaSegmentVideo.clusterBytes.length - + mediaSegmentDummy.clusterBytes.length, - true, DEFAULT_TIMECODE_SCALE, new int[] {ID_DUMMY, ID_VP9, ID_OPUS}, null), - mediaSegmentVideo.clusterBytes, - mediaSegmentDummy.clusterBytes, - mediaSegmentAudio.clusterBytes); - consume(testInputData); + byte[] media = createFrameData(100); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addUnsupportedTrack() + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .addOpusTrack(TEST_CHANNEL_COUNT, TEST_SAMPLE_RATE, TEST_CODEC_DELAY, + TEST_SEEK_PRE_ROLL, TEST_OPUS_CODEC_PRIVATE) + .addSimpleBlockMedia(1 /* trackNumber */, 0 /* clusterTimecode */, 0 /* blockTimecode */, + true /* keyframe */, false /* invisible */, media) + .addSimpleBlockMedia(2 /* trackNumber */, 0 /* clusterTimecode */, 0 /* blockTimecode */, + true /* keyframe */, false /* invisible */, media) + .addSimpleBlockMedia(17 /* trackNumber */, 0 /* clusterTimecode */, 0 /* blockTimecode */, + true /* keyframe */, false /* invisible */, media) + .build(1); + + consume(data); + assertEquals(2, extractorOutput.numberOfTracks); - assertVideoFormat(); - assertAudioFormat(ID_OPUS); - assertSample(mediaSegmentVideo, 0, true, false, false, videoOutput); - assertSample(mediaSegmentAudio, 0, true, false, false, audioOutput); + assertVp9VideoFormat(); + assertAudioFormat(MimeTypes.AUDIO_OPUS); + assertSample(media, 0, true, false, false, videoOutput); + assertSample(media, 0, true, false, false, audioOutput); } public void testReadBlock() throws IOException, InterruptedException { - MediaSegment mediaSegment = - createMediaSegment(100, 0, 0, true, false, false, false, false, 2); - byte[] testInputData = joinByteArrays( - createInitializationSegment( - 1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_OPUS}, null), - mediaSegment.clusterBytes); - consume(testInputData); - assertAudioFormat(ID_OPUS); - assertSample(mediaSegment, 0, true, false, false, audioOutput); + 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) + .addBlockMedia(2 /* trackNumber */, 0 /* clusterTimecode */, 0 /* blockTimecode */, + true /* keyframe */, false /* invisible */, media) + .build(1); + + consume(data); + + assertAudioFormat(MimeTypes.AUDIO_OPUS); + assertSample(media, 0, true, false, false, audioOutput); } public void testReadBlockNonKeyframe() throws IOException, InterruptedException { - MediaSegment mediaSegment = - createMediaSegment(100, 0, 0, false, false, false, false, false, 1); - byte[] testInputData = joinByteArrays( - createInitializationSegment( - 1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, - new int[] { ID_VP9 }, null), - mediaSegment.clusterBytes); - consume(testInputData); - assertVideoFormat(); - assertSample(mediaSegment, 0, false, false, false, videoOutput); + byte[] media = createFrameData(100); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .addBlockMedia(1 /* trackNumber */, 0 /* clusterTimecode */, 0 /* blockTimecode */, + false /* keyframe */, false /* invisible */, media) + .build(1); + + consume(data); + + assertVp9VideoFormat(); + assertSample(media, 0, false, false, false, videoOutput); } public void testReadEncryptedFrame() throws IOException, InterruptedException { - MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true, true, true, 1); + byte[] media = createFrameData(100); ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1); - byte[] testInputData = joinByteArrays( - createInitializationSegment( - 1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, settings), - mediaSegment.clusterBytes); - consume(testInputData); - assertVideoFormat(); - assertSample(mediaSegment, 0, true, false, true, videoOutput); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, settings) + .addSimpleBlockEncryptedMedia(1 /* trackNumber */, 0 /* clusterTimecode */, + 0 /* blockTimecode */, true /* keyframe */, false /* invisible */, + true /* validSignalByte */, media) + .build(1); + + consume(data); + + assertVp9VideoFormat(); + assertSample(media, 0, true, false, true, videoOutput); } public void testReadEncryptedFrameWithInvalidSignalByte() throws IOException, InterruptedException { - MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true, true, false, 1); + byte[] media = createFrameData(100); ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1); - byte[] testInputData = joinByteArrays( - createInitializationSegment( - 1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, settings), - mediaSegment.clusterBytes); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, settings) + .addSimpleBlockEncryptedMedia(1 /* trackNumber */, 0 /* clusterTimecode */, + 0 /* blockTimecode */, true /* keyframe */, false /* invisible */, + false /* validSignalByte */, media) + .build(1); + try { - consume(testInputData); + consume(data); fail(); } catch (ParserException exception) { assertEquals("Extension bit is set in signal byte", exception.getMessage()); @@ -378,41 +484,51 @@ public class WebmExtractorTest extends InstrumentationTestCase { } public void testReadSampleInvisible() throws IOException, InterruptedException { - MediaSegment mediaSegment = - createMediaSegment(100, 12, 13, false, true, true, false, false, 1); - byte[] testInputData = joinByteArrays( - createInitializationSegment( - 1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, null), - mediaSegment.clusterBytes); - consume(testInputData); - assertVideoFormat(); - assertSample(mediaSegment, 25000, false, true, false, videoOutput); + byte[] media = createFrameData(100); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .addSimpleBlockMedia(1 /* trackNumber */, 12 /* clusterTimecode */, 13 /* blockTimecode */, + false /* keyframe */, true /* invisible */, media) + .build(1); + + consume(data); + + assertVp9VideoFormat(); + assertSample(media, 25000, false, true, false, videoOutput); } public void testReadSampleCustomTimescale() throws IOException, InterruptedException { - MediaSegment mediaSegment = - createMediaSegment(100, 12, 13, false, false, true, false, false, 1); - byte[] testInputData = joinByteArrays( - createInitializationSegment( - 1, mediaSegment.clusterBytes.length, true, 1000, new int[] {ID_VP9}, null), - mediaSegment.clusterBytes); - consume(testInputData); - assertVideoFormat(); - assertSample(mediaSegment, 25, false, false, false, videoOutput); + byte[] media = createFrameData(100); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(1000, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .addSimpleBlockMedia(1 /* trackNumber */, 12 /* clusterTimecode */, 13 /* blockTimecode */, + false /* keyframe */, false /* invisible */, media) + .build(1); + + consume(data); + + assertVp9VideoFormat(); + assertSample(media, 25, false, false, false, videoOutput); } public void testReadSampleNegativeSimpleBlockTimecode() throws IOException, InterruptedException { - MediaSegment mediaSegment = - createMediaSegment(100, 13, -12, true, true, true, false, false, 1); - byte[] testInputData = joinByteArrays( - createInitializationSegment( - 1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, - new int[] {ID_VP9}, null), - mediaSegment.clusterBytes); - consume(testInputData); - assertVideoFormat(); - assertSample(mediaSegment, 1000, true, true, false, videoOutput); + byte[] media = createFrameData(100); + byte[] data = new StreamBuilder() + .setHeader(WEBM_DOC_TYPE) + .setInfo(DEFAULT_TIMECODE_SCALE, TEST_DURATION_US) + .addVp9Track(TEST_WIDTH, TEST_HEIGHT, null) + .addSimpleBlockMedia(1 /* trackNumber */, 13 /* clusterTimecode */, -12 /* blockTimecode */, + true /* keyframe */, true /* invisible */, media) + .build(1); + + consume(data); + + assertVp9VideoFormat(); + assertSample(media, 1000, true, true, false, videoOutput); } private void consume(byte[] data) throws IOException, InterruptedException { @@ -431,25 +547,25 @@ public class WebmExtractorTest extends InstrumentationTestCase { return input; } - private void assertVideoFormat() { + private void assertVp9VideoFormat() { MediaFormat format = videoOutput.format; assertEquals(TEST_WIDTH, format.width); assertEquals(TEST_HEIGHT, format.height); assertEquals(MimeTypes.VIDEO_VP9, format.mimeType); } - private void assertAudioFormat(int codecId) { + private void assertAudioFormat(String expectedMimeType) { MediaFormat format = audioOutput.format; assertEquals(TEST_CHANNEL_COUNT, format.channelCount); assertEquals(TEST_SAMPLE_RATE, format.sampleRate); - if (codecId == ID_OPUS) { - assertEquals(MimeTypes.AUDIO_OPUS, format.mimeType); + assertEquals(expectedMimeType, format.mimeType); + if (MimeTypes.AUDIO_OPUS.equals(expectedMimeType)) { assertEquals(3, format.initializationData.size()); - assertEquals(TEST_OPUS_CODEC_PRIVATE_SIZE, format.initializationData.get(0).length); + android.test.MoreAsserts.assertEquals(TEST_OPUS_CODEC_PRIVATE, + format.initializationData.get(0)); assertEquals(TEST_CODEC_DELAY, ByteBuffer.wrap(format.initializationData.get(1)).getLong()); assertEquals(TEST_SEEK_PRE_ROLL, ByteBuffer.wrap(format.initializationData.get(2)).getLong()); - } else if (codecId == ID_VORBIS) { - assertEquals(MimeTypes.AUDIO_VORBIS, format.mimeType); + } else if (MimeTypes.AUDIO_VORBIS.equals(expectedMimeType)) { assertEquals(2, format.initializationData.size()); assertEquals(TEST_VORBIS_INFO_SIZE, format.initializationData.get(0).length); assertEquals(TEST_VORBIS_BOOKS_SIZE, format.initializationData.get(1).length); @@ -467,237 +583,21 @@ public class WebmExtractorTest extends InstrumentationTestCase { } } - private void assertSample(MediaSegment mediaSegment, int timeUs, boolean keyframe, + private void assertSample(byte[] expectedMedia, int timeUs, boolean keyframe, boolean invisible, boolean encrypted, TestTrackOutput output) { - byte[] expectedOutput = mediaSegment.data; if (encrypted) { - expectedOutput = joinByteArrays(new byte[] {(byte) TEST_INITIALIZATION_VECTOR.length}, - TEST_INITIALIZATION_VECTOR, expectedOutput); + expectedMedia = StreamBuilder.joinByteArrays( + new byte[] {(byte) StreamBuilder.TEST_INITIALIZATION_VECTOR.length}, + StreamBuilder.TEST_INITIALIZATION_VECTOR, expectedMedia); } - assertTrue(Arrays.equals(expectedOutput, output.sampleData)); + 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); } - private byte[] createInitializationSegment(int cuePoints, int mediaSegmentSize, - boolean docTypeIsWebm, int timecodeScale, int[] codecIds, - ContentEncodingSettings contentEncodingSettings) { - byte[] tracksElement = createTracksElement(codecIds, contentEncodingSettings); - byte[] infoElement = createInfoElement(timecodeScale); - byte[] cuesElement = createCuesElement(CUE_POINT_ELEMENT_BYTE_SIZE * cuePoints); - int initalizationSegmentSize = infoElement.length + tracksElement.length - + cuesElement.length + CUE_POINT_ELEMENT_BYTE_SIZE * cuePoints; - byte[] segmentElement = createSegmentElement(initalizationSegmentSize + mediaSegmentSize); - byte[] bytes = joinByteArrays(createEbmlElement(1, docTypeIsWebm, 2), - segmentElement, infoElement, tracksElement, cuesElement); - for (int i = 0; i < cuePoints; i++) { - bytes = joinByteArrays(bytes, createCuePointElement(10 * i, initalizationSegmentSize)); - } - return bytes; - } - - private byte[] createTracksElement(int[] codecIds, - ContentEncodingSettings contentEncodingSettings) { - byte[] trackBytes = new byte[0]; - for (int codecId : codecIds) { - switch (codecId) { - case ID_VP9: - trackBytes = joinByteArrays(trackBytes, - createVideoTrackEntry(true, TEST_WIDTH, TEST_HEIGHT, contentEncodingSettings)); - break; - case ID_OPUS: - trackBytes = joinByteArrays(trackBytes, createOpusAudioTrackEntry(TEST_CHANNEL_COUNT)); - break; - case ID_VORBIS: - trackBytes = joinByteArrays(trackBytes, createVorbisAudioTrackEntry(TEST_CHANNEL_COUNT)); - break; - case ID_DUMMY: - trackBytes = joinByteArrays(trackBytes, createUnsupportedTrackEntry()); - break; - } - } - byte[] tracksSize = getIntegerBytes(trackBytes.length); - byte[] tracksHeader = createByteArray( - 0x16, 0x54, 0xAE, 0x6B, // Tracks - 0x01, 0x00, 0x00, 0x00, tracksSize[0], tracksSize[1], tracksSize[2], tracksSize[3]); - return joinByteArrays(tracksHeader, trackBytes); - } - - private static MediaSegment createMediaSegment(int dataLength, int clusterTimecode, - int blockTimecode, boolean keyframe, boolean invisible, boolean simple, - boolean encrypted, boolean validSignalByte, int trackNumber) { - byte[] data = createFrameData(dataLength); - byte[] blockBytes; - if (simple) { - blockBytes = createSimpleBlockElement(data.length, blockTimecode, - keyframe, invisible, true, encrypted, validSignalByte, trackNumber); - } else { - blockBytes = createBlockElement(data.length, blockTimecode, - keyframe, invisible, true, trackNumber); - } - byte[] clusterBytes = - createClusterElement(blockBytes.length + data.length, clusterTimecode); - return new MediaSegment(joinByteArrays(clusterBytes, blockBytes, data), data); - } - - private static byte[] joinByteArrays(byte[]... byteArrays) { - int length = 0; - for (byte[] byteArray : byteArrays) { - length += byteArray.length; - } - byte[] joined = new byte[length]; - length = 0; - for (byte[] byteArray : byteArrays) { - System.arraycopy(byteArray, 0, joined, length, byteArray.length); - length += byteArray.length; - } - return joined; - } - - private static byte[] createEbmlElement( - int ebmlReadVersion, boolean docTypeIsWebm, int docTypeReadVersion) { - return createByteArray( - 0x1A, 0x45, 0xDF, 0xA3, // EBML - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, // size=15 - 0x42, 0xF7, // EBMLReadVersion - 0x81, ebmlReadVersion, // size=1 - 0x42, 0x82, // DocType - 0x84, 0x77, 0x65, 0x62, docTypeIsWebm ? 0x6D : 0x42, // size=4 value=webm/B - 0x42, 0x85, // DocTypeReadVersion - 0x81, docTypeReadVersion); // size=1 - } - - private static byte[] createSegmentElement(int size) { - byte[] sizeBytes = getIntegerBytes(size); - return createByteArray( - 0x18, 0x53, 0x80, 0x67, // Segment - 0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3]); - } - - private static byte[] createInfoElement(int timecodeScale) { - byte[] scaleBytes = getIntegerBytes(timecodeScale); - return createByteArray( - 0x15, 0x49, 0xA9, 0x66, // Info - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, // size=19 - 0x2A, 0xD7, 0xB1, // TimecodeScale - 0x84, scaleBytes[0], scaleBytes[1], scaleBytes[2], scaleBytes[3], // size=4 - 0x44, 0x89, // Duration - 0x88, 0x40, 0xC3, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00); // size=8 value=9920.0 - } - - private static byte[] createVideoTrackEntry( - boolean codecIsVp9, int pixelWidth, int pixelHeight, - ContentEncodingSettings contentEncodingSettings) { - byte[] widthBytes = getIntegerBytes(pixelWidth); - byte[] heightBytes = getIntegerBytes(pixelHeight); - if (contentEncodingSettings != null) { - byte[] orderBytes = getIntegerBytes(contentEncodingSettings.order); - byte[] scopeBytes = getIntegerBytes(contentEncodingSettings.scope); - byte[] typeBytes = getIntegerBytes(contentEncodingSettings.type); - byte[] algorithmBytes = getIntegerBytes(contentEncodingSettings.algorithm); - byte[] cipherModeBytes = getIntegerBytes(contentEncodingSettings.aesCipherMode); - return createByteArray( - 0xAE, // TrackEntry - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, // size=69 - 0x86, // CodecID - 0x85, 0x56, 0x5F, 0x56, 0x50, codecIsVp9 ? 0x39 : 0x30, // size=5 value=V_VP9/0 - 0xD7, // TrackNumber - 0x81, 0x01, // size=1 value=1 - 0x83, // TrackType - 0x81, 0x01, // size=1 value=1 - 0x6D, 0x80, // ContentEncodings - 0xA4, // size=36 - 0x62, 0x40, // ContentEncoding - 0xA1, // size=33 - 0x50, 0x31, // ContentEncodingOrder - 0x81, orderBytes[3], - 0x50, 0x32, // ContentEncodingScope - 0x81, scopeBytes[3], - 0x50, 0x33, // ContentEncodingType - 0x81, typeBytes[3], - 0x50, 0x35, // ContentEncryption - 0x92, // size=18 - 0x47, 0xE1, // ContentEncAlgo - 0x81, algorithmBytes[3], - 0x47, 0xE2, // ContentEncKeyID - 0x84, // size=4 - TEST_ENCRYPTION_KEY_ID[0], TEST_ENCRYPTION_KEY_ID[1], - TEST_ENCRYPTION_KEY_ID[2], TEST_ENCRYPTION_KEY_ID[3], // value=binary - 0x47, 0xE7, // ContentEncAESSettings - 0x84, // size=4 - 0x47, 0xE8, // AESSettingsCipherMode - 0x81, cipherModeBytes[3], - 0xE0, // Video - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // size=8 - 0xB0, // PixelWidth - 0x82, widthBytes[2], widthBytes[3], // size=2 - 0xBA, // PixelHeight - 0x82, heightBytes[2], heightBytes[3]); // size=2 - } else { - return createByteArray( - 0xAE, // TrackEntry - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, // size=30 - 0x86, // CodecID - 0x85, 0x56, 0x5F, 0x56, 0x50, codecIsVp9 ? 0x39 : 0x30, // size=5 value=V_VP9/0 - 0xD7, // TrackNumber - 0x81, 0x01, // size=1 value=1 - 0x83, // TrackType - 0x81, 0x01, // size=1 value=1 - 0xE0, // Video - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // size=8 - 0xB0, // PixelWidth - 0x82, widthBytes[2], widthBytes[3], // size=2 - 0xBA, // PixelHeight - 0x82, heightBytes[2], heightBytes[3]); // size=2 - } - } - - private static byte[] createOpusAudioTrackEntry(int channelCount) { - byte[] channelCountBytes = getIntegerBytes(channelCount); - return createByteArray( - 0xAE, // TrackEntry - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, // size=54 - 0x86, // CodecID - 0x86, 0x41, 0x5F, 0x4F, 0x50, 0x55, 0x53, // size=6 value=A_OPUS - 0xD7, // TrackNumber - 0x81, 0x02, // size=1 value=2 - 0x83, // TrackType - 0x81, 0x02, // size=1 value=2 - 0x56, 0xAA, // CodecDelay - 0x83, 0x63, 0x2E, 0xA0, // size=3 value=6500000 - 0x56, 0xBB, // SeekPreRoll - 0x84, 0x04, 0xC4, 0xB4, 0x00, // size=4 value=80000000 - 0xE1, // Audio - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, // size=13 - 0x9F, // Channels - 0x81, channelCountBytes[3], // size=1 - 0xB5, // SamplingFrequency - 0x88, 0x40, 0xE7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, // size=8 value=48000 - 0x63, 0xA2, // CodecPrivate - 0x82, 0x00, 0x00); // size=2 - } - - private byte[] createVorbisAudioTrackEntry(int channelCount) { - byte[] channelCountBytes = getIntegerBytes(channelCount); - byte[] tracksElement = createByteArray( - 0xAE, // TrackEntry - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x99, // size=4249 (42+4207) - 0x86, // CodecID - 0x88, 0x41, 0x5f, 0x56, 0x4f, 0x52, 0x42, 0x49, 0x53, // size=8 value=A_VORBIS - 0xD7, // TrackNumber - 0x81, 0x02, // size=1 value=2 - 0x83, // TrackType - 0x81, 0x02, // size=1 value=2 - 0xE1, // Audio - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, // size=13 - 0x9F, // Channels - 0x81, channelCountBytes[3], // size=1 - 0xB5, // SamplingFrequency - 0x88, 0x40, 0xE7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, // size=8 value=48000 - 0x63, 0xA2, // CodecPrivate - 0x50, 0x6F); // size=4207 + private byte[] getVorbisCodecPrivate() { byte[] codecPrivate = new byte[4207]; try { getInstrumentation().getContext().getResources().getAssets().open(TEST_VORBIS_CODEC_PRIVATE) @@ -705,99 +605,7 @@ public class WebmExtractorTest extends InstrumentationTestCase { } catch (IOException e) { fail(); // should never happen } - return joinByteArrays(tracksElement, codecPrivate); - } - - private static byte[] createUnsupportedTrackEntry() { - return createByteArray( - 0xAE, // TrackEntry - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, // size=32 - 0x86, // CodecID - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, // size =17 - 0x44, 0x5f, 0x57, 0x45, 0x42, 0x56, 0x54, 0x54, - 0x2f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, // value=D_WEBVTT/metadata - 0xD7, // TrackNumber - 0x81, 0x03, // size=1 value=3 - 0x83, // TrackType - 0x81, 0x11); // size=1 value=11 - } - - private static byte[] createCuesElement(int size) { - byte[] sizeBytes = getIntegerBytes(size); - return createByteArray( - 0x1C, 0x53, 0xBB, 0x6B, // Cues - 0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3]); - } - - private static byte[] createCuePointElement(int cueTime, int cueClusterPosition) { - byte[] positionBytes = getIntegerBytes(cueClusterPosition); - return createByteArray( - 0xBB, // CuePoint - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, // size=22 - 0xB3, // CueTime - 0x81, cueTime, // size=1 - 0xB7, // CueTrackPositions - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, // size=10 - 0xF1, // CueClusterPosition - 0x88, 0x00, 0x00, 0x00, 0x00, positionBytes[0], positionBytes[1], - positionBytes[2], positionBytes[3]); // size=8 - } - - private static byte[] createClusterElement(int size, int timecode) { - byte[] sizeBytes = getIntegerBytes(size); - byte[] timeBytes = getIntegerBytes(timecode); - return createByteArray( - 0x1F, 0x43, 0xB6, 0x75, // Cluster - 0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3], - 0xE7, // Timecode - 0x84, timeBytes[0], timeBytes[1], timeBytes[2], timeBytes[3]); // size=4 - } - - private static byte[] createSimpleBlockElement( - int size, int timecode, boolean keyframe, boolean invisible, boolean noLacing, - boolean encrypted, boolean validSignalByte, int trackNumber) { - byte[] sizeBytes = getIntegerBytes(size + 5 + (encrypted ? 9 : 0)); - byte[] timeBytes = getIntegerBytes(timecode); - byte[] trackNumberBytes = getIntegerBytes(trackNumber); - byte flags = (byte) - ((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00) | (noLacing ? 0x00 : 0x06)); - byte[] simpleBlock = createByteArray( - 0xA3, // SimpleBlock - 0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3], - 0x40, trackNumberBytes[3], // Track number size=2 - timeBytes[2], timeBytes[3], flags); // Timecode and flags - if (encrypted) { - simpleBlock = joinByteArrays( - simpleBlock, createByteArray(validSignalByte ? 0x01 : 0x80), - Arrays.copyOfRange(TEST_INITIALIZATION_VECTOR, 0, 8)); - } - return simpleBlock; - } - - private static byte[] createBlockElement( - int size, int timecode, boolean keyframe, boolean invisible, boolean noLacing, - int trackNumber) { - int blockSize = size + 5; - byte[] blockSizeBytes = getIntegerBytes(blockSize); - byte[] timeBytes = getIntegerBytes(timecode); - byte[] trackNumberBytes = getIntegerBytes(trackNumber); - // Size of blockgroup = id + size + size of reference block + length of data. - int blockGroupElementSize = 1 + 8 + (keyframe ? 0 : 3) + blockSize; - byte[] sizeBytes = getIntegerBytes(blockGroupElementSize); - byte flags = (byte) ((invisible ? 0x08 : 0x00) | (noLacing ? 0x00 : 0x06)); - byte[] blockGroupHeader = createByteArray( - 0xA0, // BlockGroup - 0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3]); - byte[] referenceBlock = keyframe ? new byte[0] : createByteArray( - 0xFB, // ReferenceBlock - 0x81, 0x00); // size=1 value=0 - byte[] blockData = createByteArray( - 0xA1, // Block - 0x01, 0x00, 0x00, 0x00, - blockSizeBytes[0], blockSizeBytes[1], blockSizeBytes[2], blockSizeBytes[3], - 0x40, trackNumberBytes[3], // Track number size=2 - timeBytes[2], timeBytes[3], flags); // Timecode and flags - return joinByteArrays(blockGroupHeader, referenceBlock, blockData); + return codecPrivate; } private static byte[] createFrameData(int size) { @@ -808,35 +616,6 @@ public class WebmExtractorTest extends InstrumentationTestCase { return data; } - private static byte[] getIntegerBytes(int value) { - return createByteArray( - (value & 0xFF000000) >> 24, - (value & 0x00FF0000) >> 16, - (value & 0x0000FF00) >> 8, - (value & 0x000000FF)); - } - - 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; - } - - /** Used by {@link #createMediaSegment} to return both cluster and video bytes together. */ - private static final class MediaSegment { - - private final byte[] clusterBytes; - private final byte[] data; - - private MediaSegment(byte[] clusterBytes, byte[] data) { - this.clusterBytes = clusterBytes; - this.data = data; - } - - } - /** Used by {@link #assertIndex(IndexPoint...)} to validate index elements. */ private static final class IndexPoint { @@ -852,26 +631,6 @@ public class WebmExtractorTest extends InstrumentationTestCase { } - /** Used by {@link #createVideoTrackEntry} to create a Track header with Encryption. */ - private static final class ContentEncodingSettings { - - private final int order; - private final int scope; - private final int type; - private final int algorithm; - private final int aesCipherMode; - - private ContentEncodingSettings(int order, int scope, int type, int algorithm, - int aesCipherMode) { - this.order = order; - this.scope = scope; - this.type = type; - this.algorithm = algorithm; - this.aesCipherMode = aesCipherMode; - } - - } - /** Implements {@link ExtractorOutput} for test purposes. */ public class TestExtractorOutput implements ExtractorOutput { @@ -922,7 +681,8 @@ public class WebmExtractorTest extends InstrumentationTestCase { InterruptedException { byte[] newData = new byte[length]; input.readFully(newData, 0, length); - sampleData = sampleData == null ? newData : joinByteArrays(sampleData, newData); + sampleData = + sampleData == null ? newData : StreamBuilder.joinByteArrays(sampleData, newData); return length; } @@ -930,7 +690,8 @@ 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 : joinByteArrays(sampleData, newData); + sampleData = + sampleData == null ? newData : StreamBuilder.joinByteArrays(sampleData, newData); } @Override