diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 8a72373fe9..e23bf592a5 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -1,5 +1,13 @@
# Release notes #
+### 2.11.3 (2020-02-19) ###
+
+* SmoothStreaming: Fix regression that broke playback in 2.11.2
+ ([#6981](https://github.com/google/ExoPlayer/issues/6981)).
+* DRM: Fix issue switching from protected content that uses a 16-byte
+ initialization vector to one that uses an 8-byte initialization vector
+ ([#6982](https://github.com/google/ExoPlayer/issues/6982)).
+
### 2.11.2 (2020-02-13) ###
* Add Java FLAC extractor
diff --git a/constants.gradle b/constants.gradle
index 0594ab01e7..e29e5cb7c8 100644
--- a/constants.gradle
+++ b/constants.gradle
@@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
- releaseVersion = '2.11.2'
- releaseVersionCode = 2011002
+ releaseVersion = '2.11.3'
+ releaseVersionCode = 2011003
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
index d6760a359b..51d077270a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
- public static final String VERSION = "2.11.2";
+ public static final String VERSION = "2.11.3";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.2";
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.3";
/**
* The version of the library expressed as an integer, for example 1002003.
@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final int VERSION_INT = 2011002;
+ public static final int VERSION_INT = 2011003;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
index 379ca971b5..b865d5bb6f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
@@ -25,27 +25,41 @@ import com.google.android.exoplayer2.util.Util;
public final class CryptoInfo {
/**
+ * The 16 byte initialization vector. If the initialization vector of the content is shorter than
+ * 16 bytes, 0 byte padding is appended to extend the vector to the required 16 byte length.
+ *
* @see android.media.MediaCodec.CryptoInfo#iv
*/
public byte[] iv;
/**
+ * The 16 byte key id.
+ *
* @see android.media.MediaCodec.CryptoInfo#key
*/
public byte[] key;
/**
+ * The type of encryption that has been applied. Must be one of the {@link C.CryptoMode} values.
+ *
* @see android.media.MediaCodec.CryptoInfo#mode
*/
- @C.CryptoMode
- public int mode;
+ @C.CryptoMode public int mode;
/**
+ * The number of leading unencrypted bytes in each sub-sample. If null, all bytes are treated as
+ * encrypted and {@link #numBytesOfEncryptedData} must be specified.
+ *
* @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData
*/
public int[] numBytesOfClearData;
/**
+ * The number of trailing encrypted bytes in each sub-sample. If null, all bytes are treated as
+ * clear and {@link #numBytesOfClearData} must be specified.
+ *
* @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData
*/
public int[] numBytesOfEncryptedData;
/**
+ * The number of subSamples that make up the buffer's contents.
+ *
* @see android.media.MediaCodec.CryptoInfo#numSubSamples
*/
public int numSubSamples;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
index 856ca89385..804f69f0f9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
@@ -51,7 +51,13 @@ import java.lang.reflect.Constructor;
*
AC3 ({@link Ac3Extractor})
* AC4 ({@link Ac4Extractor})
* AMR ({@link AmrExtractor})
- * FLAC (only available if the FLAC extension is built and included)
+ * FLAC
+ *
+ * - If available, the FLAC extension extractor is used.
+ *
- Otherwise, the core {@link FlacExtractor} is used. Note that Android devices do not
+ * generally include a FLAC decoder before API 27. This can be worked around by using
+ * the FLAC extension or the FFmpeg extension.
+ *
*
*/
public final class DefaultExtractorsFactory implements ExtractorsFactory {
@@ -247,10 +253,6 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
: 0));
extractors[12] = new Ac4Extractor();
- // Prefer the FLAC extension extractor because it outputs raw audio, which can be handled by the
- // framework on all API levels, unlike the core library FLAC extractor, which outputs FLAC audio
- // frames and so relies on having a FLAC decoder (e.g., a MediaCodec decoder that handles FLAC
- // (from API 27), or the FFmpeg extension with FLAC enabled).
if (FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR != null) {
try {
extractors[13] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java
index 68761cef19..3779fe33e5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData;
@@ -27,6 +28,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.Arrays;
/** A queue of media sample data. */
/* package */ class SampleDataQueue {
@@ -228,10 +230,14 @@ import java.nio.ByteBuffer;
int ivSize = signalByte & 0x7F;
// Read the initialization vector.
- if (buffer.cryptoInfo.iv == null) {
- buffer.cryptoInfo.iv = new byte[16];
+ CryptoInfo cryptoInfo = buffer.cryptoInfo;
+ if (cryptoInfo.iv == null) {
+ cryptoInfo.iv = new byte[16];
+ } else {
+ // Zero out cryptoInfo.iv so that if ivSize < 16, the remaining bytes are correctly set to 0.
+ Arrays.fill(cryptoInfo.iv, (byte) 0);
}
- readData(offset, buffer.cryptoInfo.iv, ivSize);
+ readData(offset, cryptoInfo.iv, ivSize);
offset += ivSize;
// Read the subsample count, if present.
@@ -246,11 +252,11 @@ import java.nio.ByteBuffer;
}
// Write the clear and encrypted subsample sizes.
- int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData;
+ @Nullable int[] clearDataSizes = cryptoInfo.numBytesOfClearData;
if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
clearDataSizes = new int[subsampleCount];
}
- int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData;
+ @Nullable int[] encryptedDataSizes = cryptoInfo.numBytesOfEncryptedData;
if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
encryptedDataSizes = new int[subsampleCount];
}
@@ -271,12 +277,12 @@ import java.nio.ByteBuffer;
// Populate the cryptoInfo.
CryptoData cryptoData = extrasHolder.cryptoData;
- buffer.cryptoInfo.set(
+ cryptoInfo.set(
subsampleCount,
clearDataSizes,
encryptedDataSizes,
cryptoData.encryptionKey,
- buffer.cryptoInfo.iv,
+ cryptoInfo.iv,
cryptoData.cryptoMode,
cryptoData.encryptedBlocks,
cryptoData.clearBlocks);
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
index 583bdcb7be..427e81d29f 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source;
+import static com.google.android.exoplayer2.C.BUFFER_FLAG_ENCRYPTED;
import static com.google.android.exoplayer2.C.BUFFER_FLAG_KEY_FRAME;
import static com.google.android.exoplayer2.C.RESULT_BUFFER_READ;
import static com.google.android.exoplayer2.C.RESULT_FORMAT_READ;
@@ -22,6 +23,7 @@ import static com.google.android.exoplayer2.C.RESULT_NOTHING_READ;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.Long.MIN_VALUE;
import static java.util.Arrays.copyOfRange;
+import static org.junit.Assert.assertArrayEquals;
import static org.mockito.Mockito.when;
import androidx.annotation.Nullable;
@@ -114,17 +116,13 @@ public final class SampleQueueTest {
C.BUFFER_FLAG_KEY_FRAME, C.BUFFER_FLAG_ENCRYPTED, 0, C.BUFFER_FLAG_ENCRYPTED,
};
private static final long[] ENCRYPTED_SAMPLE_TIMESTAMPS = new long[] {0, 1000, 2000, 3000};
- private static final Format[] ENCRYPTED_SAMPLES_FORMATS =
+ private static final Format[] ENCRYPTED_SAMPLE_FORMATS =
new Format[] {FORMAT_ENCRYPTED, FORMAT_ENCRYPTED, FORMAT_1, FORMAT_ENCRYPTED};
/** Encrypted samples require the encryption preamble. */
- private static final int[] ENCRYPTED_SAMPLES_SIZES = new int[] {1, 3, 1, 3};
+ private static final int[] ENCRYPTED_SAMPLE_SIZES = new int[] {1, 3, 1, 3};
- private static final int[] ENCRYPTED_SAMPLES_OFFSETS = new int[] {7, 4, 3, 0};
- private static final byte[] ENCRYPTED_SAMPLES_DATA = new byte[8];
-
- static {
- Arrays.fill(ENCRYPTED_SAMPLES_DATA, (byte) 1);
- }
+ private static final int[] ENCRYPTED_SAMPLE_OFFSETS = new int[] {7, 4, 3, 0};
+ private static final byte[] ENCRYPTED_SAMPLE_DATA = new byte[] {1, 1, 1, 1, 1, 1, 1, 1};
private static final TrackOutput.CryptoData DUMMY_CRYPTO_DATA =
new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0);
@@ -461,6 +459,60 @@ public final class SampleQueueTest {
/* decodeOnlyUntilUs= */ 0);
assertThat(result).isEqualTo(RESULT_FORMAT_READ);
assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession);
+ assertReadEncryptedSample(/* sampleIndex= */ 3);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testTrailingCryptoInfoInitializationVectorBytesZeroed() {
+ when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
+ DrmSession mockPlaceholderDrmSession =
+ (DrmSession) Mockito.mock(DrmSession.class);
+ when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
+ when(mockDrmSessionManager.acquirePlaceholderSession(
+ ArgumentMatchers.any(), ArgumentMatchers.anyInt()))
+ .thenReturn(mockPlaceholderDrmSession);
+
+ writeFormat(ENCRYPTED_SAMPLE_FORMATS[0]);
+ byte[] sampleData = new byte[] {0, 1, 2};
+ byte[] initializationVector = new byte[] {7, 6, 5, 4, 3, 2, 1, 0};
+ byte[] encryptedSampleData =
+ TestUtil.joinByteArrays(
+ new byte[] {
+ 0x08, // subsampleEncryption = false (1 bit), ivSize = 8 (7 bits).
+ },
+ initializationVector,
+ sampleData);
+ writeSample(
+ encryptedSampleData, /* timestampUs= */ 0, BUFFER_FLAG_KEY_FRAME | BUFFER_FLAG_ENCRYPTED);
+
+ int result =
+ sampleQueue.read(
+ formatHolder,
+ inputBuffer,
+ /* formatRequired= */ false,
+ /* loadingFinished= */ false,
+ /* decodeOnlyUntilUs= */ 0);
+ assertThat(result).isEqualTo(RESULT_FORMAT_READ);
+
+ // Fill cryptoInfo.iv with non-zero data. When the 8 byte initialization vector is written into
+ // it, we expect the trailing 8 bytes to be zeroed.
+ inputBuffer.cryptoInfo.iv = new byte[16];
+ Arrays.fill(inputBuffer.cryptoInfo.iv, (byte) 1);
+
+ result =
+ sampleQueue.read(
+ formatHolder,
+ inputBuffer,
+ /* formatRequired= */ false,
+ /* loadingFinished= */ false,
+ /* decodeOnlyUntilUs= */ 0);
+ assertThat(result).isEqualTo(RESULT_BUFFER_READ);
+
+ // Assert cryptoInfo.iv contains the 8-byte initialization vector and that the trailing 8 bytes
+ // have been zeroed.
+ byte[] expectedInitializationVector = Arrays.copyOf(initializationVector, 16);
+ assertArrayEquals(expectedInitializationVector, inputBuffer.cryptoInfo.iv);
}
@Test
@@ -995,11 +1047,11 @@ public final class SampleQueueTest {
private void writeTestDataWithEncryptedSections() {
writeTestData(
- ENCRYPTED_SAMPLES_DATA,
- ENCRYPTED_SAMPLES_SIZES,
- ENCRYPTED_SAMPLES_OFFSETS,
+ ENCRYPTED_SAMPLE_DATA,
+ ENCRYPTED_SAMPLE_SIZES,
+ ENCRYPTED_SAMPLE_OFFSETS,
ENCRYPTED_SAMPLE_TIMESTAMPS,
- ENCRYPTED_SAMPLES_FORMATS,
+ ENCRYPTED_SAMPLE_FORMATS,
ENCRYPTED_SAMPLES_FLAGS);
}
@@ -1033,7 +1085,12 @@ public final class SampleQueueTest {
/** Writes a single sample to {@code sampleQueue}. */
private void writeSample(byte[] data, long timestampUs, int sampleFlags) {
sampleQueue.sampleData(new ParsableByteArray(data), data.length);
- sampleQueue.sampleMetadata(timestampUs, sampleFlags, data.length, 0, null);
+ sampleQueue.sampleMetadata(
+ timestampUs,
+ sampleFlags,
+ data.length,
+ /* offset= */ 0,
+ (sampleFlags & C.BUFFER_FLAG_ENCRYPTED) != 0 ? DUMMY_CRYPTO_DATA : null);
}
/**
@@ -1206,7 +1263,7 @@ public final class SampleQueueTest {
}
private void assertReadEncryptedSample(int sampleIndex) {
- byte[] sampleData = new byte[ENCRYPTED_SAMPLES_SIZES[sampleIndex]];
+ byte[] sampleData = new byte[ENCRYPTED_SAMPLE_SIZES[sampleIndex]];
Arrays.fill(sampleData, (byte) 1);
boolean isKeyFrame = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0;
boolean isEncrypted = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0;
@@ -1216,7 +1273,7 @@ public final class SampleQueueTest {
isEncrypted,
sampleData,
/* offset= */ 0,
- ENCRYPTED_SAMPLES_SIZES[sampleIndex] - (isEncrypted ? 2 : 0));
+ ENCRYPTED_SAMPLE_SIZES[sampleIndex] - (isEncrypted ? 2 : 0));
}
/**
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java
index 22dfb04f13..d005dac8da 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java
@@ -113,9 +113,12 @@ public class DefaultSsChunkSource implements SsChunkSource {
Track track = new Track(manifestTrackIndex, streamElement.type, streamElement.timescale,
C.TIME_UNSET, manifest.durationUs, format, Track.TRANSFORMATION_NONE,
trackEncryptionBoxes, nalUnitLengthFieldLength, null, null);
- FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
- FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
- | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track, null);
+ FragmentedMp4Extractor extractor =
+ new FragmentedMp4Extractor(
+ FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
+ | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX,
+ /* timestampAdjuster= */ null,
+ track);
extractorWrappers[i] = new ChunkExtractorWrapper(extractor, streamElement.type, format);
}
}