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:
parent
5b3b4e64f9
commit
d399c00f9e
@ -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`.
|
||||||
|
@ -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)) {
|
||||||
|
@ -375,7 +375,7 @@ import java.util.List;
|
|||||||
isTimestampMaster,
|
isTimestampMaster,
|
||||||
timestampAdjuster,
|
timestampAdjuster,
|
||||||
previous,
|
previous,
|
||||||
mediaPlaylist.drmInitData,
|
segment.drmInitData,
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
encryptionIv);
|
encryptionIv);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user