diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5b9d4d2bc3..535996d0b7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -61,6 +61,7 @@ on earlier releases, but only when embedded in a non-FLAC container such as Matroska or MP4. * Javadocs: Add favicon for easier identification in browser tabs +* FMP4: Add support for encrypted AC-4 tracks. ### 2.11.1 (2019-12-20) ### 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..f251741b6d 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,14 +1265,19 @@ 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)) { + // AC4 samples need to be prefixed with a clear sample header. + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE); Ac4Util.getAc4SampleHeader(sampleSize, scratch); currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; - sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; + } else { + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0); } + sampleSize += sampleBytesWritten; parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; } @@ -1471,8 +1476,11 @@ public class FragmentedMp4Extractor implements Extractor { */ private static final class TrackBundle { + private static final int SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH = 8; + public final TrackOutput output; public final TrackFragment fragment; + public final ParsableByteArray scratch; public Track track; public DefaultSampleValues defaultSampleValues; @@ -1487,6 +1495,7 @@ public class FragmentedMp4Extractor implements Extractor { public TrackBundle(TrackOutput output) { this.output = output; fragment = new TrackFragment(); + scratch = new ParsableByteArray(); encryptionSignalByte = new ParsableByteArray(1); defaultInitializationVector = new ParsableByteArray(); } @@ -1555,9 +1564,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,23 +1589,61 @@ 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) { + // The sample is fully encrypted, except for the additional clear header that the extractor + // is going to prefix. We need to synthesize subsample encryption data that takes the header + // into account. + scratch.reset(SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); + // subsampleCount = 1 (unsigned short) + scratch.data[0] = (byte) 0; + scratch.data[1] = (byte) 1; + // clearDataSize = clearHeaderSize (unsigned short) + scratch.data[2] = (byte) ((clearHeaderSize >> 8) & 0xFF); + scratch.data[3] = (byte) (clearHeaderSize & 0xFF); + // encryptedDataSize = sampleSize (unsigned short) + scratch.data[4] = (byte) ((sampleSize >> 24) & 0xFF); + scratch.data[5] = (byte) ((sampleSize >> 16) & 0xFF); + scratch.data[6] = (byte) ((sampleSize >> 8) & 0xFF); + scratch.data[7] = (byte) (sampleSize & 0xFF); + output.sampleData(scratch, SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); + return 1 + vectorSize + SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH; + } + ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData; int subsampleCount = subsampleEncryptionData.readUnsignedShort(); subsampleEncryptionData.skipBytes(-2); int subsampleDataLength = 2 + 6 * subsampleCount; + + if (clearHeaderSize != 0) { + // We need to account for the additional clear header by adding clearHeaderSize to + // clearDataSize for the first subsample specified in the subsample encryption data. + scratch.reset(subsampleDataLength); + scratch.readBytes(subsampleEncryptionData.data, /* offset= */ 0, subsampleDataLength); + subsampleEncryptionData.skipBytes(subsampleDataLength); + + int clearDataSize = (scratch.data[2] & 0xFF) << 8 | (scratch.data[3] & 0xFF); + int adjustedClearDataSize = clearDataSize + clearHeaderSize; + scratch.data[2] = (byte) ((adjustedClearDataSize >> 8) & 0xFF); + scratch.data[3] = (byte) (adjustedClearDataSize & 0xFF); + subsampleEncryptionData = scratch; + } + output.sampleData(subsampleEncryptionData, subsampleDataLength); return 1 + vectorSize + subsampleDataLength; } 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 0000000000..e3a4f6d6c6 Binary files /dev/null and b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4 differ diff --git a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump new file mode 100644 index 0000000000..02db599cd7 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump @@ -0,0 +1,145 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=950]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = -1683793742 + metadata = null + initializationData: + total output bytes = 7936 + sample count = 19 + sample 0: + time = 0 + flags = 1073741825 + data = length 384, hash 96EFFFF3 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 1: + time = 40000 + flags = 1073741825 + data = length 384, hash 899279C6 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 2: + time = 80000 + flags = 1073741825 + data = length 384, hash 9EA9F45 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 3: + time = 120000 + flags = 1073741825 + data = length 384, hash 82D362A9 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 4: + time = 160000 + flags = 1073741825 + data = length 384, hash B8705CFB + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 5: + time = 200000 + flags = 1073741825 + data = length 384, hash 58B5628E + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 6: + time = 240000 + flags = 1073741825 + data = length 384, hash 87F3C13B + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 7: + time = 280000 + flags = 1073741825 + data = length 384, hash 54333DC5 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 8: + time = 320000 + flags = 1073741825 + data = length 384, hash 1C49C4B3 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 9: + time = 360000 + flags = 1073741825 + data = length 384, hash 5FDC324F + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 10: + time = 400000 + flags = 1073741825 + data = length 384, hash B2A7F444 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 11: + time = 440000 + flags = 1073741825 + data = length 512, hash 5FD06C1E + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 12: + time = 480000 + flags = 1073741825 + data = length 537, hash 7ABBDCB + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 13: + time = 520000 + flags = 1073741825 + data = length 616, hash 3F657E23 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 14: + time = 560000 + flags = 1073741825 + data = length 453, hash 8FCF0529 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 15: + time = 600000 + flags = 1073741825 + data = length 383, hash 7F8C9E19 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 16: + time = 640000 + flags = 1073741825 + data = length 410, hash 3727858D + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 17: + time = 680000 + flags = 1073741825 + data = length 391, hash E2931212 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 18: + time = 720000 + flags = 1073741825 + data = length 410, hash 63017D46 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump new file mode 100644 index 0000000000..8b45dd0a50 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump @@ -0,0 +1,109 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=950]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = -1683793742 + metadata = null + initializationData: + total output bytes = 5632 + sample count = 13 + sample 0: + time = 240000 + flags = 1073741825 + data = length 384, hash 87F3C13B + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 1: + time = 280000 + flags = 1073741825 + data = length 384, hash 54333DC5 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 2: + time = 320000 + flags = 1073741825 + data = length 384, hash 1C49C4B3 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 3: + time = 360000 + flags = 1073741825 + data = length 384, hash 5FDC324F + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 4: + time = 400000 + flags = 1073741825 + data = length 384, hash B2A7F444 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 5: + time = 440000 + flags = 1073741825 + data = length 512, hash 5FD06C1E + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 6: + time = 480000 + flags = 1073741825 + data = length 537, hash 7ABBDCB + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 7: + time = 520000 + flags = 1073741825 + data = length 616, hash 3F657E23 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 8: + time = 560000 + flags = 1073741825 + data = length 453, hash 8FCF0529 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 9: + time = 600000 + flags = 1073741825 + data = length 383, hash 7F8C9E19 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 10: + time = 640000 + flags = 1073741825 + data = length 410, hash 3727858D + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 11: + time = 680000 + flags = 1073741825 + data = length 391, hash E2931212 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 12: + time = 720000 + flags = 1073741825 + data = length 410, hash 63017D46 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump new file mode 100644 index 0000000000..a6be34dec7 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump @@ -0,0 +1,73 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=950]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = -1683793742 + metadata = null + initializationData: + total output bytes = 3200 + sample count = 7 + sample 0: + time = 480000 + flags = 1073741825 + data = length 537, hash 7ABBDCB + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 1: + time = 520000 + flags = 1073741825 + data = length 616, hash 3F657E23 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 2: + time = 560000 + flags = 1073741825 + data = length 453, hash 8FCF0529 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 3: + time = 600000 + flags = 1073741825 + data = length 383, hash 7F8C9E19 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 4: + time = 640000 + flags = 1073741825 + data = length 410, hash 3727858D + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 5: + time = 680000 + flags = 1073741825 + data = length 391, hash E2931212 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 6: + time = 720000 + flags = 1073741825 + data = length 410, hash 63017D46 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.3.dump b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.3.dump new file mode 100644 index 0000000000..08fce46009 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.3.dump @@ -0,0 +1,37 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=950]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = -1683793742 + metadata = null + initializationData: + total output bytes = 410 + sample count = 1 + sample 0: + time = 720000 + flags = 1073741825 + data = length 410, hash 63017D46 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 +tracksEnded = true diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index 1f49aee293..28e0b9e0f8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -57,6 +57,12 @@ public final class FragmentedMp4ExtractorTest { getExtractorFactory(Collections.emptyList()), "mp4/sample_ac4_fragmented.mp4"); } + @Test + public void testSampleWithProtectedAc4Track() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(Collections.emptyList()), "mp4/sample_ac4_protected.mp4"); + } + private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); }