From 651b79d3ed6372715fde1b9318e5ec18061541a7 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 15 Jan 2022 13:42:48 +0100 Subject: [PATCH 1/5] Catch properly BehindLiveWindowExceptions Instead of trying to reload the play queue manager and then throwing an error, BehindLiveWindowExceptions now make the app seek to the default playback position, like recommended by ExoPlayer. The buffering state is shown in this case. Error handling of other exceptions is not changed. --- .../org/schabi/newpipe/player/Player.java | 71 +++++++++++++------ 1 file changed, 48 insertions(+), 23 deletions(-) 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 993357ac4..179486bb1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -2517,8 +2517,34 @@ public final class Player implements Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error); saveStreamProgressState(); + boolean isBehindLiveWindowException = false; - // create error notification + switch (error.type) { + case ExoPlaybackException.TYPE_SOURCE: + isBehindLiveWindowException = processSourceError(error.getSourceException()); + if (!isBehindLiveWindowException) { + createErrorNotification(error); + } + 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) { + fragmentListener.onPlayerError(error); + } + } + + private void createErrorNotification(@NonNull final ExoPlaybackException error) { final ErrorInfo errorInfo; if (currentMetadata == null) { errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM, @@ -2530,37 +2556,36 @@ public final class Player implements currentMetadata.getMetadata()); } ErrorUtil.createNotification(context, errorInfo); - - switch (error.type) { - case ExoPlaybackException.TYPE_SOURCE: - processSourceError(error.getSourceException()); - break; - case ExoPlaybackException.TYPE_UNEXPECTED: - setRecovery(); - reloadPlayQueueManager(); - break; - case ExoPlaybackException.TYPE_REMOTE: - case ExoPlaybackException.TYPE_RENDERER: - default: - onPlaybackShutdown(); - break; - } - - if (fragmentListener != null) { - fragmentListener.onPlayerError(error); - } } - private void processSourceError(final IOException error) { + /** + * Process an {@link IOException} returned by {@link ExoPlaybackException#getSourceException()} + * for {@link ExoPlaybackException#TYPE_SOURCE} exceptions. + * + *

+ * This method sets the recovery position and sends an error message to the play queue if the + * exception is not a {@link BehindLiveWindowException}. + *

+ * @param error the source error which was thrown by ExoPlayer + * @return whether the exception thrown is a {@link BehindLiveWindowException} ({@code false} + * is always returned if ExoPlayer or the play queue is null) + */ + private boolean processSourceError(final IOException error) { if (exoPlayerIsNull() || playQueue == null) { - return; + return false; } + setRecovery(); if (error instanceof BehindLiveWindowException) { - reloadPlayQueueManager(); + simpleExoPlayer.seekToDefaultPosition(); + simpleExoPlayer.prepare(); + // Inform the user that we are reloading the stream by switching to the buffering state + onBuffering(); + return true; } else { playQueue.error(); + return false; } } //endregion From 94f774b82d1659ae5ef07b6c0a0789d310d2e779 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 15 Jan 2022 13:50:35 +0100 Subject: [PATCH 2/5] 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. --- .../player/helper/PlayerDataSource.java | 7 +- .../playback/CustomHlsPlaylistTracker.java | 782 ++++++++++++++++++ 2 files changed, 787 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/CustomHlsPlaylistTracker.java 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 708b72ff2..1fce25e78 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 @@ -16,6 +16,8 @@ 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 { private static final int MANIFEST_MINIMUM_RETRY = 5; private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE; @@ -44,8 +46,9 @@ public class PlayerDataSource { public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() { return new HlsMediaSource.Factory(cachelessDataSourceFactory) .setAllowChunklessPreparation(true) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); + .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy( + MANIFEST_MINIMUM_RETRY)) + .setPlaylistTrackerFactory(CustomHlsPlaylistTracker.FACTORY); } 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 new file mode 100644 index 000000000..28f6b01fe --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/playback/CustomHlsPlaylistTracker.java @@ -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}. + * + *

+ * 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; + } + + 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); + } + + 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(); + } + } +} From d0637a883236349f087ab11a1001729a123b1ca3 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 15 Jan 2022 21:03:37 +0100 Subject: [PATCH 3/5] Suppress SonarLint NullPointerException warnings in CustomHlsPlaylistTracker They seem to be wrong, by looking at the class work and at the return of CustomHlsPlaylistTracker's methods. --- .../newpipe/player/playback/CustomHlsPlaylistTracker.java | 2 ++ 1 file changed, 2 insertions(+) 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 index 28f6b01fe..99d6bfa07 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/CustomHlsPlaylistTracker.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/CustomHlsPlaylistTracker.java @@ -411,6 +411,7 @@ public final class CustomHlsPlaylistTracker implements HlsPlaylistTracker, return anyExclusionFailed; } + @SuppressWarnings("squid:S2259") private HlsMediaPlaylist getLatestPlaylistSnapshot( @Nullable final HlsMediaPlaylist oldPlaylist, @NonNull final HlsMediaPlaylist loadedPlaylist) { @@ -684,6 +685,7 @@ public final class CustomHlsPlaylistTracker implements HlsPlaylistTracker, mediaPlaylistLoadable.type); } + @SuppressWarnings("squid:S2259") private void processLoadedPlaylist(final HlsMediaPlaylist loadedPlaylist, final LoadEventInfo loadEventInfo) { final HlsMediaPlaylist oldPlaylist = playlistSnapshot; 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 4/5] 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(); - } - } -} From 52cc4a0a05e58177e83362ff38db6b728a33ff06 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sun, 30 Jan 2022 20:41:08 +0100 Subject: [PATCH 5/5] Add JavaDoc for PlayerDataSource.PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT --- .../org/schabi/newpipe/player/helper/PlayerDataSource.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 c898c6ff5..a2f0d7149 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 @@ -21,6 +21,12 @@ public class PlayerDataSource { public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000; + /** + * An approximately 4.3 times greater value than the + * {@link DefaultHlsPlaylistTracker#DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT default} + * to ensure that (very) low latency livestreams which got stuck for a moment don't crash too + * early. + */ 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;