From cc5e981e894b4e4ed6dd9d16a0537c87f7b162d6 Mon Sep 17 00:00:00 2001 From: ybai001 Date: Mon, 23 Dec 2019 10:49:03 +0800 Subject: [PATCH 1/3] Add AC-4 DRM Support --- .../extractor/mp4/FragmentedMp4Extractor.java | 4 +++- .../exoplayer2/source/SampleDataQueue.java | 17 ++++++++++++----- .../android/exoplayer2/source/SampleQueue.java | 3 ++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 1172f8665a..792545b610 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -1222,6 +1222,7 @@ public class FragmentedMp4Extractor implements Extractor { * @throws InterruptedException If the thread is interrupted. */ private boolean readSample(ExtractorInput input) throws IOException, InterruptedException { + int outputSampleEncryptionDataSize = 0; if (parserState == STATE_READING_SAMPLE_START) { if (currentTrackBundle == null) { @Nullable TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles); @@ -1269,6 +1270,7 @@ public class FragmentedMp4Extractor implements Extractor { } sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData(); sampleSize += sampleBytesWritten; + outputSampleEncryptionDataSize = sampleBytesWritten; parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; isAc4HeaderRequired = @@ -1338,7 +1340,7 @@ public class FragmentedMp4Extractor implements Extractor { } } else { if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); + Ac4Util.getAc4SampleHeader(sampleSize - outputSampleEncryptionDataSize, scratch); int length = scratch.limit(); output.sampleData(scratch, length); sampleSize += length; 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..26a95b8de2 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 @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; import com.google.android.exoplayer2.source.SampleQueue.SampleExtrasHolder; import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; @@ -114,11 +115,13 @@ import java.nio.ByteBuffer; * * @param buffer The buffer to populate. * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + * @param mimeType The MIME type. */ - public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder, + String mimeType) { // Read encryption data if the sample is encrypted. if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder); + readEncryptionData(buffer, extrasHolder, mimeType); } // Read sample data, extracting supplemental data into a separate buffer if needed. if (buffer.hasSupplementalData()) { @@ -215,8 +218,10 @@ import java.nio.ByteBuffer; * * @param buffer The buffer into which the encryption data should be written. * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + * @param mimeType The MIME type. */ - private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder, + String mimeType) { long offset = extrasHolder.offset; // Read the signal byte. @@ -265,8 +270,10 @@ import java.nio.ByteBuffer; encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); } } else { - clearDataSizes[0] = 0; - encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); + int addedHeaderSize = MimeTypes.AUDIO_AC4.equals(mimeType) ? 7 : 0; + clearDataSizes[0] = addedHeaderSize; + encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset) + - addedHeaderSize; } // Populate the cryptoInfo. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index cc15d9d275..bfd3d7c4ed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -323,7 +323,8 @@ public class SampleQueue implements TrackOutput { readSampleMetadata( formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilUs, extrasHolder); if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream() && !buffer.isFlagsOnly()) { - sampleDataQueue.readToBuffer(buffer, extrasHolder); + sampleDataQueue.readToBuffer(buffer, extrasHolder, + downstreamFormat == null ? null : downstreamFormat.sampleMimeType); } return result; } From 4ce72d9d6d7cab11169430d69809ec55be694ec2 Mon Sep 17 00:00:00 2001 From: ybai001 Date: Thu, 9 Jan 2020 15:23:05 +0800 Subject: [PATCH 2/3] Update AC-4 DRM code based on comments Update cleardatasize[0] in extractor rather than sampleDataQueue. --- .../extractor/mp4/FragmentedMp4Extractor.java | 75 ++++++++++++++++--- .../exoplayer2/source/SampleDataQueue.java | 17 ++--- .../exoplayer2/source/SampleQueue.java | 3 +- 3 files changed, 70 insertions(+), 25 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index a4a70ce7e5..66ed1a5c92 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -1265,10 +1265,17 @@ public class FragmentedMp4Extractor implements Extractor { sampleSize -= Atom.HEADER_SIZE; input.skipFully(Atom.HEADER_SIZE); } - sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData(); - sampleSize += sampleBytesWritten; - if (MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType)) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); + + boolean isAc4HeaderRequired = + MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType); + + int encryptionDataBytesWritten = currentTrackBundle.outputSampleEncryptionData( + sampleSize, isAc4HeaderRequired ? Ac4Util.SAMPLE_HEADER_SIZE : 0); + sampleBytesWritten = encryptionDataBytesWritten; + sampleSize += encryptionDataBytesWritten; + + if (isAc4HeaderRequired) { + Ac4Util.getAc4SampleHeader(sampleSize - encryptionDataBytesWritten, scratch); currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; @@ -1555,9 +1562,13 @@ public class FragmentedMp4Extractor implements Extractor { /** * Outputs the encryption data for the current sample. * + * @param sampleSize The size of the current sample in bytes, excluding any additional clear + * header that will be prefixed to the sample by the extractor. + * @param clearHeaderSize The size of a clear header that will be prefixed to the sample by the + * extractor, or 0. * @return The number of written bytes. */ - public int outputSampleEncryptionData() { + public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) { TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); if (encryptionBox == null) { return 0; @@ -1576,24 +1587,66 @@ public class FragmentedMp4Extractor implements Extractor { vectorSize = initVectorData.length; } - boolean subsampleEncryption = fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); + boolean haveSubsampleEncryptionTable = + fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); + boolean writeSubsampleEncryptionData = + haveSubsampleEncryptionTable | clearHeaderSize != 0; // Write the signal byte, containing the vector size and the subsample encryption flag. - encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0)); + encryptionSignalByte.data[0] = + (byte) (vectorSize | (writeSubsampleEncryptionData ? 0x80 : 0)); encryptionSignalByte.setPosition(0); output.sampleData(encryptionSignalByte, 1); // Write the vector. output.sampleData(initializationVectorData, vectorSize); - // If we don't have subsample encryption data, we're done. - if (!subsampleEncryption) { + + if (!writeSubsampleEncryptionData) { return 1 + vectorSize; } - // Write the subsample encryption data. + + if (!haveSubsampleEncryptionTable) { + // Need to synthesize subsample encryption data. The sample is fully encrypted except + // for the additional header that the extractor is going to prefix, so we need to write the + // following to output.sampleData: + // subsampleCount (unsigned short) = 1 + // clearDataSizes[0] (unsigned short) = clearHeaderSize + // encryptedDataSizes[0] (unsigned int) = sampleSize + ParsableByteArray encryptionData = new ParsableByteArray(8); + encryptionData.data[0] = (byte)0; + encryptionData.data[1] = (byte)1; + encryptionData.data[2] = (byte)((clearHeaderSize & 0xFF00) >>> 8); + encryptionData.data[3] = (byte)( clearHeaderSize & 0x00FF); + encryptionData.data[4] = (byte)((sampleSize & 0xFF000000) >>> 24); + encryptionData.data[5] = (byte)((sampleSize & 0x00FF0000) >>> 16); + encryptionData.data[6] = (byte)((sampleSize & 0x0000FF00) >>> 8); + encryptionData.data[7] = (byte)( sampleSize & 0x000000FF); + encryptionData.setPosition(0); + output.sampleData(encryptionData, 8); + return 1 + vectorSize + 8; + } + ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData; int subsampleCount = subsampleEncryptionData.readUnsignedShort(); subsampleEncryptionData.skipBytes(-2); int subsampleDataLength = 2 + 6 * subsampleCount; - output.sampleData(subsampleEncryptionData, subsampleDataLength); + + if (clearHeaderSize > 0) { + // On the way through, we need to re-write the 3rd and 4th bytes, which hold + // clearDataSizes[0], so that clearHeaderSize is added into the value. This must be done + // without modifying subsampleEncryptionData itself. + ParsableByteArray subsampleEncryptionData2 = new ParsableByteArray(subsampleDataLength); + subsampleEncryptionData2.readBytes(subsampleEncryptionData.data, + subsampleEncryptionData.getPosition(), subsampleDataLength); + int clearDataSize = (subsampleEncryptionData2.data[2] & 0xFF) << 8 + | (subsampleEncryptionData2.data[3] & 0xFF) + clearHeaderSize; + subsampleEncryptionData2.data[2] = (byte)((clearDataSize & 0xFF00) >>> 8); + subsampleEncryptionData2.data[3] = (byte)( clearDataSize & 0x00FF); + subsampleEncryptionData2.setPosition(0); + output.sampleData(subsampleEncryptionData2, subsampleDataLength); + subsampleEncryptionData.skipBytes(subsampleDataLength); + } else { + output.sampleData(subsampleEncryptionData, subsampleDataLength); + } return 1 + vectorSize + subsampleDataLength; } 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 26a95b8de2..68761cef19 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 @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; import com.google.android.exoplayer2.source.SampleQueue.SampleExtrasHolder; import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; @@ -115,13 +114,11 @@ import java.nio.ByteBuffer; * * @param buffer The buffer to populate. * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. - * @param mimeType The MIME type. */ - public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder, - String mimeType) { + public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { // Read encryption data if the sample is encrypted. if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder, mimeType); + readEncryptionData(buffer, extrasHolder); } // Read sample data, extracting supplemental data into a separate buffer if needed. if (buffer.hasSupplementalData()) { @@ -218,10 +215,8 @@ import java.nio.ByteBuffer; * * @param buffer The buffer into which the encryption data should be written. * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. - * @param mimeType The MIME type. */ - private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder, - String mimeType) { + private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { long offset = extrasHolder.offset; // Read the signal byte. @@ -270,10 +265,8 @@ import java.nio.ByteBuffer; encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); } } else { - int addedHeaderSize = MimeTypes.AUDIO_AC4.equals(mimeType) ? 7 : 0; - clearDataSizes[0] = addedHeaderSize; - encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset) - - addedHeaderSize; + clearDataSizes[0] = 0; + encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); } // Populate the cryptoInfo. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index bfd3d7c4ed..cc15d9d275 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -323,8 +323,7 @@ public class SampleQueue implements TrackOutput { readSampleMetadata( formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilUs, extrasHolder); if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream() && !buffer.isFlagsOnly()) { - sampleDataQueue.readToBuffer(buffer, extrasHolder, - downstreamFormat == null ? null : downstreamFormat.sampleMimeType); + sampleDataQueue.readToBuffer(buffer, extrasHolder); } return result; } From 74e01f4e979941fe7be2985c3f921659ee55c1b1 Mon Sep 17 00:00:00 2001 From: ybai001 Date: Fri, 10 Jan 2020 14:19:30 +0800 Subject: [PATCH 3/3] Add protected AC-4 fmp4 test case --- .../test/assets/mp4/sample_ac4_protected.mp4 | Bin 0 -> 8815 bytes .../mp4/sample_ac4_protected.mp4.0.dump | 145 ++++++++++++++++++ .../mp4/sample_ac4_protected.mp4.1.dump | 109 +++++++++++++ .../mp4/sample_ac4_protected.mp4.2.dump | 73 +++++++++ .../mp4/sample_ac4_protected.mp4.3.dump | 37 +++++ .../mp4/FragmentedMp4ExtractorTest.java | 6 + 6 files changed, 370 insertions(+) create mode 100644 library/core/src/test/assets/mp4/sample_ac4_protected.mp4 create mode 100644 library/core/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_protected.mp4.3.dump diff --git a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4 b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..e3a4f6d6c6a653ea15a59b56f17d439e8664afd8 GIT binary patch literal 8815 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 closedCaptionFormats) { return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); }