From 6314a0ec82343ee79075e7f8794a69471599b019 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 20 Sep 2017 05:38:24 -0700 Subject: [PATCH] Add support for Widevine encrypted HLS This includes both cbcs and cenc. Will only work for streams that require a single pssh. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169382884 --- .../android/exoplayer2/drm/DrmInitData.java | 10 ++- .../extractor/mp4/FragmentedMp4Extractor.java | 16 +++-- .../playlist/HlsMediaPlaylistParserTest.java | 17 ++--- .../exoplayer2/source/hls/HlsChunkSource.java | 8 +-- .../exoplayer2/source/hls/HlsMediaChunk.java | 36 +++++----- .../source/hls/offline/HlsDownloader.java | 5 +- .../source/hls/playlist/HlsMediaPlaylist.java | 45 ++++++------- .../hls/playlist/HlsPlaylistParser.java | 65 ++++++++++++++----- .../smoothstreaming/DefaultSsChunkSource.java | 2 +- 9 files changed, 122 insertions(+), 82 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index c8e76ec2914..e346ab800f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -58,7 +58,15 @@ public DrmInitData(List schemeDatas) { * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ public DrmInitData(SchemeData... schemeDatas) { - this(null, true, schemeDatas); + this(null, schemeDatas); + } + + /** + * @param schemeType The protection scheme type, or null if not applicable or unknown. + * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. + */ + public DrmInitData(@Nullable String schemeType, SchemeData... schemeDatas) { + this(schemeType, true, schemeDatas); } private DrmInitData(@Nullable String schemeType, boolean cloneSchemeDatas, 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 d9ab47546a7..4807e052772 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 @@ -120,6 +120,9 @@ public Extractor[] createExtractors() { @Flags private final int flags; private final Track sideloadedTrack; + // Manifest DRM data. + private final DrmInitData sideloadedDrmInitData; + // Track-linked data bundle, accessible as a whole through trackID. private final SparseArray trackBundles; @@ -179,7 +182,7 @@ public FragmentedMp4Extractor(@Flags int flags) { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) { - this(flags, timestampAdjuster, null); + this(flags, timestampAdjuster, null, null); } /** @@ -187,12 +190,14 @@ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjus * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor * will not receive a moov box in the input data. + * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, - Track sideloadedTrack) { + Track sideloadedTrack, DrmInitData sideloadedDrmInitData) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; this.sideloadedTrack = sideloadedTrack; + this.sideloadedDrmInitData = sideloadedDrmInitData; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); @@ -402,7 +407,8 @@ private void onContainerAtomRead(ContainerAtom container) throws ParserException private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); - DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); + DrmInitData drmInitData = sideloadedDrmInitData != null ? sideloadedDrmInitData + : getDrmInitDataFromAtoms(moov.leafChildren); // Read declaration of track fragments in the Moov box. ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); @@ -456,7 +462,9 @@ private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { parseMoof(moof, trackBundles, flags, extendedTypeScratch); - DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren); + // If drm init data is sideloaded, we ignore pssh boxes. + DrmInitData drmInitData = sideloadedDrmInitData != null ? null + : getDrmInitDataFromAtoms(moof.leafChildren); if (drmInitData != null) { int trackCount = trackBundles.size(); for (int i = 0; i < trackCount; i++) { diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index e2eb173df8a..b036f13a0f7 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -85,9 +85,8 @@ public void testParseMediaPlaylist() { Segment segment = segments.get(0); assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence); assertEquals(7975000, segment.durationUs); - assertFalse(segment.isEncrypted); - assertEquals(null, segment.encryptionKeyUri); - assertEquals(null, segment.encryptionIV); + assertNull(segment.fullSegmentEncryptionKeyUri); + assertNull(segment.encryptionIV); assertEquals(51370, segment.byterangeLength); assertEquals(0, segment.byterangeOffset); assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url); @@ -95,8 +94,7 @@ public void testParseMediaPlaylist() { segment = segments.get(1); assertEquals(0, segment.relativeDiscontinuitySequence); assertEquals(7975000, segment.durationUs); - assertTrue(segment.isEncrypted); - assertEquals("https://priv.example.com/key.php?r=2680", segment.encryptionKeyUri); + assertEquals("https://priv.example.com/key.php?r=2680", segment.fullSegmentEncryptionKeyUri); assertEquals("0x1566B", segment.encryptionIV); assertEquals(51501, segment.byterangeLength); assertEquals(2147483648L, segment.byterangeOffset); @@ -105,8 +103,7 @@ public void testParseMediaPlaylist() { segment = segments.get(2); assertEquals(0, segment.relativeDiscontinuitySequence); assertEquals(7941000, segment.durationUs); - assertFalse(segment.isEncrypted); - assertEquals(null, segment.encryptionKeyUri); + assertNull(segment.fullSegmentEncryptionKeyUri); assertEquals(null, segment.encryptionIV); assertEquals(51501, segment.byterangeLength); assertEquals(2147535149L, segment.byterangeOffset); @@ -115,8 +112,7 @@ public void testParseMediaPlaylist() { segment = segments.get(3); assertEquals(1, segment.relativeDiscontinuitySequence); assertEquals(7975000, segment.durationUs); - assertTrue(segment.isEncrypted); - assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri); + assertEquals("https://priv.example.com/key.php?r=2682", segment.fullSegmentEncryptionKeyUri); // 0xA7A == 2682. assertNotNull(segment.encryptionIV); assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault())); @@ -127,8 +123,7 @@ public void testParseMediaPlaylist() { segment = segments.get(4); assertEquals(1, segment.relativeDiscontinuitySequence); assertEquals(7975000, segment.durationUs); - assertTrue(segment.isEncrypted); - assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri); + assertEquals("https://priv.example.com/key.php?r=2682", segment.fullSegmentEncryptionKeyUri); // 0xA7B == 2683. assertNotNull(segment.encryptionIV); assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault())); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 0ad9dd1a6ec..a5688e8bc55 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -276,9 +276,9 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu // Handle encryption. HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); - // Check if encryption is specified. - if (segment.isEncrypted) { - Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); + // Check if the segment is completely encrypted using the identity key format. + if (segment.fullSegmentEncryptionKeyUri != null) { + Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.fullSegmentEncryptionKeyUri); if (!keyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex, @@ -314,7 +314,7 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl, muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, - isTimestampMaster, timestampAdjuster, previous, segment.keyFormat, encryptionKey, + isTimestampMaster, timestampAdjuster, previous, mediaPlaylist.drmInitData, encryptionKey, encryptionIv); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index e3e1bab48d6..91513b536e3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -18,6 +18,7 @@ import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -32,7 +33,6 @@ import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.MimeTypes; @@ -88,6 +88,7 @@ private final boolean shouldSpliceIn; private final boolean needNewExtractor; private final List muxedCaptionFormats; + private final DrmInitData drmInitData; private final boolean isPackedAudio; private final Id3Decoder id3Decoder; @@ -117,20 +118,21 @@ * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. - * @param keyFormat A string describing the format for {@code keyData}, or null if the chunk is - * not encrypted. - * @param keyData Data specifying how to obtain the keys to decrypt the chunk, or null if the - * chunk is not encrypted. - * @param encryptionIv The AES initialization vector, or null if the chunk is not encrypted. + * @param drmInitData A {@link DrmInitData} to sideload to the extractor. + * @param fullSegmentEncryptionKey The key to decrypt the full segment, or null if the segment is + * not fully encrypted. + * @param encryptionIv The AES initialization vector, or null if the segment is not fully + * encrypted. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, HlsUrl hlsUrl, List muxedCaptionFormats, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, boolean isMasterTimestampSource, - TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, String keyFormat, - byte[] keyData, byte[] encryptionIv) { - super(buildDataSource(dataSource, keyFormat, keyData, encryptionIv), dataSpec, hlsUrl.format, - trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, DrmInitData drmInitData, + byte[] fullSegmentEncryptionKey, byte[] encryptionIv) { + super(buildDataSource(dataSource, fullSegmentEncryptionKey, encryptionIv), dataSpec, + hlsUrl.format, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, + chunkIndex); this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; @@ -139,6 +141,7 @@ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initData this.timestampAdjuster = timestampAdjuster; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; + this.drmInitData = drmInitData; lastPathSegment = dataSpec.uri.getLastPathSegment(); isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION) || lastPathSegment.endsWith(AC3_FILE_EXTENSION) @@ -331,14 +334,13 @@ private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, Inte // Internal factory methods. /** - * If the content is encrypted using the "identity" key format, returns an - * {@link Aes128DataSource} that wraps the original in order to decrypt the loaded data. Else - * returns the original. + * If the segment is fully encrypted, returns an {@link Aes128DataSource} that wraps the original + * in order to decrypt the loaded data. Else returns the original. */ - private static DataSource buildDataSource(DataSource dataSource, String keyFormat, byte[] keyData, + private static DataSource buildDataSource(DataSource dataSource, byte[] fullSegmentEncryptionKey, byte[] encryptionIv) { - if (HlsMediaPlaylist.KEYFORMAT_IDENTITY.equals(keyFormat)) { - return new Aes128DataSource(dataSource, keyData, encryptionIv); + if (fullSegmentEncryptionKey != null) { + return new Aes128DataSource(dataSource, fullSegmentEncryptionKey, encryptionIv); } return dataSource; } @@ -357,7 +359,7 @@ private Extractor createExtractor() { extractor = previousExtractor; } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { - extractor = new FragmentedMp4Extractor(0, timestampAdjuster); + extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData); } else { // MPEG-2 TS segments, but we need a new extractor. // This flag ensures the change of pid between streams does not affect the sample queues. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java index ac8ec5ee5e9..5ac61294a47 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -117,8 +117,9 @@ private static void addSegment(ArrayList segments, HlsMediaPlaylist med HlsMediaPlaylist.Segment hlsSegment, HashSet encryptionKeyUris) throws IOException, InterruptedException { long startTimeUs = mediaPlaylist.startTimeUs + hlsSegment.relativeStartTimeUs; - if (hlsSegment.isEncrypted) { - Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.encryptionKeyUri); + if (hlsSegment.fullSegmentEncryptionKeyUri != null) { + Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, + hlsSegment.fullSegmentEncryptionKeyUri); if (encryptionKeyUris.add(keyUri)) { segments.add(new Segment(startTimeUs, new DataSpec(keyUri))); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 1b573f41c2d..b21ecb02d50 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -18,6 +18,7 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.drm.DrmInitData; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; @@ -50,17 +51,10 @@ public static final class Segment implements Comparable { */ public final long relativeStartTimeUs; /** - * Whether the segment is encrypted, as defined by #EXT-X-KEY. + * The encryption identity key uri as defined by #EXT-X-KEY, or null if the segment does not use + * full segment encryption with identity key. */ - public final boolean isEncrypted; - /** - * The key format as defined by #EXT-X-KEY, or null if the segment is not encrypted. - */ - public final String keyFormat; - /** - * The encryption key uri as defined by #EXT-X-KEY, or null if the segment is not encrypted. - */ - public final String encryptionKeyUri; + public final String fullSegmentEncryptionKeyUri; /** * The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not * encrypted. @@ -77,7 +71,7 @@ public static final class Segment implements Comparable { public final long byterangeLength; public Segment(String uri, long byterangeOffset, long byterangeLength) { - this(uri, 0, -1, C.TIME_UNSET, false, null, null, null, byterangeOffset, byterangeLength); + this(uri, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength); } /** @@ -85,23 +79,19 @@ public Segment(String uri, long byterangeOffset, long byterangeLength) { * @param durationUs See {@link #durationUs}. * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}. * @param relativeStartTimeUs See {@link #relativeStartTimeUs}. - * @param isEncrypted See {@link #isEncrypted}. - * @param keyFormat See {@link #keyFormat}. - * @param encryptionKeyUri See {@link #encryptionKeyUri}. + * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}. * @param encryptionIV See {@link #encryptionIV}. * @param byterangeOffset See {@link #byterangeOffset}. * @param byterangeLength See {@link #byterangeLength}. */ public Segment(String url, long durationUs, int relativeDiscontinuitySequence, - long relativeStartTimeUs, boolean isEncrypted, String keyFormat, String encryptionKeyUri, + long relativeStartTimeUs, String fullSegmentEncryptionKeyUri, String encryptionIV, long byterangeOffset, long byterangeLength) { this.url = url; this.durationUs = durationUs; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeStartTimeUs = relativeStartTimeUs; - this.isEncrypted = isEncrypted; - this.keyFormat = keyFormat; - this.encryptionKeyUri = encryptionKeyUri; + this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri; this.encryptionIV = encryptionIV; this.byterangeOffset = byterangeOffset; this.byterangeLength = byterangeLength; @@ -115,11 +105,6 @@ public int compareTo(@NonNull Long relativeStartTimeUs) { } - /** - * The identity key format, as defined by #EXT-X-KEY. - */ - public static final String KEYFORMAT_IDENTITY = "identity"; - /** * Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE. */ @@ -176,6 +161,11 @@ public int compareTo(@NonNull Long relativeStartTimeUs) { * Whether the playlist contains a #EXT-X-PROGRAM-DATE-TIME tag. */ public final boolean hasProgramDateTime; + /** + * DRM initialization data for sample decryption, or null if none of the segment uses sample + * encryption. + */ + public final DrmInitData drmInitData; /** * The initialization segment, as defined by #EXT-X-MAP. */ @@ -203,6 +193,7 @@ public int compareTo(@NonNull Long relativeStartTimeUs) { * @param hasIndependentSegmentsTag See {@link #hasIndependentSegmentsTag}. * @param hasEndTag See {@link #hasEndTag}. * @param hasProgramDateTime See {@link #hasProgramDateTime}. + * @param drmInitData See {@link #drmInitData}. * @param initializationSegment See {@link #initializationSegment}. * @param segments See {@link #segments}. */ @@ -210,7 +201,7 @@ public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, List segments) { + DrmInitData drmInitData, Segment initializationSegment, List segments) { super(baseUri, tags); this.playlistType = playlistType; this.startTimeUs = startTimeUs; @@ -222,6 +213,7 @@ public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, List