Add support for variable substition in HLS

Issue:#4422

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=209958623
This commit is contained in:
aquilescanta 2018-08-23 10:27:24 -07:00 committed by Oliver Woodman
parent 4d8a5c44b3
commit 24d04a26e4
5 changed files with 204 additions and 47 deletions

View File

@ -70,6 +70,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 variable substitution
([#4422](https://github.com/google/ExoPlayer/issues/4422)).
* Add support for PlayReady. * Add support for PlayReady.
* Add support for alternative EXT-X-KEY tags. * Add support for alternative EXT-X-KEY tags.
* Set the bitrate on primary track sample formats * Set the bitrate on primary track sample formats

View File

@ -21,6 +21,7 @@ import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
/** Represents an HLS master playlist. */ /** Represents an HLS master playlist. */
public final class HlsMasterPlaylist extends HlsPlaylist { public final class HlsMasterPlaylist extends HlsPlaylist {
@ -35,7 +36,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
/* subtitles= */ Collections.emptyList(), /* subtitles= */ Collections.emptyList(),
/* muxedAudioFormat= */ null, /* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ Collections.emptyList(), /* muxedCaptionFormats= */ Collections.emptyList(),
/* hasIndependentSegments= */ false); /* hasIndependentSegments= */ false,
/* variableDefinitions= */ Collections.emptyMap());
public static final int GROUP_INDEX_VARIANT = 0; public static final int GROUP_INDEX_VARIANT = 0;
public static final int GROUP_INDEX_AUDIO = 1; public static final int GROUP_INDEX_AUDIO = 1;
@ -110,6 +112,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
* captions information. * captions information.
*/ */
public final List<Format> muxedCaptionFormats; public final List<Format> muxedCaptionFormats;
/** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */
public final Map<String, String> variableDefinitions;
/** /**
* @param baseUri See {@link #baseUri}. * @param baseUri See {@link #baseUri}.
@ -120,6 +124,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
* @param muxedAudioFormat See {@link #muxedAudioFormat}. * @param muxedAudioFormat See {@link #muxedAudioFormat}.
* @param muxedCaptionFormats See {@link #muxedCaptionFormats}. * @param muxedCaptionFormats See {@link #muxedCaptionFormats}.
* @param hasIndependentSegments See {@link #hasIndependentSegments}. * @param hasIndependentSegments See {@link #hasIndependentSegments}.
* @param variableDefinitions See {@link #variableDefinitions}.
*/ */
public HlsMasterPlaylist( public HlsMasterPlaylist(
String baseUri, String baseUri,
@ -129,7 +134,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
List<HlsUrl> subtitles, List<HlsUrl> subtitles,
Format muxedAudioFormat, Format muxedAudioFormat,
List<Format> muxedCaptionFormats, List<Format> muxedCaptionFormats,
boolean hasIndependentSegments) { boolean hasIndependentSegments,
Map<String, String> variableDefinitions) {
super(baseUri, tags, hasIndependentSegments); super(baseUri, tags, hasIndependentSegments);
this.variants = Collections.unmodifiableList(variants); this.variants = Collections.unmodifiableList(variants);
this.audios = Collections.unmodifiableList(audios); this.audios = Collections.unmodifiableList(audios);
@ -137,6 +143,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
this.muxedAudioFormat = muxedAudioFormat; this.muxedAudioFormat = muxedAudioFormat;
this.muxedCaptionFormats = muxedCaptionFormats != null this.muxedCaptionFormats = muxedCaptionFormats != null
? Collections.unmodifiableList(muxedCaptionFormats) : null; ? Collections.unmodifiableList(muxedCaptionFormats) : null;
this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions);
} }
@Override @Override
@ -149,7 +156,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
copyRenditionsList(subtitles, GROUP_INDEX_SUBTITLE, streamKeys), copyRenditionsList(subtitles, GROUP_INDEX_SUBTITLE, streamKeys),
muxedAudioFormat, muxedAudioFormat,
muxedCaptionFormats, muxedCaptionFormats,
hasIndependentSegments); hasIndependentSegments,
variableDefinitions);
} }
/** /**
@ -169,7 +177,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
emptyList, emptyList,
/* muxedAudioFormat= */ null, /* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ null, /* muxedCaptionFormats= */ null,
/* hasIndependentSegments= */ false); /* hasIndependentSegments= */ false,
/* variableDefinitions= */ Collections.emptyMap());
} }
private static List<HlsUrl> copyRenditionsList( private static List<HlsUrl> copyRenditionsList(

View File

@ -40,6 +40,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -57,6 +58,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String TAG_VERSION = "#EXT-X-VERSION"; private static final String TAG_VERSION = "#EXT-X-VERSION";
private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE"; private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE";
private static final String TAG_DEFINE = "#EXT-X-DEFINE";
private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF"; private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF";
private static final String TAG_MEDIA = "#EXT-X-MEDIA"; private static final String TAG_MEDIA = "#EXT-X-MEDIA";
private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION"; private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION";
@ -147,6 +149,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT"); private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT");
private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT"); private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT");
private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED"); private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED");
private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\"");
private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\"");
private static final Pattern REGEX_VARIABLE_REFERENCE =
Pattern.compile("\\{\\$([a-zA-Z0-9\\-_]+)\\}");
private final HlsMasterPlaylist masterPlaylist; private final HlsMasterPlaylist masterPlaylist;
@ -239,6 +245,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
throws IOException { throws IOException {
HashSet<String> variantUrls = new HashSet<>(); HashSet<String> variantUrls = new HashSet<>();
HashMap<String, String> audioGroupIdToCodecs = new HashMap<>(); HashMap<String, String> audioGroupIdToCodecs = new HashMap<>();
HashMap<String, String> variableDefinitions = new HashMap<>();
ArrayList<HlsMasterPlaylist.HlsUrl> variants = new ArrayList<>(); ArrayList<HlsMasterPlaylist.HlsUrl> variants = new ArrayList<>();
ArrayList<HlsMasterPlaylist.HlsUrl> audios = new ArrayList<>(); ArrayList<HlsMasterPlaylist.HlsUrl> audios = new ArrayList<>();
ArrayList<HlsMasterPlaylist.HlsUrl> subtitles = new ArrayList<>(); ArrayList<HlsMasterPlaylist.HlsUrl> subtitles = new ArrayList<>();
@ -258,7 +265,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
tags.add(line); tags.add(line);
} }
if (line.equals(TAG_INDEPENDENT_SEGMENTS)) { if (line.startsWith(TAG_DEFINE)) {
variableDefinitions.put(
/* key= */ parseStringAttr(line, REGEX_NAME, variableDefinitions),
/* value= */ parseStringAttr(line, REGEX_VALUE, variableDefinitions));
} else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {
hasIndependentSegmentsTag = true; hasIndependentSegmentsTag = true;
} else if (line.startsWith(TAG_MEDIA)) { } else if (line.startsWith(TAG_MEDIA)) {
// Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF // Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF
@ -267,13 +278,15 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} else if (line.startsWith(TAG_STREAM_INF)) { } else if (line.startsWith(TAG_STREAM_INF)) {
noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE); noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
int bitrate = parseIntAttr(line, REGEX_BANDWIDTH); int bitrate = parseIntAttr(line, REGEX_BANDWIDTH);
String averageBandwidthString = parseOptionalStringAttr(line, REGEX_AVERAGE_BANDWIDTH); String averageBandwidthString =
parseOptionalStringAttr(line, REGEX_AVERAGE_BANDWIDTH, variableDefinitions);
if (averageBandwidthString != null) { if (averageBandwidthString != null) {
// If available, the average bandwidth attribute is used as the variant's bitrate. // If available, the average bandwidth attribute is used as the variant's bitrate.
bitrate = Integer.parseInt(averageBandwidthString); bitrate = Integer.parseInt(averageBandwidthString);
} }
String codecs = parseOptionalStringAttr(line, REGEX_CODECS); String codecs = parseOptionalStringAttr(line, REGEX_CODECS, variableDefinitions);
String resolutionString = parseOptionalStringAttr(line, REGEX_RESOLUTION); String resolutionString =
parseOptionalStringAttr(line, REGEX_RESOLUTION, variableDefinitions);
int width; int width;
int height; int height;
if (resolutionString != null) { if (resolutionString != null) {
@ -290,15 +303,18 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
height = Format.NO_VALUE; height = Format.NO_VALUE;
} }
float frameRate = Format.NO_VALUE; float frameRate = Format.NO_VALUE;
String frameRateString = parseOptionalStringAttr(line, REGEX_FRAME_RATE); String frameRateString =
parseOptionalStringAttr(line, REGEX_FRAME_RATE, variableDefinitions);
if (frameRateString != null) { if (frameRateString != null) {
frameRate = Float.parseFloat(frameRateString); frameRate = Float.parseFloat(frameRateString);
} }
String audioGroupId = parseOptionalStringAttr(line, REGEX_AUDIO); String audioGroupId = parseOptionalStringAttr(line, REGEX_AUDIO, variableDefinitions);
if (audioGroupId != null && codecs != null) { if (audioGroupId != null && codecs != null) {
audioGroupIdToCodecs.put(audioGroupId, Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO)); audioGroupIdToCodecs.put(audioGroupId, Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO));
} }
line = iterator.next(); // #EXT-X-STREAM-INF's URI. line =
replaceVariableReferences(
iterator.next(), variableDefinitions); // #EXT-X-STREAM-INF's URI.
if (variantUrls.add(line)) { if (variantUrls.add(line)) {
Format format = Format format =
Format.createVideoContainerFormat( Format.createVideoContainerFormat(
@ -321,12 +337,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
for (int i = 0; i < mediaTags.size(); i++) { for (int i = 0; i < mediaTags.size(); i++) {
line = mediaTags.get(i); line = mediaTags.get(i);
@C.SelectionFlags int selectionFlags = parseSelectionFlags(line); @C.SelectionFlags int selectionFlags = parseSelectionFlags(line);
String uri = parseOptionalStringAttr(line, REGEX_URI); String uri = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions);
String name = parseStringAttr(line, REGEX_NAME); String name = parseStringAttr(line, REGEX_NAME, variableDefinitions);
String language = parseOptionalStringAttr(line, REGEX_LANGUAGE); String language = parseOptionalStringAttr(line, REGEX_LANGUAGE, variableDefinitions);
String groupId = parseOptionalStringAttr(line, REGEX_GROUP_ID); String groupId = parseOptionalStringAttr(line, REGEX_GROUP_ID, variableDefinitions);
Format format; Format format;
switch (parseStringAttr(line, REGEX_TYPE)) { switch (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) {
case TYPE_AUDIO: case TYPE_AUDIO:
String codecs = audioGroupIdToCodecs.get(groupId); String codecs = audioGroupIdToCodecs.get(groupId);
String sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null; String sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null;
@ -363,7 +379,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
subtitles.add(new HlsMasterPlaylist.HlsUrl(uri, format)); subtitles.add(new HlsMasterPlaylist.HlsUrl(uri, format));
break; break;
case TYPE_CLOSED_CAPTIONS: case TYPE_CLOSED_CAPTIONS:
String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID); String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions);
String mimeType; String mimeType;
int accessibilityChannel; int accessibilityChannel;
if (instreamId.startsWith("CC")) { if (instreamId.startsWith("CC")) {
@ -405,7 +421,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
subtitles, subtitles,
muxedAudioFormat, muxedAudioFormat,
muxedCaptionFormats, muxedCaptionFormats,
hasIndependentSegmentsTag); hasIndependentSegmentsTag,
variableDefinitions);
} }
@C.SelectionFlags @C.SelectionFlags
@ -433,6 +450,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments; boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments;
boolean hasEndTag = false; boolean hasEndTag = false;
Segment initializationSegment = null; Segment initializationSegment = null;
HashMap<String, String> variableDefinitions = new HashMap<>();
List<Segment> segments = new ArrayList<>(); List<Segment> segments = new ArrayList<>();
List<String> tags = new ArrayList<>(); List<String> tags = new ArrayList<>();
@ -465,7 +483,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
if (line.startsWith(TAG_PLAYLIST_TYPE)) { if (line.startsWith(TAG_PLAYLIST_TYPE)) {
String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE); String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE, variableDefinitions);
if ("VOD".equals(playlistTypeString)) { if ("VOD".equals(playlistTypeString)) {
playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD; playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD;
} else if ("EVENT".equals(playlistTypeString)) { } else if ("EVENT".equals(playlistTypeString)) {
@ -474,8 +492,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} else if (line.startsWith(TAG_START)) { } else if (line.startsWith(TAG_START)) {
startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND); startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
} else if (line.startsWith(TAG_INIT_SEGMENT)) { } else if (line.startsWith(TAG_INIT_SEGMENT)) {
String uri = parseStringAttr(line, REGEX_URI); String uri = parseStringAttr(line, REGEX_URI, variableDefinitions);
String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE); String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);
if (byteRange != null) { if (byteRange != null) {
String[] splitByteRange = byteRange.split("@"); String[] splitByteRange = byteRange.split("@");
segmentByteRangeLength = Long.parseLong(splitByteRange[0]); segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
@ -493,24 +511,39 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segmentMediaSequence = mediaSequence; segmentMediaSequence = mediaSequence;
} else if (line.startsWith(TAG_VERSION)) { } else if (line.startsWith(TAG_VERSION)) {
version = parseIntAttr(line, REGEX_VERSION); version = parseIntAttr(line, REGEX_VERSION);
} else if (line.startsWith(TAG_DEFINE)) {
String importName = parseOptionalStringAttr(line, REGEX_IMPORT, variableDefinitions);
if (importName != null) {
String value = masterPlaylist.variableDefinitions.get(importName);
if (value != null) {
variableDefinitions.put(importName, value);
} else {
// The master playlist does not declare the imported variable. Ignore.
}
} else {
variableDefinitions.put(
parseStringAttr(line, REGEX_NAME, variableDefinitions),
parseStringAttr(line, REGEX_VALUE, variableDefinitions));
}
} else if (line.startsWith(TAG_MEDIA_DURATION)) { } else if (line.startsWith(TAG_MEDIA_DURATION)) {
segmentDurationUs = segmentDurationUs =
(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, "", variableDefinitions);
} else if (line.startsWith(TAG_KEY)) { } else if (line.startsWith(TAG_KEY)) {
String method = parseStringAttr(line, REGEX_METHOD); String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
String keyFormat = parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY); String keyFormat =
parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);
encryptionKeyUri = null; encryptionKeyUri = null;
encryptionIV = null; encryptionIV = null;
if (METHOD_NONE.equals(method)) { if (METHOD_NONE.equals(method)) {
currentSchemeDatas.clear(); currentSchemeDatas.clear();
cachedDrmInitData = null; cachedDrmInitData = null;
} else /* !METHOD_NONE.equals(method) */ { } else /* !METHOD_NONE.equals(method) */ {
encryptionIV = parseOptionalStringAttr(line, REGEX_IV); encryptionIV = parseOptionalStringAttr(line, REGEX_IV, variableDefinitions);
if (KEYFORMAT_IDENTITY.equals(keyFormat)) { 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, variableDefinitions);
} else { } else {
// 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.
@ -524,9 +557,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
SchemeData schemeData; SchemeData schemeData;
if (KEYFORMAT_PLAYREADY.equals(keyFormat)) { if (KEYFORMAT_PLAYREADY.equals(keyFormat)) {
schemeData = parsePlayReadySchemeData(line); schemeData = parsePlayReadySchemeData(line, variableDefinitions);
} else { } else {
schemeData = parseWidevineSchemeData(line, keyFormat); schemeData = parseWidevineSchemeData(line, keyFormat, variableDefinitions);
} }
if (schemeData != null) { if (schemeData != null) {
cachedDrmInitData = null; cachedDrmInitData = null;
@ -535,7 +568,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
} }
} else if (line.startsWith(TAG_BYTERANGE)) { } else if (line.startsWith(TAG_BYTERANGE)) {
String byteRange = parseStringAttr(line, REGEX_BYTERANGE); String byteRange = parseStringAttr(line, REGEX_BYTERANGE, variableDefinitions);
String[] splitByteRange = byteRange.split("@"); String[] splitByteRange = byteRange.split("@");
segmentByteRangeLength = Long.parseLong(splitByteRange[0]); segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) { if (splitByteRange.length > 1) {
@ -587,7 +620,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segments.add( segments.add(
new Segment( new Segment(
line, replaceVariableReferences(line, variableDefinitions),
initializationSegment, initializationSegment,
segmentTitle, segmentTitle,
segmentDurationUs, segmentDurationUs,
@ -627,23 +660,28 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segments); segments);
} }
private static @Nullable SchemeData parsePlayReadySchemeData(String line) throws ParserException { private static @Nullable SchemeData parsePlayReadySchemeData(
String keyFormatVersions = parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1"); String line, Map<String, String> variableDefinitions) throws ParserException {
String keyFormatVersions =
parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions);
if (!"1".equals(keyFormatVersions)) { if (!"1".equals(keyFormatVersions)) {
// Not supported. // Not supported.
return null; return null;
} }
String uriString = parseStringAttr(line, REGEX_URI); String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);
byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT); byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT);
byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data); byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data);
return new SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData); return new SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData);
} }
private static @Nullable SchemeData parseWidevineSchemeData(String line, String keyFormat) private static @Nullable SchemeData parseWidevineSchemeData(
String line, String keyFormat, Map<String, String> variableDefinitions)
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, variableDefinitions);
return new SchemeData(C.WIDEVINE_UUID, MimeTypes.VIDEO_MP4, return new SchemeData(
C.WIDEVINE_UUID,
MimeTypes.VIDEO_MP4,
Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT)); Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT));
} }
if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) { if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) {
@ -657,19 +695,21 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
private static int parseIntAttr(String line, Pattern pattern) throws ParserException { private static int parseIntAttr(String line, Pattern pattern) throws ParserException {
return Integer.parseInt(parseStringAttr(line, pattern)); return Integer.parseInt(parseStringAttr(line, pattern, Collections.emptyMap()));
} }
private static long parseLongAttr(String line, Pattern pattern) throws ParserException { private static long parseLongAttr(String line, Pattern pattern) throws ParserException {
return Long.parseLong(parseStringAttr(line, pattern)); return Long.parseLong(parseStringAttr(line, pattern, Collections.emptyMap()));
} }
private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException { private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException {
return Double.parseDouble(parseStringAttr(line, pattern)); return Double.parseDouble(parseStringAttr(line, pattern, Collections.emptyMap()));
} }
private static String parseStringAttr(String line, Pattern pattern) throws ParserException { private static String parseStringAttr(
String value = parseOptionalStringAttr(line, pattern); String line, Pattern pattern, Map<String, String> variableDefinitions)
throws ParserException {
String value = parseOptionalStringAttr(line, pattern, variableDefinitions);
if (value != null) { if (value != null) {
return value; return value;
} else { } else {
@ -677,14 +717,39 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
} }
private static @Nullable String parseOptionalStringAttr(String line, Pattern pattern) { private static @Nullable String parseOptionalStringAttr(
return parseOptionalStringAttr(line, pattern, null); String line, Pattern pattern, Map<String, String> variableDefinitions) {
return parseOptionalStringAttr(line, pattern, null, variableDefinitions);
} }
private static @PolyNull String parseOptionalStringAttr( private static @PolyNull String parseOptionalStringAttr(
String line, Pattern pattern, @PolyNull String defaultValue) { String line,
Pattern pattern,
@PolyNull String defaultValue,
Map<String, String> variableDefinitions) {
Matcher matcher = pattern.matcher(line); Matcher matcher = pattern.matcher(line);
return matcher.find() ? matcher.group(1) : defaultValue; String value = matcher.find() ? matcher.group(1) : defaultValue;
return variableDefinitions.isEmpty() || value == null
? value
: replaceVariableReferences(value, variableDefinitions);
}
private static String replaceVariableReferences(
String string, Map<String, String> variableDefinitions) {
Matcher matcher = REGEX_VARIABLE_REFERENCE.matcher(string);
// TODO: Replace StringBuffer with StringBuilder once Java 9 is available.
StringBuffer stringWithReplacements = new StringBuffer();
while (matcher.find()) {
String groupName = matcher.group(1);
if (variableDefinitions.containsKey(groupName)) {
matcher.appendReplacement(
stringWithReplacements, Matcher.quoteReplacement(variableDefinitions.get(groupName)));
} else {
// The variable is not defined. The value is ignored.
}
}
matcher.appendTail(stringWithReplacements);
return stringWithReplacements.toString();
} }
private static boolean parseOptionalBooleanAttribute( private static boolean parseOptionalBooleanAttribute(

View File

@ -117,6 +117,15 @@ public class HlsMasterPlaylistParserTest {
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
+ "http://example.com/spaces_in_codecs.m3u8\n"; + "http://example.com/spaces_in_codecs.m3u8\n";
private static final String PLAYLIST_WITH_VARIABLE_SUBSTITUTION =
" #EXTM3U \n"
+ "\n"
+ "#EXT-X-DEFINE:NAME=\"codecs\",VALUE=\"mp4a.40.5\"\n"
+ "#EXT-X-DEFINE:NAME=\"tricky\",VALUE=\"This/{$nested}/reference/shouldnt/work\"\n"
+ "#EXT-X-DEFINE:NAME=\"nested\",VALUE=\"This should not be inserted\"\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"{$codecs}\"\n"
+ "http://example.com/{$tricky}\n";
@Test @Test
public void testParseMasterPlaylist() throws IOException { public void testParseMasterPlaylist() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
@ -218,6 +227,15 @@ public class HlsMasterPlaylistParserTest {
assertThat(playlistWithoutIndependentSegments.hasIndependentSegments).isFalse(); assertThat(playlistWithoutIndependentSegments.hasIndependentSegments).isFalse();
} }
@Test
public void testVariableSubstitution() throws IOException {
HlsMasterPlaylist playlistWithSubstitutions =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_VARIABLE_SUBSTITUTION);
HlsMasterPlaylist.HlsUrl variant = playlistWithSubstitutions.variants.get(0);
assertThat(variant.format.codecs).isEqualTo("mp4a.40.5");
assertThat(variant.url).isEqualTo("http://example.com/This/{$nested}/reference/shouldnt/work");
}
private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString)
throws IOException { throws IOException {
Uri playlistUri = Uri.parse(uri); Uri playlistUri = Uri.parse(uri);

View File

@ -25,6 +25,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -397,9 +398,71 @@ public class HlsMediaPlaylistParserTest {
/* subtitles= */ Collections.emptyList(), /* subtitles= */ Collections.emptyList(),
/* muxedAudioFormat= */ null, /* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ null, /* muxedCaptionFormats= */ null,
/* hasIndependentSegments= */ true); /* hasIndependentSegments= */ true,
/* variableDefinitions */ Collections.emptyMap());
HlsMediaPlaylist playlistWithInheritance = HlsMediaPlaylist playlistWithInheritance =
(HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream); (HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream);
assertThat(playlistWithInheritance.hasIndependentSegments).isTrue(); assertThat(playlistWithInheritance.hasIndependentSegments).isTrue();
} }
@Test
public void testVariableSubstitution() throws IOException {
Uri playlistUri = Uri.parse("https://example.com/substitution.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-VERSION:8\n"
+ "#EXT-X-DEFINE:NAME=\"underscore_1\",VALUE=\"{\"\n"
+ "#EXT-X-DEFINE:NAME=\"dash-1\",VALUE=\"replaced_value.ts\"\n"
+ "#EXT-X-TARGETDURATION:5\n"
+ "#EXT-X-MEDIA-SEQUENCE:10\n"
+ "#EXTINF:5.005,\n"
+ "segment1.ts\n"
+ "#EXT-X-MAP:URI=\"{$dash-1}\""
+ "#EXTINF:5.005,\n"
+ "segment{$underscore_1}$name_1}\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
Segment segment = playlist.segments.get(1);
assertThat(segment.initializationSegment.url).isEqualTo("replaced_value.ts");
assertThat(segment.url).isEqualTo("segment{$name_1}");
}
@Test
public void testInheritedVariableSubstitution() throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-VERSION:8\n"
+ "#EXT-X-TARGETDURATION:5\n"
+ "#EXT-X-MEDIA-SEQUENCE:10\n"
+ "#EXT-X-DEFINE:IMPORT=\"imported_base\"\n"
+ "#EXTINF:5.005,\n"
+ "{$imported_base}1.ts\n"
+ "#EXTINF:5.005,\n"
+ "{$imported_base}2.ts\n"
+ "#EXTINF:5.005,\n"
+ "{$imported_base}3.ts\n"
+ "#EXTINF:5.005,\n"
+ "{$imported_base}4.ts\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HashMap<String, String> variableDefinitions = new HashMap<>();
variableDefinitions.put("imported_base", "long_path");
HlsMasterPlaylist masterPlaylist =
new HlsMasterPlaylist(
/* baseUri= */ "",
/* tags= */ Collections.emptyList(),
/* variants= */ Collections.emptyList(),
/* audios= */ Collections.emptyList(),
/* subtitles= */ Collections.emptyList(),
/* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ Collections.emptyList(),
/* hasIndependentSegments= */ false,
variableDefinitions);
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream);
for (int i = 1; i <= 4; i++) {
assertThat(playlist.segments.get(i - 1).url).isEqualTo("long_path" + i + ".ts");
}
}
} }