Skip to content

Commit

Permalink
Add support for multiple alternative EXT-X-KEY in HLS
Browse files Browse the repository at this point in the history
Also add support for parsing PlayReady DRM information

Issue:#4180

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=208094290
  • Loading branch information
AquilesCanta authored and ojw28 committed Aug 13, 2018
1 parent 5b3b4e6 commit d399c00
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 52 deletions.
2 changes: 2 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,16 @@ public boolean hasData() {
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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ public void getNextChunk(
isTimestampMaster,
timestampAdjuster,
previous,
mediaPlaylist.drmInitData,
segment.drmInitData,
encryptionKey,
encryptionIv);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public static final class Segment implements Comparable<Long> {
* 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.
Expand Down Expand Up @@ -91,6 +96,7 @@ public Segment(String uri, long byterangeOffset, long byterangeLength) {
/* durationUs= */ 0,
/* relativeDiscontinuitySequence= */ -1,
/* relativeStartTimeUs= */ C.TIME_UNSET,
/* drmInitData= */ null,
/* fullSegmentEncryptionKeyUri= */ null,
/* encryptionIV= */ null,
byterangeOffset,
Expand All @@ -105,6 +111,7 @@ public Segment(String uri, long byterangeOffset, long byterangeLength) {
* @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}.
Expand All @@ -118,6 +125,7 @@ public Segment(
long durationUs,
int relativeDiscontinuitySequence,
long relativeStartTimeUs,
@Nullable DrmInitData drmInitData,
@Nullable String fullSegmentEncryptionKeyUri,
@Nullable String encryptionIV,
long byterangeOffset,
Expand All @@ -129,6 +137,7 @@ public Segment(
this.durationUs = durationUs;
this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
this.relativeStartTimeUs = relativeStartTimeUs;
this.drmInitData = drmInitData;
this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri;
this.encryptionIV = encryptionIV;
this.byterangeOffset = byterangeOffset;
Expand Down Expand Up @@ -199,10 +208,10 @@ public int compareTo(@NonNull Long relativeStartTimeUs) {
*/
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.
*/
Expand All @@ -225,8 +234,8 @@ public int compareTo(@NonNull Long relativeStartTimeUs) {
* @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(
Expand All @@ -243,7 +252,7 @@ public HlsMediaPlaylist(
boolean hasIndependentSegments,
boolean hasEndTag,
boolean hasProgramDateTime,
DrmInitData drmInitData,
@Nullable DrmInitData protectionSchemes,
List<Segment> segments) {
super(baseUri, tags, hasIndependentSegments);
this.playlistType = playlistType;
Expand All @@ -255,7 +264,7 @@ public HlsMediaPlaylist(
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);
Expand Down Expand Up @@ -323,7 +332,7 @@ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) {
hasIndependentSegments,
hasEndTag,
hasProgramDateTime,
drmInitData,
protectionSchemes,
segments);
}

Expand Down Expand Up @@ -357,7 +366,7 @@ public HlsMediaPlaylist copyWithMasterPlaylistInfo(HlsMasterPlaylist masterPlayl
hasIndependentSegments || masterPlaylist.hasIndependentSegments,
hasEndTag,
hasProgramDateTime,
drmInitData,
protectionSchemes,
segments);
}

Expand All @@ -383,7 +392,7 @@ public HlsMediaPlaylist copyWithEndTag() {
hasIndependentSegments,
/* hasEndTag= */ true,
hasProgramDateTime,
drmInitData,
protectionSchemes,
segments);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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;
Expand All @@ -40,6 +41,7 @@
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;
Expand Down Expand Up @@ -82,6 +84,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
// 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_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_WIDEVINE_PSSH_BINARY =
"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
Expand Down Expand Up @@ -128,8 +131,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
+ "|"
+ METHOD_SAMPLE_AES_CTR
+ ")"
+ "\\s*(,|$)");
+ "\\s*(?:,|$)");
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_IV = Pattern.compile("IV=([^,.*]+)");
private static final Pattern REGEX_TYPE = Pattern.compile("TYPE=(" + TYPE_AUDIO + "|" + TYPE_VIDEO
Expand Down Expand Up @@ -422,9 +427,12 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
long segmentMediaSequence = 0;
boolean hasGapTag = false;

DrmInitData playlistProtectionSchemes = null;
String encryptionKeyUri = null;
String encryptionIV = null;
DrmInitData drmInitData = null;
TreeMap<String, SchemeData> currentSchemeDatas = new TreeMap<>();
String encryptionScheme = null;
DrmInitData cachedDrmInitData = null;

String line;
while (iterator.hasNext()) {
Expand Down Expand Up @@ -469,30 +477,39 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
(long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "");
} else if (line.startsWith(TAG_KEY)) {
String method = parseOptionalStringAttr(line, REGEX_METHOD);
String keyFormat = parseOptionalStringAttr(line, REGEX_KEYFORMAT);
String method = parseStringAttr(line, REGEX_METHOD);
String keyFormat = parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY);
encryptionKeyUri = 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);
if (KEYFORMAT_IDENTITY.equals(keyFormat) || keyFormat == null) {
if (KEYFORMAT_IDENTITY.equals(keyFormat)) {
if (METHOD_AES_128.equals(method)) {
// The segment is fully encrypted using an identity key.
encryptionKeyUri = parseStringAttr(line, REGEX_URI);
} else {
// Do nothing. Samples are encrypted using an identity key, but this is not supported.
// Hopefully, a traditional DRM alternative is also provided.
}
} else if (method != null) {
SchemeData schemeData = parseWidevineSchemeData(line, keyFormat);
} else {
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) {
drmInitData =
new DrmInitData(
(METHOD_SAMPLE_AES_CENC.equals(method)
|| METHOD_SAMPLE_AES_CTR.equals(method))
? C.CENC_TYPE_cenc
: C.CENC_TYPE_cbcs,
schemeData);
cachedDrmInitData = null;
currentSchemeDatas.put(keyFormat, schemeData);
}
}
}
Expand Down Expand Up @@ -529,10 +546,24 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
} else {
segmentEncryptionIV = Long.toHexString(segmentMediaSequence);
}

segmentMediaSequence++;
if (segmentByteRangeLength == C.LENGTH_UNSET) {
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(
new Segment(
line,
Expand All @@ -541,6 +572,7 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
segmentDurationUs,
relativeDiscontinuitySequence,
segmentStartTimeUs,
cachedDrmInitData,
encryptionKeyUri,
segmentEncryptionIV,
segmentByteRangeOffset,
Expand Down Expand Up @@ -570,11 +602,23 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
hasIndependentSegmentsTag,
hasEndTag,
/* hasProgramDateTime= */ playlistStartTimeUs != 0,
drmInitData,
playlistProtectionSchemes,
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 {
if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) {
String uriString = parseStringAttr(line, REGEX_URI);
Expand Down Expand Up @@ -604,11 +648,12 @@ private static double parseDoubleAttr(String line, Pattern pattern) throws Parse
}

private static String parseStringAttr(String line, Pattern pattern) throws ParserException {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1);
String value = parseOptionalStringAttr(line, pattern);
if (value != null) {
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) {
Expand Down
Loading

0 comments on commit d399c00

Please sign in to comment.