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)).
|
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
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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");
|
* 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;
|
* Creates an instance.
|
||||||
private HlsMediaPlaylist primaryUrlSnapshot;
|
*
|
||||||
private boolean isLive;
|
* @param url See {@link #url}.
|
||||||
private long initialStartTimeUs;
|
*/
|
||||||
|
public PlaylistStuckException(String url) {
|
||||||
/**
|
this.url = 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.
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user