mirror of
https://github.com/androidx/media.git
synced 2025-05-05 14:40:50 +08:00
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:
parent
798b29e3ef
commit
f1fe1c40a6
@ -6,7 +6,9 @@
|
||||
extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)).
|
||||
* DASH: Fix playback getting stuck when playing representations that have both
|
||||
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
|
||||
([#4249](https://github.com/google/ExoPlayer/issues/4249)).
|
||||
* Fix inconsistent `Player.EventListener` invocations for recursive player state
|
||||
|
@ -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.SinglePeriodTimeline;
|
||||
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.HlsPlaylist;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
|
||||
@ -58,6 +59,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
|
||||
private HlsExtractorFactory extractorFactory;
|
||||
private @Nullable ParsingLoadable.Parser<HlsPlaylist> playlistParser;
|
||||
private @Nullable HlsPlaylistTracker playlistTracker;
|
||||
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||
private int minLoadableRetryCount;
|
||||
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
|
||||
* HlsPlaylistParser}.
|
||||
*
|
||||
* <p>Must not be called after calling {@link #setPlaylistTracker} on the same builder.
|
||||
*
|
||||
* @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists.
|
||||
* @return This factory, for convenience.
|
||||
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||
*/
|
||||
public Factory setPlaylistParser(ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
|
||||
Assertions.checkState(!isCreateCalled);
|
||||
Assertions.checkState(playlistTracker == null, "A playlist tracker has already been set.");
|
||||
this.playlistParser = Assertions.checkNotNull(playlistParser);
|
||||
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
|
||||
* 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
|
||||
public HlsMediaSource createMediaSource(Uri playlistUri) {
|
||||
isCreateCalled = true;
|
||||
if (playlistParser == null) {
|
||||
playlistParser = new HlsPlaylistParser();
|
||||
if (playlistTracker == null) {
|
||||
playlistTracker =
|
||||
new DefaultHlsPlaylistTracker(
|
||||
hlsDataSourceFactory,
|
||||
minLoadableRetryCount,
|
||||
playlistParser != null ? playlistParser : new HlsPlaylistParser());
|
||||
}
|
||||
return new HlsMediaSource(
|
||||
playlistUri,
|
||||
@ -196,7 +223,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
extractorFactory,
|
||||
compositeSequenceableLoaderFactory,
|
||||
minLoadableRetryCount,
|
||||
playlistParser,
|
||||
playlistTracker,
|
||||
allowChunklessPreparation,
|
||||
tag);
|
||||
}
|
||||
@ -233,12 +260,10 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
private final HlsDataSourceFactory dataSourceFactory;
|
||||
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||
private final int minLoadableRetryCount;
|
||||
private final ParsingLoadable.Parser<HlsPlaylist> playlistParser;
|
||||
private final boolean allowChunklessPreparation;
|
||||
private final HlsPlaylistTracker playlistTracker;
|
||||
private final @Nullable Object tag;
|
||||
|
||||
private HlsPlaylistTracker playlistTracker;
|
||||
|
||||
/**
|
||||
* @param manifestUri The {@link Uri} of the HLS manifest.
|
||||
* @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests,
|
||||
@ -276,8 +301,13 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
int minLoadableRetryCount,
|
||||
Handler eventHandler,
|
||||
MediaSourceEventListener eventListener) {
|
||||
this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory),
|
||||
HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener,
|
||||
this(
|
||||
manifestUri,
|
||||
new DefaultHlsDataSourceFactory(dataSourceFactory),
|
||||
HlsExtractorFactory.DEFAULT,
|
||||
minLoadableRetryCount,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
new HlsPlaylistParser());
|
||||
}
|
||||
|
||||
@ -309,7 +339,8 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
extractorFactory,
|
||||
new DefaultCompositeSequenceableLoaderFactory(),
|
||||
minLoadableRetryCount,
|
||||
playlistParser,
|
||||
new DefaultHlsPlaylistTracker(
|
||||
dataSourceFactory, minLoadableRetryCount, new HlsPlaylistParser()),
|
||||
/* allowChunklessPreparation= */ false,
|
||||
/* tag= */ null);
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
@ -323,7 +354,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
HlsExtractorFactory extractorFactory,
|
||||
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
||||
int minLoadableRetryCount,
|
||||
ParsingLoadable.Parser<HlsPlaylist> playlistParser,
|
||||
HlsPlaylistTracker playlistTracker,
|
||||
boolean allowChunklessPreparation,
|
||||
@Nullable Object tag) {
|
||||
this.manifestUri = manifestUri;
|
||||
@ -331,7 +362,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
this.extractorFactory = extractorFactory;
|
||||
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
this.playlistParser = playlistParser;
|
||||
this.playlistTracker = playlistTracker;
|
||||
this.allowChunklessPreparation = allowChunklessPreparation;
|
||||
this.tag = tag;
|
||||
}
|
||||
@ -339,9 +370,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
@Override
|
||||
public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
|
||||
EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
|
||||
playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher,
|
||||
minLoadableRetryCount, this, playlistParser);
|
||||
playlistTracker.start();
|
||||
playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -373,7 +402,6 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
public void releaseSourceInternal() {
|
||||
if (playlistTracker != null) {
|
||||
playlistTracker.release();
|
||||
playlistTracker = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -16,66 +16,28 @@
|
||||
package com.google.android.exoplayer2.source.hls.playlist;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.Nullable;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Tracks playlists linked to a provided playlist url. The provided url might reference an HLS
|
||||
* master playlist or a media playlist.
|
||||
* Tracks playlists associated to an HLS stream and provides snapshots.
|
||||
*
|
||||
* <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 {
|
||||
|
||||
/**
|
||||
* Thrown when a playlist is considered to be stuck due to a server side error.
|
||||
*/
|
||||
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 {
|
||||
/** Listener for primary playlist changes. */
|
||||
interface PrimaryPlaylistListener {
|
||||
|
||||
/**
|
||||
* Called when the primary playlist changes.
|
||||
@ -85,10 +47,8 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
||||
void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on playlist loading events.
|
||||
*/
|
||||
public interface PlaylistEventListener {
|
||||
/** Called on playlist loading events. */
|
||||
interface PlaylistEventListener {
|
||||
|
||||
/**
|
||||
* Called a playlist changes.
|
||||
@ -105,141 +65,107 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
||||
boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/** Thrown when a playlist is considered to be stuck due to a server side error. */
|
||||
final class PlaylistStuckException extends IOException {
|
||||
|
||||
private final Uri initialPlaylistUri;
|
||||
private final HlsDataSourceFactory dataSourceFactory;
|
||||
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;
|
||||
/** The url of the stuck playlist. */
|
||||
public final String url;
|
||||
|
||||
/**
|
||||
* @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media
|
||||
* playlist or a master playlist.
|
||||
* @param dataSourceFactory A factory for {@link DataSource} instances.
|
||||
* @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.
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param url See {@link #url}.
|
||||
*/
|
||||
public HlsPlaylistTracker(Uri initialPlaylistUri, HlsDataSourceFactory dataSourceFactory,
|
||||
EventDispatcher eventDispatcher, int minRetryCount,
|
||||
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;
|
||||
public PlaylistStuckException(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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.
|
||||
*
|
||||
* @param listener The listener.
|
||||
*/
|
||||
public void addListener(PlaylistEventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
void addListener(PlaylistEventListener listener);
|
||||
|
||||
/**
|
||||
* Unregisters a listener.
|
||||
*
|
||||
* @param listener The listener to unregister.
|
||||
*/
|
||||
public 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);
|
||||
}
|
||||
void removeListener(PlaylistEventListener listener);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public HlsMasterPlaylist getMasterPlaylist() {
|
||||
return masterPlaylist;
|
||||
}
|
||||
@Nullable
|
||||
HlsMasterPlaylist getMasterPlaylist();
|
||||
|
||||
/**
|
||||
* Returns the most recent snapshot available of the playlist referenced by the provided
|
||||
* {@link HlsUrl}.
|
||||
* Returns the most recent snapshot available of the playlist referenced by the provided {@link
|
||||
* HlsUrl}.
|
||||
*
|
||||
* @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
|
||||
* be null if no snapshot has been loaded yet.
|
||||
*/
|
||||
public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) {
|
||||
HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
|
||||
if (snapshot != null) {
|
||||
maybeSetPrimaryUrl(url);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
@Nullable
|
||||
HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url);
|
||||
|
||||
/**
|
||||
* Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no
|
||||
* media playlist has been loaded.
|
||||
*/
|
||||
public long getInitialStartTimeUs() {
|
||||
return initialStartTimeUs;
|
||||
}
|
||||
long getInitialStartTimeUs();
|
||||
|
||||
/**
|
||||
* 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
|
||||
* playlist is not valid then some of the segments may no longer be available.
|
||||
|
||||
*
|
||||
* @param url The {@link HlsUrl}.
|
||||
* @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
|
||||
* valid.
|
||||
*/
|
||||
public 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();
|
||||
}
|
||||
boolean isSnapshotValid(HlsUrl url);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {
|
||||
initialPlaylistLoader.maybeThrowError();
|
||||
if (primaryHlsUrl != null) {
|
||||
maybeThrowPlaylistRefreshError(primaryHlsUrl);
|
||||
}
|
||||
}
|
||||
void maybeThrowPrimaryPlaylistRefreshError() throws IOException;
|
||||
|
||||
/**
|
||||
* If the playlist is having trouble refreshing the playlist referenced by the given
|
||||
* {@link HlsUrl}, this method throws the underlying error.
|
||||
* If the playlist is having trouble refreshing the playlist referenced by the given {@link
|
||||
* HlsUrl}, this method throws the underlying error.
|
||||
*
|
||||
* @param url The {@link HlsUrl}.
|
||||
* @throws IOException The underyling error.
|
||||
*/
|
||||
public void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException {
|
||||
playlistBundles.get(url).maybeThrowPlaylistRefreshError();
|
||||
}
|
||||
void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public void refreshPlaylist(HlsUrl url) {
|
||||
playlistBundles.get(url).loadPlaylist();
|
||||
}
|
||||
void refreshPlaylist(HlsUrl url);
|
||||
|
||||
/**
|
||||
* Returns whether this is live content.
|
||||
* Returns whether the tracked playlists describe a live stream.
|
||||
*
|
||||
* @return True if the content is live. False otherwise.
|
||||
*/
|
||||
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("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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
boolean isLive();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user