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
This commit is contained in:
aquilescanta 2018-08-09 12:40:23 -07:00 committed by Oliver Woodman
parent 5b3b4e64f9
commit d399c00f9e
6 changed files with 196 additions and 52 deletions

View File

@ -61,6 +61,8 @@
* Allow configuration of the Loader retry delay * Allow configuration of the Loader retry delay
([#3370](https://github.com/google/ExoPlayer/issues/3370)). ([#3370](https://github.com/google/ExoPlayer/issues/3370)).
* HLS: * HLS:
* Add support for PlayReady.
* Add support for alternative EXT-X-KEY tags.
* Set the bitrate on primary track sample formats * Set the bitrate on primary track sample formats
([#3297](https://github.com/google/ExoPlayer/issues/3297)). ([#3297](https://github.com/google/ExoPlayer/issues/3297)).
* Pass HTTP response headers to `HlsExtractorFactory.createExtractor`. * Pass HTTP response headers to `HlsExtractorFactory.createExtractor`.

View File

@ -356,6 +356,16 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
return data != null; 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 @Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SchemeData)) { if (!(obj instanceof SchemeData)) {

View File

@ -375,7 +375,7 @@ import java.util.List;
isTimestampMaster, isTimestampMaster,
timestampAdjuster, timestampAdjuster,
previous, previous,
mediaPlaylist.drmInitData, segment.drmInitData,
encryptionKey, encryptionKey,
encryptionIv); encryptionIv);
} }

View File

@ -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. * The start time of the segment in microseconds, relative to the start of the playlist.
*/ */
public final long relativeStartTimeUs; 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 * 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. * full segment encryption with identity key.
@ -91,6 +96,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
/* durationUs= */ 0, /* durationUs= */ 0,
/* relativeDiscontinuitySequence= */ -1, /* relativeDiscontinuitySequence= */ -1,
/* relativeStartTimeUs= */ C.TIME_UNSET, /* relativeStartTimeUs= */ C.TIME_UNSET,
/* drmInitData= */ null,
/* fullSegmentEncryptionKeyUri= */ null, /* fullSegmentEncryptionKeyUri= */ null,
/* encryptionIV= */ null, /* encryptionIV= */ null,
byterangeOffset, byterangeOffset,
@ -105,6 +111,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param durationUs See {@link #durationUs}. * @param durationUs See {@link #durationUs}.
* @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}. * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}.
* @param relativeStartTimeUs See {@link #relativeStartTimeUs}. * @param relativeStartTimeUs See {@link #relativeStartTimeUs}.
* @param drmInitData See {@link #drmInitData}.
* @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}. * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}.
* @param encryptionIV See {@link #encryptionIV}. * @param encryptionIV See {@link #encryptionIV}.
* @param byterangeOffset See {@link #byterangeOffset}. * @param byterangeOffset See {@link #byterangeOffset}.
@ -118,6 +125,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
long durationUs, long durationUs,
int relativeDiscontinuitySequence, int relativeDiscontinuitySequence,
long relativeStartTimeUs, long relativeStartTimeUs,
@Nullable DrmInitData drmInitData,
@Nullable String fullSegmentEncryptionKeyUri, @Nullable String fullSegmentEncryptionKeyUri,
@Nullable String encryptionIV, @Nullable String encryptionIV,
long byterangeOffset, long byterangeOffset,
@ -129,6 +137,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this.durationUs = durationUs; this.durationUs = durationUs;
this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
this.relativeStartTimeUs = relativeStartTimeUs; this.relativeStartTimeUs = relativeStartTimeUs;
this.drmInitData = drmInitData;
this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri; this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri;
this.encryptionIV = encryptionIV; this.encryptionIV = encryptionIV;
this.byterangeOffset = byterangeOffset; this.byterangeOffset = byterangeOffset;
@ -199,10 +208,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
*/ */
public final boolean hasProgramDateTime; public final boolean hasProgramDateTime;
/** /**
* DRM initialization data for sample decryption, or null if none of the segment uses sample * Contains the CDM protection schemes used by segments in this playlist. Does not contain any key
* encryption. * 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. * The list of segments in the playlist.
*/ */
@ -225,8 +234,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param targetDurationUs See {@link #targetDurationUs}. * @param targetDurationUs See {@link #targetDurationUs}.
* @param hasIndependentSegments See {@link #hasIndependentSegments}. * @param hasIndependentSegments See {@link #hasIndependentSegments}.
* @param hasEndTag See {@link #hasEndTag}. * @param hasEndTag See {@link #hasEndTag}.
* @param protectionSchemes See {@link #protectionSchemes}.
* @param hasProgramDateTime See {@link #hasProgramDateTime}. * @param hasProgramDateTime See {@link #hasProgramDateTime}.
* @param drmInitData See {@link #drmInitData}.
* @param segments See {@link #segments}. * @param segments See {@link #segments}.
*/ */
public HlsMediaPlaylist( public HlsMediaPlaylist(
@ -243,7 +252,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
boolean hasIndependentSegments, boolean hasIndependentSegments,
boolean hasEndTag, boolean hasEndTag,
boolean hasProgramDateTime, boolean hasProgramDateTime,
DrmInitData drmInitData, @Nullable DrmInitData protectionSchemes,
List<Segment> segments) { List<Segment> segments) {
super(baseUri, tags, hasIndependentSegments); super(baseUri, tags, hasIndependentSegments);
this.playlistType = playlistType; this.playlistType = playlistType;
@ -255,7 +264,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this.targetDurationUs = targetDurationUs; this.targetDurationUs = targetDurationUs;
this.hasEndTag = hasEndTag; this.hasEndTag = hasEndTag;
this.hasProgramDateTime = hasProgramDateTime; this.hasProgramDateTime = hasProgramDateTime;
this.drmInitData = drmInitData; this.protectionSchemes = protectionSchemes;
this.segments = Collections.unmodifiableList(segments); this.segments = Collections.unmodifiableList(segments);
if (!segments.isEmpty()) { if (!segments.isEmpty()) {
Segment last = segments.get(segments.size() - 1); Segment last = segments.get(segments.size() - 1);
@ -323,7 +332,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
hasIndependentSegments, hasIndependentSegments,
hasEndTag, hasEndTag,
hasProgramDateTime, hasProgramDateTime,
drmInitData, protectionSchemes,
segments); segments);
} }
@ -357,7 +366,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
hasIndependentSegments || masterPlaylist.hasIndependentSegments, hasIndependentSegments || masterPlaylist.hasIndependentSegments,
hasEndTag, hasEndTag,
hasProgramDateTime, hasProgramDateTime,
drmInitData, protectionSchemes,
segments); segments);
} }
@ -383,7 +392,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
hasIndependentSegments, hasIndependentSegments,
/* hasEndTag= */ true, /* hasEndTag= */ true,
hasProgramDateTime, hasProgramDateTime,
drmInitData, protectionSchemes,
segments); segments);
} }

View File

@ -23,6 +23,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; 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.UnrecognizedInputFormatException;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
@ -40,6 +41,7 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.TreeMap;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.PolyNull; import org.checkerframework.checker.nullness.qual.PolyNull;
@ -82,6 +84,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
// Replaced by METHOD_SAMPLE_AES_CTR. Keep for backward compatibility. // Replaced by METHOD_SAMPLE_AES_CTR. Keep for backward compatibility.
private static final String METHOD_SAMPLE_AES_CENC = "SAMPLE-AES-CENC"; private static final String METHOD_SAMPLE_AES_CENC = "SAMPLE-AES-CENC";
private static final String METHOD_SAMPLE_AES_CTR = "SAMPLE-AES-CTR"; private static final String METHOD_SAMPLE_AES_CTR = "SAMPLE-AES-CTR";
private static final String KEYFORMAT_PLAYREADY = "com.microsoft.playready";
private static final String KEYFORMAT_IDENTITY = "identity"; private static final String KEYFORMAT_IDENTITY = "identity";
private static final String KEYFORMAT_WIDEVINE_PSSH_BINARY = private static final String KEYFORMAT_WIDEVINE_PSSH_BINARY =
"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"; "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
@ -128,8 +131,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
+ "|" + "|"
+ METHOD_SAMPLE_AES_CTR + METHOD_SAMPLE_AES_CTR
+ ")" + ")"
+ "\\s*(,|$)"); + "\\s*(?:,|$)");
private static final Pattern REGEX_KEYFORMAT = Pattern.compile("KEYFORMAT=\"(.+?)\""); private static final Pattern REGEX_KEYFORMAT = Pattern.compile("KEYFORMAT=\"(.+?)\"");
private static final Pattern REGEX_KEYFORMATVERSIONS =
Pattern.compile("KEYFORMATVERSIONS=\"(.+?)\"");
private static final Pattern REGEX_URI = Pattern.compile("URI=\"(.+?)\""); private static final Pattern REGEX_URI = Pattern.compile("URI=\"(.+?)\"");
private static final Pattern REGEX_IV = Pattern.compile("IV=([^,.*]+)"); private static final Pattern REGEX_IV = Pattern.compile("IV=([^,.*]+)");
private static final Pattern REGEX_TYPE = Pattern.compile("TYPE=(" + TYPE_AUDIO + "|" + TYPE_VIDEO private static final Pattern REGEX_TYPE = Pattern.compile("TYPE=(" + TYPE_AUDIO + "|" + TYPE_VIDEO
@ -422,9 +427,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
long segmentMediaSequence = 0; long segmentMediaSequence = 0;
boolean hasGapTag = false; boolean hasGapTag = false;
DrmInitData playlistProtectionSchemes = null;
String encryptionKeyUri = null; String encryptionKeyUri = null;
String encryptionIV = null; String encryptionIV = null;
DrmInitData drmInitData = null; TreeMap<String, SchemeData> currentSchemeDatas = new TreeMap<>();
String encryptionScheme = null;
DrmInitData cachedDrmInitData = null;
String line; String line;
while (iterator.hasNext()) { while (iterator.hasNext()) {
@ -469,13 +477,16 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
(long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND); (long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, ""); segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "");
} else if (line.startsWith(TAG_KEY)) { } else if (line.startsWith(TAG_KEY)) {
String method = parseOptionalStringAttr(line, REGEX_METHOD); String method = parseStringAttr(line, REGEX_METHOD);
String keyFormat = parseOptionalStringAttr(line, REGEX_KEYFORMAT); String keyFormat = parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY);
encryptionKeyUri = null; encryptionKeyUri = null;
encryptionIV = null; encryptionIV = null;
if (!METHOD_NONE.equals(method)) { if (METHOD_NONE.equals(method)) {
currentSchemeDatas.clear();
cachedDrmInitData = null;
} else /* !METHOD_NONE.equals(method) */ {
encryptionIV = parseOptionalStringAttr(line, REGEX_IV); encryptionIV = parseOptionalStringAttr(line, REGEX_IV);
if (KEYFORMAT_IDENTITY.equals(keyFormat) || keyFormat == null) { if (KEYFORMAT_IDENTITY.equals(keyFormat)) {
if (METHOD_AES_128.equals(method)) { if (METHOD_AES_128.equals(method)) {
// The segment is fully encrypted using an identity key. // The segment is fully encrypted using an identity key.
encryptionKeyUri = parseStringAttr(line, REGEX_URI); encryptionKeyUri = parseStringAttr(line, REGEX_URI);
@ -483,16 +494,22 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
// Do nothing. Samples are encrypted using an identity key, but this is not supported. // Do nothing. Samples are encrypted using an identity key, but this is not supported.
// Hopefully, a traditional DRM alternative is also provided. // Hopefully, a traditional DRM alternative is also provided.
} }
} else if (method != null) { } else {
SchemeData schemeData = parseWidevineSchemeData(line, keyFormat); if (encryptionScheme == null) {
encryptionScheme =
METHOD_SAMPLE_AES_CENC.equals(method) || METHOD_SAMPLE_AES_CTR.equals(method)
? C.CENC_TYPE_cenc
: C.CENC_TYPE_cbcs;
}
SchemeData schemeData;
if (KEYFORMAT_PLAYREADY.equals(keyFormat)) {
schemeData = parsePlayReadySchemeData(line);
} else {
schemeData = parseWidevineSchemeData(line, keyFormat);
}
if (schemeData != null) { if (schemeData != null) {
drmInitData = cachedDrmInitData = null;
new DrmInitData( currentSchemeDatas.put(keyFormat, schemeData);
(METHOD_SAMPLE_AES_CENC.equals(method)
|| METHOD_SAMPLE_AES_CTR.equals(method))
? C.CENC_TYPE_cenc
: C.CENC_TYPE_cbcs,
schemeData);
} }
} }
} }
@ -529,10 +546,24 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} else { } else {
segmentEncryptionIV = Long.toHexString(segmentMediaSequence); segmentEncryptionIV = Long.toHexString(segmentMediaSequence);
} }
segmentMediaSequence++; segmentMediaSequence++;
if (segmentByteRangeLength == C.LENGTH_UNSET) { if (segmentByteRangeLength == C.LENGTH_UNSET) {
segmentByteRangeOffset = 0; segmentByteRangeOffset = 0;
} }
if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
if (playlistProtectionSchemes == null) {
SchemeData[] playlistSchemeDatas = new SchemeData[schemeDatas.length];
for (int i = 0; i < schemeDatas.length; i++) {
playlistSchemeDatas[i] = schemeDatas[i].copyWithData(null);
}
playlistProtectionSchemes = new DrmInitData(encryptionScheme, playlistSchemeDatas);
}
}
segments.add( segments.add(
new Segment( new Segment(
line, line,
@ -541,6 +572,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segmentDurationUs, segmentDurationUs,
relativeDiscontinuitySequence, relativeDiscontinuitySequence,
segmentStartTimeUs, segmentStartTimeUs,
cachedDrmInitData,
encryptionKeyUri, encryptionKeyUri,
segmentEncryptionIV, segmentEncryptionIV,
segmentByteRangeOffset, segmentByteRangeOffset,
@ -570,11 +602,23 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
hasIndependentSegmentsTag, hasIndependentSegmentsTag,
hasEndTag, hasEndTag,
/* hasProgramDateTime= */ playlistStartTimeUs != 0, /* hasProgramDateTime= */ playlistStartTimeUs != 0,
drmInitData, playlistProtectionSchemes,
segments); segments);
} }
private static SchemeData parseWidevineSchemeData(String line, String keyFormat) private static @Nullable SchemeData parsePlayReadySchemeData(String line) throws ParserException {
String keyFormatVersions = parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1");
if (!"1".equals(keyFormatVersions)) {
// Not supported.
return null;
}
String uriString = parseStringAttr(line, REGEX_URI);
byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT);
byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data);
return new SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData);
}
private static @Nullable SchemeData parseWidevineSchemeData(String line, String keyFormat)
throws ParserException { throws ParserException {
if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) { if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) {
String uriString = parseStringAttr(line, REGEX_URI); String uriString = parseStringAttr(line, REGEX_URI);
@ -604,11 +648,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
private static String parseStringAttr(String line, Pattern pattern) throws ParserException { private static String parseStringAttr(String line, Pattern pattern) throws ParserException {
Matcher matcher = pattern.matcher(line); String value = parseOptionalStringAttr(line, pattern);
if (matcher.find() && matcher.groupCount() == 1) { if (value != null) {
return matcher.group(1); return value;
} else {
throw new ParserException("Couldn't match " + pattern.pattern() + " in " + line);
} }
throw new ParserException("Couldn't match " + pattern.pattern() + " in " + line);
} }
private static @Nullable String parseOptionalStringAttr(String line, Pattern pattern) { private static @Nullable String parseOptionalStringAttr(String line, Pattern pattern) {

View File

@ -24,7 +24,6 @@ import com.google.android.exoplayer2.util.Util;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.Test; import org.junit.Test;
@ -72,8 +71,7 @@ public class HlsMediaPlaylistParserTest {
+ "#EXTINF:7.975,\n" + "#EXTINF:7.975,\n"
+ "https://priv.example.com/fileSequence2683.ts\n" + "https://priv.example.com/fileSequence2683.ts\n"
+ "#EXT-X-ENDLIST"; + "#EXT-X-ENDLIST";
InputStream inputStream = InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream); HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
@ -83,6 +81,7 @@ public class HlsMediaPlaylistParserTest {
assertThat(mediaPlaylist.mediaSequence).isEqualTo(2679); assertThat(mediaPlaylist.mediaSequence).isEqualTo(2679);
assertThat(mediaPlaylist.version).isEqualTo(3); assertThat(mediaPlaylist.version).isEqualTo(3);
assertThat(mediaPlaylist.hasEndTag).isTrue(); assertThat(mediaPlaylist.hasEndTag).isTrue();
assertThat(mediaPlaylist.protectionSchemes).isNull();
List<Segment> segments = mediaPlaylist.segments; List<Segment> segments = mediaPlaylist.segments;
assertThat(segments).isNotNull(); assertThat(segments).isNotNull();
assertThat(segments).hasSize(5); assertThat(segments).hasSize(5);
@ -162,12 +161,17 @@ public class HlsMediaPlaylistParserTest {
+ "#EXTINF:8,\n" + "#EXTINF:8,\n"
+ "https://priv.example.com/2.ts\n" + "https://priv.example.com/2.ts\n"
+ "#EXT-X-ENDLIST\n"; + "#EXT-X-ENDLIST\n";
InputStream inputStream = InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cbcs); assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cbcs);
assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); 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 @Test
@ -186,12 +190,12 @@ public class HlsMediaPlaylistParserTest {
+ "#EXTINF:8,\n" + "#EXTINF:8,\n"
+ "https://priv.example.com/2.ts\n" + "https://priv.example.com/2.ts\n"
+ "#EXT-X-ENDLIST\n"; + "#EXT-X-ENDLIST\n";
InputStream inputStream = InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc); assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cenc);
assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue();
assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse();
} }
@Test @Test
@ -210,12 +214,89 @@ public class HlsMediaPlaylistParserTest {
+ "#EXTINF:8,\n" + "#EXTINF:8,\n"
+ "https://priv.example.com/2.ts\n" + "https://priv.example.com/2.ts\n"
+ "#EXT-X-ENDLIST\n"; + "#EXT-X-ENDLIST\n";
InputStream inputStream = InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc); assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cenc);
assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); 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 @Test
@ -243,8 +324,7 @@ public class HlsMediaPlaylistParserTest {
+ "02/00/42.ts\n" + "02/00/42.ts\n"
+ "#EXTINF:5.005,\n" + "#EXTINF:5.005,\n"
+ "02/00/47.ts\n"; + "02/00/47.ts\n";
InputStream inputStream = InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
@ -272,8 +352,7 @@ public class HlsMediaPlaylistParserTest {
+ "#EXT-X-MAP:URI=\"init2.ts\"" + "#EXT-X-MAP:URI=\"init2.ts\""
+ "#EXTINF:5.005,\n" + "#EXTINF:5.005,\n"
+ "02/00/47.ts\n"; + "02/00/47.ts\n";
InputStream inputStream = InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
@ -303,8 +382,7 @@ public class HlsMediaPlaylistParserTest {
+ "#EXT-X-MAP:URI=\"init2.ts\"" + "#EXT-X-MAP:URI=\"init2.ts\""
+ "#EXTINF:5.005,\n" + "#EXTINF:5.005,\n"
+ "02/00/47.ts\n"; + "02/00/47.ts\n";
InputStream inputStream = InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.hasIndependentSegments).isFalse(); assertThat(playlist.hasIndependentSegments).isFalse();