compile group: 'androidx.media3', name: 'media3-exoplayer-hls', version: '1.5.0-alpha01'
Artifact androidx.media3:media3-exoplayer-hls:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)
HLS playlists parsing logic.
Creates an instance where media playlists are parsed without inheriting attributes from a
multivariant playlist.
Creates an instance where parsed media playlists inherit attributes from the given multivariant
playlist.
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.hls.playlist;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.castNonNull;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Base64;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.DrmInitData;
import androidx.media3.common.DrmInitData.SchemeData;
import androidx.media3.common.Format;
import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.ParserException;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.UriUtil;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.hls.HlsTrackMetadataEntry;
import androidx.media3.exoplayer.hls.HlsTrackMetadataEntry.VariantInfo;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Part;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.RenditionReport;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Segment;
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Rendition;
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Variant;
import androidx.media3.exoplayer.upstream.ParsingLoadable;
import androidx.media3.extractor.mp4.PsshAtomUtil;
import com.google.common.collect.Iterables;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.PolyNull;
/** HLS playlists parsing logic. */
@UnstableApi
public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlaylist> {
/** Exception thrown when merging a delta update fails. */
public static final class DeltaUpdateException extends IOException {}
private static final String LOG_TAG = "HlsPlaylistParser";
private static final String PLAYLIST_HEADER = "#EXTM3U";
private static final String TAG_PREFIX = "#EXT";
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_DEFINE = "#EXT-X-DEFINE";
private static final String TAG_SERVER_CONTROL = "#EXT-X-SERVER-CONTROL";
private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF";
private static final String TAG_PART_INF = "#EXT-X-PART-INF";
private static final String TAG_PART = "#EXT-X-PART";
private static final String TAG_I_FRAME_STREAM_INF = "#EXT-X-I-FRAME-STREAM-INF";
private static final String TAG_IFRAME = "#EXT-X-I-FRAMES-ONLY";
private static final String TAG_MEDIA = "#EXT-X-MEDIA";
private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION";
private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
private static final String TAG_DISCONTINUITY_SEQUENCE = "#EXT-X-DISCONTINUITY-SEQUENCE";
private static final String TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME";
private static final String TAG_INIT_SEGMENT = "#EXT-X-MAP";
private static final String TAG_INDEPENDENT_SEGMENTS = "#EXT-X-INDEPENDENT-SEGMENTS";
private static final String TAG_MEDIA_DURATION = "#EXTINF";
private static final String TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE";
private static final String TAG_START = "#EXT-X-START";
private static final String TAG_ENDLIST = "#EXT-X-ENDLIST";
private static final String TAG_KEY = "#EXT-X-KEY";
private static final String TAG_SESSION_KEY = "#EXT-X-SESSION-KEY";
private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE";
private static final String TAG_GAP = "#EXT-X-GAP";
private static final String TAG_SKIP = "#EXT-X-SKIP";
private static final String TAG_PRELOAD_HINT = "#EXT-X-PRELOAD-HINT";
private static final String TAG_RENDITION_REPORT = "#EXT-X-RENDITION-REPORT";
private static final String TYPE_AUDIO = "AUDIO";
private static final String TYPE_VIDEO = "VIDEO";
private static final String TYPE_SUBTITLES = "SUBTITLES";
private static final String TYPE_CLOSED_CAPTIONS = "CLOSED-CAPTIONS";
private static final String TYPE_PART = "PART";
private static final String TYPE_MAP = "MAP";
private static final String METHOD_NONE = "NONE";
private static final String METHOD_AES_128 = "AES-128";
private static final String METHOD_SAMPLE_AES = "SAMPLE-AES";
// Replaced by METHOD_SAMPLE_AES_CTR. Keep for backward compatibility.
private static final String METHOD_SAMPLE_AES_CENC = "SAMPLE-AES-CENC";
private static final String METHOD_SAMPLE_AES_CTR = "SAMPLE-AES-CTR";
private static final String KEYFORMAT_PLAYREADY = "com.microsoft.playready";
private static final String KEYFORMAT_IDENTITY = "identity";
private static final String KEYFORMAT_WIDEVINE_PSSH_BINARY =
"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
private static final String KEYFORMAT_WIDEVINE_PSSH_JSON = "com.widevine";
private static final String BOOLEAN_TRUE = "YES";
private static final String BOOLEAN_FALSE = "NO";
private static final String ATTR_CLOSED_CAPTIONS_NONE = "CLOSED-CAPTIONS=NONE";
private static final Pattern REGEX_AVERAGE_BANDWIDTH =
Pattern.compile("AVERAGE-BANDWIDTH=(\\d+)\\b");
private static final Pattern REGEX_VIDEO = Pattern.compile("VIDEO=\"(.+?)\"");
private static final Pattern REGEX_AUDIO = Pattern.compile("AUDIO=\"(.+?)\"");
private static final Pattern REGEX_SUBTITLES = Pattern.compile("SUBTITLES=\"(.+?)\"");
private static final Pattern REGEX_CLOSED_CAPTIONS = Pattern.compile("CLOSED-CAPTIONS=\"(.+?)\"");
private static final Pattern REGEX_BANDWIDTH = Pattern.compile("[^-]BANDWIDTH=(\\d+)\\b");
private static final Pattern REGEX_CHANNELS = Pattern.compile("CHANNELS=\"(.+?)\"");
private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
private static final Pattern REGEX_FRAME_RATE = Pattern.compile("FRAME-RATE=([\\d\\.]+)\\b");
private static final Pattern REGEX_TARGET_DURATION =
Pattern.compile(TAG_TARGET_DURATION + ":(\\d+)\\b");
private static final Pattern REGEX_ATTR_DURATION = Pattern.compile("DURATION=([\\d\\.]+)\\b");
private static final Pattern REGEX_PART_TARGET_DURATION =
Pattern.compile("PART-TARGET=([\\d\\.]+)\\b");
private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b");
private static final Pattern REGEX_PLAYLIST_TYPE =
Pattern.compile(TAG_PLAYLIST_TYPE + ":(.+)\\b");
private static final Pattern REGEX_CAN_SKIP_UNTIL =
Pattern.compile("CAN-SKIP-UNTIL=([\\d\\.]+)\\b");
private static final Pattern REGEX_CAN_SKIP_DATE_RANGES =
compileBooleanAttrPattern("CAN-SKIP-DATERANGES");
private static final Pattern REGEX_SKIPPED_SEGMENTS =
Pattern.compile("SKIPPED-SEGMENTS=(\\d+)\\b");
private static final Pattern REGEX_HOLD_BACK = Pattern.compile("[:|,]HOLD-BACK=([\\d\\.]+)\\b");
private static final Pattern REGEX_PART_HOLD_BACK =
Pattern.compile("PART-HOLD-BACK=([\\d\\.]+)\\b");
private static final Pattern REGEX_CAN_BLOCK_RELOAD =
compileBooleanAttrPattern("CAN-BLOCK-RELOAD");
private static final Pattern REGEX_MEDIA_SEQUENCE =
Pattern.compile(TAG_MEDIA_SEQUENCE + ":(\\d+)\\b");
private static final Pattern REGEX_MEDIA_DURATION =
Pattern.compile(TAG_MEDIA_DURATION + ":([\\d\\.]+)\\b");
private static final Pattern REGEX_MEDIA_TITLE =
Pattern.compile(TAG_MEDIA_DURATION + ":[\\d\\.]+\\b,(.+)");
private static final Pattern REGEX_LAST_MSN = Pattern.compile("LAST-MSN" + "=(\\d+)\\b");
private static final Pattern REGEX_LAST_PART = Pattern.compile("LAST-PART" + "=(\\d+)\\b");
private static final Pattern REGEX_TIME_OFFSET = Pattern.compile("TIME-OFFSET=(-?[\\d\\.]+)\\b");
private static final Pattern REGEX_BYTERANGE =
Pattern.compile(TAG_BYTERANGE + ":(\\d+(?:@\\d+)?)\\b");
private static final Pattern REGEX_ATTR_BYTERANGE =
Pattern.compile("BYTERANGE=\"(\\d+(?:@\\d+)?)\\b\"");
private static final Pattern REGEX_BYTERANGE_START = Pattern.compile("BYTERANGE-START=(\\d+)\\b");
private static final Pattern REGEX_BYTERANGE_LENGTH =
Pattern.compile("BYTERANGE-LENGTH=(\\d+)\\b");
private static final Pattern REGEX_METHOD =
Pattern.compile(
"METHOD=("
+ METHOD_NONE
+ "|"
+ METHOD_AES_128
+ "|"
+ METHOD_SAMPLE_AES
+ "|"
+ METHOD_SAMPLE_AES_CENC
+ "|"
+ METHOD_SAMPLE_AES_CTR
+ ")"
+ "\\s*(?:,|$)");
private static final Pattern REGEX_KEYFORMAT = Pattern.compile("KEYFORMAT=\"(.+?)\"");
private static final Pattern REGEX_KEYFORMATVERSIONS =
Pattern.compile("KEYFORMATVERSIONS=\"(.+?)\"");
private static final Pattern REGEX_URI = Pattern.compile("URI=\"(.+?)\"");
private static final Pattern REGEX_IV = Pattern.compile("IV=([^,.*]+)");
private static final Pattern REGEX_TYPE =
Pattern.compile(
"TYPE=("
+ TYPE_AUDIO
+ "|"
+ TYPE_VIDEO
+ "|"
+ TYPE_SUBTITLES
+ "|"
+ TYPE_CLOSED_CAPTIONS
+ ")");
private static final Pattern REGEX_PRELOAD_HINT_TYPE =
Pattern.compile("TYPE=(" + TYPE_PART + "|" + TYPE_MAP + ")");
private static final Pattern REGEX_LANGUAGE = Pattern.compile("LANGUAGE=\"(.+?)\"");
private static final Pattern REGEX_NAME = Pattern.compile("NAME=\"(.+?)\"");
private static final Pattern REGEX_GROUP_ID = Pattern.compile("GROUP-ID=\"(.+?)\"");
private static final Pattern REGEX_CHARACTERISTICS = Pattern.compile("CHARACTERISTICS=\"(.+?)\"");
private static final Pattern REGEX_INSTREAM_ID =
Pattern.compile("INSTREAM-ID=\"((?:CC|SERVICE)\\d+)\"");
private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT");
private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT");
private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED");
private static final Pattern REGEX_INDEPENDENT = compileBooleanAttrPattern("INDEPENDENT");
private static final Pattern REGEX_GAP = compileBooleanAttrPattern("GAP");
private static final Pattern REGEX_PRECISE = compileBooleanAttrPattern("PRECISE");
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 HlsMultivariantPlaylist multivariantPlaylist;
@Nullable private final HlsMediaPlaylist previousMediaPlaylist;
/**
* Creates an instance where media playlists are parsed without inheriting attributes from a
* multivariant playlist.
*/
public HlsPlaylistParser() {
this(HlsMultivariantPlaylist.EMPTY, /* previousMediaPlaylist= */ null);
}
/**
* Creates an instance where parsed media playlists inherit attributes from the given multivariant
* playlist.
*
* @param multivariantPlaylist The multivariant playlist from which media playlists will inherit
* attributes.
* @param previousMediaPlaylist The previous media playlist from which the new media playlist may
* inherit skipped segments.
*/
public HlsPlaylistParser(
HlsMultivariantPlaylist multivariantPlaylist,
@Nullable HlsMediaPlaylist previousMediaPlaylist) {
this.multivariantPlaylist = multivariantPlaylist;
this.previousMediaPlaylist = previousMediaPlaylist;
}
@Override
public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
Queue<String> extraLines = new ArrayDeque<>();
String line;
try {
if (!checkPlaylistHeader(reader)) {
throw ParserException.createForMalformedManifest(
/* message= */ "Input does not start with the #EXTM3U header.", /* cause= */ null);
}
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) {
// Do nothing.
} else if (line.startsWith(TAG_STREAM_INF)) {
extraLines.add(line);
return parseMultivariantPlaylist(new LineIterator(extraLines, reader), uri.toString());
} else if (line.startsWith(TAG_TARGET_DURATION)
|| line.startsWith(TAG_MEDIA_SEQUENCE)
|| line.startsWith(TAG_MEDIA_DURATION)
|| line.startsWith(TAG_KEY)
|| line.startsWith(TAG_BYTERANGE)
|| line.equals(TAG_DISCONTINUITY)
|| line.equals(TAG_DISCONTINUITY_SEQUENCE)
|| line.equals(TAG_ENDLIST)) {
extraLines.add(line);
return parseMediaPlaylist(
multivariantPlaylist,
previousMediaPlaylist,
new LineIterator(extraLines, reader),
uri.toString());
} else {
extraLines.add(line);
}
}
} finally {
Util.closeQuietly(reader);
}
throw ParserException.createForMalformedManifest(
"Failed to parse the playlist, could not identify any tags.", /* cause= */ null);
}
private static boolean checkPlaylistHeader(BufferedReader reader) throws IOException {
int last = reader.read();
if (last == 0xEF) {
if (reader.read() != 0xBB || reader.read() != 0xBF) {
return false;
}
// The playlist contains a Byte Order Mark, which gets discarded.
last = reader.read();
}
last = skipIgnorableWhitespace(reader, true, last);
int playlistHeaderLength = PLAYLIST_HEADER.length();
for (int i = 0; i < playlistHeaderLength; i++) {
if (last != PLAYLIST_HEADER.charAt(i)) {
return false;
}
last = reader.read();
}
last = skipIgnorableWhitespace(reader, false, last);
return Util.isLinebreak(last);
}
private static int skipIgnorableWhitespace(BufferedReader reader, boolean skipLinebreaks, int c)
throws IOException {
while (c != -1 && Character.isWhitespace(c) && (skipLinebreaks || !Util.isLinebreak(c))) {
c = reader.read();
}
return c;
}
private static HlsMultivariantPlaylist parseMultivariantPlaylist(
LineIterator iterator, String baseUri) throws IOException {
HashMap<Uri, ArrayList<VariantInfo>> urlToVariantInfos = new HashMap<>();
HashMap<String, String> variableDefinitions = new HashMap<>();
ArrayList<Variant> variants = new ArrayList<>();
ArrayList<Rendition> videos = new ArrayList<>();
ArrayList<Rendition> audios = new ArrayList<>();
ArrayList<Rendition> subtitles = new ArrayList<>();
ArrayList<Rendition> closedCaptions = new ArrayList<>();
ArrayList<String> mediaTags = new ArrayList<>();
ArrayList<DrmInitData> sessionKeyDrmInitData = new ArrayList<>();
ArrayList<String> tags = new ArrayList<>();
Format muxedAudioFormat = null;
List<Format> muxedCaptionFormats = null;
boolean noClosedCaptions = false;
boolean hasIndependentSegmentsTag = false;
String line;
while (iterator.hasNext()) {
line = iterator.next();
if (line.startsWith(TAG_PREFIX)) {
// We expose all tags through the playlist.
tags.add(line);
}
boolean isIFrameOnlyVariant = line.startsWith(TAG_I_FRAME_STREAM_INF);
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;
} else if (line.startsWith(TAG_MEDIA)) {
// Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF
// tags.
mediaTags.add(line);
} else if (line.startsWith(TAG_SESSION_KEY)) {
String keyFormat =
parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);
SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);
if (schemeData != null) {
String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
String scheme = parseEncryptionScheme(method);
sessionKeyDrmInitData.add(new DrmInitData(scheme, schemeData));
}
} else if (line.startsWith(TAG_STREAM_INF) || isIFrameOnlyVariant) {
noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
int roleFlags = isIFrameOnlyVariant ? C.ROLE_FLAG_TRICK_PLAY : 0;
int peakBitrate = parseIntAttr(line, REGEX_BANDWIDTH);
int averageBitrate = parseOptionalIntAttr(line, REGEX_AVERAGE_BANDWIDTH, -1);
String codecs = parseOptionalStringAttr(line, REGEX_CODECS, variableDefinitions);
String resolutionString =
parseOptionalStringAttr(line, REGEX_RESOLUTION, variableDefinitions);
int width;
int height;
if (resolutionString != null) {
String[] widthAndHeight = Util.split(resolutionString, "x");
width = Integer.parseInt(widthAndHeight[0]);
height = Integer.parseInt(widthAndHeight[1]);
if (width <= 0 || height <= 0) {
// Resolution string is invalid.
width = Format.NO_VALUE;
height = Format.NO_VALUE;
}
} else {
width = Format.NO_VALUE;
height = Format.NO_VALUE;
}
float frameRate = Format.NO_VALUE;
String frameRateString =
parseOptionalStringAttr(line, REGEX_FRAME_RATE, variableDefinitions);
if (frameRateString != null) {
frameRate = Float.parseFloat(frameRateString);
}
String videoGroupId = parseOptionalStringAttr(line, REGEX_VIDEO, variableDefinitions);
String audioGroupId = parseOptionalStringAttr(line, REGEX_AUDIO, variableDefinitions);
String subtitlesGroupId =
parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions);
String closedCaptionsGroupId =
parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions);
Uri uri;
if (isIFrameOnlyVariant) {
uri =
UriUtil.resolveToUri(baseUri, parseStringAttr(line, REGEX_URI, variableDefinitions));
} else if (!iterator.hasNext()) {
throw ParserException.createForMalformedManifest(
"#EXT-X-STREAM-INF must be followed by another line", /* cause= */ null);
} else {
// The following line contains #EXT-X-STREAM-INF's URI.
line = replaceVariableReferences(iterator.next(), variableDefinitions);
uri = UriUtil.resolveToUri(baseUri, line);
}
Format format =
new Format.Builder()
.setId(variants.size())
.setContainerMimeType(MimeTypes.APPLICATION_M3U8)
.setCodecs(codecs)
.setAverageBitrate(averageBitrate)
.setPeakBitrate(peakBitrate)
.setWidth(width)
.setHeight(height)
.setFrameRate(frameRate)
.setRoleFlags(roleFlags)
.build();
Variant variant =
new Variant(
uri, format, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId);
variants.add(variant);
@Nullable ArrayList<VariantInfo> variantInfosForUrl = urlToVariantInfos.get(uri);
if (variantInfosForUrl == null) {
variantInfosForUrl = new ArrayList<>();
urlToVariantInfos.put(uri, variantInfosForUrl);
}
variantInfosForUrl.add(
new VariantInfo(
averageBitrate,
peakBitrate,
videoGroupId,
audioGroupId,
subtitlesGroupId,
closedCaptionsGroupId));
}
}
// TODO: Don't deduplicate variants by URL.
ArrayList<Variant> deduplicatedVariants = new ArrayList<>();
HashSet<Uri> urlsInDeduplicatedVariants = new HashSet<>();
for (int i = 0; i < variants.size(); i++) {
Variant variant = variants.get(i);
if (urlsInDeduplicatedVariants.add(variant.url)) {
Assertions.checkState(variant.format.metadata == null);
HlsTrackMetadataEntry hlsMetadataEntry =
new HlsTrackMetadataEntry(
/* groupId= */ null,
/* name= */ null,
checkNotNull(urlToVariantInfos.get(variant.url)));
Metadata metadata = new Metadata(hlsMetadataEntry);
Format format = variant.format.buildUpon().setMetadata(metadata).build();
deduplicatedVariants.add(variant.copyWithFormat(format));
}
}
for (int i = 0; i < mediaTags.size(); i++) {
line = mediaTags.get(i);
String groupId = parseStringAttr(line, REGEX_GROUP_ID, variableDefinitions);
String name = parseStringAttr(line, REGEX_NAME, variableDefinitions);
Format.Builder formatBuilder =
new Format.Builder()
.setId(groupId + ":" + name)
.setLabel(name)
.setContainerMimeType(MimeTypes.APPLICATION_M3U8)
.setSelectionFlags(parseSelectionFlags(line))
.setRoleFlags(parseRoleFlags(line, variableDefinitions))
.setLanguage(parseOptionalStringAttr(line, REGEX_LANGUAGE, variableDefinitions));
@Nullable String referenceUri = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions);
@Nullable Uri uri = referenceUri == null ? null : UriUtil.resolveToUri(baseUri, referenceUri);
Metadata metadata =
new Metadata(new HlsTrackMetadataEntry(groupId, name, Collections.emptyList()));
switch (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) {
case TYPE_VIDEO:
@Nullable Variant variant = getVariantWithVideoGroup(variants, groupId);
if (variant != null) {
Format variantFormat = variant.format;
@Nullable
String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO);
formatBuilder
.setCodecs(codecs)
.setSampleMimeType(MimeTypes.getMediaMimeType(codecs))
.setWidth(variantFormat.width)
.setHeight(variantFormat.height)
.setFrameRate(variantFormat.frameRate);
}
if (uri == null) {
// TODO: Remove this case and add a Rendition with a null uri to videos.
} else {
formatBuilder.setMetadata(metadata);
videos.add(new Rendition(uri, formatBuilder.build(), groupId, name));
}
break;
case TYPE_AUDIO:
@Nullable String sampleMimeType = null;
variant = getVariantWithAudioGroup(variants, groupId);
if (variant != null) {
@Nullable
String codecs = Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_AUDIO);
formatBuilder.setCodecs(codecs);
sampleMimeType = MimeTypes.getMediaMimeType(codecs);
}
@Nullable
String channelsString =
parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions);
if (channelsString != null) {
int channelCount = Integer.parseInt(Util.splitAtFirst(channelsString, "/")[0]);
formatBuilder.setChannelCount(channelCount);
if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType) && channelsString.endsWith("/JOC")) {
sampleMimeType = MimeTypes.AUDIO_E_AC3_JOC;
formatBuilder.setCodecs(MimeTypes.CODEC_E_AC3_JOC);
}
}
formatBuilder.setSampleMimeType(sampleMimeType);
if (uri != null) {
formatBuilder.setMetadata(metadata);
audios.add(new Rendition(uri, formatBuilder.build(), groupId, name));
} else if (variant != null) {
// TODO: Remove muxedAudioFormat and add a Rendition with a null uri to audios.
muxedAudioFormat = formatBuilder.build();
}
break;
case TYPE_SUBTITLES:
sampleMimeType = null;
variant = getVariantWithSubtitleGroup(variants, groupId);
if (variant != null) {
@Nullable
String codecs = Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_TEXT);
formatBuilder.setCodecs(codecs);
sampleMimeType = MimeTypes.getMediaMimeType(codecs);
}
if (sampleMimeType == null) {
sampleMimeType = MimeTypes.TEXT_VTT;
}
formatBuilder.setSampleMimeType(sampleMimeType).setMetadata(metadata);
if (uri != null) {
subtitles.add(new Rendition(uri, formatBuilder.build(), groupId, name));
} else {
Log.w(LOG_TAG, "EXT-X-MEDIA tag with missing mandatory URI attribute: skipping");
}
break;
case TYPE_CLOSED_CAPTIONS:
String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions);
int accessibilityChannel;
if (instreamId.startsWith("CC")) {
sampleMimeType = MimeTypes.APPLICATION_CEA608;
accessibilityChannel = Integer.parseInt(instreamId.substring(2));
} else /* starts with SERVICE */ {
sampleMimeType = MimeTypes.APPLICATION_CEA708;
accessibilityChannel = Integer.parseInt(instreamId.substring(7));
}
if (muxedCaptionFormats == null) {
muxedCaptionFormats = new ArrayList<>();
}
formatBuilder
.setSampleMimeType(sampleMimeType)
.setAccessibilityChannel(accessibilityChannel);
muxedCaptionFormats.add(formatBuilder.build());
// TODO: Remove muxedCaptionFormats and add a Rendition with a null uri to closedCaptions.
break;
default:
// Do nothing.
break;
}
}
if (noClosedCaptions) {
muxedCaptionFormats = Collections.emptyList();
}
return new HlsMultivariantPlaylist(
baseUri,
tags,
deduplicatedVariants,
videos,
audios,
subtitles,
closedCaptions,
muxedAudioFormat,
muxedCaptionFormats,
hasIndependentSegmentsTag,
variableDefinitions,
sessionKeyDrmInitData);
}
@Nullable
private static Variant getVariantWithAudioGroup(ArrayList<Variant> variants, String groupId) {
for (int i = 0; i < variants.size(); i++) {
Variant variant = variants.get(i);
if (groupId.equals(variant.audioGroupId)) {
return variant;
}
}
return null;
}
@Nullable
private static Variant getVariantWithVideoGroup(ArrayList<Variant> variants, String groupId) {
for (int i = 0; i < variants.size(); i++) {
Variant variant = variants.get(i);
if (groupId.equals(variant.videoGroupId)) {
return variant;
}
}
return null;
}
@Nullable
private static Variant getVariantWithSubtitleGroup(ArrayList<Variant> variants, String groupId) {
for (int i = 0; i < variants.size(); i++) {
Variant variant = variants.get(i);
if (groupId.equals(variant.subtitleGroupId)) {
return variant;
}
}
return null;
}
private static HlsMediaPlaylist parseMediaPlaylist(
HlsMultivariantPlaylist multivariantPlaylist,
@Nullable HlsMediaPlaylist previousMediaPlaylist,
LineIterator iterator,
String baseUri)
throws IOException {
@HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;
long startOffsetUs = C.TIME_UNSET;
long mediaSequence = 0;
int version = 1; // Default version == 1.
long targetDurationUs = C.TIME_UNSET;
long partTargetDurationUs = C.TIME_UNSET;
boolean hasIndependentSegmentsTag = multivariantPlaylist.hasIndependentSegments;
boolean hasEndTag = false;
@Nullable Segment initializationSegment = null;
HashMap<String, String> variableDefinitions = new HashMap<>();
HashMap<String, Segment> urlToInferredInitSegment = new HashMap<>();
List<Segment> segments = new ArrayList<>();
List<Part> trailingParts = new ArrayList<>();
@Nullable Part preloadPart = null;
List<RenditionReport> renditionReports = new ArrayList<>();
List<String> tags = new ArrayList<>();
long segmentDurationUs = 0;
String segmentTitle = "";
boolean hasDiscontinuitySequence = false;
int playlistDiscontinuitySequence = 0;
int relativeDiscontinuitySequence = 0;
long playlistStartTimeUs = 0;
long segmentStartTimeUs = 0;
boolean preciseStart = false;
long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET;
long partStartTimeUs = 0;
long partByteRangeOffset = 0;
boolean isIFrameOnly = false;
long segmentMediaSequence = 0;
boolean hasGapTag = false;
HlsMediaPlaylist.ServerControl serverControl =
new HlsMediaPlaylist.ServerControl(
/* skipUntilUs= */ C.TIME_UNSET,
/* canSkipDateRanges= */ false,
/* holdBackUs= */ C.TIME_UNSET,
/* partHoldBackUs= */ C.TIME_UNSET,
/* canBlockReload= */ false);
@Nullable DrmInitData playlistProtectionSchemes = null;
@Nullable String fullSegmentEncryptionKeyUri = null;
@Nullable String fullSegmentEncryptionIV = null;
TreeMap<String, SchemeData> currentSchemeDatas = new TreeMap<>();
@Nullable String encryptionScheme = null;
@Nullable DrmInitData cachedDrmInitData = null;
String line;
while (iterator.hasNext()) {
line = iterator.next();
if (line.startsWith(TAG_PREFIX)) {
// We expose all tags through the playlist.
tags.add(line);
}
if (line.startsWith(TAG_PLAYLIST_TYPE)) {
String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE, variableDefinitions);
if ("VOD".equals(playlistTypeString)) {
playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD;
} else if ("EVENT".equals(playlistTypeString)) {
playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT;
}
} else if (line.equals(TAG_IFRAME)) {
isIFrameOnly = true;
} else if (line.startsWith(TAG_START)) {
startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
preciseStart =
parseOptionalBooleanAttribute(line, REGEX_PRECISE, /* defaultValue= */ false);
} else if (line.startsWith(TAG_SERVER_CONTROL)) {
serverControl = parseServerControl(line);
} else if (line.startsWith(TAG_PART_INF)) {
double partTargetDurationSeconds = parseDoubleAttr(line, REGEX_PART_TARGET_DURATION);
partTargetDurationUs = (long) (partTargetDurationSeconds * C.MICROS_PER_SECOND);
} else if (line.startsWith(TAG_INIT_SEGMENT)) {
String uri = parseStringAttr(line, REGEX_URI, variableDefinitions);
String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);
if (byteRange != null) {
String[] splitByteRange = Util.split(byteRange, "@");
segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) {
segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
}
}
if (segmentByteRangeLength == C.LENGTH_UNSET) {
// The segment has no byte range defined.
segmentByteRangeOffset = 0;
}
if (fullSegmentEncryptionKeyUri != null && fullSegmentEncryptionIV == null) {
// See RFC 8216, Section 4.3.2.5.
throw ParserException.createForMalformedManifest(
"The encryption IV attribute must be present when an initialization segment is"
+ " encrypted with METHOD=AES-128.",
/* cause= */ null);
}
initializationSegment =
new Segment(
uri,
segmentByteRangeOffset,
segmentByteRangeLength,
fullSegmentEncryptionKeyUri,
fullSegmentEncryptionIV);
if (segmentByteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset += segmentByteRangeLength;
}
segmentByteRangeLength = C.LENGTH_UNSET;
} else if (line.startsWith(TAG_TARGET_DURATION)) {
targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND;
} else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
mediaSequence = parseLongAttr(line, REGEX_MEDIA_SEQUENCE);
segmentMediaSequence = mediaSequence;
} else if (line.startsWith(TAG_VERSION)) {
version = parseIntAttr(line, REGEX_VERSION);
} else if (line.startsWith(TAG_DEFINE)) {
String importName = parseOptionalStringAttr(line, REGEX_IMPORT, variableDefinitions);
if (importName != null) {
String value = multivariantPlaylist.variableDefinitions.get(importName);
if (value != null) {
variableDefinitions.put(importName, value);
} else {
// The multivariant 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)) {
segmentDurationUs = parseTimeSecondsToUs(line, REGEX_MEDIA_DURATION);
segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "", variableDefinitions);
} else if (line.startsWith(TAG_SKIP)) {
int skippedSegmentCount = parseIntAttr(line, REGEX_SKIPPED_SEGMENTS);
checkState(previousMediaPlaylist != null && segments.isEmpty());
int startIndex = (int) (mediaSequence - castNonNull(previousMediaPlaylist).mediaSequence);
int endIndex = startIndex + skippedSegmentCount;
if (startIndex < 0 || endIndex > previousMediaPlaylist.segments.size()) {
// Throw to force a reload if not all segments are available in the previous playlist.
throw new DeltaUpdateException();
}
for (int i = startIndex; i < endIndex; i++) {
Segment segment = previousMediaPlaylist.segments.get(i);
if (mediaSequence != previousMediaPlaylist.mediaSequence) {
// If the media sequences of the playlists are not the same, we need to recreate the
// object with the updated relative start time and the relative discontinuity
// sequence. With identical playlist media sequences these values do not change.
int newRelativeDiscontinuitySequence =
previousMediaPlaylist.discontinuitySequence
- playlistDiscontinuitySequence
+ segment.relativeDiscontinuitySequence;
segment = segment.copyWith(segmentStartTimeUs, newRelativeDiscontinuitySequence);
}
segments.add(segment);
segmentStartTimeUs += segment.durationUs;
partStartTimeUs = segmentStartTimeUs;
if (segment.byteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset = segment.byteRangeOffset + segment.byteRangeLength;
}
relativeDiscontinuitySequence = segment.relativeDiscontinuitySequence;
initializationSegment = segment.initializationSegment;
cachedDrmInitData = segment.drmInitData;
fullSegmentEncryptionKeyUri = segment.fullSegmentEncryptionKeyUri;
if (segment.encryptionIV == null
|| !segment.encryptionIV.equals(Long.toHexString(segmentMediaSequence))) {
fullSegmentEncryptionIV = segment.encryptionIV;
}
segmentMediaSequence++;
}
} else if (line.startsWith(TAG_KEY)) {
String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
String keyFormat =
parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);
fullSegmentEncryptionKeyUri = null;
fullSegmentEncryptionIV = null;
if (METHOD_NONE.equals(method)) {
currentSchemeDatas.clear();
cachedDrmInitData = null;
} else /* !METHOD_NONE.equals(method) */ {
fullSegmentEncryptionIV = parseOptionalStringAttr(line, REGEX_IV, variableDefinitions);
if (KEYFORMAT_IDENTITY.equals(keyFormat)) {
if (METHOD_AES_128.equals(method)) {
// The segment is fully encrypted using an identity key.
fullSegmentEncryptionKeyUri = parseStringAttr(line, REGEX_URI, variableDefinitions);
} else {
// Do nothing. Samples are encrypted using an identity key, but this is not supported.
// Hopefully, a traditional DRM alternative is also provided.
}
} else {
if (encryptionScheme == null) {
encryptionScheme = parseEncryptionScheme(method);
}
SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);
if (schemeData != null) {
cachedDrmInitData = null;
currentSchemeDatas.put(keyFormat, schemeData);
}
}
}
} else if (line.startsWith(TAG_BYTERANGE)) {
String byteRange = parseStringAttr(line, REGEX_BYTERANGE, variableDefinitions);
String[] splitByteRange = Util.split(byteRange, "@");
segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) {
segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
}
} else if (line.startsWith(TAG_DISCONTINUITY_SEQUENCE)) {
hasDiscontinuitySequence = true;
playlistDiscontinuitySequence = Integer.parseInt(line.substring(line.indexOf(':') + 1));
} else if (line.equals(TAG_DISCONTINUITY)) {
relativeDiscontinuitySequence++;
} else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {
if (playlistStartTimeUs == 0) {
long programDatetimeUs =
Util.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;
}
} else if (line.equals(TAG_GAP)) {
hasGapTag = true;
} else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {
hasIndependentSegmentsTag = true;
} else if (line.equals(TAG_ENDLIST)) {
hasEndTag = true;
} else if (line.startsWith(TAG_RENDITION_REPORT)) {
long lastMediaSequence = parseOptionalLongAttr(line, REGEX_LAST_MSN, C.INDEX_UNSET);
int lastPartIndex = parseOptionalIntAttr(line, REGEX_LAST_PART, C.INDEX_UNSET);
String uri = parseStringAttr(line, REGEX_URI, variableDefinitions);
Uri playlistUri = Uri.parse(UriUtil.resolve(baseUri, uri));
renditionReports.add(new RenditionReport(playlistUri, lastMediaSequence, lastPartIndex));
} else if (line.startsWith(TAG_PRELOAD_HINT)) {
if (preloadPart != null) {
continue;
}
String type = parseStringAttr(line, REGEX_PRELOAD_HINT_TYPE, variableDefinitions);
if (!TYPE_PART.equals(type)) {
continue;
}
String url = parseStringAttr(line, REGEX_URI, variableDefinitions);
long byteRangeStart =
parseOptionalLongAttr(line, REGEX_BYTERANGE_START, /* defaultValue= */ C.LENGTH_UNSET);
long byteRangeLength =
parseOptionalLongAttr(line, REGEX_BYTERANGE_LENGTH, /* defaultValue= */ C.LENGTH_UNSET);
@Nullable
String segmentEncryptionIV =
getSegmentEncryptionIV(
segmentMediaSequence, fullSegmentEncryptionKeyUri, fullSegmentEncryptionIV);
if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
if (playlistProtectionSchemes == null) {
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
}
}
if (byteRangeStart == C.LENGTH_UNSET || byteRangeLength != C.LENGTH_UNSET) {
// Skip preload part if it is an unbounded range request.
preloadPart =
new Part(
url,
initializationSegment,
/* durationUs= */ 0,
relativeDiscontinuitySequence,
partStartTimeUs,
cachedDrmInitData,
fullSegmentEncryptionKeyUri,
segmentEncryptionIV,
byteRangeStart != C.LENGTH_UNSET ? byteRangeStart : 0,
byteRangeLength,
/* hasGapTag= */ false,
/* isIndependent= */ false,
/* isPreload= */ true);
}
} else if (line.startsWith(TAG_PART)) {
@Nullable
String segmentEncryptionIV =
getSegmentEncryptionIV(
segmentMediaSequence, fullSegmentEncryptionKeyUri, fullSegmentEncryptionIV);
String url = parseStringAttr(line, REGEX_URI, variableDefinitions);
long partDurationUs =
(long) (parseDoubleAttr(line, REGEX_ATTR_DURATION) * C.MICROS_PER_SECOND);
boolean isIndependent =
parseOptionalBooleanAttribute(line, REGEX_INDEPENDENT, /* defaultValue= */ false);
// The first part of a segment is always independent if the segments are independent.
isIndependent |= hasIndependentSegmentsTag && trailingParts.isEmpty();
boolean isGap = parseOptionalBooleanAttribute(line, REGEX_GAP, /* defaultValue= */ false);
@Nullable
String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);
long partByteRangeLength = C.LENGTH_UNSET;
if (byteRange != null) {
String[] splitByteRange = Util.split(byteRange, "@");
partByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) {
partByteRangeOffset = Long.parseLong(splitByteRange[1]);
}
}
if (partByteRangeLength == C.LENGTH_UNSET) {
partByteRangeOffset = 0;
}
if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
if (playlistProtectionSchemes == null) {
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
}
}
trailingParts.add(
new Part(
url,
initializationSegment,
partDurationUs,
relativeDiscontinuitySequence,
partStartTimeUs,
cachedDrmInitData,
fullSegmentEncryptionKeyUri,
segmentEncryptionIV,
partByteRangeOffset,
partByteRangeLength,
isGap,
isIndependent,
/* isPreload= */ false));
partStartTimeUs += partDurationUs;
if (partByteRangeLength != C.LENGTH_UNSET) {
partByteRangeOffset += partByteRangeLength;
}
} else if (!line.startsWith("#")) {
@Nullable
String segmentEncryptionIV =
getSegmentEncryptionIV(
segmentMediaSequence, fullSegmentEncryptionKeyUri, fullSegmentEncryptionIV);
segmentMediaSequence++;
String segmentUri = replaceVariableReferences(line, variableDefinitions);
@Nullable Segment inferredInitSegment = urlToInferredInitSegment.get(segmentUri);
if (segmentByteRangeLength == C.LENGTH_UNSET) {
// The segment has no byte range defined.
segmentByteRangeOffset = 0;
} else if (isIFrameOnly && initializationSegment == null && inferredInitSegment == null) {
// The segment is a resource byte range without an initialization segment.
// As per RFC 8216, Section 4.3.3.6, we assume the initialization section exists in the
// bytes preceding the first segment in this segment's URL.
// We assume the implicit initialization segment is unencrypted, since there's no way for
// the playlist to provide an initialization vector for it.
inferredInitSegment =
new Segment(
segmentUri,
/* byteRangeOffset= */ 0,
segmentByteRangeOffset,
/* fullSegmentEncryptionKeyUri= */ null,
/* encryptionIV= */ null);
urlToInferredInitSegment.put(segmentUri, inferredInitSegment);
}
if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
if (playlistProtectionSchemes == null) {
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
}
}
segments.add(
new Segment(
segmentUri,
initializationSegment != null ? initializationSegment : inferredInitSegment,
segmentTitle,
segmentDurationUs,
relativeDiscontinuitySequence,
segmentStartTimeUs,
cachedDrmInitData,
fullSegmentEncryptionKeyUri,
segmentEncryptionIV,
segmentByteRangeOffset,
segmentByteRangeLength,
hasGapTag,
trailingParts));
segmentStartTimeUs += segmentDurationUs;
partStartTimeUs = segmentStartTimeUs;
segmentDurationUs = 0;
segmentTitle = "";
trailingParts = new ArrayList<>();
if (segmentByteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset += segmentByteRangeLength;
}
segmentByteRangeLength = C.LENGTH_UNSET;
hasGapTag = false;
}
}
Map<Uri, RenditionReport> renditionReportMap = new HashMap<>();
for (int i = 0; i < renditionReports.size(); i++) {
RenditionReport renditionReport = renditionReports.get(i);
long lastMediaSequence = renditionReport.lastMediaSequence;
if (lastMediaSequence == C.INDEX_UNSET) {
lastMediaSequence = mediaSequence + segments.size() - (trailingParts.isEmpty() ? 1 : 0);
}
int lastPartIndex = renditionReport.lastPartIndex;
if (lastPartIndex == C.INDEX_UNSET && partTargetDurationUs != C.TIME_UNSET) {
List<Part> lastParts =
trailingParts.isEmpty() ? Iterables.getLast(segments).parts : trailingParts;
lastPartIndex = lastParts.size() - 1;
}
renditionReportMap.put(
renditionReport.playlistUri,
new RenditionReport(renditionReport.playlistUri, lastMediaSequence, lastPartIndex));
}
if (preloadPart != null) {
trailingParts.add(preloadPart);
}
return new HlsMediaPlaylist(
playlistType,
baseUri,
tags,
startOffsetUs,
preciseStart,
playlistStartTimeUs,
hasDiscontinuitySequence,
playlistDiscontinuitySequence,
mediaSequence,
version,
targetDurationUs,
partTargetDurationUs,
hasIndependentSegmentsTag,
hasEndTag,
/* hasProgramDateTime= */ playlistStartTimeUs != 0,
playlistProtectionSchemes,
segments,
trailingParts,
serverControl,
renditionReportMap);
}
private static DrmInitData getPlaylistProtectionSchemes(
@Nullable String encryptionScheme, SchemeData[] schemeDatas) {
SchemeData[] playlistSchemeDatas = new SchemeData[schemeDatas.length];
for (int i = 0; i < schemeDatas.length; i++) {
playlistSchemeDatas[i] = schemeDatas[i].copyWithData(null);
}
return new DrmInitData(encryptionScheme, playlistSchemeDatas);
}
@Nullable
private static String getSegmentEncryptionIV(
long segmentMediaSequence,
@Nullable String fullSegmentEncryptionKeyUri,
@Nullable String fullSegmentEncryptionIV) {
if (fullSegmentEncryptionKeyUri == null) {
return null;
} else if (fullSegmentEncryptionIV != null) {
return fullSegmentEncryptionIV;
}
return Long.toHexString(segmentMediaSequence);
}
private static @C.SelectionFlags int parseSelectionFlags(String line) {
int flags = 0;
if (parseOptionalBooleanAttribute(line, REGEX_DEFAULT, false)) {
flags |= C.SELECTION_FLAG_DEFAULT;
}
if (parseOptionalBooleanAttribute(line, REGEX_FORCED, false)) {
flags |= C.SELECTION_FLAG_FORCED;
}
if (parseOptionalBooleanAttribute(line, REGEX_AUTOSELECT, false)) {
flags |= C.SELECTION_FLAG_AUTOSELECT;
}
return flags;
}
private static @C.RoleFlags int parseRoleFlags(
String line, Map<String, String> variableDefinitions) {
String concatenatedCharacteristics =
parseOptionalStringAttr(line, REGEX_CHARACTERISTICS, variableDefinitions);
if (TextUtils.isEmpty(concatenatedCharacteristics)) {
return 0;
}
String[] characteristics = Util.split(concatenatedCharacteristics, ",");
@C.RoleFlags int roleFlags = 0;
if (Util.contains(characteristics, "public.accessibility.describes-video")) {
roleFlags |= C.ROLE_FLAG_DESCRIBES_VIDEO;
}
if (Util.contains(characteristics, "public.accessibility.transcribes-spoken-dialog")) {
roleFlags |= C.ROLE_FLAG_TRANSCRIBES_DIALOG;
}
if (Util.contains(characteristics, "public.accessibility.describes-music-and-sound")) {
roleFlags |= C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND;
}
if (Util.contains(characteristics, "public.easy-to-read")) {
roleFlags |= C.ROLE_FLAG_EASY_TO_READ;
}
return roleFlags;
}
@Nullable
private static SchemeData parseDrmSchemeData(
String line, String keyFormat, Map<String, String> variableDefinitions)
throws ParserException {
String keyFormatVersions =
parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions);
if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) {
String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);
return new SchemeData(
C.WIDEVINE_UUID,
MimeTypes.VIDEO_MP4,
Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT));
} else if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) {
return new SchemeData(C.WIDEVINE_UUID, "hls", Util.getUtf8Bytes(line));
} else if (KEYFORMAT_PLAYREADY.equals(keyFormat) && "1".equals(keyFormatVersions)) {
String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);
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);
}
return null;
}
private static HlsMediaPlaylist.ServerControl parseServerControl(String line) {
double skipUntilSeconds =
parseOptionalDoubleAttr(line, REGEX_CAN_SKIP_UNTIL, /* defaultValue= */ C.TIME_UNSET);
long skipUntilUs =
skipUntilSeconds == C.TIME_UNSET
? C.TIME_UNSET
: (long) (skipUntilSeconds * C.MICROS_PER_SECOND);
boolean canSkipDateRanges =
parseOptionalBooleanAttribute(line, REGEX_CAN_SKIP_DATE_RANGES, /* defaultValue= */ false);
double holdBackSeconds =
parseOptionalDoubleAttr(line, REGEX_HOLD_BACK, /* defaultValue= */ C.TIME_UNSET);
long holdBackUs =
holdBackSeconds == C.TIME_UNSET
? C.TIME_UNSET
: (long) (holdBackSeconds * C.MICROS_PER_SECOND);
double partHoldBackSeconds = parseOptionalDoubleAttr(line, REGEX_PART_HOLD_BACK, C.TIME_UNSET);
long partHoldBackUs =
partHoldBackSeconds == C.TIME_UNSET
? C.TIME_UNSET
: (long) (partHoldBackSeconds * C.MICROS_PER_SECOND);
boolean canBlockReload =
parseOptionalBooleanAttribute(line, REGEX_CAN_BLOCK_RELOAD, /* defaultValue= */ false);
return new HlsMediaPlaylist.ServerControl(
skipUntilUs, canSkipDateRanges, holdBackUs, partHoldBackUs, canBlockReload);
}
private static String parseEncryptionScheme(String method) {
return METHOD_SAMPLE_AES_CENC.equals(method) || METHOD_SAMPLE_AES_CTR.equals(method)
? C.CENC_TYPE_cenc
: C.CENC_TYPE_cbcs;
}
private static int parseIntAttr(String line, Pattern pattern) throws ParserException {
return Integer.parseInt(parseStringAttr(line, pattern, Collections.emptyMap()));
}
private static int parseOptionalIntAttr(String line, Pattern pattern, int defaultValue) {
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return Integer.parseInt(checkNotNull(matcher.group(1)));
}
return defaultValue;
}
private static long parseLongAttr(String line, Pattern pattern) throws ParserException {
return Long.parseLong(parseStringAttr(line, pattern, Collections.emptyMap()));
}
private static long parseOptionalLongAttr(String line, Pattern pattern, long defaultValue) {
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return Long.parseLong(checkNotNull(matcher.group(1)));
}
return defaultValue;
}
private static long parseTimeSecondsToUs(String line, Pattern pattern) throws ParserException {
String timeValueSeconds = parseStringAttr(line, pattern, Collections.emptyMap());
BigDecimal timeValue = new BigDecimal(timeValueSeconds);
return timeValue.multiply(new BigDecimal(C.MICROS_PER_SECOND)).longValue();
}
private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException {
return Double.parseDouble(parseStringAttr(line, pattern, Collections.emptyMap()));
}
private static String parseStringAttr(
String line, Pattern pattern, Map<String, String> variableDefinitions)
throws ParserException {
String value = parseOptionalStringAttr(line, pattern, variableDefinitions);
if (value != null) {
return value;
} else {
throw ParserException.createForMalformedManifest(
"Couldn't match " + pattern.pattern() + " in " + line, /* cause= */ null);
}
}
@Nullable
private static String parseOptionalStringAttr(
String line, Pattern pattern, Map<String, String> variableDefinitions) {
return parseOptionalStringAttr(line, pattern, null, variableDefinitions);
}
private static @PolyNull String parseOptionalStringAttr(
String line,
Pattern pattern,
@PolyNull String defaultValue,
Map<String, String> variableDefinitions) {
Matcher matcher = pattern.matcher(line);
@PolyNull String value = matcher.find() ? checkNotNull(matcher.group(1)) : defaultValue;
return variableDefinitions.isEmpty() || value == null
? value
: replaceVariableReferences(value, variableDefinitions);
}
private static double parseOptionalDoubleAttr(String line, Pattern pattern, double defaultValue) {
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return Double.parseDouble(checkNotNull(matcher.group(1)));
}
return defaultValue;
}
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(
String line, Pattern pattern, boolean defaultValue) {
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return BOOLEAN_TRUE.equals(matcher.group(1));
}
return defaultValue;
}
private static Pattern compileBooleanAttrPattern(String attribute) {
return Pattern.compile(attribute + "=(" + BOOLEAN_FALSE + "|" + BOOLEAN_TRUE + ")");
}
private static class LineIterator {
private final BufferedReader reader;
private final Queue<String> extraLines;
@Nullable private String next;
public LineIterator(Queue<String> extraLines, BufferedReader reader) {
this.extraLines = extraLines;
this.reader = reader;
}
@EnsuresNonNullIf(expression = "next", result = true)
public boolean hasNext() throws IOException {
if (next != null) {
return true;
}
if (!extraLines.isEmpty()) {
next = checkNotNull(extraLines.poll());
return true;
}
while ((next = reader.readLine()) != null) {
next = next.trim();
if (!next.isEmpty()) {
return true;
}
}
return false;
}
/** Return the next line, or throw {@link NoSuchElementException} if none. */
public String next() throws IOException {
if (hasNext()) {
String result = next;
next = null;
return result;
} else {
throw new NoSuchElementException();
}
}
}
}