From e103e4817c16e12141a548aed5dfa7dda5ac6157 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Thu, 20 Jan 2022 17:07:06 +0100
Subject: [PATCH] Apply suggested changes and remove the
CustomHlsPlaylistTracker class
---
.../org/schabi/newpipe/player/Player.java | 23 +-
.../player/helper/PlayerDataSource.java | 14 +-
.../playback/CustomHlsPlaylistTracker.java | 784 ------------------
3 files changed, 22 insertions(+), 799 deletions(-)
delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/CustomHlsPlaylistTracker.java
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 179486bb1..5bf239a86 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -2517,29 +2517,30 @@ public final class Player implements
Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error);
saveStreamProgressState();
- boolean isBehindLiveWindowException = false;
+ boolean isCatchableException = false;
switch (error.type) {
case ExoPlaybackException.TYPE_SOURCE:
- isBehindLiveWindowException = processSourceError(error.getSourceException());
- if (!isBehindLiveWindowException) {
- createErrorNotification(error);
- }
+ isCatchableException = processSourceError(error.getSourceException());
break;
case ExoPlaybackException.TYPE_UNEXPECTED:
- createErrorNotification(error);
setRecovery();
reloadPlayQueueManager();
break;
case ExoPlaybackException.TYPE_REMOTE:
case ExoPlaybackException.TYPE_RENDERER:
default:
- createErrorNotification(error);
onPlaybackShutdown();
break;
}
- if (fragmentListener != null && !isBehindLiveWindowException) {
+ if (isCatchableException) {
+ return;
+ }
+
+ createErrorNotification(error);
+
+ if (fragmentListener != null) {
fragmentListener.onPlayerError(error);
}
}
@@ -2583,10 +2584,10 @@ public final class Player implements
// Inform the user that we are reloading the stream by switching to the buffering state
onBuffering();
return true;
- } else {
- playQueue.error();
- return false;
}
+
+ playQueue.error();
+ return false;
}
//endregion
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
index 1fce25e78..c898c6ff5 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
@@ -9,6 +9,7 @@ import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
@@ -16,12 +17,13 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
-import org.schabi.newpipe.player.playback.CustomHlsPlaylistTracker;
-
public class PlayerDataSource {
+
+ public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
+
+ private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15;
private static final int MANIFEST_MINIMUM_RETRY = 5;
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
- public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
private final DataSource.Factory cacheDataSourceFactory;
private final DataSource.Factory cachelessDataSourceFactory;
@@ -48,7 +50,11 @@ public class PlayerDataSource {
.setAllowChunklessPreparation(true)
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(
MANIFEST_MINIMUM_RETRY))
- .setPlaylistTrackerFactory(CustomHlsPlaylistTracker.FACTORY);
+ .setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy,
+ playlistParserFactory) ->
+ new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy,
+ playlistParserFactory, PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT)
+ );
}
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/CustomHlsPlaylistTracker.java b/app/src/main/java/org/schabi/newpipe/player/playback/CustomHlsPlaylistTracker.java
deleted file mode 100644
index 99d6bfa07..000000000
--- a/app/src/main/java/org/schabi/newpipe/player/playback/CustomHlsPlaylistTracker.java
+++ /dev/null
@@ -1,784 +0,0 @@
-/*
- * 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}.
- *
- *
- * 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.
- *
- */
-public final class CustomHlsPlaylistTracker implements HlsPlaylistTracker,
- Loader.Callback> {
-
- /**
- * 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 playlistBundles;
- private final List 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 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 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 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 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 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 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 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;
- }
-
- @SuppressWarnings("squid:S2259")
- 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 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> {
-
- 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 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 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 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 mediaPlaylistParser = playlistParserFactory
- .createPlaylistParser(masterPlaylist, playlistSnapshot);
- final ParsingLoadable 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);
- }
-
- @SuppressWarnings("squid:S2259")
- 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 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();
- }
- }
-}