Extract HlsPlaylistTracker interface

This allows injection of custom implementations and configuration of
DefaultHlsPlaylistTracker without modifying the HlsMediaSource interface.

Issue:#2844

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=198846607
This commit is contained in:
aquilescanta 2018-06-01 02:22:31 -07:00 committed by Oliver Woodman
parent 798b29e3ef
commit f1fe1c40a6
4 changed files with 699 additions and 547 deletions

View File

@ -6,7 +6,9 @@
extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)). extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)).
* DASH: Fix playback getting stuck when playing representations that have both * DASH: Fix playback getting stuck when playing representations that have both
sidx atoms and non-zero presentationTimeOffset values. sidx atoms and non-zero presentationTimeOffset values.
* HLS: Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags. * HLS:
* Allow injection of custom playlist trackers.
* Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags.
* Mitigate memory leaks when `MediaSource` loads are slow to cancel * Mitigate memory leaks when `MediaSource` loads are slow to cancel
([#4249](https://github.com/google/ExoPlayer/issues/4249)). ([#4249](https://github.com/google/ExoPlayer/issues/4249)).
* Fix inconsistent `Player.EventListener` invocations for recursive player state * Fix inconsistent `Player.EventListener` invocations for recursive player state

View File

@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat
import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
@ -58,6 +59,7 @@ public final class HlsMediaSource extends BaseMediaSource
private HlsExtractorFactory extractorFactory; private HlsExtractorFactory extractorFactory;
private @Nullable ParsingLoadable.Parser<HlsPlaylist> playlistParser; private @Nullable ParsingLoadable.Parser<HlsPlaylist> playlistParser;
private @Nullable HlsPlaylistTracker playlistTracker;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private int minLoadableRetryCount; private int minLoadableRetryCount;
private boolean allowChunklessPreparation; private boolean allowChunklessPreparation;
@ -136,16 +138,37 @@ public final class HlsMediaSource extends BaseMediaSource
* Sets the parser to parse HLS playlists. The default is an instance of {@link * Sets the parser to parse HLS playlists. The default is an instance of {@link
* HlsPlaylistParser}. * HlsPlaylistParser}.
* *
* <p>Must not be called after calling {@link #setPlaylistTracker} on the same builder.
*
* @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists.
* @return This factory, for convenience. * @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called. * @throws IllegalStateException If one of the {@code create} methods has already been called.
*/ */
public Factory setPlaylistParser(ParsingLoadable.Parser<HlsPlaylist> playlistParser) { public Factory setPlaylistParser(ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
Assertions.checkState(!isCreateCalled); Assertions.checkState(!isCreateCalled);
Assertions.checkState(playlistTracker == null, "A playlist tracker has already been set.");
this.playlistParser = Assertions.checkNotNull(playlistParser); this.playlistParser = Assertions.checkNotNull(playlistParser);
return this; return this;
} }
/**
* Sets the HLS playlist tracker. The default is an instance of {@link
* DefaultHlsPlaylistTracker}. Playlist trackers must not be shared by {@link HlsMediaSource}
* instances.
*
* <p>Must not be called after calling {@link #setPlaylistParser} on the same builder.
*
* @param playlistTracker A tracker for HLS playlists.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setPlaylistTracker(HlsPlaylistTracker playlistTracker) {
Assertions.checkState(!isCreateCalled);
Assertions.checkState(playlistParser == null, "A playlist parser has already been set.");
this.playlistTracker = Assertions.checkNotNull(playlistTracker);
return this;
}
/** /**
* Sets the factory to create composite {@link SequenceableLoader}s for when this media source * Sets the factory to create composite {@link SequenceableLoader}s for when this media source
* loads data from multiple streams (video, audio etc...). The default is an instance of {@link * loads data from multiple streams (video, audio etc...). The default is an instance of {@link
@ -187,8 +210,12 @@ public final class HlsMediaSource extends BaseMediaSource
@Override @Override
public HlsMediaSource createMediaSource(Uri playlistUri) { public HlsMediaSource createMediaSource(Uri playlistUri) {
isCreateCalled = true; isCreateCalled = true;
if (playlistParser == null) { if (playlistTracker == null) {
playlistParser = new HlsPlaylistParser(); playlistTracker =
new DefaultHlsPlaylistTracker(
hlsDataSourceFactory,
minLoadableRetryCount,
playlistParser != null ? playlistParser : new HlsPlaylistParser());
} }
return new HlsMediaSource( return new HlsMediaSource(
playlistUri, playlistUri,
@ -196,7 +223,7 @@ public final class HlsMediaSource extends BaseMediaSource
extractorFactory, extractorFactory,
compositeSequenceableLoaderFactory, compositeSequenceableLoaderFactory,
minLoadableRetryCount, minLoadableRetryCount,
playlistParser, playlistTracker,
allowChunklessPreparation, allowChunklessPreparation,
tag); tag);
} }
@ -233,12 +260,10 @@ public final class HlsMediaSource extends BaseMediaSource
private final HlsDataSourceFactory dataSourceFactory; private final HlsDataSourceFactory dataSourceFactory;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final int minLoadableRetryCount; private final int minLoadableRetryCount;
private final ParsingLoadable.Parser<HlsPlaylist> playlistParser;
private final boolean allowChunklessPreparation; private final boolean allowChunklessPreparation;
private final HlsPlaylistTracker playlistTracker;
private final @Nullable Object tag; private final @Nullable Object tag;
private HlsPlaylistTracker playlistTracker;
/** /**
* @param manifestUri The {@link Uri} of the HLS manifest. * @param manifestUri The {@link Uri} of the HLS manifest.
* @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests,
@ -276,8 +301,13 @@ public final class HlsMediaSource extends BaseMediaSource
int minLoadableRetryCount, int minLoadableRetryCount,
Handler eventHandler, Handler eventHandler,
MediaSourceEventListener eventListener) { MediaSourceEventListener eventListener) {
this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), this(
HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener, manifestUri,
new DefaultHlsDataSourceFactory(dataSourceFactory),
HlsExtractorFactory.DEFAULT,
minLoadableRetryCount,
eventHandler,
eventListener,
new HlsPlaylistParser()); new HlsPlaylistParser());
} }
@ -309,7 +339,8 @@ public final class HlsMediaSource extends BaseMediaSource
extractorFactory, extractorFactory,
new DefaultCompositeSequenceableLoaderFactory(), new DefaultCompositeSequenceableLoaderFactory(),
minLoadableRetryCount, minLoadableRetryCount,
playlistParser, new DefaultHlsPlaylistTracker(
dataSourceFactory, minLoadableRetryCount, new HlsPlaylistParser()),
/* allowChunklessPreparation= */ false, /* allowChunklessPreparation= */ false,
/* tag= */ null); /* tag= */ null);
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
@ -323,7 +354,7 @@ public final class HlsMediaSource extends BaseMediaSource
HlsExtractorFactory extractorFactory, HlsExtractorFactory extractorFactory,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
int minLoadableRetryCount, int minLoadableRetryCount,
ParsingLoadable.Parser<HlsPlaylist> playlistParser, HlsPlaylistTracker playlistTracker,
boolean allowChunklessPreparation, boolean allowChunklessPreparation,
@Nullable Object tag) { @Nullable Object tag) {
this.manifestUri = manifestUri; this.manifestUri = manifestUri;
@ -331,7 +362,7 @@ public final class HlsMediaSource extends BaseMediaSource
this.extractorFactory = extractorFactory; this.extractorFactory = extractorFactory;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
this.playlistParser = playlistParser; this.playlistTracker = playlistTracker;
this.allowChunklessPreparation = allowChunklessPreparation; this.allowChunklessPreparation = allowChunklessPreparation;
this.tag = tag; this.tag = tag;
} }
@ -339,9 +370,7 @@ public final class HlsMediaSource extends BaseMediaSource
@Override @Override
public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this);
minLoadableRetryCount, this, playlistParser);
playlistTracker.start();
} }
@Override @Override
@ -373,7 +402,6 @@ public final class HlsMediaSource extends BaseMediaSource
public void releaseSourceInternal() { public void releaseSourceInternal() {
if (playlistTracker != null) { if (playlistTracker != null) {
playlistTracker.release(); playlistTracker.release();
playlistTracker = null;
} }
} }

View File

@ -0,0 +1,566 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.UriUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
/** Default implementation for {@link HlsPlaylistTracker}. */
public final class DefaultHlsPlaylistTracker
implements HlsPlaylistTracker, Loader.Callback<ParsingLoadable<HlsPlaylist>> {
/**
* Coefficient applied on the target duration of a playlist to determine the amount of time after
* which an unchanging playlist is considered stuck.
*/
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5;
private final HlsDataSourceFactory dataSourceFactory;
private final ParsingLoadable.Parser<HlsPlaylist> playlistParser;
private final int minRetryCount;
private final IdentityHashMap<HlsUrl, MediaPlaylistBundle> playlistBundles;
private final List<PlaylistEventListener> listeners;
private final Loader initialPlaylistLoader;
private Handler playlistRefreshHandler;
private EventDispatcher eventDispatcher;
private PrimaryPlaylistListener primaryPlaylistListener;
private HlsMasterPlaylist masterPlaylist;
private HlsUrl primaryHlsUrl;
private HlsMediaPlaylist primaryUrlSnapshot;
private boolean isLive;
private long initialStartTimeUs;
/**
* @param dataSourceFactory A factory for {@link DataSource} instances.
* @param minRetryCount The minimum number of times loads must be retried before {@link
* #maybeThrowPlaylistRefreshError(HlsUrl)} and {@link
* #maybeThrowPrimaryPlaylistRefreshError()} propagate any loading errors.
* @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists.
*/
public DefaultHlsPlaylistTracker(
HlsDataSourceFactory dataSourceFactory,
int minRetryCount,
ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
this.dataSourceFactory = dataSourceFactory;
this.minRetryCount = minRetryCount;
this.playlistParser = playlistParser;
listeners = new ArrayList<>();
initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist");
playlistBundles = new IdentityHashMap<>();
initialStartTimeUs = C.TIME_UNSET;
}
// HlsPlaylistTracker implementation.
@Override
public void start(
Uri initialPlaylistUri,
EventDispatcher eventDispatcher,
PrimaryPlaylistListener primaryPlaylistListener) {
this.playlistRefreshHandler = new Handler();
this.eventDispatcher = eventDispatcher;
this.primaryPlaylistListener = primaryPlaylistListener;
ParsingLoadable<HlsPlaylist> masterPlaylistLoadable =
new ParsingLoadable<>(
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
initialPlaylistUri,
C.DATA_TYPE_MANIFEST,
playlistParser);
initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount);
}
@Override
public void release() {
initialPlaylistLoader.release();
for (MediaPlaylistBundle bundle : playlistBundles.values()) {
bundle.release();
}
playlistRefreshHandler.removeCallbacksAndMessages(null);
playlistBundles.clear();
}
@Override
public void addListener(PlaylistEventListener listener) {
listeners.add(listener);
}
@Override
public void removeListener(PlaylistEventListener listener) {
listeners.remove(listener);
}
@Override
public HlsMasterPlaylist getMasterPlaylist() {
return masterPlaylist;
}
@Override
public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) {
HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
if (snapshot != null) {
maybeSetPrimaryUrl(url);
}
return snapshot;
}
@Override
public long getInitialStartTimeUs() {
return initialStartTimeUs;
}
@Override
public boolean isSnapshotValid(HlsUrl url) {
return playlistBundles.get(url).isSnapshotValid();
}
@Override
public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {
initialPlaylistLoader.maybeThrowError();
if (primaryHlsUrl != null) {
maybeThrowPlaylistRefreshError(primaryHlsUrl);
}
}
@Override
public void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException {
playlistBundles.get(url).maybeThrowPlaylistRefreshError();
}
@Override
public void refreshPlaylist(HlsUrl url) {
playlistBundles.get(url).loadPlaylist();
}
@Override
public boolean isLive() {
return isLive;
}
// Loader.Callback implementation.
@Override
public void onLoadCompleted(
ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {
HlsPlaylist result = loadable.getResult();
HlsMasterPlaylist masterPlaylist;
boolean isMediaPlaylist = result instanceof HlsMediaPlaylist;
if (isMediaPlaylist) {
masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri);
} else /* result instanceof HlsMasterPlaylist */ {
masterPlaylist = (HlsMasterPlaylist) result;
}
this.masterPlaylist = masterPlaylist;
primaryHlsUrl = masterPlaylist.variants.get(0);
ArrayList<HlsUrl> urls = new ArrayList<>();
urls.addAll(masterPlaylist.variants);
urls.addAll(masterPlaylist.audios);
urls.addAll(masterPlaylist.subtitles);
createBundles(urls);
MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl);
if (isMediaPlaylist) {
// We don't need to load the playlist again. We can use the same result.
primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result);
} else {
primaryBundle.loadPlaylist();
}
eventDispatcher.loadCompleted(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded());
}
@Override
public void onLoadCanceled(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
boolean released) {
eventDispatcher.loadCanceled(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded());
}
@Override
public @Loader.RetryAction int onLoadError(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error) {
boolean isFatal = error instanceof ParserException;
eventDispatcher.loadError(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded(),
error,
isFatal);
return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
}
// Internal methods.
private boolean maybeSelectNewPrimaryUrl() {
List<HlsUrl> variants = masterPlaylist.variants;
int variantsSize = variants.size();
long currentTimeMs = SystemClock.elapsedRealtime();
for (int i = 0; i < variantsSize; i++) {
MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i));
if (currentTimeMs > bundle.blacklistUntilMs) {
primaryHlsUrl = bundle.playlistUrl;
bundle.loadPlaylist();
return true;
}
}
return false;
}
private void maybeSetPrimaryUrl(HlsUrl url) {
if (url == primaryHlsUrl
|| !masterPlaylist.variants.contains(url)
|| (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) {
// Ignore if the primary url is unchanged, if the url is not a variant url, or if the last
// primary snapshot contains an end tag.
return;
}
primaryHlsUrl = url;
playlistBundles.get(primaryHlsUrl).loadPlaylist();
}
private void createBundles(List<HlsUrl> urls) {
int listSize = urls.size();
for (int i = 0; i < listSize; i++) {
HlsUrl url = urls.get(i);
MediaPlaylistBundle bundle = new MediaPlaylistBundle(url);
playlistBundles.put(url, bundle);
}
}
/**
* Called by the bundles when a snapshot changes.
*
* @param url The url of the playlist.
* @param newSnapshot The new snapshot.
*/
private void onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) {
if (url == primaryHlsUrl) {
if (primaryUrlSnapshot == null) {
// This is the first primary url snapshot.
isLive = !newSnapshot.hasEndTag;
initialStartTimeUs = newSnapshot.startTimeUs;
}
primaryUrlSnapshot = newSnapshot;
primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);
}
int listenersSize = listeners.size();
for (int i = 0; i < listenersSize; i++) {
listeners.get(i).onPlaylistChanged();
}
}
private boolean notifyPlaylistError(HlsUrl playlistUrl, boolean shouldBlacklist) {
int listenersSize = listeners.size();
boolean anyBlacklistingFailed = false;
for (int i = 0; i < listenersSize; i++) {
anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, shouldBlacklist);
}
return anyBlacklistingFailed;
}
private HlsMediaPlaylist getLatestPlaylistSnapshot(
HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
if (!loadedPlaylist.isNewerThan(oldPlaylist)) {
if (loadedPlaylist.hasEndTag) {
// If the loaded playlist has an end tag but is not newer than the old playlist then we have
// an inconsistent state. This is typically caused by the server incorrectly resetting the
// media sequence when appending the end tag. We resolve this case as best we can by
// returning the old playlist with the end tag appended.
return oldPlaylist.copyWithEndTag();
} else {
return oldPlaylist;
}
}
long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist);
int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist);
return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence);
}
private long getLoadedPlaylistStartTimeUs(
HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
if (loadedPlaylist.hasProgramDateTime) {
return loadedPlaylist.startTimeUs;
}
long primarySnapshotStartTimeUs =
primaryUrlSnapshot != null ? primaryUrlSnapshot.startTimeUs : 0;
if (oldPlaylist == null) {
return primarySnapshotStartTimeUs;
}
int oldPlaylistSize = oldPlaylist.segments.size();
Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
if (firstOldOverlappingSegment != null) {
return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs;
} else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) {
return oldPlaylist.getEndTimeUs();
} else {
// No segments overlap, we assume the new playlist start coincides with the primary playlist.
return primarySnapshotStartTimeUs;
}
}
private int getLoadedPlaylistDiscontinuitySequence(
HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
if (loadedPlaylist.hasDiscontinuitySequence) {
return loadedPlaylist.discontinuitySequence;
}
// TODO: Improve cross-playlist discontinuity adjustment.
int primaryUrlDiscontinuitySequence =
primaryUrlSnapshot != null ? primaryUrlSnapshot.discontinuitySequence : 0;
if (oldPlaylist == null) {
return primaryUrlDiscontinuitySequence;
}
Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
if (firstOldOverlappingSegment != null) {
return oldPlaylist.discontinuitySequence
+ firstOldOverlappingSegment.relativeDiscontinuitySequence
- loadedPlaylist.segments.get(0).relativeDiscontinuitySequence;
}
return primaryUrlDiscontinuitySequence;
}
private static Segment getFirstOldOverlappingSegment(
HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence);
List<Segment> oldSegments = oldPlaylist.segments;
return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null;
}
/** Holds all information related to a specific Media Playlist. */
private final class MediaPlaylistBundle
implements Loader.Callback<ParsingLoadable<HlsPlaylist>>, Runnable {
private final HlsUrl playlistUrl;
private final Loader mediaPlaylistLoader;
private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable;
private HlsMediaPlaylist playlistSnapshot;
private long lastSnapshotLoadMs;
private long lastSnapshotChangeMs;
private long earliestNextLoadTimeMs;
private long blacklistUntilMs;
private boolean loadPending;
private IOException playlistError;
public MediaPlaylistBundle(HlsUrl playlistUrl) {
this.playlistUrl = playlistUrl;
mediaPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MediaPlaylist");
mediaPlaylistLoadable =
new ParsingLoadable<>(
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url),
C.DATA_TYPE_MANIFEST,
playlistParser);
}
public HlsMediaPlaylist getPlaylistSnapshot() {
return playlistSnapshot;
}
public boolean isSnapshotValid() {
if (playlistSnapshot == null) {
return false;
}
long currentTimeMs = SystemClock.elapsedRealtime();
long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs));
return playlistSnapshot.hasEndTag
|| playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT
|| playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
|| lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs;
}
public void release() {
mediaPlaylistLoader.release();
}
public void loadPlaylist() {
blacklistUntilMs = 0;
if (loadPending || mediaPlaylistLoader.isLoading()) {
// Load already pending or in progress. Do nothing.
return;
}
long currentTimeMs = SystemClock.elapsedRealtime();
if (currentTimeMs < earliestNextLoadTimeMs) {
loadPending = true;
playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs);
} else {
loadPlaylistImmediately();
}
}
public void maybeThrowPlaylistRefreshError() throws IOException {
mediaPlaylistLoader.maybeThrowError();
if (playlistError != null) {
throw playlistError;
}
}
// Loader.Callback implementation.
@Override
public void onLoadCompleted(
ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {
HlsPlaylist result = loadable.getResult();
if (result instanceof HlsMediaPlaylist) {
processLoadedPlaylist((HlsMediaPlaylist) result);
eventDispatcher.loadCompleted(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded());
} else {
playlistError = new ParserException("Loaded playlist has unexpected type.");
}
}
@Override
public void onLoadCanceled(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
boolean released) {
eventDispatcher.loadCanceled(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded());
}
@Override
public @Loader.RetryAction int onLoadError(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error) {
boolean isFatal = error instanceof ParserException;
eventDispatcher.loadError(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded(),
error,
isFatal);
boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error);
boolean shouldRetryIfNotFatal =
notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist;
if (isFatal) {
return Loader.DONT_RETRY_FATAL;
}
if (shouldBlacklist) {
shouldRetryIfNotFatal |= blacklistPlaylist();
}
return shouldRetryIfNotFatal ? Loader.RETRY : Loader.DONT_RETRY;
}
// Runnable implementation.
@Override
public void run() {
loadPending = false;
loadPlaylistImmediately();
}
// Internal methods.
private void loadPlaylistImmediately() {
mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount);
}
private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) {
HlsMediaPlaylist oldPlaylist = playlistSnapshot;
long currentTimeMs = SystemClock.elapsedRealtime();
lastSnapshotLoadMs = currentTimeMs;
playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist);
if (playlistSnapshot != oldPlaylist) {
playlistError = null;
lastSnapshotChangeMs = currentTimeMs;
onPlaylistUpdated(playlistUrl, playlistSnapshot);
} else if (!playlistSnapshot.hasEndTag) {
if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size()
< playlistSnapshot.mediaSequence) {
// The media sequence jumped backwards. The server has probably reset.
playlistError = new PlaylistResetException(playlistUrl.url);
notifyPlaylistError(playlistUrl, false);
} else if (currentTimeMs - lastSnapshotChangeMs
> C.usToMs(playlistSnapshot.targetDurationUs)
* PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) {
// The playlist seems to be stuck. Blacklist it.
playlistError = new PlaylistStuckException(playlistUrl.url);
notifyPlaylistError(playlistUrl, true);
blacklistPlaylist();
}
}
// Do not allow the playlist to load again within the target duration if we obtained a new
// snapshot, or half the target duration otherwise.
earliestNextLoadTimeMs =
currentTimeMs
+ C.usToMs(
playlistSnapshot != oldPlaylist
? playlistSnapshot.targetDurationUs
: (playlistSnapshot.targetDurationUs / 2));
// Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the
// next load will be scheduled when refreshPlaylist is called, or when this playlist becomes
// the primary.
if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) {
loadPlaylist();
}
}
/**
* Blacklists the playlist.
*
* @return Whether the playlist is the primary, despite being blacklisted.
*/
private boolean blacklistPlaylist() {
blacklistUntilMs =
SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS;
return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl();
}
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2016 The Android Open Source Project * Copyright (C) 2018 The Android Open Source Project
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,66 +16,28 @@
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.support.annotation.Nullable;
import android.os.SystemClock;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.UriUtil;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
/** /**
* Tracks playlists linked to a provided playlist url. The provided url might reference an HLS * Tracks playlists associated to an HLS stream and provides snapshots.
* master playlist or a media playlist. *
* <p>The playlist tracker is responsible for exposing the seeking window, which is defined by the
* segments that one of the playlists exposes. This playlist is called primary and needs to be
* periodically refreshed in the case of live streams. Note that the primary playlist is one of the
* media playlists while the master playlist is an optional kind of playlist defined by the HLS
* specification (RFC 8216).
*
* <p>Playlist loads might encounter errors. The tracker may choose to blacklist them to ensure a
* primary playlist is always available.
*/ */
public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable<HlsPlaylist>> { public interface HlsPlaylistTracker {
/** /** Listener for primary playlist changes. */
* Thrown when a playlist is considered to be stuck due to a server side error. interface PrimaryPlaylistListener {
*/
public static final class PlaylistStuckException extends IOException {
/**
* The url of the stuck playlist.
*/
public final String url;
private PlaylistStuckException(String url) {
this.url = url;
}
}
/**
* Thrown when the media sequence of a new snapshot indicates the server has reset.
*/
public static final class PlaylistResetException extends IOException {
/**
* The url of the reset playlist.
*/
public final String url;
private PlaylistResetException(String url) {
this.url = url;
}
}
/**
* Listener for primary playlist changes.
*/
public interface PrimaryPlaylistListener {
/** /**
* Called when the primary playlist changes. * Called when the primary playlist changes.
@ -85,10 +47,8 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist); void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
} }
/** /** Called on playlist loading events. */
* Called on playlist loading events. interface PlaylistEventListener {
*/
public interface PlaylistEventListener {
/** /**
* Called a playlist changes. * Called a playlist changes.
@ -105,141 +65,107 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist); boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist);
} }
/** /** Thrown when a playlist is considered to be stuck due to a server side error. */
* Coefficient applied on the target duration of a playlist to determine the amount of time after final class PlaylistStuckException extends IOException {
* which an unchanging playlist is considered stuck.
*/
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5;
private final Uri initialPlaylistUri; /** The url of the stuck playlist. */
private final HlsDataSourceFactory dataSourceFactory; public final String url;
private final ParsingLoadable.Parser<HlsPlaylist> playlistParser;
private final int minRetryCount;
private final IdentityHashMap<HlsUrl, MediaPlaylistBundle> playlistBundles;
private final Handler playlistRefreshHandler;
private final PrimaryPlaylistListener primaryPlaylistListener;
private final List<PlaylistEventListener> listeners;
private final Loader initialPlaylistLoader;
private final EventDispatcher eventDispatcher;
private HlsMasterPlaylist masterPlaylist;
private HlsUrl primaryHlsUrl;
private HlsMediaPlaylist primaryUrlSnapshot;
private boolean isLive;
private long initialStartTimeUs;
/** /**
* @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media * Creates an instance.
* playlist or a master playlist. *
* @param dataSourceFactory A factory for {@link DataSource} instances. * @param url See {@link #url}.
* @param eventDispatcher A dispatcher to notify of events.
* @param minRetryCount The minimum number of times loads must be retried before
* {@link #maybeThrowPlaylistRefreshError(HlsUrl)} and
* {@link #maybeThrowPrimaryPlaylistRefreshError()} propagate any loading errors.
* @param primaryPlaylistListener A callback for the primary playlist change events.
*/ */
public HlsPlaylistTracker(Uri initialPlaylistUri, HlsDataSourceFactory dataSourceFactory, public PlaylistStuckException(String url) {
EventDispatcher eventDispatcher, int minRetryCount, this.url = url;
PrimaryPlaylistListener primaryPlaylistListener,
ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
this.initialPlaylistUri = initialPlaylistUri;
this.dataSourceFactory = dataSourceFactory;
this.eventDispatcher = eventDispatcher;
this.minRetryCount = minRetryCount;
this.primaryPlaylistListener = primaryPlaylistListener;
this.playlistParser = playlistParser;
listeners = new ArrayList<>();
initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist");
playlistBundles = new IdentityHashMap<>();
playlistRefreshHandler = new Handler();
initialStartTimeUs = C.TIME_UNSET;
} }
}
/** Thrown when the media sequence of a new snapshot indicates the server has reset. */
final class PlaylistResetException extends IOException {
/** The url of the reset playlist. */
public final String url;
/**
* Creates an instance.
*
* @param url See {@link #url}.
*/
public PlaylistResetException(String url) {
this.url = url;
}
}
/**
* Starts the playlist tracker.
*
* <p>Must be called from the playback thread. A tracker may be restarted after a {@link
* #release()} call.
*
* @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master
* playlist.
* @param eventDispatcher A dispatcher to notify of events.
* @param listener A callback for the primary playlist change events.
*/
void start(
Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener);
/** Releases all acquired resources. Must be called once per {@link #start} call. */
void release();
/** /**
* Registers a listener to receive events from the playlist tracker. * Registers a listener to receive events from the playlist tracker.
* *
* @param listener The listener. * @param listener The listener.
*/ */
public void addListener(PlaylistEventListener listener) { void addListener(PlaylistEventListener listener);
listeners.add(listener);
}
/** /**
* Unregisters a listener. * Unregisters a listener.
* *
* @param listener The listener to unregister. * @param listener The listener to unregister.
*/ */
public void removeListener(PlaylistEventListener listener) { void removeListener(PlaylistEventListener listener);
listeners.remove(listener);
}
/**
* Starts tracking all the playlists related to the provided Uri.
*/
public void start() {
ParsingLoadable<HlsPlaylist> masterPlaylistLoadable = new ParsingLoadable<>(
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), initialPlaylistUri,
C.DATA_TYPE_MANIFEST, playlistParser);
initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount);
}
/** /**
* Returns the master playlist. * Returns the master playlist.
* *
* <p>If the uri passed to {@link #start} points to a media playlist, an {@link HlsMasterPlaylist}
* with a single variant for said media playlist is returned.
*
* @return The master playlist. Null if the initial playlist has yet to be loaded. * @return The master playlist. Null if the initial playlist has yet to be loaded.
*/ */
public HlsMasterPlaylist getMasterPlaylist() { @Nullable
return masterPlaylist; HlsMasterPlaylist getMasterPlaylist();
}
/** /**
* Returns the most recent snapshot available of the playlist referenced by the provided * Returns the most recent snapshot available of the playlist referenced by the provided {@link
* {@link HlsUrl}. * HlsUrl}.
* *
* @param url The {@link HlsUrl} corresponding to the requested media playlist. * @param url The {@link HlsUrl} corresponding to the requested media playlist.
* @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May * @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May
* be null if no snapshot has been loaded yet. * be null if no snapshot has been loaded yet.
*/ */
public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { @Nullable
HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url);
if (snapshot != null) {
maybeSetPrimaryUrl(url);
}
return snapshot;
}
/** /**
* Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no * Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no
* media playlist has been loaded. * media playlist has been loaded.
*/ */
public long getInitialStartTimeUs() { long getInitialStartTimeUs();
return initialStartTimeUs;
}
/** /**
* Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is * Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
* valid, meaning all the segments referenced by the playlist are expected to be available. If the * valid, meaning all the segments referenced by the playlist are expected to be available. If the
* playlist is not valid then some of the segments may no longer be available. * playlist is not valid then some of the segments may no longer be available.
*
* @param url The {@link HlsUrl}. * @param url The {@link HlsUrl}.
* @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is * @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
* valid. * valid.
*/ */
public boolean isSnapshotValid(HlsUrl url) { boolean isSnapshotValid(HlsUrl url);
return playlistBundles.get(url).isSnapshotValid();
}
/**
* Releases the playlist tracker.
*/
public void release() {
initialPlaylistLoader.release();
for (MediaPlaylistBundle bundle : playlistBundles.values()) {
bundle.release();
}
playlistRefreshHandler.removeCallbacksAndMessages(null);
playlistBundles.clear();
}
/** /**
* If the tracker is having trouble refreshing the master playlist or the primary playlist, this * If the tracker is having trouble refreshing the master playlist or the primary playlist, this
@ -247,401 +173,31 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
* *
* @throws IOException The underlying error. * @throws IOException The underlying error.
*/ */
public void maybeThrowPrimaryPlaylistRefreshError() throws IOException { void maybeThrowPrimaryPlaylistRefreshError() throws IOException;
initialPlaylistLoader.maybeThrowError();
if (primaryHlsUrl != null) {
maybeThrowPlaylistRefreshError(primaryHlsUrl);
}
}
/** /**
* If the playlist is having trouble refreshing the playlist referenced by the given * If the playlist is having trouble refreshing the playlist referenced by the given {@link
* {@link HlsUrl}, this method throws the underlying error. * HlsUrl}, this method throws the underlying error.
* *
* @param url The {@link HlsUrl}. * @param url The {@link HlsUrl}.
* @throws IOException The underyling error. * @throws IOException The underyling error.
*/ */
public void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException { void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException;
playlistBundles.get(url).maybeThrowPlaylistRefreshError();
}
/** /**
* Triggers a playlist refresh and whitelists it. * Requests a playlist refresh and whitelists it.
*
* <p>The playlist tracker may choose the delay the playlist refresh. The request is discarded if
* a refresh was already pending.
* *
* @param url The {@link HlsUrl} of the playlist to be refreshed. * @param url The {@link HlsUrl} of the playlist to be refreshed.
*/ */
public void refreshPlaylist(HlsUrl url) { void refreshPlaylist(HlsUrl url);
playlistBundles.get(url).loadPlaylist();
}
/** /**
* Returns whether this is live content. * Returns whether the tracked playlists describe a live stream.
* *
* @return True if the content is live. False otherwise. * @return True if the content is live. False otherwise.
*/ */
public boolean isLive() { boolean isLive();
return isLive;
}
// Loader.Callback implementation.
@Override
public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs) {
HlsPlaylist result = loadable.getResult();
HlsMasterPlaylist masterPlaylist;
boolean isMediaPlaylist = result instanceof HlsMediaPlaylist;
if (isMediaPlaylist) {
masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri);
} else /* result instanceof HlsMasterPlaylist */ {
masterPlaylist = (HlsMasterPlaylist) result;
}
this.masterPlaylist = masterPlaylist;
primaryHlsUrl = masterPlaylist.variants.get(0);
ArrayList<HlsUrl> urls = new ArrayList<>();
urls.addAll(masterPlaylist.variants);
urls.addAll(masterPlaylist.audios);
urls.addAll(masterPlaylist.subtitles);
createBundles(urls);
MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl);
if (isMediaPlaylist) {
// We don't need to load the playlist again. We can use the same result.
primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result);
} else {
primaryBundle.loadPlaylist();
}
eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
}
@Override
public void onLoadCanceled(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
}
@Override
public @Loader.RetryAction int onLoadError(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error) {
boolean isFatal = error instanceof ParserException;
eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded(), error, isFatal);
return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
}
// Internal methods.
private boolean maybeSelectNewPrimaryUrl() {
List<HlsUrl> variants = masterPlaylist.variants;
int variantsSize = variants.size();
long currentTimeMs = SystemClock.elapsedRealtime();
for (int i = 0; i < variantsSize; i++) {
MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i));
if (currentTimeMs > bundle.blacklistUntilMs) {
primaryHlsUrl = bundle.playlistUrl;
bundle.loadPlaylist();
return true;
}
}
return false;
}
private void maybeSetPrimaryUrl(HlsUrl url) {
if (url == primaryHlsUrl
|| !masterPlaylist.variants.contains(url)
|| (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) {
// Ignore if the primary url is unchanged, if the url is not a variant url, or if the last
// primary snapshot contains an end tag.
return;
}
primaryHlsUrl = url;
playlistBundles.get(primaryHlsUrl).loadPlaylist();
}
private void createBundles(List<HlsUrl> urls) {
int listSize = urls.size();
for (int i = 0; i < listSize; i++) {
HlsUrl url = urls.get(i);
MediaPlaylistBundle bundle = new MediaPlaylistBundle(url);
playlistBundles.put(url, bundle);
}
}
/**
* Called by the bundles when a snapshot changes.
*
* @param url The url of the playlist.
* @param newSnapshot The new snapshot.
*/
private void onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) {
if (url == primaryHlsUrl) {
if (primaryUrlSnapshot == null) {
// This is the first primary url snapshot.
isLive = !newSnapshot.hasEndTag;
initialStartTimeUs = newSnapshot.startTimeUs;
}
primaryUrlSnapshot = newSnapshot;
primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);
}
int listenersSize = listeners.size();
for (int i = 0; i < listenersSize; i++) {
listeners.get(i).onPlaylistChanged();
}
}
private boolean notifyPlaylistError(HlsUrl playlistUrl, boolean shouldBlacklist) {
int listenersSize = listeners.size();
boolean anyBlacklistingFailed = false;
for (int i = 0; i < listenersSize; i++) {
anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, shouldBlacklist);
}
return anyBlacklistingFailed;
}
private HlsMediaPlaylist getLatestPlaylistSnapshot(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
if (!loadedPlaylist.isNewerThan(oldPlaylist)) {
if (loadedPlaylist.hasEndTag) {
// If the loaded playlist has an end tag but is not newer than the old playlist then we have
// an inconsistent state. This is typically caused by the server incorrectly resetting the
// media sequence when appending the end tag. We resolve this case as best we can by
// returning the old playlist with the end tag appended.
return oldPlaylist.copyWithEndTag();
} else {
return oldPlaylist;
}
}
long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist);
int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist);
return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence);
}
private long getLoadedPlaylistStartTimeUs(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
if (loadedPlaylist.hasProgramDateTime) {
return loadedPlaylist.startTimeUs;
}
long primarySnapshotStartTimeUs = primaryUrlSnapshot != null
? primaryUrlSnapshot.startTimeUs : 0;
if (oldPlaylist == null) {
return primarySnapshotStartTimeUs;
}
int oldPlaylistSize = oldPlaylist.segments.size();
Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
if (firstOldOverlappingSegment != null) {
return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs;
} else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) {
return oldPlaylist.getEndTimeUs();
} else {
// No segments overlap, we assume the new playlist start coincides with the primary playlist.
return primarySnapshotStartTimeUs;
}
}
private int getLoadedPlaylistDiscontinuitySequence(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
if (loadedPlaylist.hasDiscontinuitySequence) {
return loadedPlaylist.discontinuitySequence;
}
// TODO: Improve cross-playlist discontinuity adjustment.
int primaryUrlDiscontinuitySequence = primaryUrlSnapshot != null
? primaryUrlSnapshot.discontinuitySequence : 0;
if (oldPlaylist == null) {
return primaryUrlDiscontinuitySequence;
}
Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
if (firstOldOverlappingSegment != null) {
return oldPlaylist.discontinuitySequence
+ firstOldOverlappingSegment.relativeDiscontinuitySequence
- loadedPlaylist.segments.get(0).relativeDiscontinuitySequence;
}
return primaryUrlDiscontinuitySequence;
}
private static Segment getFirstOldOverlappingSegment(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence);
List<Segment> oldSegments = oldPlaylist.segments;
return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null;
}
/**
* Holds all information related to a specific Media Playlist.
*/
private final class MediaPlaylistBundle implements Loader.Callback<ParsingLoadable<HlsPlaylist>>,
Runnable {
private final HlsUrl playlistUrl;
private final Loader mediaPlaylistLoader;
private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable;
private HlsMediaPlaylist playlistSnapshot;
private long lastSnapshotLoadMs;
private long lastSnapshotChangeMs;
private long earliestNextLoadTimeMs;
private long blacklistUntilMs;
private boolean loadPending;
private IOException playlistError;
public MediaPlaylistBundle(HlsUrl playlistUrl) {
this.playlistUrl = playlistUrl;
mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist");
mediaPlaylistLoadable = new ParsingLoadable<>(
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST,
playlistParser);
}
public HlsMediaPlaylist getPlaylistSnapshot() {
return playlistSnapshot;
}
public boolean isSnapshotValid() {
if (playlistSnapshot == null) {
return false;
}
long currentTimeMs = SystemClock.elapsedRealtime();
long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs));
return playlistSnapshot.hasEndTag
|| playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT
|| playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
|| lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs;
}
public void release() {
mediaPlaylistLoader.release();
}
public void loadPlaylist() {
blacklistUntilMs = 0;
if (loadPending || mediaPlaylistLoader.isLoading()) {
// Load already pending or in progress. Do nothing.
return;
}
long currentTimeMs = SystemClock.elapsedRealtime();
if (currentTimeMs < earliestNextLoadTimeMs) {
loadPending = true;
playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs);
} else {
loadPlaylistImmediately();
}
}
public void maybeThrowPlaylistRefreshError() throws IOException {
mediaPlaylistLoader.maybeThrowError();
if (playlistError != null) {
throw playlistError;
}
}
// Loader.Callback implementation.
@Override
public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs) {
HlsPlaylist result = loadable.getResult();
if (result instanceof HlsMediaPlaylist) {
processLoadedPlaylist((HlsMediaPlaylist) result);
eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
} else {
playlistError = new ParserException("Loaded playlist has unexpected type.");
}
}
@Override
public void onLoadCanceled(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
}
@Override
public @Loader.RetryAction int onLoadError(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error) {
boolean isFatal = error instanceof ParserException;
eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded(), error, isFatal);
boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error);
boolean shouldRetryIfNotFatal =
notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist;
if (isFatal) {
return Loader.DONT_RETRY_FATAL;
}
if (shouldBlacklist) {
shouldRetryIfNotFatal |= blacklistPlaylist();
}
return shouldRetryIfNotFatal ? Loader.RETRY : Loader.DONT_RETRY;
}
// Runnable implementation.
@Override
public void run() {
loadPending = false;
loadPlaylistImmediately();
}
// Internal methods.
private void loadPlaylistImmediately() {
mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount);
}
private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) {
HlsMediaPlaylist oldPlaylist = playlistSnapshot;
long currentTimeMs = SystemClock.elapsedRealtime();
lastSnapshotLoadMs = currentTimeMs;
playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist);
if (playlistSnapshot != oldPlaylist) {
playlistError = null;
lastSnapshotChangeMs = currentTimeMs;
onPlaylistUpdated(playlistUrl, playlistSnapshot);
} else if (!playlistSnapshot.hasEndTag) {
if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size()
< playlistSnapshot.mediaSequence) {
// The media sequence jumped backwards. The server has probably reset.
playlistError = new PlaylistResetException(playlistUrl.url);
notifyPlaylistError(playlistUrl, false);
} else if (currentTimeMs - lastSnapshotChangeMs
> C.usToMs(playlistSnapshot.targetDurationUs)
* PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) {
// The playlist seems to be stuck. Blacklist it.
playlistError = new PlaylistStuckException(playlistUrl.url);
notifyPlaylistError(playlistUrl, true);
blacklistPlaylist();
}
}
// Do not allow the playlist to load again within the target duration if we obtained a new
// snapshot, or half the target duration otherwise.
earliestNextLoadTimeMs = currentTimeMs + C.usToMs(playlistSnapshot != oldPlaylist
? playlistSnapshot.targetDurationUs : (playlistSnapshot.targetDurationUs / 2));
// Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the
// next load will be scheduled when refreshPlaylist is called, or when this playlist becomes
// the primary.
if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) {
loadPlaylist();
}
}
/**
* Blacklists the playlist.
*
* @return Whether the playlist is the primary, despite being blacklisted.
*/
private boolean blacklistPlaylist() {
blacklistUntilMs = SystemClock.elapsedRealtime()
+ ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS;
return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl();
}
}
} }