• 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.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