mirror of
https://github.com/androidx/media.git
synced 2025-05-14 02:59:52 +08:00
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:
parent
4d8a5c44b3
commit
24d04a26e4
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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,24 +660,29 @@ 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(
|
||||||
Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT));
|
C.WIDEVINE_UUID,
|
||||||
|
MimeTypes.VIDEO_MP4,
|
||||||
|
Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT));
|
||||||
}
|
}
|
||||||
if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) {
|
if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) {
|
||||||
try {
|
try {
|
||||||
@ -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(
|
||||||
|
@ -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);
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user