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