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.os.Handler; 20 import android.os.SystemClock; 21 import androidx.annotation.Nullable; 22 import com.google.android.exoplayer2.C; 23 import com.google.android.exoplayer2.ParserException; 24 import com.google.android.exoplayer2.source.LoadEventInfo; 25 import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; 26 import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; 27 import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant; 28 import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; 29 import com.google.android.exoplayer2.upstream.DataSource; 30 import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; 31 import com.google.android.exoplayer2.upstream.Loader; 32 import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; 33 import com.google.android.exoplayer2.upstream.ParsingLoadable; 34 import com.google.android.exoplayer2.util.Assertions; 35 import com.google.android.exoplayer2.util.Util; 36 import java.io.IOException; 37 import java.util.ArrayList; 38 import java.util.HashMap; 39 import java.util.List; 40 41 /** Default implementation for {@link HlsPlaylistTracker}. */ 42 public final class DefaultHlsPlaylistTracker 43 implements HlsPlaylistTracker, Loader.Callback<ParsingLoadable<HlsPlaylist>> { 44 45 /** Factory for {@link DefaultHlsPlaylistTracker} instances. */ 46 public static final Factory FACTORY = DefaultHlsPlaylistTracker::new; 47 48 /** 49 * Default coefficient applied on the target duration of a playlist to determine the amount of 50 * time after which an unchanging playlist is considered stuck. 51 */ 52 public static final double DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5; 53 54 private final HlsDataSourceFactory dataSourceFactory; 55 private final HlsPlaylistParserFactory playlistParserFactory; 56 private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; 57 private final HashMap<Uri, MediaPlaylistBundle> playlistBundles; 58 private final List<PlaylistEventListener> listeners; 59 private final double playlistStuckTargetDurationCoefficient; 60 61 @Nullable private ParsingLoadable.Parser<HlsPlaylist> mediaPlaylistParser; 62 @Nullable private EventDispatcher eventDispatcher; 63 @Nullable private Loader initialPlaylistLoader; 64 @Nullable private Handler playlistRefreshHandler; 65 @Nullable private PrimaryPlaylistListener primaryPlaylistListener; 66 @Nullable private HlsMasterPlaylist masterPlaylist; 67 @Nullable private Uri primaryMediaPlaylistUrl; 68 @Nullable private HlsMediaPlaylist primaryMediaPlaylistSnapshot; 69 private boolean isLive; 70 private long initialStartTimeUs; 71 72 /** 73 * Creates an instance. 74 * 75 * @param dataSourceFactory A factory for {@link DataSource} instances. 76 * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. 77 * @param playlistParserFactory An {@link HlsPlaylistParserFactory}. 78 */ DefaultHlsPlaylistTracker( HlsDataSourceFactory dataSourceFactory, LoadErrorHandlingPolicy loadErrorHandlingPolicy, HlsPlaylistParserFactory playlistParserFactory)79 public DefaultHlsPlaylistTracker( 80 HlsDataSourceFactory dataSourceFactory, 81 LoadErrorHandlingPolicy loadErrorHandlingPolicy, 82 HlsPlaylistParserFactory playlistParserFactory) { 83 this( 84 dataSourceFactory, 85 loadErrorHandlingPolicy, 86 playlistParserFactory, 87 DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT); 88 } 89 90 /** 91 * Creates an instance. 92 * 93 * @param dataSourceFactory A factory for {@link DataSource} instances. 94 * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. 95 * @param playlistParserFactory An {@link HlsPlaylistParserFactory}. 96 * @param playlistStuckTargetDurationCoefficient A coefficient to apply to the target duration of 97 * media playlists in order to determine that a non-changing playlist is stuck. Once a 98 * playlist is deemed stuck, a {@link PlaylistStuckException} is thrown via {@link 99 * #maybeThrowPlaylistRefreshError(Uri)}. 100 */ DefaultHlsPlaylistTracker( HlsDataSourceFactory dataSourceFactory, LoadErrorHandlingPolicy loadErrorHandlingPolicy, HlsPlaylistParserFactory playlistParserFactory, double playlistStuckTargetDurationCoefficient)101 public DefaultHlsPlaylistTracker( 102 HlsDataSourceFactory dataSourceFactory, 103 LoadErrorHandlingPolicy loadErrorHandlingPolicy, 104 HlsPlaylistParserFactory playlistParserFactory, 105 double playlistStuckTargetDurationCoefficient) { 106 this.dataSourceFactory = dataSourceFactory; 107 this.playlistParserFactory = playlistParserFactory; 108 this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; 109 this.playlistStuckTargetDurationCoefficient = playlistStuckTargetDurationCoefficient; 110 listeners = new ArrayList<>(); 111 playlistBundles = new HashMap<>(); 112 initialStartTimeUs = C.TIME_UNSET; 113 } 114 115 // HlsPlaylistTracker implementation. 116 117 @Override start( Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener primaryPlaylistListener)118 public void start( 119 Uri initialPlaylistUri, 120 EventDispatcher eventDispatcher, 121 PrimaryPlaylistListener primaryPlaylistListener) { 122 this.playlistRefreshHandler = Util.createHandler(); 123 this.eventDispatcher = eventDispatcher; 124 this.primaryPlaylistListener = primaryPlaylistListener; 125 ParsingLoadable<HlsPlaylist> masterPlaylistLoadable = 126 new ParsingLoadable<>( 127 dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), 128 initialPlaylistUri, 129 C.DATA_TYPE_MANIFEST, 130 playlistParserFactory.createPlaylistParser()); 131 Assertions.checkState(initialPlaylistLoader == null); 132 initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist"); 133 long elapsedRealtime = 134 initialPlaylistLoader.startLoading( 135 masterPlaylistLoadable, 136 this, 137 loadErrorHandlingPolicy.getMinimumLoadableRetryCount(masterPlaylistLoadable.type)); 138 eventDispatcher.loadStarted( 139 new LoadEventInfo(masterPlaylistLoadable.dataSpec, elapsedRealtime), 140 masterPlaylistLoadable.type); 141 } 142 143 @Override stop()144 public void stop() { 145 primaryMediaPlaylistUrl = null; 146 primaryMediaPlaylistSnapshot = null; 147 masterPlaylist = null; 148 initialStartTimeUs = C.TIME_UNSET; 149 initialPlaylistLoader.release(); 150 initialPlaylistLoader = null; 151 for (MediaPlaylistBundle bundle : playlistBundles.values()) { 152 bundle.release(); 153 } 154 playlistRefreshHandler.removeCallbacksAndMessages(null); 155 playlistRefreshHandler = null; 156 playlistBundles.clear(); 157 } 158 159 @Override addListener(PlaylistEventListener listener)160 public void addListener(PlaylistEventListener listener) { 161 listeners.add(listener); 162 } 163 164 @Override removeListener(PlaylistEventListener listener)165 public void removeListener(PlaylistEventListener listener) { 166 listeners.remove(listener); 167 } 168 169 @Override 170 @Nullable getMasterPlaylist()171 public HlsMasterPlaylist getMasterPlaylist() { 172 return masterPlaylist; 173 } 174 175 @Override 176 @Nullable getPlaylistSnapshot(Uri url, boolean isForPlayback)177 public HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback) { 178 HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); 179 if (snapshot != null && isForPlayback) { 180 maybeSetPrimaryUrl(url); 181 } 182 return snapshot; 183 } 184 185 @Override getInitialStartTimeUs()186 public long getInitialStartTimeUs() { 187 return initialStartTimeUs; 188 } 189 190 @Override isSnapshotValid(Uri url)191 public boolean isSnapshotValid(Uri url) { 192 return playlistBundles.get(url).isSnapshotValid(); 193 } 194 195 @Override maybeThrowPrimaryPlaylistRefreshError()196 public void maybeThrowPrimaryPlaylistRefreshError() throws IOException { 197 if (initialPlaylistLoader != null) { 198 initialPlaylistLoader.maybeThrowError(); 199 } 200 if (primaryMediaPlaylistUrl != null) { 201 maybeThrowPlaylistRefreshError(primaryMediaPlaylistUrl); 202 } 203 } 204 205 @Override maybeThrowPlaylistRefreshError(Uri url)206 public void maybeThrowPlaylistRefreshError(Uri url) throws IOException { 207 playlistBundles.get(url).maybeThrowPlaylistRefreshError(); 208 } 209 210 @Override refreshPlaylist(Uri url)211 public void refreshPlaylist(Uri url) { 212 playlistBundles.get(url).loadPlaylist(); 213 } 214 215 @Override isLive()216 public boolean isLive() { 217 return isLive; 218 } 219 220 // Loader.Callback implementation. 221 222 @Override onLoadCompleted( ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs)223 public void onLoadCompleted( 224 ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) { 225 HlsPlaylist result = loadable.getResult(); 226 HlsMasterPlaylist masterPlaylist; 227 boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; 228 if (isMediaPlaylist) { 229 masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); 230 } else /* result instanceof HlsMasterPlaylist */ { 231 masterPlaylist = (HlsMasterPlaylist) result; 232 } 233 this.masterPlaylist = masterPlaylist; 234 mediaPlaylistParser = playlistParserFactory.createPlaylistParser(masterPlaylist); 235 primaryMediaPlaylistUrl = masterPlaylist.variants.get(0).url; 236 createBundles(masterPlaylist.mediaPlaylistUrls); 237 MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryMediaPlaylistUrl); 238 if (isMediaPlaylist) { 239 // We don't need to load the playlist again. We can use the same result. 240 primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs); 241 } else { 242 primaryBundle.loadPlaylist(); 243 } 244 eventDispatcher.loadCompleted( 245 new LoadEventInfo( 246 loadable.dataSpec, 247 loadable.getUri(), 248 loadable.getResponseHeaders(), 249 elapsedRealtimeMs, 250 loadDurationMs, 251 loadable.bytesLoaded()), 252 C.DATA_TYPE_MANIFEST); 253 } 254 255 @Override onLoadCanceled( ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released)256 public void onLoadCanceled( 257 ParsingLoadable<HlsPlaylist> loadable, 258 long elapsedRealtimeMs, 259 long loadDurationMs, 260 boolean released) { 261 eventDispatcher.loadCanceled( 262 new LoadEventInfo( 263 loadable.dataSpec, 264 loadable.getUri(), 265 loadable.getResponseHeaders(), 266 elapsedRealtimeMs, 267 loadDurationMs, 268 loadable.bytesLoaded()), 269 C.DATA_TYPE_MANIFEST); 270 } 271 272 @Override onLoadError( ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error, int errorCount)273 public LoadErrorAction onLoadError( 274 ParsingLoadable<HlsPlaylist> loadable, 275 long elapsedRealtimeMs, 276 long loadDurationMs, 277 IOException error, 278 int errorCount) { 279 long retryDelayMs = 280 loadErrorHandlingPolicy.getRetryDelayMsFor( 281 loadable.type, loadDurationMs, error, errorCount); 282 boolean isFatal = retryDelayMs == C.TIME_UNSET; 283 eventDispatcher.loadError( 284 new LoadEventInfo( 285 loadable.dataSpec, 286 loadable.getUri(), 287 loadable.getResponseHeaders(), 288 elapsedRealtimeMs, 289 loadDurationMs, 290 loadable.bytesLoaded()), 291 C.DATA_TYPE_MANIFEST, 292 error, 293 isFatal); 294 return isFatal 295 ? Loader.DONT_RETRY_FATAL 296 : Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs); 297 } 298 299 // Internal methods. 300 maybeSelectNewPrimaryUrl()301 private boolean maybeSelectNewPrimaryUrl() { 302 List<Variant> variants = masterPlaylist.variants; 303 int variantsSize = variants.size(); 304 long currentTimeMs = SystemClock.elapsedRealtime(); 305 for (int i = 0; i < variantsSize; i++) { 306 MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i).url); 307 if (currentTimeMs > bundle.blacklistUntilMs) { 308 primaryMediaPlaylistUrl = bundle.playlistUrl; 309 bundle.loadPlaylist(); 310 return true; 311 } 312 } 313 return false; 314 } 315 maybeSetPrimaryUrl(Uri url)316 private void maybeSetPrimaryUrl(Uri url) { 317 if (url.equals(primaryMediaPlaylistUrl) 318 || !isVariantUrl(url) 319 || (primaryMediaPlaylistSnapshot != null && primaryMediaPlaylistSnapshot.hasEndTag)) { 320 // Ignore if the primary media playlist URL is unchanged, if the media playlist is not 321 // referenced directly by a variant, or it the last primary snapshot contains an end tag. 322 return; 323 } 324 primaryMediaPlaylistUrl = url; 325 playlistBundles.get(primaryMediaPlaylistUrl).loadPlaylist(); 326 } 327 328 /** Returns whether any of the variants in the master playlist have the specified playlist URL. */ isVariantUrl(Uri playlistUrl)329 private boolean isVariantUrl(Uri playlistUrl) { 330 List<Variant> variants = masterPlaylist.variants; 331 for (int i = 0; i < variants.size(); i++) { 332 if (playlistUrl.equals(variants.get(i).url)) { 333 return true; 334 } 335 } 336 return false; 337 } 338 createBundles(List<Uri> urls)339 private void createBundles(List<Uri> urls) { 340 int listSize = urls.size(); 341 for (int i = 0; i < listSize; i++) { 342 Uri url = urls.get(i); 343 MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); 344 playlistBundles.put(url, bundle); 345 } 346 } 347 348 /** 349 * Called by the bundles when a snapshot changes. 350 * 351 * @param url The url of the playlist. 352 * @param newSnapshot The new snapshot. 353 */ onPlaylistUpdated(Uri url, HlsMediaPlaylist newSnapshot)354 private void onPlaylistUpdated(Uri url, HlsMediaPlaylist newSnapshot) { 355 if (url.equals(primaryMediaPlaylistUrl)) { 356 if (primaryMediaPlaylistSnapshot == null) { 357 // This is the first primary url snapshot. 358 isLive = !newSnapshot.hasEndTag; 359 initialStartTimeUs = newSnapshot.startTimeUs; 360 } 361 primaryMediaPlaylistSnapshot = newSnapshot; 362 primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); 363 } 364 int listenersSize = listeners.size(); 365 for (int i = 0; i < listenersSize; i++) { 366 listeners.get(i).onPlaylistChanged(); 367 } 368 } 369 notifyPlaylistError(Uri playlistUrl, long blacklistDurationMs)370 private boolean notifyPlaylistError(Uri playlistUrl, long blacklistDurationMs) { 371 int listenersSize = listeners.size(); 372 boolean anyBlacklistingFailed = false; 373 for (int i = 0; i < listenersSize; i++) { 374 anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, blacklistDurationMs); 375 } 376 return anyBlacklistingFailed; 377 } 378 getLatestPlaylistSnapshot( HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist)379 private HlsMediaPlaylist getLatestPlaylistSnapshot( 380 HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { 381 if (!loadedPlaylist.isNewerThan(oldPlaylist)) { 382 if (loadedPlaylist.hasEndTag) { 383 // If the loaded playlist has an end tag but is not newer than the old playlist then we have 384 // an inconsistent state. This is typically caused by the server incorrectly resetting the 385 // media sequence when appending the end tag. We resolve this case as best we can by 386 // returning the old playlist with the end tag appended. 387 return oldPlaylist.copyWithEndTag(); 388 } else { 389 return oldPlaylist; 390 } 391 } 392 long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist); 393 int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist); 394 return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence); 395 } 396 getLoadedPlaylistStartTimeUs( HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist)397 private long getLoadedPlaylistStartTimeUs( 398 HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { 399 if (loadedPlaylist.hasProgramDateTime) { 400 return loadedPlaylist.startTimeUs; 401 } 402 long primarySnapshotStartTimeUs = 403 primaryMediaPlaylistSnapshot != null ? primaryMediaPlaylistSnapshot.startTimeUs : 0; 404 if (oldPlaylist == null) { 405 return primarySnapshotStartTimeUs; 406 } 407 int oldPlaylistSize = oldPlaylist.segments.size(); 408 Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); 409 if (firstOldOverlappingSegment != null) { 410 return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs; 411 } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) { 412 return oldPlaylist.getEndTimeUs(); 413 } else { 414 // No segments overlap, we assume the new playlist start coincides with the primary playlist. 415 return primarySnapshotStartTimeUs; 416 } 417 } 418 getLoadedPlaylistDiscontinuitySequence( HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist)419 private int getLoadedPlaylistDiscontinuitySequence( 420 HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { 421 if (loadedPlaylist.hasDiscontinuitySequence) { 422 return loadedPlaylist.discontinuitySequence; 423 } 424 // TODO: Improve cross-playlist discontinuity adjustment. 425 int primaryUrlDiscontinuitySequence = 426 primaryMediaPlaylistSnapshot != null 427 ? primaryMediaPlaylistSnapshot.discontinuitySequence 428 : 0; 429 if (oldPlaylist == null) { 430 return primaryUrlDiscontinuitySequence; 431 } 432 Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); 433 if (firstOldOverlappingSegment != null) { 434 return oldPlaylist.discontinuitySequence 435 + firstOldOverlappingSegment.relativeDiscontinuitySequence 436 - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence; 437 } 438 return primaryUrlDiscontinuitySequence; 439 } 440 getFirstOldOverlappingSegment( HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist)441 private static Segment getFirstOldOverlappingSegment( 442 HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { 443 int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence); 444 List<Segment> oldSegments = oldPlaylist.segments; 445 return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null; 446 } 447 448 /** Holds all information related to a specific Media Playlist. */ 449 private final class MediaPlaylistBundle 450 implements Loader.Callback<ParsingLoadable<HlsPlaylist>>, Runnable { 451 452 private final Uri playlistUrl; 453 private final Loader mediaPlaylistLoader; 454 private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable; 455 456 @Nullable private HlsMediaPlaylist playlistSnapshot; 457 private long lastSnapshotLoadMs; 458 private long lastSnapshotChangeMs; 459 private long earliestNextLoadTimeMs; 460 private long blacklistUntilMs; 461 private boolean loadPending; 462 private IOException playlistError; 463 MediaPlaylistBundle(Uri playlistUrl)464 public MediaPlaylistBundle(Uri playlistUrl) { 465 this.playlistUrl = playlistUrl; 466 mediaPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MediaPlaylist"); 467 mediaPlaylistLoadable = 468 new ParsingLoadable<>( 469 dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), 470 playlistUrl, 471 C.DATA_TYPE_MANIFEST, 472 mediaPlaylistParser); 473 } 474 475 @Nullable getPlaylistSnapshot()476 public HlsMediaPlaylist getPlaylistSnapshot() { 477 return playlistSnapshot; 478 } 479 isSnapshotValid()480 public boolean isSnapshotValid() { 481 if (playlistSnapshot == null) { 482 return false; 483 } 484 long currentTimeMs = SystemClock.elapsedRealtime(); 485 long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs)); 486 return playlistSnapshot.hasEndTag 487 || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT 488 || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD 489 || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs; 490 } 491 release()492 public void release() { 493 mediaPlaylistLoader.release(); 494 } 495 loadPlaylist()496 public void loadPlaylist() { 497 blacklistUntilMs = 0; 498 if (loadPending || mediaPlaylistLoader.isLoading() || mediaPlaylistLoader.hasFatalError()) { 499 // Load already pending, in progress, or a fatal error has been encountered. Do nothing. 500 return; 501 } 502 long currentTimeMs = SystemClock.elapsedRealtime(); 503 if (currentTimeMs < earliestNextLoadTimeMs) { 504 loadPending = true; 505 playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs); 506 } else { 507 loadPlaylistImmediately(); 508 } 509 } 510 maybeThrowPlaylistRefreshError()511 public void maybeThrowPlaylistRefreshError() throws IOException { 512 mediaPlaylistLoader.maybeThrowError(); 513 if (playlistError != null) { 514 throw playlistError; 515 } 516 } 517 518 // Loader.Callback implementation. 519 520 @Override onLoadCompleted( ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs)521 public void onLoadCompleted( 522 ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) { 523 HlsPlaylist result = loadable.getResult(); 524 if (result instanceof HlsMediaPlaylist) { 525 processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs); 526 eventDispatcher.loadCompleted( 527 new LoadEventInfo( 528 loadable.dataSpec, 529 loadable.getUri(), 530 loadable.getResponseHeaders(), 531 elapsedRealtimeMs, 532 loadDurationMs, 533 loadable.bytesLoaded()), 534 C.DATA_TYPE_MANIFEST); 535 } else { 536 playlistError = new ParserException("Loaded playlist has unexpected type."); 537 } 538 } 539 540 @Override onLoadCanceled( ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released)541 public void onLoadCanceled( 542 ParsingLoadable<HlsPlaylist> loadable, 543 long elapsedRealtimeMs, 544 long loadDurationMs, 545 boolean released) { 546 eventDispatcher.loadCanceled( 547 new LoadEventInfo( 548 loadable.dataSpec, 549 loadable.getUri(), 550 loadable.getResponseHeaders(), 551 elapsedRealtimeMs, 552 loadDurationMs, 553 loadable.bytesLoaded()), 554 C.DATA_TYPE_MANIFEST); 555 } 556 557 @Override onLoadError( ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error, int errorCount)558 public LoadErrorAction onLoadError( 559 ParsingLoadable<HlsPlaylist> loadable, 560 long elapsedRealtimeMs, 561 long loadDurationMs, 562 IOException error, 563 int errorCount) { 564 LoadErrorAction loadErrorAction; 565 566 long blacklistDurationMs = 567 loadErrorHandlingPolicy.getBlacklistDurationMsFor( 568 loadable.type, loadDurationMs, error, errorCount); 569 boolean shouldBlacklist = blacklistDurationMs != C.TIME_UNSET; 570 571 boolean blacklistingFailed = 572 notifyPlaylistError(playlistUrl, blacklistDurationMs) || !shouldBlacklist; 573 if (shouldBlacklist) { 574 blacklistingFailed |= blacklistPlaylist(blacklistDurationMs); 575 } 576 577 if (blacklistingFailed) { 578 long retryDelay = 579 loadErrorHandlingPolicy.getRetryDelayMsFor( 580 loadable.type, loadDurationMs, error, errorCount); 581 loadErrorAction = 582 retryDelay != C.TIME_UNSET 583 ? Loader.createRetryAction(false, retryDelay) 584 : Loader.DONT_RETRY_FATAL; 585 } else { 586 loadErrorAction = Loader.DONT_RETRY; 587 } 588 589 eventDispatcher.loadError( 590 new LoadEventInfo( 591 loadable.dataSpec, 592 loadable.getUri(), 593 loadable.getResponseHeaders(), 594 elapsedRealtimeMs, 595 loadDurationMs, 596 loadable.bytesLoaded()), 597 C.DATA_TYPE_MANIFEST, 598 error, 599 /* wasCanceled= */ !loadErrorAction.isRetry()); 600 601 return loadErrorAction; 602 } 603 604 // Runnable implementation. 605 606 @Override run()607 public void run() { 608 loadPending = false; 609 loadPlaylistImmediately(); 610 } 611 612 // Internal methods. 613 loadPlaylistImmediately()614 private void loadPlaylistImmediately() { 615 long elapsedRealtime = 616 mediaPlaylistLoader.startLoading( 617 mediaPlaylistLoadable, 618 this, 619 loadErrorHandlingPolicy.getMinimumLoadableRetryCount(mediaPlaylistLoadable.type)); 620 eventDispatcher.loadStarted( 621 new LoadEventInfo(mediaPlaylistLoadable.dataSpec, elapsedRealtime), 622 mediaPlaylistLoadable.type); 623 } 624 processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist, long loadDurationMs)625 private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist, long loadDurationMs) { 626 HlsMediaPlaylist oldPlaylist = playlistSnapshot; 627 long currentTimeMs = SystemClock.elapsedRealtime(); 628 lastSnapshotLoadMs = currentTimeMs; 629 playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); 630 if (playlistSnapshot != oldPlaylist) { 631 playlistError = null; 632 lastSnapshotChangeMs = currentTimeMs; 633 onPlaylistUpdated(playlistUrl, playlistSnapshot); 634 } else if (!playlistSnapshot.hasEndTag) { 635 if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() 636 < playlistSnapshot.mediaSequence) { 637 // TODO: Allow customization of playlist resets handling. 638 // The media sequence jumped backwards. The server has probably reset. We do not try 639 // blacklisting in this case. 640 playlistError = new PlaylistResetException(playlistUrl); 641 notifyPlaylistError(playlistUrl, C.TIME_UNSET); 642 } else if (currentTimeMs - lastSnapshotChangeMs 643 > C.usToMs(playlistSnapshot.targetDurationUs) 644 * playlistStuckTargetDurationCoefficient) { 645 // TODO: Allow customization of stuck playlists handling. 646 playlistError = new PlaylistStuckException(playlistUrl); 647 long blacklistDurationMs = 648 loadErrorHandlingPolicy.getBlacklistDurationMsFor( 649 C.DATA_TYPE_MANIFEST, loadDurationMs, playlistError, /* errorCount= */ 1); 650 notifyPlaylistError(playlistUrl, blacklistDurationMs); 651 if (blacklistDurationMs != C.TIME_UNSET) { 652 blacklistPlaylist(blacklistDurationMs); 653 } 654 } 655 } 656 // Do not allow the playlist to load again within the target duration if we obtained a new 657 // snapshot, or half the target duration otherwise. 658 earliestNextLoadTimeMs = 659 currentTimeMs 660 + C.usToMs( 661 playlistSnapshot != oldPlaylist 662 ? playlistSnapshot.targetDurationUs 663 : (playlistSnapshot.targetDurationUs / 2)); 664 // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the 665 // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes 666 // the primary. 667 if (playlistUrl.equals(primaryMediaPlaylistUrl) && !playlistSnapshot.hasEndTag) { 668 loadPlaylist(); 669 } 670 } 671 672 /** 673 * Blacklists the playlist. 674 * 675 * @param blacklistDurationMs The number of milliseconds for which the playlist should be 676 * blacklisted. 677 * @return Whether the playlist is the primary, despite being blacklisted. 678 */ blacklistPlaylist(long blacklistDurationMs)679 private boolean blacklistPlaylist(long blacklistDurationMs) { 680 blacklistUntilMs = SystemClock.elapsedRealtime() + blacklistDurationMs; 681 return playlistUrl.equals(primaryMediaPlaylistUrl) && !maybeSelectNewPrimaryUrl(); 682 } 683 } 684 } 685