• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.source.hls.playlist;
17 
18 import android.net.Uri;
19 import android.text.TextUtils;
20 import android.util.Base64;
21 import androidx.annotation.Nullable;
22 import com.google.android.exoplayer2.C;
23 import com.google.android.exoplayer2.Format;
24 import com.google.android.exoplayer2.ParserException;
25 import com.google.android.exoplayer2.drm.DrmInitData;
26 import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
27 import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
28 import com.google.android.exoplayer2.metadata.Metadata;
29 import com.google.android.exoplayer2.source.UnrecognizedInputFormatException;
30 import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry;
31 import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry.VariantInfo;
32 import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition;
33 import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
34 import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
35 import com.google.android.exoplayer2.upstream.ParsingLoadable;
36 import com.google.android.exoplayer2.util.Assertions;
37 import com.google.android.exoplayer2.util.MimeTypes;
38 import com.google.android.exoplayer2.util.UriUtil;
39 import com.google.android.exoplayer2.util.Util;
40 import java.io.BufferedReader;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.InputStreamReader;
44 import java.util.ArrayDeque;
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.HashMap;
48 import java.util.HashSet;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.NoSuchElementException;
52 import java.util.Queue;
53 import java.util.TreeMap;
54 import java.util.regex.Matcher;
55 import java.util.regex.Pattern;
56 import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
57 import org.checkerframework.checker.nullness.qual.PolyNull;
58 
59 /**
60  * HLS playlists parsing logic.
61  */
62 public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlaylist> {
63 
64   private static final String PLAYLIST_HEADER = "#EXTM3U";
65 
66   private static final String TAG_PREFIX = "#EXT";
67 
68   private static final String TAG_VERSION = "#EXT-X-VERSION";
69   private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE";
70   private static final String TAG_DEFINE = "#EXT-X-DEFINE";
71   private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF";
72   private static final String TAG_I_FRAME_STREAM_INF = "#EXT-X-I-FRAME-STREAM-INF";
73   private static final String TAG_IFRAME = "#EXT-X-I-FRAMES-ONLY";
74   private static final String TAG_MEDIA = "#EXT-X-MEDIA";
75   private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION";
76   private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
77   private static final String TAG_DISCONTINUITY_SEQUENCE = "#EXT-X-DISCONTINUITY-SEQUENCE";
78   private static final String TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME";
79   private static final String TAG_INIT_SEGMENT = "#EXT-X-MAP";
80   private static final String TAG_INDEPENDENT_SEGMENTS = "#EXT-X-INDEPENDENT-SEGMENTS";
81   private static final String TAG_MEDIA_DURATION = "#EXTINF";
82   private static final String TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE";
83   private static final String TAG_START = "#EXT-X-START";
84   private static final String TAG_ENDLIST = "#EXT-X-ENDLIST";
85   private static final String TAG_KEY = "#EXT-X-KEY";
86   private static final String TAG_SESSION_KEY = "#EXT-X-SESSION-KEY";
87   private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE";
88   private static final String TAG_GAP = "#EXT-X-GAP";
89 
90   private static final String TYPE_AUDIO = "AUDIO";
91   private static final String TYPE_VIDEO = "VIDEO";
92   private static final String TYPE_SUBTITLES = "SUBTITLES";
93   private static final String TYPE_CLOSED_CAPTIONS = "CLOSED-CAPTIONS";
94 
95   private static final String METHOD_NONE = "NONE";
96   private static final String METHOD_AES_128 = "AES-128";
97   private static final String METHOD_SAMPLE_AES = "SAMPLE-AES";
98   // Replaced by METHOD_SAMPLE_AES_CTR. Keep for backward compatibility.
99   private static final String METHOD_SAMPLE_AES_CENC = "SAMPLE-AES-CENC";
100   private static final String METHOD_SAMPLE_AES_CTR = "SAMPLE-AES-CTR";
101   private static final String KEYFORMAT_PLAYREADY = "com.microsoft.playready";
102   private static final String KEYFORMAT_IDENTITY = "identity";
103   private static final String KEYFORMAT_WIDEVINE_PSSH_BINARY =
104       "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
105   private static final String KEYFORMAT_WIDEVINE_PSSH_JSON = "com.widevine";
106 
107   private static final String BOOLEAN_TRUE = "YES";
108   private static final String BOOLEAN_FALSE = "NO";
109 
110   private static final String ATTR_CLOSED_CAPTIONS_NONE = "CLOSED-CAPTIONS=NONE";
111 
112   private static final Pattern REGEX_AVERAGE_BANDWIDTH =
113       Pattern.compile("AVERAGE-BANDWIDTH=(\\d+)\\b");
114   private static final Pattern REGEX_VIDEO = Pattern.compile("VIDEO=\"(.+?)\"");
115   private static final Pattern REGEX_AUDIO = Pattern.compile("AUDIO=\"(.+?)\"");
116   private static final Pattern REGEX_SUBTITLES = Pattern.compile("SUBTITLES=\"(.+?)\"");
117   private static final Pattern REGEX_CLOSED_CAPTIONS = Pattern.compile("CLOSED-CAPTIONS=\"(.+?)\"");
118   private static final Pattern REGEX_BANDWIDTH = Pattern.compile("[^-]BANDWIDTH=(\\d+)\\b");
119   private static final Pattern REGEX_CHANNELS = Pattern.compile("CHANNELS=\"(.+?)\"");
120   private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
121   private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
122   private static final Pattern REGEX_FRAME_RATE = Pattern.compile("FRAME-RATE=([\\d\\.]+)\\b");
123   private static final Pattern REGEX_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION
124       + ":(\\d+)\\b");
125   private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b");
126   private static final Pattern REGEX_PLAYLIST_TYPE = Pattern.compile(TAG_PLAYLIST_TYPE
127       + ":(.+)\\b");
128   private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE
129       + ":(\\d+)\\b");
130   private static final Pattern REGEX_MEDIA_DURATION = Pattern.compile(TAG_MEDIA_DURATION
131       + ":([\\d\\.]+)\\b");
132   private static final Pattern REGEX_MEDIA_TITLE =
133       Pattern.compile(TAG_MEDIA_DURATION + ":[\\d\\.]+\\b,(.+)");
134   private static final Pattern REGEX_TIME_OFFSET = Pattern.compile("TIME-OFFSET=(-?[\\d\\.]+)\\b");
135   private static final Pattern REGEX_BYTERANGE = Pattern.compile(TAG_BYTERANGE
136       + ":(\\d+(?:@\\d+)?)\\b");
137   private static final Pattern REGEX_ATTR_BYTERANGE =
138       Pattern.compile("BYTERANGE=\"(\\d+(?:@\\d+)?)\\b\"");
139   private static final Pattern REGEX_METHOD =
140       Pattern.compile(
141           "METHOD=("
142               + METHOD_NONE
143               + "|"
144               + METHOD_AES_128
145               + "|"
146               + METHOD_SAMPLE_AES
147               + "|"
148               + METHOD_SAMPLE_AES_CENC
149               + "|"
150               + METHOD_SAMPLE_AES_CTR
151               + ")"
152               + "\\s*(?:,|$)");
153   private static final Pattern REGEX_KEYFORMAT = Pattern.compile("KEYFORMAT=\"(.+?)\"");
154   private static final Pattern REGEX_KEYFORMATVERSIONS =
155       Pattern.compile("KEYFORMATVERSIONS=\"(.+?)\"");
156   private static final Pattern REGEX_URI = Pattern.compile("URI=\"(.+?)\"");
157   private static final Pattern REGEX_IV = Pattern.compile("IV=([^,.*]+)");
158   private static final Pattern REGEX_TYPE = Pattern.compile("TYPE=(" + TYPE_AUDIO + "|" + TYPE_VIDEO
159       + "|" + TYPE_SUBTITLES + "|" + TYPE_CLOSED_CAPTIONS + ")");
160   private static final Pattern REGEX_LANGUAGE = Pattern.compile("LANGUAGE=\"(.+?)\"");
161   private static final Pattern REGEX_NAME = Pattern.compile("NAME=\"(.+?)\"");
162   private static final Pattern REGEX_GROUP_ID = Pattern.compile("GROUP-ID=\"(.+?)\"");
163   private static final Pattern REGEX_CHARACTERISTICS = Pattern.compile("CHARACTERISTICS=\"(.+?)\"");
164   private static final Pattern REGEX_INSTREAM_ID =
165       Pattern.compile("INSTREAM-ID=\"((?:CC|SERVICE)\\d+)\"");
166   private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT");
167   private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT");
168   private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED");
169   private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\"");
170   private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\"");
171   private static final Pattern REGEX_VARIABLE_REFERENCE =
172       Pattern.compile("\\{\\$([a-zA-Z0-9\\-_]+)\\}");
173 
174   private final HlsMasterPlaylist masterPlaylist;
175 
176   /**
177    * Creates an instance where media playlists are parsed without inheriting attributes from a
178    * master playlist.
179    */
HlsPlaylistParser()180   public HlsPlaylistParser() {
181     this(HlsMasterPlaylist.EMPTY);
182   }
183 
184   /**
185    * Creates an instance where parsed media playlists inherit attributes from the given master
186    * playlist.
187    *
188    * @param masterPlaylist The master playlist from which media playlists will inherit attributes.
189    */
HlsPlaylistParser(HlsMasterPlaylist masterPlaylist)190   public HlsPlaylistParser(HlsMasterPlaylist masterPlaylist) {
191     this.masterPlaylist = masterPlaylist;
192   }
193 
194   @Override
parse(Uri uri, InputStream inputStream)195   public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException {
196     BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
197     Queue<String> extraLines = new ArrayDeque<>();
198     String line;
199     try {
200       if (!checkPlaylistHeader(reader)) {
201         throw new UnrecognizedInputFormatException("Input does not start with the #EXTM3U header.",
202             uri);
203       }
204       while ((line = reader.readLine()) != null) {
205         line = line.trim();
206         if (line.isEmpty()) {
207           // Do nothing.
208         } else if (line.startsWith(TAG_STREAM_INF)) {
209           extraLines.add(line);
210           return parseMasterPlaylist(new LineIterator(extraLines, reader), uri.toString());
211         } else if (line.startsWith(TAG_TARGET_DURATION)
212             || line.startsWith(TAG_MEDIA_SEQUENCE)
213             || line.startsWith(TAG_MEDIA_DURATION)
214             || line.startsWith(TAG_KEY)
215             || line.startsWith(TAG_BYTERANGE)
216             || line.equals(TAG_DISCONTINUITY)
217             || line.equals(TAG_DISCONTINUITY_SEQUENCE)
218             || line.equals(TAG_ENDLIST)) {
219           extraLines.add(line);
220           return parseMediaPlaylist(
221               masterPlaylist, new LineIterator(extraLines, reader), uri.toString());
222         } else {
223           extraLines.add(line);
224         }
225       }
226     } finally {
227       Util.closeQuietly(reader);
228     }
229     throw new ParserException("Failed to parse the playlist, could not identify any tags.");
230   }
231 
checkPlaylistHeader(BufferedReader reader)232   private static boolean checkPlaylistHeader(BufferedReader reader) throws IOException {
233     int last = reader.read();
234     if (last == 0xEF) {
235       if (reader.read() != 0xBB || reader.read() != 0xBF) {
236         return false;
237       }
238       // The playlist contains a Byte Order Mark, which gets discarded.
239       last = reader.read();
240     }
241     last = skipIgnorableWhitespace(reader, true, last);
242     int playlistHeaderLength = PLAYLIST_HEADER.length();
243     for (int i = 0; i < playlistHeaderLength; i++) {
244       if (last != PLAYLIST_HEADER.charAt(i)) {
245         return false;
246       }
247       last = reader.read();
248     }
249     last = skipIgnorableWhitespace(reader, false, last);
250     return Util.isLinebreak(last);
251   }
252 
skipIgnorableWhitespace(BufferedReader reader, boolean skipLinebreaks, int c)253   private static int skipIgnorableWhitespace(BufferedReader reader, boolean skipLinebreaks, int c)
254       throws IOException {
255     while (c != -1 && Character.isWhitespace(c) && (skipLinebreaks || !Util.isLinebreak(c))) {
256       c = reader.read();
257     }
258     return c;
259   }
260 
parseMasterPlaylist(LineIterator iterator, String baseUri)261   private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri)
262       throws IOException {
263     HashMap<Uri, ArrayList<VariantInfo>> urlToVariantInfos = new HashMap<>();
264     HashMap<String, String> variableDefinitions = new HashMap<>();
265     ArrayList<Variant> variants = new ArrayList<>();
266     ArrayList<Rendition> videos = new ArrayList<>();
267     ArrayList<Rendition> audios = new ArrayList<>();
268     ArrayList<Rendition> subtitles = new ArrayList<>();
269     ArrayList<Rendition> closedCaptions = new ArrayList<>();
270     ArrayList<String> mediaTags = new ArrayList<>();
271     ArrayList<DrmInitData> sessionKeyDrmInitData = new ArrayList<>();
272     ArrayList<String> tags = new ArrayList<>();
273     Format muxedAudioFormat = null;
274     List<Format> muxedCaptionFormats = null;
275     boolean noClosedCaptions = false;
276     boolean hasIndependentSegmentsTag = false;
277 
278     String line;
279     while (iterator.hasNext()) {
280       line = iterator.next();
281 
282       if (line.startsWith(TAG_PREFIX)) {
283         // We expose all tags through the playlist.
284         tags.add(line);
285       }
286       boolean isIFrameOnlyVariant = line.startsWith(TAG_I_FRAME_STREAM_INF);
287 
288       if (line.startsWith(TAG_DEFINE)) {
289         variableDefinitions.put(
290             /* key= */ parseStringAttr(line, REGEX_NAME, variableDefinitions),
291             /* value= */ parseStringAttr(line, REGEX_VALUE, variableDefinitions));
292       } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {
293         hasIndependentSegmentsTag = true;
294       } else if (line.startsWith(TAG_MEDIA)) {
295         // Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF
296         // tags.
297         mediaTags.add(line);
298       } else if (line.startsWith(TAG_SESSION_KEY)) {
299         String keyFormat =
300             parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);
301         SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);
302         if (schemeData != null) {
303           String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
304           String scheme = parseEncryptionScheme(method);
305           sessionKeyDrmInitData.add(new DrmInitData(scheme, schemeData));
306         }
307       } else if (line.startsWith(TAG_STREAM_INF) || isIFrameOnlyVariant) {
308         noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
309         int roleFlags = isIFrameOnlyVariant ? C.ROLE_FLAG_TRICK_PLAY : 0;
310         int peakBitrate = parseIntAttr(line, REGEX_BANDWIDTH);
311         int averageBitrate = parseOptionalIntAttr(line, REGEX_AVERAGE_BANDWIDTH, -1);
312         String codecs = parseOptionalStringAttr(line, REGEX_CODECS, variableDefinitions);
313         String resolutionString =
314             parseOptionalStringAttr(line, REGEX_RESOLUTION, variableDefinitions);
315         int width;
316         int height;
317         if (resolutionString != null) {
318           String[] widthAndHeight = resolutionString.split("x");
319           width = Integer.parseInt(widthAndHeight[0]);
320           height = Integer.parseInt(widthAndHeight[1]);
321           if (width <= 0 || height <= 0) {
322             // Resolution string is invalid.
323             width = Format.NO_VALUE;
324             height = Format.NO_VALUE;
325           }
326         } else {
327           width = Format.NO_VALUE;
328           height = Format.NO_VALUE;
329         }
330         float frameRate = Format.NO_VALUE;
331         String frameRateString =
332             parseOptionalStringAttr(line, REGEX_FRAME_RATE, variableDefinitions);
333         if (frameRateString != null) {
334           frameRate = Float.parseFloat(frameRateString);
335         }
336         String videoGroupId = parseOptionalStringAttr(line, REGEX_VIDEO, variableDefinitions);
337         String audioGroupId = parseOptionalStringAttr(line, REGEX_AUDIO, variableDefinitions);
338         String subtitlesGroupId =
339             parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions);
340         String closedCaptionsGroupId =
341             parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions);
342         Uri uri;
343         if (isIFrameOnlyVariant) {
344           uri =
345               UriUtil.resolveToUri(baseUri, parseStringAttr(line, REGEX_URI, variableDefinitions));
346         } else if (!iterator.hasNext()) {
347           throw new ParserException("#EXT-X-STREAM-INF must be followed by another line");
348         } else {
349           // The following line contains #EXT-X-STREAM-INF's URI.
350           line = replaceVariableReferences(iterator.next(), variableDefinitions);
351           uri = UriUtil.resolveToUri(baseUri, line);
352         }
353 
354         Format format =
355             new Format.Builder()
356                 .setId(variants.size())
357                 .setContainerMimeType(MimeTypes.APPLICATION_M3U8)
358                 .setCodecs(codecs)
359                 .setAverageBitrate(averageBitrate)
360                 .setPeakBitrate(peakBitrate)
361                 .setWidth(width)
362                 .setHeight(height)
363                 .setFrameRate(frameRate)
364                 .setRoleFlags(roleFlags)
365                 .build();
366         Variant variant =
367             new Variant(
368                 uri, format, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId);
369         variants.add(variant);
370         @Nullable ArrayList<VariantInfo> variantInfosForUrl = urlToVariantInfos.get(uri);
371         if (variantInfosForUrl == null) {
372           variantInfosForUrl = new ArrayList<>();
373           urlToVariantInfos.put(uri, variantInfosForUrl);
374         }
375         variantInfosForUrl.add(
376             new VariantInfo(
377                 averageBitrate,
378                 peakBitrate,
379                 videoGroupId,
380                 audioGroupId,
381                 subtitlesGroupId,
382                 closedCaptionsGroupId));
383       }
384     }
385 
386     // TODO: Don't deduplicate variants by URL.
387     ArrayList<Variant> deduplicatedVariants = new ArrayList<>();
388     HashSet<Uri> urlsInDeduplicatedVariants = new HashSet<>();
389     for (int i = 0; i < variants.size(); i++) {
390       Variant variant = variants.get(i);
391       if (urlsInDeduplicatedVariants.add(variant.url)) {
392         Assertions.checkState(variant.format.metadata == null);
393         HlsTrackMetadataEntry hlsMetadataEntry =
394             new HlsTrackMetadataEntry(
395                 /* groupId= */ null,
396                 /* name= */ null,
397                 Assertions.checkNotNull(urlToVariantInfos.get(variant.url)));
398         Metadata metadata = new Metadata(hlsMetadataEntry);
399         Format format = variant.format.buildUpon().setMetadata(metadata).build();
400         deduplicatedVariants.add(variant.copyWithFormat(format));
401       }
402     }
403 
404     for (int i = 0; i < mediaTags.size(); i++) {
405       line = mediaTags.get(i);
406       String groupId = parseStringAttr(line, REGEX_GROUP_ID, variableDefinitions);
407       String name = parseStringAttr(line, REGEX_NAME, variableDefinitions);
408       Format.Builder formatBuilder =
409           new Format.Builder()
410               .setId(groupId + ":" + name)
411               .setLabel(name)
412               .setContainerMimeType(MimeTypes.APPLICATION_M3U8)
413               .setSelectionFlags(parseSelectionFlags(line))
414               .setRoleFlags(parseRoleFlags(line, variableDefinitions))
415               .setLanguage(parseOptionalStringAttr(line, REGEX_LANGUAGE, variableDefinitions));
416 
417       @Nullable String referenceUri = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions);
418       @Nullable Uri uri = referenceUri == null ? null : UriUtil.resolveToUri(baseUri, referenceUri);
419       Metadata metadata =
420           new Metadata(new HlsTrackMetadataEntry(groupId, name, Collections.emptyList()));
421       switch (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) {
422         case TYPE_VIDEO:
423           @Nullable Variant variant = getVariantWithVideoGroup(variants, groupId);
424           if (variant != null) {
425             Format variantFormat = variant.format;
426             @Nullable
427             String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO);
428             formatBuilder
429                 .setCodecs(codecs)
430                 .setSampleMimeType(MimeTypes.getMediaMimeType(codecs))
431                 .setWidth(variantFormat.width)
432                 .setHeight(variantFormat.height)
433                 .setFrameRate(variantFormat.frameRate);
434           }
435           if (uri == null) {
436             // TODO: Remove this case and add a Rendition with a null uri to videos.
437           } else {
438             formatBuilder.setMetadata(metadata);
439             videos.add(new Rendition(uri, formatBuilder.build(), groupId, name));
440           }
441           break;
442         case TYPE_AUDIO:
443           @Nullable String sampleMimeType = null;
444           variant = getVariantWithAudioGroup(variants, groupId);
445           if (variant != null) {
446             @Nullable
447             String codecs = Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_AUDIO);
448             formatBuilder.setCodecs(codecs);
449             sampleMimeType = MimeTypes.getMediaMimeType(codecs);
450           }
451           @Nullable
452           String channelsString =
453               parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions);
454           if (channelsString != null) {
455             int channelCount = Integer.parseInt(Util.splitAtFirst(channelsString, "/")[0]);
456             formatBuilder.setChannelCount(channelCount);
457             if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType) && channelsString.endsWith("/JOC")) {
458               sampleMimeType = MimeTypes.AUDIO_E_AC3_JOC;
459             }
460           }
461           formatBuilder.setSampleMimeType(sampleMimeType);
462           if (uri == null) {
463             // TODO: Remove muxedAudioFormat and add a Rendition with a null uri to audios.
464             muxedAudioFormat = formatBuilder.build();
465           } else {
466             formatBuilder.setMetadata(metadata);
467             audios.add(new Rendition(uri, formatBuilder.build(), groupId, name));
468           }
469           break;
470         case TYPE_SUBTITLES:
471           sampleMimeType = null;
472           variant = getVariantWithSubtitleGroup(variants, groupId);
473           if (variant != null) {
474             @Nullable
475             String codecs = Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_TEXT);
476             formatBuilder.setCodecs(codecs);
477             sampleMimeType = MimeTypes.getMediaMimeType(codecs);
478           }
479           if (sampleMimeType == null) {
480             sampleMimeType = MimeTypes.TEXT_VTT;
481           }
482           formatBuilder.setSampleMimeType(sampleMimeType).setMetadata(metadata);
483           subtitles.add(new Rendition(uri, formatBuilder.build(), groupId, name));
484           break;
485         case TYPE_CLOSED_CAPTIONS:
486           String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions);
487           int accessibilityChannel;
488           if (instreamId.startsWith("CC")) {
489             sampleMimeType = MimeTypes.APPLICATION_CEA608;
490             accessibilityChannel = Integer.parseInt(instreamId.substring(2));
491           } else /* starts with SERVICE */ {
492             sampleMimeType = MimeTypes.APPLICATION_CEA708;
493             accessibilityChannel = Integer.parseInt(instreamId.substring(7));
494           }
495           if (muxedCaptionFormats == null) {
496             muxedCaptionFormats = new ArrayList<>();
497           }
498           formatBuilder
499               .setSampleMimeType(sampleMimeType)
500               .setAccessibilityChannel(accessibilityChannel);
501           muxedCaptionFormats.add(formatBuilder.build());
502           // TODO: Remove muxedCaptionFormats and add a Rendition with a null uri to closedCaptions.
503           break;
504         default:
505           // Do nothing.
506           break;
507       }
508     }
509 
510     if (noClosedCaptions) {
511       muxedCaptionFormats = Collections.emptyList();
512     }
513 
514     return new HlsMasterPlaylist(
515         baseUri,
516         tags,
517         deduplicatedVariants,
518         videos,
519         audios,
520         subtitles,
521         closedCaptions,
522         muxedAudioFormat,
523         muxedCaptionFormats,
524         hasIndependentSegmentsTag,
525         variableDefinitions,
526         sessionKeyDrmInitData);
527   }
528 
529   @Nullable
getVariantWithAudioGroup(ArrayList<Variant> variants, String groupId)530   private static Variant getVariantWithAudioGroup(ArrayList<Variant> variants, String groupId) {
531     for (int i = 0; i < variants.size(); i++) {
532       Variant variant = variants.get(i);
533       if (groupId.equals(variant.audioGroupId)) {
534         return variant;
535       }
536     }
537     return null;
538   }
539 
540   @Nullable
getVariantWithVideoGroup(ArrayList<Variant> variants, String groupId)541   private static Variant getVariantWithVideoGroup(ArrayList<Variant> variants, String groupId) {
542     for (int i = 0; i < variants.size(); i++) {
543       Variant variant = variants.get(i);
544       if (groupId.equals(variant.videoGroupId)) {
545         return variant;
546       }
547     }
548     return null;
549   }
550 
551   @Nullable
getVariantWithSubtitleGroup(ArrayList<Variant> variants, String groupId)552   private static Variant getVariantWithSubtitleGroup(ArrayList<Variant> variants, String groupId) {
553     for (int i = 0; i < variants.size(); i++) {
554       Variant variant = variants.get(i);
555       if (groupId.equals(variant.subtitleGroupId)) {
556         return variant;
557       }
558     }
559     return null;
560   }
561 
parseMediaPlaylist( HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri)562   private static HlsMediaPlaylist parseMediaPlaylist(
563       HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException {
564     @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;
565     long startOffsetUs = C.TIME_UNSET;
566     long mediaSequence = 0;
567     int version = 1; // Default version == 1.
568     long targetDurationUs = C.TIME_UNSET;
569     boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments;
570     boolean hasEndTag = false;
571     @Nullable Segment initializationSegment = null;
572     HashMap<String, String> variableDefinitions = new HashMap<>();
573     HashMap<String, Segment> urlToInferredInitSegment = new HashMap<>();
574     List<Segment> segments = new ArrayList<>();
575     List<String> tags = new ArrayList<>();
576 
577     long segmentDurationUs = 0;
578     String segmentTitle = "";
579     boolean hasDiscontinuitySequence = false;
580     int playlistDiscontinuitySequence = 0;
581     int relativeDiscontinuitySequence = 0;
582     long playlistStartTimeUs = 0;
583     long segmentStartTimeUs = 0;
584     long segmentByteRangeOffset = 0;
585     long segmentByteRangeLength = C.LENGTH_UNSET;
586     boolean isIFrameOnly = false;
587     long segmentMediaSequence = 0;
588     boolean hasGapTag = false;
589 
590     DrmInitData playlistProtectionSchemes = null;
591     String fullSegmentEncryptionKeyUri = null;
592     String fullSegmentEncryptionIV = null;
593     TreeMap<String, SchemeData> currentSchemeDatas = new TreeMap<>();
594     String encryptionScheme = null;
595     DrmInitData cachedDrmInitData = null;
596 
597     String line;
598     while (iterator.hasNext()) {
599       line = iterator.next();
600 
601       if (line.startsWith(TAG_PREFIX)) {
602         // We expose all tags through the playlist.
603         tags.add(line);
604       }
605 
606       if (line.startsWith(TAG_PLAYLIST_TYPE)) {
607         String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE, variableDefinitions);
608         if ("VOD".equals(playlistTypeString)) {
609           playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD;
610         } else if ("EVENT".equals(playlistTypeString)) {
611           playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT;
612         }
613       } else if (line.equals(TAG_IFRAME)) {
614         isIFrameOnly = true;
615       } else if (line.startsWith(TAG_START)) {
616         startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
617       } else if (line.startsWith(TAG_INIT_SEGMENT)) {
618         String uri = parseStringAttr(line, REGEX_URI, variableDefinitions);
619         String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);
620         if (byteRange != null) {
621           String[] splitByteRange = byteRange.split("@");
622           segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
623           if (splitByteRange.length > 1) {
624             segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
625           }
626         }
627         if (fullSegmentEncryptionKeyUri != null && fullSegmentEncryptionIV == null) {
628           // See RFC 8216, Section 4.3.2.5.
629           throw new ParserException(
630               "The encryption IV attribute must be present when an initialization segment is "
631                   + "encrypted with METHOD=AES-128.");
632         }
633         initializationSegment =
634             new Segment(
635                 uri,
636                 segmentByteRangeOffset,
637                 segmentByteRangeLength,
638                 fullSegmentEncryptionKeyUri,
639                 fullSegmentEncryptionIV);
640         segmentByteRangeOffset = 0;
641         segmentByteRangeLength = C.LENGTH_UNSET;
642       } else if (line.startsWith(TAG_TARGET_DURATION)) {
643         targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND;
644       } else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
645         mediaSequence = parseLongAttr(line, REGEX_MEDIA_SEQUENCE);
646         segmentMediaSequence = mediaSequence;
647       } else if (line.startsWith(TAG_VERSION)) {
648         version = parseIntAttr(line, REGEX_VERSION);
649       } else if (line.startsWith(TAG_DEFINE)) {
650         String importName = parseOptionalStringAttr(line, REGEX_IMPORT, variableDefinitions);
651         if (importName != null) {
652           String value = masterPlaylist.variableDefinitions.get(importName);
653           if (value != null) {
654             variableDefinitions.put(importName, value);
655           } else {
656             // The master playlist does not declare the imported variable. Ignore.
657           }
658         } else {
659           variableDefinitions.put(
660               parseStringAttr(line, REGEX_NAME, variableDefinitions),
661               parseStringAttr(line, REGEX_VALUE, variableDefinitions));
662         }
663       } else if (line.startsWith(TAG_MEDIA_DURATION)) {
664         segmentDurationUs =
665             (long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
666         segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "", variableDefinitions);
667       } else if (line.startsWith(TAG_KEY)) {
668         String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
669         String keyFormat =
670             parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);
671         fullSegmentEncryptionKeyUri = null;
672         fullSegmentEncryptionIV = null;
673         if (METHOD_NONE.equals(method)) {
674           currentSchemeDatas.clear();
675           cachedDrmInitData = null;
676         } else /* !METHOD_NONE.equals(method) */ {
677           fullSegmentEncryptionIV = parseOptionalStringAttr(line, REGEX_IV, variableDefinitions);
678           if (KEYFORMAT_IDENTITY.equals(keyFormat)) {
679             if (METHOD_AES_128.equals(method)) {
680               // The segment is fully encrypted using an identity key.
681               fullSegmentEncryptionKeyUri = parseStringAttr(line, REGEX_URI, variableDefinitions);
682             } else {
683               // Do nothing. Samples are encrypted using an identity key, but this is not supported.
684               // Hopefully, a traditional DRM alternative is also provided.
685             }
686           } else {
687             if (encryptionScheme == null) {
688               encryptionScheme = parseEncryptionScheme(method);
689             }
690             SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);
691             if (schemeData != null) {
692               cachedDrmInitData = null;
693               currentSchemeDatas.put(keyFormat, schemeData);
694             }
695           }
696         }
697       } else if (line.startsWith(TAG_BYTERANGE)) {
698         String byteRange = parseStringAttr(line, REGEX_BYTERANGE, variableDefinitions);
699         String[] splitByteRange = byteRange.split("@");
700         segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
701         if (splitByteRange.length > 1) {
702           segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
703         }
704       } else if (line.startsWith(TAG_DISCONTINUITY_SEQUENCE)) {
705         hasDiscontinuitySequence = true;
706         playlistDiscontinuitySequence = Integer.parseInt(line.substring(line.indexOf(':') + 1));
707       } else if (line.equals(TAG_DISCONTINUITY)) {
708         relativeDiscontinuitySequence++;
709       } else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {
710         if (playlistStartTimeUs == 0) {
711           long programDatetimeUs =
712               C.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
713           playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;
714         }
715       } else if (line.equals(TAG_GAP)) {
716         hasGapTag = true;
717       } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {
718         hasIndependentSegmentsTag = true;
719       } else if (line.equals(TAG_ENDLIST)) {
720         hasEndTag = true;
721       } else if (!line.startsWith("#")) {
722         String segmentEncryptionIV;
723         if (fullSegmentEncryptionKeyUri == null) {
724           segmentEncryptionIV = null;
725         } else if (fullSegmentEncryptionIV != null) {
726           segmentEncryptionIV = fullSegmentEncryptionIV;
727         } else {
728           segmentEncryptionIV = Long.toHexString(segmentMediaSequence);
729         }
730 
731         segmentMediaSequence++;
732         String segmentUri = replaceVariableReferences(line, variableDefinitions);
733         @Nullable Segment inferredInitSegment = urlToInferredInitSegment.get(segmentUri);
734         if (segmentByteRangeLength == C.LENGTH_UNSET) {
735           // The segment is not byte range defined.
736           segmentByteRangeOffset = 0;
737         } else if (isIFrameOnly && initializationSegment == null && inferredInitSegment == null) {
738           // The segment is a resource byte range without an initialization segment.
739           // As per RFC 8216, Section 4.3.3.6, we assume the initialization section exists in the
740           // bytes preceding the first segment in this segment's URL.
741           // We assume the implicit initialization segment is unencrypted, since there's no way for
742           // the playlist to provide an initialization vector for it.
743           inferredInitSegment =
744               new Segment(
745                   segmentUri,
746                   /* byteRangeOffset= */ 0,
747                   segmentByteRangeOffset,
748                   /* fullSegmentEncryptionKeyUri= */ null,
749                   /* encryptionIV= */ null);
750           urlToInferredInitSegment.put(segmentUri, inferredInitSegment);
751         }
752 
753         if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
754           SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
755           cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
756           if (playlistProtectionSchemes == null) {
757             SchemeData[] playlistSchemeDatas = new SchemeData[schemeDatas.length];
758             for (int i = 0; i < schemeDatas.length; i++) {
759               playlistSchemeDatas[i] = schemeDatas[i].copyWithData(null);
760             }
761             playlistProtectionSchemes = new DrmInitData(encryptionScheme, playlistSchemeDatas);
762           }
763         }
764 
765         segments.add(
766             new Segment(
767                 segmentUri,
768                 initializationSegment != null ? initializationSegment : inferredInitSegment,
769                 segmentTitle,
770                 segmentDurationUs,
771                 relativeDiscontinuitySequence,
772                 segmentStartTimeUs,
773                 cachedDrmInitData,
774                 fullSegmentEncryptionKeyUri,
775                 segmentEncryptionIV,
776                 segmentByteRangeOffset,
777                 segmentByteRangeLength,
778                 hasGapTag));
779         segmentStartTimeUs += segmentDurationUs;
780         segmentDurationUs = 0;
781         segmentTitle = "";
782         if (segmentByteRangeLength != C.LENGTH_UNSET) {
783           segmentByteRangeOffset += segmentByteRangeLength;
784         }
785         segmentByteRangeLength = C.LENGTH_UNSET;
786         hasGapTag = false;
787       }
788     }
789     return new HlsMediaPlaylist(
790         playlistType,
791         baseUri,
792         tags,
793         startOffsetUs,
794         playlistStartTimeUs,
795         hasDiscontinuitySequence,
796         playlistDiscontinuitySequence,
797         mediaSequence,
798         version,
799         targetDurationUs,
800         hasIndependentSegmentsTag,
801         hasEndTag,
802         /* hasProgramDateTime= */ playlistStartTimeUs != 0,
803         playlistProtectionSchemes,
804         segments);
805   }
806 
807   @C.SelectionFlags
parseSelectionFlags(String line)808   private static int parseSelectionFlags(String line) {
809     int flags = 0;
810     if (parseOptionalBooleanAttribute(line, REGEX_DEFAULT, false)) {
811       flags |= C.SELECTION_FLAG_DEFAULT;
812     }
813     if (parseOptionalBooleanAttribute(line, REGEX_FORCED, false)) {
814       flags |= C.SELECTION_FLAG_FORCED;
815     }
816     if (parseOptionalBooleanAttribute(line, REGEX_AUTOSELECT, false)) {
817       flags |= C.SELECTION_FLAG_AUTOSELECT;
818     }
819     return flags;
820   }
821 
822   @C.RoleFlags
parseRoleFlags(String line, Map<String, String> variableDefinitions)823   private static int parseRoleFlags(String line, Map<String, String> variableDefinitions) {
824     String concatenatedCharacteristics =
825         parseOptionalStringAttr(line, REGEX_CHARACTERISTICS, variableDefinitions);
826     if (TextUtils.isEmpty(concatenatedCharacteristics)) {
827       return 0;
828     }
829     String[] characteristics = Util.split(concatenatedCharacteristics, ",");
830     @C.RoleFlags int roleFlags = 0;
831     if (Util.contains(characteristics, "public.accessibility.describes-video")) {
832       roleFlags |= C.ROLE_FLAG_DESCRIBES_VIDEO;
833     }
834     if (Util.contains(characteristics, "public.accessibility.transcribes-spoken-dialog")) {
835       roleFlags |= C.ROLE_FLAG_TRANSCRIBES_DIALOG;
836     }
837     if (Util.contains(characteristics, "public.accessibility.describes-music-and-sound")) {
838       roleFlags |= C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND;
839     }
840     if (Util.contains(characteristics, "public.easy-to-read")) {
841       roleFlags |= C.ROLE_FLAG_EASY_TO_READ;
842     }
843     return roleFlags;
844   }
845 
846   @Nullable
parseDrmSchemeData( String line, String keyFormat, Map<String, String> variableDefinitions)847   private static SchemeData parseDrmSchemeData(
848       String line, String keyFormat, Map<String, String> variableDefinitions)
849       throws ParserException {
850     String keyFormatVersions =
851         parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions);
852     if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) {
853       String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);
854       return new SchemeData(
855           C.WIDEVINE_UUID,
856           MimeTypes.VIDEO_MP4,
857           Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT));
858     } else if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) {
859       return new SchemeData(C.WIDEVINE_UUID, "hls", Util.getUtf8Bytes(line));
860     } else if (KEYFORMAT_PLAYREADY.equals(keyFormat) && "1".equals(keyFormatVersions)) {
861       String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);
862       byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT);
863       byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data);
864       return new SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData);
865     }
866     return null;
867   }
868 
parseEncryptionScheme(String method)869   private static String parseEncryptionScheme(String method) {
870     return METHOD_SAMPLE_AES_CENC.equals(method) || METHOD_SAMPLE_AES_CTR.equals(method)
871         ? C.CENC_TYPE_cenc
872         : C.CENC_TYPE_cbcs;
873   }
874 
parseIntAttr(String line, Pattern pattern)875   private static int parseIntAttr(String line, Pattern pattern) throws ParserException {
876     return Integer.parseInt(parseStringAttr(line, pattern, Collections.emptyMap()));
877   }
878 
parseOptionalIntAttr(String line, Pattern pattern, int defaultValue)879   private static int parseOptionalIntAttr(String line, Pattern pattern, int defaultValue) {
880     Matcher matcher = pattern.matcher(line);
881     if (matcher.find()) {
882       return Integer.parseInt(Assertions.checkNotNull(matcher.group(1)));
883     }
884     return defaultValue;
885   }
886 
parseLongAttr(String line, Pattern pattern)887   private static long parseLongAttr(String line, Pattern pattern) throws ParserException {
888     return Long.parseLong(parseStringAttr(line, pattern, Collections.emptyMap()));
889   }
890 
parseDoubleAttr(String line, Pattern pattern)891   private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException {
892     return Double.parseDouble(parseStringAttr(line, pattern, Collections.emptyMap()));
893   }
894 
parseStringAttr( String line, Pattern pattern, Map<String, String> variableDefinitions)895   private static String parseStringAttr(
896       String line, Pattern pattern, Map<String, String> variableDefinitions)
897       throws ParserException {
898     String value = parseOptionalStringAttr(line, pattern, variableDefinitions);
899     if (value != null) {
900       return value;
901     } else {
902       throw new ParserException("Couldn't match " + pattern.pattern() + " in " + line);
903     }
904   }
905 
parseOptionalStringAttr( String line, Pattern pattern, Map<String, String> variableDefinitions)906   private static @Nullable String parseOptionalStringAttr(
907       String line, Pattern pattern, Map<String, String> variableDefinitions) {
908     return parseOptionalStringAttr(line, pattern, null, variableDefinitions);
909   }
910 
parseOptionalStringAttr( String line, Pattern pattern, @PolyNull String defaultValue, Map<String, String> variableDefinitions)911   private static @PolyNull String parseOptionalStringAttr(
912       String line,
913       Pattern pattern,
914       @PolyNull String defaultValue,
915       Map<String, String> variableDefinitions) {
916     Matcher matcher = pattern.matcher(line);
917     @PolyNull
918     String value = matcher.find() ? Assertions.checkNotNull(matcher.group(1)) : defaultValue;
919     return variableDefinitions.isEmpty() || value == null
920         ? value
921         : replaceVariableReferences(value, variableDefinitions);
922   }
923 
replaceVariableReferences( String string, Map<String, String> variableDefinitions)924   private static String replaceVariableReferences(
925       String string, Map<String, String> variableDefinitions) {
926     Matcher matcher = REGEX_VARIABLE_REFERENCE.matcher(string);
927     // TODO: Replace StringBuffer with StringBuilder once Java 9 is available.
928     StringBuffer stringWithReplacements = new StringBuffer();
929     while (matcher.find()) {
930       String groupName = matcher.group(1);
931       if (variableDefinitions.containsKey(groupName)) {
932         matcher.appendReplacement(
933             stringWithReplacements, Matcher.quoteReplacement(variableDefinitions.get(groupName)));
934       } else {
935         // The variable is not defined. The value is ignored.
936       }
937     }
938     matcher.appendTail(stringWithReplacements);
939     return stringWithReplacements.toString();
940   }
941 
parseOptionalBooleanAttribute( String line, Pattern pattern, boolean defaultValue)942   private static boolean parseOptionalBooleanAttribute(
943       String line, Pattern pattern, boolean defaultValue) {
944     Matcher matcher = pattern.matcher(line);
945     if (matcher.find()) {
946       return BOOLEAN_TRUE.equals(matcher.group(1));
947     }
948     return defaultValue;
949   }
950 
compileBooleanAttrPattern(String attribute)951   private static Pattern compileBooleanAttrPattern(String attribute) {
952     return Pattern.compile(attribute + "=(" + BOOLEAN_FALSE + "|" + BOOLEAN_TRUE + ")");
953   }
954 
955   private static class LineIterator {
956 
957     private final BufferedReader reader;
958     private final Queue<String> extraLines;
959 
960     @Nullable private String next;
961 
LineIterator(Queue<String> extraLines, BufferedReader reader)962     public LineIterator(Queue<String> extraLines, BufferedReader reader) {
963       this.extraLines = extraLines;
964       this.reader = reader;
965     }
966 
967     @EnsuresNonNullIf(expression = "next", result = true)
hasNext()968     public boolean hasNext() throws IOException {
969       if (next != null) {
970         return true;
971       }
972       if (!extraLines.isEmpty()) {
973         next = Assertions.checkNotNull(extraLines.poll());
974         return true;
975       }
976       while ((next = reader.readLine()) != null) {
977         next = next.trim();
978         if (!next.isEmpty()) {
979           return true;
980         }
981       }
982       return false;
983     }
984 
985     /** Return the next line, or throw {@link NoSuchElementException} if none. */
next()986     public String next() throws IOException {
987       if (hasNext()) {
988         String result = next;
989         next = null;
990         return result;
991       } else {
992         throw new NoSuchElementException();
993       }
994     }
995 
996   }
997 
998 }
999