From d399c00f9e49be9e74f6c74321692e03816da13f Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 9 Aug 2018 12:40:23 -0700 Subject: [PATCH] Add support for multiple alternative EXT-X-KEY in HLS Also add support for parsing PlayReady DRM information Issue:#4180 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=208094290 --- RELEASENOTES.md | 2 + .../android/exoplayer2/drm/DrmInitData.java | 10 ++ .../exoplayer2/source/hls/HlsChunkSource.java | 2 +- .../source/hls/playlist/HlsMediaPlaylist.java | 27 ++-- .../hls/playlist/HlsPlaylistParser.java | 87 ++++++++++--- .../playlist/HlsMediaPlaylistParserTest.java | 120 +++++++++++++++--- 6 files changed, 196 insertions(+), 52 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9e4a608884..02f82c7a3d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -61,6 +61,8 @@ * Allow configuration of the Loader retry delay ([#3370](https://github.com/google/ExoPlayer/issues/3370)). * HLS: + * Add support for PlayReady. + * Add support for alternative EXT-X-KEY tags. * Set the bitrate on primary track sample formats ([#3297](https://github.com/google/ExoPlayer/issues/3297)). * Pass HTTP response headers to `HlsExtractorFactory.createExtractor`. 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 cd7adea1e2..b9415c74af 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 @@ -356,6 +356,16 @@ public final class DrmInitData implements Comparator, Parcelable { return data != null; } + /** + * Returns a copy of this instance with the specified data. + * + * @param data The data to include in the copy. + * @return The new instance. + */ + public SchemeData copyWithData(@Nullable byte[] data) { + return new SchemeData(uuid, licenseServerUrl, mimeType, data, requiresSecureDecryption); + } + @Override public boolean equals(@Nullable Object obj) { if (!(obj instanceof SchemeData)) { 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 eba79ab329..ae50c93b83 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 @@ -375,7 +375,7 @@ import java.util.List; isTimestampMaster, timestampAdjuster, previous, - mediaPlaylist.drmInitData, + segment.drmInitData, encryptionKey, encryptionIv); } 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 2c34a5a353..4278ae6825 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 @@ -55,6 +55,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * The start time of the segment in microseconds, relative to the start of the playlist. */ public final long relativeStartTimeUs; + /** + * DRM initialization data for sample decryption, or null if the segment does not use CDM-DRM + * protection. + */ + public final @Nullable DrmInitData drmInitData; /** * 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. @@ -91,6 +96,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { /* durationUs= */ 0, /* relativeDiscontinuitySequence= */ -1, /* relativeStartTimeUs= */ C.TIME_UNSET, + /* drmInitData= */ null, /* fullSegmentEncryptionKeyUri= */ null, /* encryptionIV= */ null, byterangeOffset, @@ -105,6 +111,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param durationUs See {@link #durationUs}. * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}. * @param relativeStartTimeUs See {@link #relativeStartTimeUs}. + * @param drmInitData See {@link #drmInitData}. * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}. * @param encryptionIV See {@link #encryptionIV}. * @param byterangeOffset See {@link #byterangeOffset}. @@ -118,6 +125,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { long durationUs, int relativeDiscontinuitySequence, long relativeStartTimeUs, + @Nullable DrmInitData drmInitData, @Nullable String fullSegmentEncryptionKeyUri, @Nullable String encryptionIV, long byterangeOffset, @@ -129,6 +137,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.durationUs = durationUs; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeStartTimeUs = relativeStartTimeUs; + this.drmInitData = drmInitData; this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri; this.encryptionIV = encryptionIV; this.byterangeOffset = byterangeOffset; @@ -199,10 +208,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public final boolean hasProgramDateTime; /** - * DRM initialization data for sample decryption, or null if none of the segment uses sample - * encryption. + * Contains the CDM protection schemes used by segments in this playlist. Does not contain any key + * acquisition data. Null if none of the segments in the playlist is CDM-encrypted. */ - public final DrmInitData drmInitData; + public final @Nullable DrmInitData protectionSchemes; /** * The list of segments in the playlist. */ @@ -225,8 +234,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param targetDurationUs See {@link #targetDurationUs}. * @param hasIndependentSegments See {@link #hasIndependentSegments}. * @param hasEndTag See {@link #hasEndTag}. + * @param protectionSchemes See {@link #protectionSchemes}. * @param hasProgramDateTime See {@link #hasProgramDateTime}. - * @param drmInitData See {@link #drmInitData}. * @param segments See {@link #segments}. */ public HlsMediaPlaylist( @@ -243,7 +252,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { boolean hasIndependentSegments, boolean hasEndTag, boolean hasProgramDateTime, - DrmInitData drmInitData, + @Nullable DrmInitData protectionSchemes, List segments) { super(baseUri, tags, hasIndependentSegments); this.playlistType = playlistType; @@ -255,7 +264,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.targetDurationUs = targetDurationUs; this.hasEndTag = hasEndTag; this.hasProgramDateTime = hasProgramDateTime; - this.drmInitData = drmInitData; + this.protectionSchemes = protectionSchemes; this.segments = Collections.unmodifiableList(segments); if (!segments.isEmpty()) { Segment last = segments.get(segments.size() - 1); @@ -323,7 +332,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { hasIndependentSegments, hasEndTag, hasProgramDateTime, - drmInitData, + protectionSchemes, segments); } @@ -357,7 +366,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { hasIndependentSegments || masterPlaylist.hasIndependentSegments, hasEndTag, hasProgramDateTime, - drmInitData, + protectionSchemes, segments); } @@ -383,7 +392,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { hasIndependentSegments, /* hasEndTag= */ true, hasProgramDateTime, - drmInitData, + protectionSchemes, segments); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 7685831835..8473fd1f17 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.source.UnrecognizedInputFormatException; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -40,6 +41,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Queue; +import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.checkerframework.checker.nullness.qual.PolyNull; @@ -82,6 +84,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser currentSchemeDatas = new TreeMap<>(); + String encryptionScheme = null; + DrmInitData cachedDrmInitData = null; String line; while (iterator.hasNext()) { @@ -469,13 +477,16 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = mediaPlaylist.segments; assertThat(segments).isNotNull(); assertThat(segments).hasSize(5); @@ -162,12 +161,17 @@ public class HlsMediaPlaylistParserTest { + "#EXTINF:8,\n" + "https://priv.example.com/2.ts\n" + "#EXT-X-ENDLIST\n"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); - assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cbcs); - assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cbcs); + assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse(); + + assertThat(playlist.segments.get(0).drmInitData).isNull(); + + assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.segments.get(1).drmInitData.get(0).hasData()).isTrue(); } @Test @@ -186,12 +190,12 @@ public class HlsMediaPlaylistParserTest { + "#EXTINF:8,\n" + "https://priv.example.com/2.ts\n" + "#EXT-X-ENDLIST\n"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); - assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc); - assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cenc); + assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse(); } @Test @@ -210,12 +214,89 @@ public class HlsMediaPlaylistParserTest { + "#EXTINF:8,\n" + "https://priv.example.com/2.ts\n" + "#EXT-X-ENDLIST\n"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); - assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc); - assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cenc); + assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse(); + } + + @Test + public void testMultipleExtXKeysForSingleSegment() throws Exception { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:6\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-MAP:URI=\"map.mp4\"\n" + + "#EXTINF:5.005,\n" + + "s000000.mp4\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES," + + "KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\"," + + "KEYFORMATVERSIONS=\"1\"," + + "URI=\"data:text/plain;base64,Tm90aGluZyB0byBzZWUgaGVyZQ==\"\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.microsoft.playready\"," + + "KEYFORMATVERSIONS=\"1\"," + + "URI=\"data:text/plain;charset=UTF-16;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\"\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.apple.streamingkeydelivery\"," + + "KEYFORMATVERSIONS=\"1\",URI=\"skd://QW5vdGhlciBlYXN0ZXIgZWdn\"\n" + + "#EXT-X-MAP:URI=\"map.mp4\"\n" + + "#EXTINF:5.005,\n" + + "s000000.mp4\n" + + "#EXTINF:5.005,\n" + + "s000001.mp4\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES," + + "KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\"," + + "KEYFORMATVERSIONS=\"1\"," + + "URI=\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\"" + + "\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.microsoft.playready\"," + + "KEYFORMATVERSIONS=\"1\"," + + "URI=\"data:text/plain;charset=UTF-16;base64,T2ssIGl0J3Mgbm90IGZ1biBhbnltb3Jl\"\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.apple.streamingkeydelivery\"," + + "KEYFORMATVERSIONS=\"1\"," + + "URI=\"skd://V2FpdCB1bnRpbCB5b3Ugc2VlIHRoZSBuZXh0IG9uZSE=\"\n" + + "#EXTINF:5.005,\n" + + "s000024.mp4\n" + + "#EXTINF:5.005,\n" + + "s000025.mp4\n" + + "#EXT-X-KEY:METHOD=NONE\n" + + "#EXTINF:5.005,\n" + + "s000026.mp4\n" + + "#EXTINF:5.005,\n" + + "s000026.mp4\n"; + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cbcs); + // Unsupported protection schemes like com.apple.streamingkeydelivery are ignored. + assertThat(playlist.protectionSchemes.schemeDataCount).isEqualTo(2); + assertThat(playlist.protectionSchemes.get(0).matches(C.PLAYREADY_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse(); + assertThat(playlist.protectionSchemes.get(1).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.get(1).hasData()).isFalse(); + + assertThat(playlist.segments.get(0).drmInitData).isNull(); + + assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.PLAYREADY_UUID)).isTrue(); + assertThat(playlist.segments.get(1).drmInitData.get(0).hasData()).isTrue(); + assertThat(playlist.segments.get(1).drmInitData.get(1).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.segments.get(1).drmInitData.get(1).hasData()).isTrue(); + + assertThat(playlist.segments.get(1).drmInitData) + .isEqualTo(playlist.segments.get(2).drmInitData); + assertThat(playlist.segments.get(2).drmInitData) + .isNotEqualTo(playlist.segments.get(3).drmInitData); + assertThat(playlist.segments.get(3).drmInitData.get(0).matches(C.PLAYREADY_UUID)).isTrue(); + assertThat(playlist.segments.get(3).drmInitData.get(0).hasData()).isTrue(); + assertThat(playlist.segments.get(3).drmInitData.get(1).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.segments.get(3).drmInitData.get(1).hasData()).isTrue(); + + assertThat(playlist.segments.get(3).drmInitData) + .isEqualTo(playlist.segments.get(4).drmInitData); + assertThat(playlist.segments.get(5).drmInitData).isNull(); + assertThat(playlist.segments.get(6).drmInitData).isNull(); } @Test @@ -243,8 +324,7 @@ public class HlsMediaPlaylistParserTest { + "02/00/42.ts\n" + "#EXTINF:5.005,\n" + "02/00/47.ts\n"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); @@ -272,8 +352,7 @@ public class HlsMediaPlaylistParserTest { + "#EXT-X-MAP:URI=\"init2.ts\"" + "#EXTINF:5.005,\n" + "02/00/47.ts\n"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); @@ -303,8 +382,7 @@ public class HlsMediaPlaylistParserTest { + "#EXT-X-MAP:URI=\"init2.ts\"" + "#EXTINF:5.005,\n" + "02/00/47.ts\n"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); assertThat(playlist.hasIndependentSegments).isFalse();