From 270543555d87fbd0d5f048e7e63430e54c7b817b Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 27 Nov 2024 09:44:44 -0800 Subject: [PATCH] Add `getSampleCryptoInfo` API to `MediaExtractorCompat` This method enables handling encrypted samples by providing the necessary decryption details. PiperOrigin-RevId: 700729949 --- RELEASENOTES.md | 2 + .../exoplayer/MediaExtractorCompatTest.java | 67 +++++++++++++++++++ .../exoplayer/MediaExtractorCompat.java | 33 +++++++++ 3 files changed, 102 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1ce272571a..36a2dba905 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,8 @@ * Reduce default values for `bufferForPlaybackMs` and `bufferForPlaybackAfterRebufferMs` in `DefaultLoadControl` to 1000 and 2000 ms respectively. + * Add `MediaExtractorCompat`, a new class that provides equivalent + functionality to platform `MediaExtractor`. * Transformer: * Update parameters of `VideoFrameProcessor.registerInputStream` and `VideoFrameProcessor.Listener.onInputStreamRegistered` to use `Format`. diff --git a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/MediaExtractorCompatTest.java b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/MediaExtractorCompatTest.java index 4fe683d68f..75704876b0 100644 --- a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/MediaExtractorCompatTest.java +++ b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/MediaExtractorCompatTest.java @@ -26,6 +26,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assume.assumeTrue; import android.content.Context; +import android.media.MediaCodec; import android.media.MediaExtractor; import android.media.MediaFormat; import android.media.metrics.LogSessionId; @@ -60,6 +61,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import com.google.common.base.Function; import com.google.common.io.Files; +import com.google.common.primitives.Bytes; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; @@ -1063,6 +1065,70 @@ public class MediaExtractorCompatTest { assertThat(psshMap.get(WIDEVINE_UUID)).isEqualTo(rawSchemeData); } + @Test + public void + getSampleCryptoInfo_forEncryptedSample_returnsTrueAndPopulatesPlatformCryptoInfoCorrectly() + throws IOException { + TrackOutput.CryptoData cryptoData = + new TrackOutput.CryptoData( + /* cryptoMode= */ C.CRYPTO_MODE_AES_CTR, + /* encryptionKey= */ new byte[] {5, 6, 7, 8}, + /* encryptedBlocks= */ 0, + /* clearBlocks= */ 0); + byte[] sampleData = new byte[] {0, 1, 2}; + byte[] initializationVector = new byte[] {7, 6, 5, 4, 3, 2, 1, 0, 7, 6, 5, 4, 3, 2, 1, 0}; + byte[] encryptedSampleData = + Bytes.concat( + new byte[] { + 0x10, // subsampleEncryption = false (1 bit), ivSize = 16 (7 bits). + }, + initializationVector, + sampleData); + TrackOutput[] outputs = new TrackOutput[1]; + fakeExtractor.addReadAction( + (input, seekPosition) -> { + outputs[0] = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_VIDEO); + outputs[0].format(PLACEHOLDER_FORMAT_VIDEO); + extractorOutput.endTracks(); + return Extractor.RESULT_CONTINUE; + }); + mediaExtractorCompat.selectTrack(0); + fakeExtractor.addReadAction( + (input, seekPosition) -> { + outputSampleData(outputs[0], encryptedSampleData); + outputs[0].sampleMetadata( + /* timeUs= */ 0, + C.BUFFER_FLAG_KEY_FRAME | C.BUFFER_FLAG_ENCRYPTED, + /* size= */ encryptedSampleData.length, + /* offset= */ 0, + cryptoData); + return Extractor.RESULT_CONTINUE; + }); + + mediaExtractorCompat.setDataSource(PLACEHOLDER_URI, /* offset= */ 0); + + MediaCodec.CryptoInfo platformCryptoInfo = new MediaCodec.CryptoInfo(); + assertThat(mediaExtractorCompat.getSampleCryptoInfo(platformCryptoInfo)).isTrue(); + // Verify platform crypto info data. + assertThat(platformCryptoInfo.numSubSamples).isEqualTo(1); + assertThat(platformCryptoInfo.numBytesOfClearData).hasLength(1); + assertThat(platformCryptoInfo.numBytesOfClearData[0]).isEqualTo(0); + assertThat(platformCryptoInfo.numBytesOfEncryptedData).hasLength(1); + assertThat(platformCryptoInfo.numBytesOfEncryptedData[0]).isEqualTo(sampleData.length); + assertThat(platformCryptoInfo.key).isEqualTo(cryptoData.encryptionKey); + assertThat(platformCryptoInfo.iv).isEqualTo(initializationVector); + assertThat(platformCryptoInfo.mode).isEqualTo(cryptoData.cryptoMode); + // Verify sample data and flags. + assertThat(mediaExtractorCompat.getSampleFlags()) + .isEqualTo(MediaExtractor.SAMPLE_FLAG_SYNC | MediaExtractor.SAMPLE_FLAG_ENCRYPTED); + ByteBuffer buffer = ByteBuffer.allocate(sampleData.length); + assertThat(mediaExtractorCompat.readSampleData(buffer, /* offset= */ 0)) + .isEqualTo(sampleData.length); + for (int i = 0; i < buffer.remaining(); i++) { + assertThat(buffer.get()).isEqualTo(sampleData[i]); + } + } + // Internal methods. private void assertReadSample(int trackIndex, long timeUs, int size, byte... sampleData) { @@ -1070,6 +1136,7 @@ public class MediaExtractorCompatTest { assertThat(mediaExtractorCompat.getSampleTime()).isEqualTo(timeUs); assertThat(mediaExtractorCompat.getSampleFlags()).isEqualTo(MediaExtractor.SAMPLE_FLAG_SYNC); assertThat(mediaExtractorCompat.getSampleSize()).isEqualTo(size); + assertThat(mediaExtractorCompat.getSampleCryptoInfo(new MediaCodec.CryptoInfo())).isFalse(); ByteBuffer buffer = ByteBuffer.allocate(100); assertThat(mediaExtractorCompat.readSampleData(buffer, /* offset= */ 0)) .isEqualTo(sampleData.length); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaExtractorCompat.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaExtractorCompat.java index bef86856e5..6965ef7586 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaExtractorCompat.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaExtractorCompat.java @@ -25,6 +25,7 @@ import static java.lang.Math.max; import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; +import android.media.MediaCodec; import android.media.MediaDataSource; import android.media.MediaExtractor; import android.media.MediaFormat; @@ -594,6 +595,38 @@ public final class MediaExtractorCompat { return sampleMetadataQueue.peekFirst().flags; } + /** + * Returns {@code true} if the current sample is at least partially encrypted and fills the + * provided {@link MediaCodec.CryptoInfo} structure with relevant decryption information. + * + * @param info The {@link MediaCodec.CryptoInfo} structure to be filled with decryption data. + * @return {@code true} if the sample is at least partially encrypted, {@code false} otherwise. + */ + public boolean getSampleCryptoInfo(MediaCodec.CryptoInfo info) { + if (!advanceToSampleOrEndOfInput()) { + return false; + } + boolean isEncrypted = + (sampleMetadataQueue.peekFirst().flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0; + if (!isEncrypted) { + return false; + } + peekNextSelectedTrackSample(sampleHolderWithBufferReplacementEnabled); + populatePlatformCryptoInfoParameters(info); + return true; + } + + private void populatePlatformCryptoInfoParameters(MediaCodec.CryptoInfo info) { + MediaCodec.CryptoInfo platformCryptoInfo = + checkNotNull(sampleHolderWithBufferReplacementEnabled.cryptoInfo).getFrameworkCryptoInfo(); + info.numSubSamples = platformCryptoInfo.numSubSamples; + info.numBytesOfClearData = platformCryptoInfo.numBytesOfClearData; + info.numBytesOfEncryptedData = platformCryptoInfo.numBytesOfEncryptedData; + info.key = platformCryptoInfo.key; + info.iv = platformCryptoInfo.iv; + info.mode = platformCryptoInfo.mode; + } + /** Sets the {@link LogSessionId} for MediaExtractorCompat. */ @RequiresApi(31) public void setLogSessionId(LogSessionId logSessionId) {