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(); - } - } -}