Use a custom HlsPlaylistTracker, based on DefaultHlsPlaylistTracker to allow more stucking on HLS livestreams
ExoPlayer's default behavior is to use a multiplication of target segment by a coefficient (3,5). This coefficient (and this behavior) cannot be customized without using a custom HlsPlaylistTracker right now. New behavior is to wait 15 seconds before throwing a PlaylistStuckException. This should improve a lot HLS live streaming on (very) low-latency livestreams with buffering issues, especially on YouTube with their HLS manifests.
This commit is contained in:
parent
651b79d3ed
commit
94f774b82d
2 changed files with 787 additions and 2 deletions
|
@ -16,6 +16,8 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.playback.CustomHlsPlaylistTracker;
|
||||||
|
|
||||||
public class PlayerDataSource {
|
public class PlayerDataSource {
|
||||||
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
||||||
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
|
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
|
||||||
|
@ -44,8 +46,9 @@ public class PlayerDataSource {
|
||||||
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
||||||
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
||||||
.setAllowChunklessPreparation(true)
|
.setAllowChunklessPreparation(true)
|
||||||
.setLoadErrorHandlingPolicy(
|
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(
|
||||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
|
MANIFEST_MINIMUM_RETRY))
|
||||||
|
.setPlaylistTrackerFactory(CustomHlsPlaylistTracker.FACTORY);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
||||||
|
|
|
@ -0,0 +1,782 @@
|
||||||
|
/*
|
||||||
|
* Original source code (DefaultHlsPlaylistTracker): Copyright (C) 2016 The Android Open Source
|
||||||
|
* Project
|
||||||
|
*
|
||||||
|
* Original source code licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use the original source code of 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 org.schabi.newpipe.player.playback;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||||
|
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.source.LoadEventInfo;
|
||||||
|
import com.google.android.exoplayer2.source.MediaLoadData;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||||
|
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Part;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.RenditionReport;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
|
||||||
|
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.HlsPlaylistParserFactory;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||||
|
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
|
||||||
|
import com.google.android.exoplayer2.upstream.Loader;
|
||||||
|
import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;
|
||||||
|
import com.google.android.exoplayer2.upstream.ParsingLoadable;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NewPipe's implementation for {@link HlsPlaylistTracker}, based on
|
||||||
|
* {@link DefaultHlsPlaylistTracker}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* It redefines the way of how
|
||||||
|
* {@link PlaylistStuckException PlaylistStuckExceptions} are thrown: instead of
|
||||||
|
* using a multiplication between the target duration of segments and
|
||||||
|
* {@link DefaultHlsPlaylistTracker#DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT}, it uses a
|
||||||
|
* constant value (see {@link #MAXIMUM_PLAYLIST_STUCK_DURATION_MS}), in order to reduce the number
|
||||||
|
* of this exception thrown, especially on (very) low-latency livestreams.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class CustomHlsPlaylistTracker implements HlsPlaylistTracker,
|
||||||
|
Loader.Callback<ParsingLoadable<HlsPlaylist>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for {@link CustomHlsPlaylistTracker} instances.
|
||||||
|
*/
|
||||||
|
public static final Factory FACTORY = CustomHlsPlaylistTracker::new;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum duration before a {@link PlaylistStuckException} is thrown, in milliseconds.
|
||||||
|
*/
|
||||||
|
private static final double MAXIMUM_PLAYLIST_STUCK_DURATION_MS = 15000;
|
||||||
|
|
||||||
|
private final HlsDataSourceFactory dataSourceFactory;
|
||||||
|
private final HlsPlaylistParserFactory playlistParserFactory;
|
||||||
|
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||||
|
private final HashMap<Uri, MediaPlaylistBundle> playlistBundles;
|
||||||
|
private final List<PlaylistEventListener> listeners;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private EventDispatcher eventDispatcher;
|
||||||
|
@Nullable
|
||||||
|
private Loader initialPlaylistLoader;
|
||||||
|
@Nullable
|
||||||
|
private Handler playlistRefreshHandler;
|
||||||
|
@Nullable
|
||||||
|
private PrimaryPlaylistListener primaryPlaylistListener;
|
||||||
|
@Nullable
|
||||||
|
private HlsMasterPlaylist masterPlaylist;
|
||||||
|
@Nullable
|
||||||
|
private Uri primaryMediaPlaylistUrl;
|
||||||
|
@Nullable
|
||||||
|
private HlsMediaPlaylist primaryMediaPlaylistSnapshot;
|
||||||
|
private boolean isLive;
|
||||||
|
private long initialStartTimeUs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param dataSourceFactory A factory for {@link DataSource} instances.
|
||||||
|
* @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
|
||||||
|
* @param playlistParserFactory An {@link HlsPlaylistParserFactory}.
|
||||||
|
*/
|
||||||
|
public CustomHlsPlaylistTracker(final HlsDataSourceFactory dataSourceFactory,
|
||||||
|
final LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
||||||
|
final HlsPlaylistParserFactory playlistParserFactory) {
|
||||||
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
|
this.playlistParserFactory = playlistParserFactory;
|
||||||
|
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
||||||
|
listeners = new ArrayList<>();
|
||||||
|
playlistBundles = new HashMap<>();
|
||||||
|
initialStartTimeUs = C.TIME_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HlsPlaylistTracker implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(@NonNull final Uri initialPlaylistUri,
|
||||||
|
@NonNull final EventDispatcher eventDispatcherObject,
|
||||||
|
@NonNull final PrimaryPlaylistListener primaryPlaylistListenerObject) {
|
||||||
|
this.playlistRefreshHandler = Util.createHandlerForCurrentLooper();
|
||||||
|
this.eventDispatcher = eventDispatcherObject;
|
||||||
|
this.primaryPlaylistListener = primaryPlaylistListenerObject;
|
||||||
|
final ParsingLoadable<HlsPlaylist> masterPlaylistLoadable = new ParsingLoadable<>(
|
||||||
|
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
|
||||||
|
initialPlaylistUri,
|
||||||
|
C.DATA_TYPE_MANIFEST,
|
||||||
|
playlistParserFactory.createPlaylistParser());
|
||||||
|
Assertions.checkState(initialPlaylistLoader == null);
|
||||||
|
initialPlaylistLoader = new Loader("CustomHlsPlaylistTracker:MasterPlaylist");
|
||||||
|
final long elapsedRealtime = initialPlaylistLoader.startLoading(masterPlaylistLoadable,
|
||||||
|
this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(
|
||||||
|
masterPlaylistLoadable.type));
|
||||||
|
eventDispatcherObject.loadStarted(new LoadEventInfo(masterPlaylistLoadable.loadTaskId,
|
||||||
|
masterPlaylistLoadable.dataSpec, elapsedRealtime),
|
||||||
|
masterPlaylistLoadable.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
primaryMediaPlaylistUrl = null;
|
||||||
|
primaryMediaPlaylistSnapshot = null;
|
||||||
|
masterPlaylist = null;
|
||||||
|
initialStartTimeUs = C.TIME_UNSET;
|
||||||
|
initialPlaylistLoader.release();
|
||||||
|
initialPlaylistLoader = null;
|
||||||
|
for (final MediaPlaylistBundle bundle : playlistBundles.values()) {
|
||||||
|
bundle.release();
|
||||||
|
}
|
||||||
|
playlistRefreshHandler.removeCallbacksAndMessages(null);
|
||||||
|
playlistRefreshHandler = null;
|
||||||
|
playlistBundles.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addListener(@NonNull final PlaylistEventListener listener) {
|
||||||
|
checkNotNull(listener);
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeListener(@NonNull final PlaylistEventListener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public HlsMasterPlaylist getMasterPlaylist() {
|
||||||
|
return masterPlaylist;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public HlsMediaPlaylist getPlaylistSnapshot(@NonNull final Uri url,
|
||||||
|
final boolean isForPlayback) {
|
||||||
|
final HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
|
||||||
|
if (snapshot != null && isForPlayback) {
|
||||||
|
maybeSetPrimaryUrl(url);
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getInitialStartTimeUs() {
|
||||||
|
return initialStartTimeUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSnapshotValid(@NonNull final Uri url) {
|
||||||
|
return playlistBundles.get(url).isSnapshotValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {
|
||||||
|
if (initialPlaylistLoader != null) {
|
||||||
|
initialPlaylistLoader.maybeThrowError();
|
||||||
|
}
|
||||||
|
if (primaryMediaPlaylistUrl != null) {
|
||||||
|
maybeThrowPlaylistRefreshError(primaryMediaPlaylistUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void maybeThrowPlaylistRefreshError(@NonNull final Uri url) throws IOException {
|
||||||
|
playlistBundles.get(url).maybeThrowPlaylistRefreshError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshPlaylist(@NonNull final Uri url) {
|
||||||
|
playlistBundles.get(url).loadPlaylist();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLive() {
|
||||||
|
return isLive;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loader.Callback implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCompleted(@NonNull final ParsingLoadable<HlsPlaylist> loadable,
|
||||||
|
final long elapsedRealtimeMs,
|
||||||
|
final long loadDurationMs) {
|
||||||
|
final HlsPlaylist result = loadable.getResult();
|
||||||
|
final HlsMasterPlaylist newMasterPlaylist;
|
||||||
|
final boolean isMediaPlaylist = result instanceof HlsMediaPlaylist;
|
||||||
|
if (isMediaPlaylist) {
|
||||||
|
newMasterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(
|
||||||
|
result.baseUri);
|
||||||
|
} else { // result instanceof HlsMasterPlaylist
|
||||||
|
newMasterPlaylist = (HlsMasterPlaylist) result;
|
||||||
|
}
|
||||||
|
this.masterPlaylist = newMasterPlaylist;
|
||||||
|
primaryMediaPlaylistUrl = newMasterPlaylist.variants.get(0).url;
|
||||||
|
createBundles(newMasterPlaylist.mediaPlaylistUrls);
|
||||||
|
final LoadEventInfo loadEventInfo = new LoadEventInfo(loadable.loadTaskId,
|
||||||
|
loadable.dataSpec, loadable.getUri(), loadable.getResponseHeaders(),
|
||||||
|
elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
|
||||||
|
final MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryMediaPlaylistUrl);
|
||||||
|
if (isMediaPlaylist) {
|
||||||
|
// We don't need to load the playlist again. We can use the same result.
|
||||||
|
primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result, loadEventInfo);
|
||||||
|
} else {
|
||||||
|
primaryBundle.loadPlaylist();
|
||||||
|
}
|
||||||
|
loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId);
|
||||||
|
eventDispatcher.loadCompleted(loadEventInfo, C.DATA_TYPE_MANIFEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCanceled(@NonNull final ParsingLoadable<HlsPlaylist> loadable,
|
||||||
|
final long elapsedRealtimeMs,
|
||||||
|
final long loadDurationMs,
|
||||||
|
final boolean released) {
|
||||||
|
final LoadEventInfo loadEventInfo = new LoadEventInfo(loadable.loadTaskId,
|
||||||
|
loadable.dataSpec, loadable.getUri(), loadable.getResponseHeaders(),
|
||||||
|
elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
|
||||||
|
loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId);
|
||||||
|
eventDispatcher.loadCanceled(loadEventInfo, C.DATA_TYPE_MANIFEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoadErrorAction onLoadError(@NonNull final ParsingLoadable<HlsPlaylist> loadable,
|
||||||
|
final long elapsedRealtimeMs,
|
||||||
|
final long loadDurationMs,
|
||||||
|
final IOException error,
|
||||||
|
final int errorCount) {
|
||||||
|
final LoadEventInfo loadEventInfo = new LoadEventInfo(loadable.loadTaskId,
|
||||||
|
loadable.dataSpec, loadable.getUri(), loadable.getResponseHeaders(),
|
||||||
|
elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
|
||||||
|
final MediaLoadData mediaLoadData = new MediaLoadData(loadable.type);
|
||||||
|
final long retryDelayMs = loadErrorHandlingPolicy.getRetryDelayMsFor(new LoadErrorInfo(
|
||||||
|
loadEventInfo, mediaLoadData, error, errorCount));
|
||||||
|
final boolean isFatal = retryDelayMs == C.TIME_UNSET;
|
||||||
|
eventDispatcher.loadError(loadEventInfo, loadable.type, error, isFatal);
|
||||||
|
if (isFatal) {
|
||||||
|
loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId);
|
||||||
|
}
|
||||||
|
return isFatal ? Loader.DONT_RETRY_FATAL : Loader.createRetryAction(false, retryDelayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods.
|
||||||
|
|
||||||
|
private boolean maybeSelectNewPrimaryUrl() {
|
||||||
|
final List<Variant> variants = masterPlaylist.variants;
|
||||||
|
final int variantsSize = variants.size();
|
||||||
|
final long currentTimeMs = SystemClock.elapsedRealtime();
|
||||||
|
for (int i = 0; i < variantsSize; i++) {
|
||||||
|
final MediaPlaylistBundle bundle = checkNotNull(playlistBundles.get(
|
||||||
|
variants.get(i).url));
|
||||||
|
if (currentTimeMs > bundle.excludeUntilMs) {
|
||||||
|
primaryMediaPlaylistUrl = bundle.playlistUrl;
|
||||||
|
bundle.loadPlaylistInternal(getRequestUriForPrimaryChange(
|
||||||
|
primaryMediaPlaylistUrl));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeSetPrimaryUrl(@NonNull final Uri url) {
|
||||||
|
if (url.equals(primaryMediaPlaylistUrl) || !isVariantUrl(url)
|
||||||
|
|| (primaryMediaPlaylistSnapshot != null
|
||||||
|
&& primaryMediaPlaylistSnapshot.hasEndTag)) {
|
||||||
|
// Ignore if the primary media playlist URL is unchanged, if the media playlist is not
|
||||||
|
// referenced directly by a variant, or it the last primary snapshot contains an end
|
||||||
|
// tag.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
primaryMediaPlaylistUrl = url;
|
||||||
|
final MediaPlaylistBundle newPrimaryBundle = playlistBundles.get(primaryMediaPlaylistUrl);
|
||||||
|
final HlsMediaPlaylist newPrimarySnapshot = newPrimaryBundle.playlistSnapshot;
|
||||||
|
if (newPrimarySnapshot != null && newPrimarySnapshot.hasEndTag) {
|
||||||
|
primaryMediaPlaylistSnapshot = newPrimarySnapshot;
|
||||||
|
primaryPlaylistListener.onPrimaryPlaylistRefreshed(newPrimarySnapshot);
|
||||||
|
} else {
|
||||||
|
// The snapshot for the new primary media playlist URL may be stale. Defer updating the
|
||||||
|
// primary snapshot until after we've refreshed it.
|
||||||
|
newPrimaryBundle.loadPlaylistInternal(getRequestUriForPrimaryChange(url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uri getRequestUriForPrimaryChange(@NonNull final Uri newPrimaryPlaylistUri) {
|
||||||
|
if (primaryMediaPlaylistSnapshot != null
|
||||||
|
&& primaryMediaPlaylistSnapshot.serverControl.canBlockReload) {
|
||||||
|
final RenditionReport renditionReport = primaryMediaPlaylistSnapshot.renditionReports
|
||||||
|
.get(newPrimaryPlaylistUri);
|
||||||
|
if (renditionReport != null) {
|
||||||
|
final Uri.Builder uriBuilder = newPrimaryPlaylistUri.buildUpon();
|
||||||
|
uriBuilder.appendQueryParameter(MediaPlaylistBundle.BLOCK_MSN_PARAM,
|
||||||
|
String.valueOf(renditionReport.lastMediaSequence));
|
||||||
|
if (renditionReport.lastPartIndex != C.INDEX_UNSET) {
|
||||||
|
uriBuilder.appendQueryParameter(MediaPlaylistBundle.BLOCK_PART_PARAM,
|
||||||
|
String.valueOf(renditionReport.lastPartIndex));
|
||||||
|
}
|
||||||
|
return uriBuilder.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newPrimaryPlaylistUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return whether any of the variants in the master playlist have the specified playlist URL.
|
||||||
|
* @param playlistUrl the playlist URL to test
|
||||||
|
*/
|
||||||
|
private boolean isVariantUrl(final Uri playlistUrl) {
|
||||||
|
final List<Variant> variants = masterPlaylist.variants;
|
||||||
|
final int variantsSize = variants.size();
|
||||||
|
for (int i = 0; i < variantsSize; i++) {
|
||||||
|
if (playlistUrl.equals(variants.get(i).url)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createBundles(@NonNull final List<Uri> urls) {
|
||||||
|
final int listSize = urls.size();
|
||||||
|
for (int i = 0; i < listSize; i++) {
|
||||||
|
final Uri url = urls.get(i);
|
||||||
|
final 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(@NonNull final Uri url, final HlsMediaPlaylist newSnapshot) {
|
||||||
|
if (url.equals(primaryMediaPlaylistUrl)) {
|
||||||
|
if (primaryMediaPlaylistSnapshot == null) {
|
||||||
|
// This is the first primary URL snapshot.
|
||||||
|
isLive = !newSnapshot.hasEndTag;
|
||||||
|
initialStartTimeUs = newSnapshot.startTimeUs;
|
||||||
|
}
|
||||||
|
primaryMediaPlaylistSnapshot = newSnapshot;
|
||||||
|
primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);
|
||||||
|
}
|
||||||
|
final int listenersSize = listeners.size();
|
||||||
|
for (int i = 0; i < listenersSize; i++) {
|
||||||
|
listeners.get(i).onPlaylistChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean notifyPlaylistError(final Uri playlistUrl, final long exclusionDurationMs) {
|
||||||
|
final int listenersSize = listeners.size();
|
||||||
|
boolean anyExclusionFailed = false;
|
||||||
|
for (int i = 0; i < listenersSize; i++) {
|
||||||
|
anyExclusionFailed |= !listeners.get(i).onPlaylistError(playlistUrl,
|
||||||
|
exclusionDurationMs);
|
||||||
|
}
|
||||||
|
return anyExclusionFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HlsMediaPlaylist getLatestPlaylistSnapshot(
|
||||||
|
@Nullable final HlsMediaPlaylist oldPlaylist,
|
||||||
|
@NonNull final 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist);
|
||||||
|
final int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist,
|
||||||
|
loadedPlaylist);
|
||||||
|
return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getLoadedPlaylistStartTimeUs(@Nullable final HlsMediaPlaylist oldPlaylist,
|
||||||
|
@NonNull final HlsMediaPlaylist loadedPlaylist) {
|
||||||
|
if (loadedPlaylist.hasProgramDateTime) {
|
||||||
|
return loadedPlaylist.startTimeUs;
|
||||||
|
}
|
||||||
|
final long primarySnapshotStartTimeUs = primaryMediaPlaylistSnapshot != null
|
||||||
|
? primaryMediaPlaylistSnapshot.startTimeUs : 0;
|
||||||
|
if (oldPlaylist == null) {
|
||||||
|
return primarySnapshotStartTimeUs;
|
||||||
|
}
|
||||||
|
final Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist,
|
||||||
|
loadedPlaylist);
|
||||||
|
if (firstOldOverlappingSegment != null) {
|
||||||
|
return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs;
|
||||||
|
} else if (oldPlaylist.segments.size() == 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(
|
||||||
|
@Nullable final HlsMediaPlaylist oldPlaylist,
|
||||||
|
@NonNull final HlsMediaPlaylist loadedPlaylist) {
|
||||||
|
if (loadedPlaylist.hasDiscontinuitySequence) {
|
||||||
|
return loadedPlaylist.discontinuitySequence;
|
||||||
|
}
|
||||||
|
// TODO: Improve cross-playlist discontinuity adjustment.
|
||||||
|
final int primaryUrlDiscontinuitySequence = primaryMediaPlaylistSnapshot != null
|
||||||
|
? primaryMediaPlaylistSnapshot.discontinuitySequence : 0;
|
||||||
|
if (oldPlaylist == null) {
|
||||||
|
return primaryUrlDiscontinuitySequence;
|
||||||
|
}
|
||||||
|
final Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist,
|
||||||
|
loadedPlaylist);
|
||||||
|
if (firstOldOverlappingSegment != null) {
|
||||||
|
return oldPlaylist.discontinuitySequence
|
||||||
|
+ firstOldOverlappingSegment.relativeDiscontinuitySequence
|
||||||
|
- loadedPlaylist.segments.get(0).relativeDiscontinuitySequence;
|
||||||
|
}
|
||||||
|
return primaryUrlDiscontinuitySequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Segment getFirstOldOverlappingSegment(
|
||||||
|
@NonNull final HlsMediaPlaylist oldPlaylist,
|
||||||
|
@NonNull final HlsMediaPlaylist loadedPlaylist) {
|
||||||
|
final int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence
|
||||||
|
- oldPlaylist.mediaSequence);
|
||||||
|
final List<Segment> oldSegments = oldPlaylist.segments;
|
||||||
|
return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hold all information related to a specific Media Playlist.
|
||||||
|
*/
|
||||||
|
private final class MediaPlaylistBundle
|
||||||
|
implements Loader.Callback<ParsingLoadable<HlsPlaylist>> {
|
||||||
|
|
||||||
|
private static final String BLOCK_MSN_PARAM = "_HLS_msn";
|
||||||
|
private static final String BLOCK_PART_PARAM = "_HLS_part";
|
||||||
|
private static final String SKIP_PARAM = "_HLS_skip";
|
||||||
|
|
||||||
|
private final Uri playlistUrl;
|
||||||
|
private final Loader mediaPlaylistLoader;
|
||||||
|
private final DataSource mediaPlaylistDataSource;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private HlsMediaPlaylist playlistSnapshot;
|
||||||
|
private long lastSnapshotLoadMs;
|
||||||
|
private long lastSnapshotChangeMs;
|
||||||
|
private long earliestNextLoadTimeMs;
|
||||||
|
private long excludeUntilMs;
|
||||||
|
private boolean loadPending;
|
||||||
|
@Nullable
|
||||||
|
private IOException playlistError;
|
||||||
|
|
||||||
|
MediaPlaylistBundle(final Uri playlistUrl) {
|
||||||
|
this.playlistUrl = playlistUrl;
|
||||||
|
mediaPlaylistLoader = new Loader("CustomHlsPlaylistTracker:MediaPlaylist");
|
||||||
|
mediaPlaylistDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public HlsMediaPlaylist getPlaylistSnapshot() {
|
||||||
|
return playlistSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSnapshotValid() {
|
||||||
|
if (playlistSnapshot == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final long currentTimeMs = SystemClock.elapsedRealtime();
|
||||||
|
final long snapshotValidityDurationMs = 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 loadPlaylist() {
|
||||||
|
loadPlaylistInternal(playlistUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void maybeThrowPlaylistRefreshError() throws IOException {
|
||||||
|
mediaPlaylistLoader.maybeThrowError();
|
||||||
|
if (playlistError != null) {
|
||||||
|
throw playlistError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release() {
|
||||||
|
mediaPlaylistLoader.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loader.Callback implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCompleted(@NonNull final ParsingLoadable<HlsPlaylist> loadable,
|
||||||
|
final long elapsedRealtimeMs,
|
||||||
|
final long loadDurationMs) {
|
||||||
|
final HlsPlaylist result = loadable.getResult();
|
||||||
|
final LoadEventInfo loadEventInfo = new LoadEventInfo(loadable.loadTaskId,
|
||||||
|
loadable.dataSpec, loadable.getUri(), loadable.getResponseHeaders(),
|
||||||
|
elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
|
||||||
|
if (result instanceof HlsMediaPlaylist) {
|
||||||
|
processLoadedPlaylist((HlsMediaPlaylist) result, loadEventInfo);
|
||||||
|
eventDispatcher.loadCompleted(loadEventInfo, C.DATA_TYPE_MANIFEST);
|
||||||
|
} else {
|
||||||
|
playlistError = new ParserException("Loaded playlist has unexpected type.");
|
||||||
|
eventDispatcher.loadError(
|
||||||
|
loadEventInfo, C.DATA_TYPE_MANIFEST, playlistError, true);
|
||||||
|
}
|
||||||
|
loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCanceled(@NonNull final ParsingLoadable<HlsPlaylist> loadable,
|
||||||
|
final long elapsedRealtimeMs,
|
||||||
|
final long loadDurationMs,
|
||||||
|
final boolean released) {
|
||||||
|
final LoadEventInfo loadEventInfo = new LoadEventInfo(loadable.loadTaskId,
|
||||||
|
loadable.dataSpec, loadable.getUri(), loadable.getResponseHeaders(),
|
||||||
|
elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
|
||||||
|
loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId);
|
||||||
|
eventDispatcher.loadCanceled(loadEventInfo, C.DATA_TYPE_MANIFEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoadErrorAction onLoadError(@NonNull final ParsingLoadable<HlsPlaylist> loadable,
|
||||||
|
final long elapsedRealtimeMs,
|
||||||
|
final long loadDurationMs,
|
||||||
|
final IOException error,
|
||||||
|
final int errorCount) {
|
||||||
|
final LoadEventInfo loadEventInfo = new LoadEventInfo(loadable.loadTaskId,
|
||||||
|
loadable.dataSpec, loadable.getUri(), loadable.getResponseHeaders(),
|
||||||
|
elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
|
||||||
|
final boolean isBlockingRequest = loadable.getUri().getQueryParameter(BLOCK_MSN_PARAM)
|
||||||
|
!= null;
|
||||||
|
final boolean deltaUpdateFailed = error instanceof HlsPlaylistParser
|
||||||
|
.DeltaUpdateException;
|
||||||
|
if (isBlockingRequest || deltaUpdateFailed) {
|
||||||
|
int responseCode = Integer.MAX_VALUE;
|
||||||
|
if (error instanceof HttpDataSource.InvalidResponseCodeException) {
|
||||||
|
responseCode = ((HttpDataSource.InvalidResponseCodeException) error)
|
||||||
|
.responseCode;
|
||||||
|
}
|
||||||
|
if (deltaUpdateFailed || responseCode == 400 || responseCode == 503) {
|
||||||
|
// Intercept failed delta updates and blocking requests producing a Bad Request
|
||||||
|
// (400) and Service Unavailable (503). In such cases, force a full,
|
||||||
|
// non-blocking request (see RFC 8216, section 6.2.5.2 and 6.3.7).
|
||||||
|
earliestNextLoadTimeMs = SystemClock.elapsedRealtime();
|
||||||
|
loadPlaylist();
|
||||||
|
castNonNull(eventDispatcher).loadError(loadEventInfo, loadable.type, error,
|
||||||
|
true);
|
||||||
|
return Loader.DONT_RETRY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final MediaLoadData mediaLoadData = new MediaLoadData(loadable.type);
|
||||||
|
final LoadErrorInfo loadErrorInfo = new LoadErrorInfo(loadEventInfo, mediaLoadData,
|
||||||
|
error, errorCount);
|
||||||
|
final LoadErrorAction loadErrorAction;
|
||||||
|
final long exclusionDurationMs = loadErrorHandlingPolicy.getBlacklistDurationMsFor(
|
||||||
|
loadErrorInfo);
|
||||||
|
final boolean shouldExclude = exclusionDurationMs != C.TIME_UNSET;
|
||||||
|
|
||||||
|
boolean exclusionFailed = notifyPlaylistError(playlistUrl, exclusionDurationMs)
|
||||||
|
|| !shouldExclude;
|
||||||
|
if (shouldExclude) {
|
||||||
|
exclusionFailed |= excludePlaylist(exclusionDurationMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exclusionFailed) {
|
||||||
|
final long retryDelay = loadErrorHandlingPolicy.getRetryDelayMsFor(loadErrorInfo);
|
||||||
|
loadErrorAction = retryDelay != C.TIME_UNSET
|
||||||
|
? Loader.createRetryAction(false, retryDelay)
|
||||||
|
: Loader.DONT_RETRY_FATAL;
|
||||||
|
} else {
|
||||||
|
loadErrorAction = Loader.DONT_RETRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean wasCanceled = !loadErrorAction.isRetry();
|
||||||
|
eventDispatcher.loadError(loadEventInfo, loadable.type, error, wasCanceled);
|
||||||
|
if (wasCanceled) {
|
||||||
|
loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId);
|
||||||
|
}
|
||||||
|
return loadErrorAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods.
|
||||||
|
|
||||||
|
private void loadPlaylistInternal(@NonNull final Uri playlistRequestUri) {
|
||||||
|
excludeUntilMs = 0;
|
||||||
|
if (loadPending || mediaPlaylistLoader.isLoading()
|
||||||
|
|| mediaPlaylistLoader.hasFatalError()) {
|
||||||
|
// Load already pending, in progress, or a fatal error has been encountered. Do
|
||||||
|
// nothing.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final long currentTimeMs = SystemClock.elapsedRealtime();
|
||||||
|
if (currentTimeMs < earliestNextLoadTimeMs) {
|
||||||
|
loadPending = true;
|
||||||
|
playlistRefreshHandler.postDelayed(
|
||||||
|
() -> {
|
||||||
|
loadPending = false;
|
||||||
|
loadPlaylistImmediately(playlistRequestUri);
|
||||||
|
},
|
||||||
|
earliestNextLoadTimeMs - currentTimeMs);
|
||||||
|
} else {
|
||||||
|
loadPlaylistImmediately(playlistRequestUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadPlaylistImmediately(@NonNull final Uri playlistRequestUri) {
|
||||||
|
final ParsingLoadable.Parser<HlsPlaylist> mediaPlaylistParser = playlistParserFactory
|
||||||
|
.createPlaylistParser(masterPlaylist, playlistSnapshot);
|
||||||
|
final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable = new ParsingLoadable<>(
|
||||||
|
mediaPlaylistDataSource, playlistRequestUri, C.DATA_TYPE_MANIFEST,
|
||||||
|
mediaPlaylistParser);
|
||||||
|
final long elapsedRealtime = mediaPlaylistLoader.startLoading(mediaPlaylistLoadable,
|
||||||
|
this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(
|
||||||
|
mediaPlaylistLoadable.type));
|
||||||
|
eventDispatcher.loadStarted(new LoadEventInfo(mediaPlaylistLoadable.loadTaskId,
|
||||||
|
mediaPlaylistLoadable.dataSpec, elapsedRealtime),
|
||||||
|
mediaPlaylistLoadable.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processLoadedPlaylist(final HlsMediaPlaylist loadedPlaylist,
|
||||||
|
final LoadEventInfo loadEventInfo) {
|
||||||
|
final HlsMediaPlaylist oldPlaylist = playlistSnapshot;
|
||||||
|
final 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) {
|
||||||
|
// TODO: Allow customization of playlist resets handling.
|
||||||
|
// The media sequence jumped backwards. The server has probably reset. We do
|
||||||
|
// not try excluding in this case.
|
||||||
|
playlistError = new PlaylistResetException(playlistUrl);
|
||||||
|
notifyPlaylistError(playlistUrl, C.TIME_UNSET);
|
||||||
|
} else if (currentTimeMs - lastSnapshotChangeMs
|
||||||
|
> MAXIMUM_PLAYLIST_STUCK_DURATION_MS) {
|
||||||
|
// TODO: Allow customization of stuck playlists handling.
|
||||||
|
playlistError = new PlaylistStuckException(playlistUrl);
|
||||||
|
final LoadErrorInfo loadErrorInfo = new LoadErrorInfo(loadEventInfo,
|
||||||
|
new MediaLoadData(C.DATA_TYPE_MANIFEST),
|
||||||
|
playlistError, 1);
|
||||||
|
final long exclusionDurationMs = loadErrorHandlingPolicy
|
||||||
|
.getBlacklistDurationMsFor(loadErrorInfo);
|
||||||
|
notifyPlaylistError(playlistUrl, exclusionDurationMs);
|
||||||
|
if (exclusionDurationMs != C.TIME_UNSET) {
|
||||||
|
excludePlaylist(exclusionDurationMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long durationUntilNextLoadUs = 0L;
|
||||||
|
if (!playlistSnapshot.serverControl.canBlockReload) {
|
||||||
|
// If blocking requests are not supported, do not allow the playlist to load again
|
||||||
|
// within the target duration if we obtained a new snapshot, or half the target
|
||||||
|
// duration otherwise.
|
||||||
|
durationUntilNextLoadUs = playlistSnapshot != oldPlaylist
|
||||||
|
? playlistSnapshot.targetDurationUs
|
||||||
|
: (playlistSnapshot.targetDurationUs / 2);
|
||||||
|
}
|
||||||
|
earliestNextLoadTimeMs = currentTimeMs + C.usToMs(durationUntilNextLoadUs);
|
||||||
|
// Schedule a load if this is the primary playlist or a playlist of a low-latency
|
||||||
|
// stream 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.
|
||||||
|
final boolean scheduleLoad = playlistSnapshot.partTargetDurationUs != C.TIME_UNSET
|
||||||
|
|| playlistUrl.equals(primaryMediaPlaylistUrl);
|
||||||
|
if (scheduleLoad && !playlistSnapshot.hasEndTag) {
|
||||||
|
loadPlaylistInternal(getMediaPlaylistUriForReload());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uri getMediaPlaylistUriForReload() {
|
||||||
|
if (playlistSnapshot == null
|
||||||
|
|| (playlistSnapshot.serverControl.skipUntilUs == C.TIME_UNSET
|
||||||
|
&& !playlistSnapshot.serverControl.canBlockReload)) {
|
||||||
|
return playlistUrl;
|
||||||
|
}
|
||||||
|
final Uri.Builder uriBuilder = playlistUrl.buildUpon();
|
||||||
|
if (playlistSnapshot.serverControl.canBlockReload) {
|
||||||
|
final long targetMediaSequence = playlistSnapshot.mediaSequence
|
||||||
|
+ playlistSnapshot.segments.size();
|
||||||
|
uriBuilder.appendQueryParameter(BLOCK_MSN_PARAM, String.valueOf(
|
||||||
|
targetMediaSequence));
|
||||||
|
if (playlistSnapshot.partTargetDurationUs != C.TIME_UNSET) {
|
||||||
|
final List<Part> trailingParts = playlistSnapshot.trailingParts;
|
||||||
|
int targetPartIndex = trailingParts.size();
|
||||||
|
if (!trailingParts.isEmpty() && Iterables.getLast(trailingParts).isPreload) {
|
||||||
|
// Ignore the preload part.
|
||||||
|
targetPartIndex--;
|
||||||
|
}
|
||||||
|
uriBuilder.appendQueryParameter(BLOCK_PART_PARAM, String.valueOf(
|
||||||
|
targetPartIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (playlistSnapshot.serverControl.skipUntilUs != C.TIME_UNSET) {
|
||||||
|
uriBuilder.appendQueryParameter(SKIP_PARAM,
|
||||||
|
playlistSnapshot.serverControl.canSkipDateRanges ? "v2" : "YES");
|
||||||
|
}
|
||||||
|
return uriBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude the playlist.
|
||||||
|
*
|
||||||
|
* @param exclusionDurationMs The number of milliseconds for which the playlist should be
|
||||||
|
* excluded.
|
||||||
|
* @return Whether the playlist is the primary, despite being excluded.
|
||||||
|
*/
|
||||||
|
private boolean excludePlaylist(final long exclusionDurationMs) {
|
||||||
|
excludeUntilMs = SystemClock.elapsedRealtime() + exclusionDurationMs;
|
||||||
|
return playlistUrl.equals(primaryMediaPlaylistUrl) && !maybeSelectNewPrimaryUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue