From 95b61eb835e884678daef6e938c4bb0175489c2d Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 10 Jun 2020 11:09:30 +0100 Subject: [PATCH] Split TrackSelection.evalauteQueueSize in discard and cancelation. The option to cancel ongoing loads as part of the queue size evalation was added recently. This split out the decision to a new method so that a TrackSelection implementation can independently cancel loads and discard upstream data. It also clarifies that evaluateQueueSize will only be called if there is no ongoing load. Issue: #2848 PiperOrigin-RevId: 315659735 --- RELEASENOTES.md | 3 + .../exoplayer2/source/MediaPeriod.java | 4 +- .../exoplayer2/source/SequenceableLoader.java | 4 +- .../exoplayer2/source/chunk/ChunkSource.java | 16 ++-- .../trackselection/TrackSelection.java | 78 +++++++++++++++---- .../exoplayer2/source/hls/HlsChunkSource.java | 18 +++++ .../source/hls/HlsSampleStreamWrapper.java | 21 +++-- 7 files changed, 106 insertions(+), 38 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0286ccec23..ce94193ffd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -88,6 +88,9 @@ ([#7332](https://github.com/google/ExoPlayer/issues/7332)). * Add `HttpDataSource.InvalidResponseCodeException#responseBody` field ([#6853](https://github.com/google/ExoPlayer/issues/6853)). + * Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an + ongoing load should be canceled. Only supported by HLS streams so far. + ([#2848](https://github.com/google/ExoPlayer/issues/2848)). * Video: Pass frame rate hint to `Surface.setFrameRate` on Android R devices. * Text: * Parse `` and `` tags in WebVTT subtitles (rendering is coming diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 2e2cf9caba..39b207e264 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -239,8 +239,8 @@ public interface MediaPeriod extends SequenceableLoader { * *

This method is only called after the period has been prepared. * - *

A period may choose to discard buffered media so that it can be re-buffered in a different - * quality. + *

A period may choose to discard buffered media or cancel ongoing loads so that media can be + * re-buffered in a different quality. * * @param positionUs The current playback position in microseconds. If playback of this period has * not yet started, the value will be the starting position in this period minus the duration diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java index 189c13ef0f..fb6af1136a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java @@ -66,8 +66,8 @@ public interface SequenceableLoader { /** * Re-evaluates the buffer given the playback position. * - *

Re-evaluation may discard buffered media so that it can be re-buffered in a different - * quality. + *

Re-evaluation may discard buffered media or cancel ongoing loads so that media can be + * re-buffered in a different quality. * * @param positionUs The current playback position in microseconds. If playback of this period has * not yet started, the value will be the starting position in this period minus the duration diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java index b119cad5b0..f32f5debfe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java @@ -38,8 +38,6 @@ public interface ChunkSource { /** * If the source is currently having difficulty providing chunks, then this method throws the * underlying error. Otherwise does nothing. - *

- * This method should only be called after the source has been prepared. * * @throws IOException The underlying error. */ @@ -47,10 +45,12 @@ public interface ChunkSource { /** * Evaluates whether {@link MediaChunk}s should be removed from the back of the queue. - *

- * Removing {@link MediaChunk}s from the back of the queue can be useful if they could be replaced - * with chunks of a significantly higher quality (e.g. because the available bandwidth has - * substantially increased). + * + *

Removing {@link MediaChunk}s from the back of the queue can be useful if they could be + * replaced with chunks of a significantly higher quality (e.g. because the available bandwidth + * has substantially increased). + * + *

Will only be called if no {@link MediaChunk} in the queue is currently loading. * * @param playbackPositionUs The current playback position. * @param queue The queue of buffered {@link MediaChunk}s. @@ -85,8 +85,6 @@ public interface ChunkSource { * Called when the {@link ChunkSampleStream} has finished loading a chunk obtained from this * source. * - *

This method should only be called when the source is enabled. - * * @param chunk The chunk whose load has been completed. */ void onChunkLoadCompleted(Chunk chunk); @@ -95,8 +93,6 @@ public interface ChunkSource { * Called when the {@link ChunkSampleStream} encounters an error loading a chunk obtained from * this source. * - *

This method should only be called when the source is enabled. - * * @param chunk The chunk whose load encountered the error. * @param cancelable Whether the load can be canceled. * @param e The error. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index ad1a6ef1f2..1b92a37f54 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -93,8 +94,8 @@ public interface TrackSelection { /** * Enables the track selection. Dynamic changes via {@link #updateSelectedTrack(long, long, long, - * List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)} will only happen after - * this call. + * List, MediaChunkIterator[])}, {@link #evaluateQueueSize(long, List)} or {@link + * #shouldCancelChunkLoad(long, Chunk, List)} will only happen after this call. * *

This method may not be called when the track selection is already enabled. */ @@ -102,8 +103,8 @@ public interface TrackSelection { /** * Disables this track selection. No further dynamic changes via {@link #updateSelectedTrack(long, - * long, long, List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)} will happen - * after this call. + * long, long, List, MediaChunkIterator[])}, {@link #evaluateQueueSize(long, List)} or {@link + * #shouldCancelChunkLoad(long, Chunk, List)} will happen after this call. * *

This method may only be called when the track selection is already enabled. */ @@ -202,7 +203,7 @@ public interface TrackSelection { /** * Updates the selected track for sources that load media in discrete {@link MediaChunk}s. * - *

This method may only be called when the selection is enabled. + *

This method will only be called when the selection is enabled. * * @param playbackPositionUs The current playback position in microseconds. If playback of the * period to which this track selection belongs has not yet started, the value will be the @@ -231,34 +232,77 @@ public interface TrackSelection { MediaChunkIterator[] mediaChunkIterators); /** - * May be called periodically by sources that load media in discrete {@link MediaChunk}s and - * support discarding of buffered chunks in order to re-buffer using a different selected track. * Returns the number of chunks that should be retained in the queue. - *

- * To avoid excessive re-buffering, implementations should normally return the size of the queue. - * An example of a case where a smaller value may be returned is if network conditions have + * + *

May be called by sources that load media in discrete {@link MediaChunk MediaChunks} and + * support discarding of buffered chunks. + * + *

To avoid excessive re-buffering, implementations should normally return the size of the + * queue. An example of a case where a smaller value may be returned is if network conditions have * improved dramatically, allowing chunks to be discarded and re-buffered in a track of * significantly higher quality. Discarding chunks may allow faster switching to a higher quality - * track in this case. This method may only be called when the selection is enabled. + * track in this case. + * + *

Note that even if the source supports discarding of buffered chunks, the actual number of + * discarded chunks is not guaranteed. The source will call {@link #updateSelectedTrack(long, + * long, long, List, MediaChunkIterator[])} with the updated queue of chunks before loading a new + * chunk to allow switching to another quality. + * + *

This method will only be called when the selection is enabled and none of the {@link + * MediaChunk MediaChunks} in the queue are currently loading. * * @param playbackPositionUs The current playback position in microseconds. If playback of the * period to which this track selection belongs has not yet started, the value will be the * starting position in the period minus the duration of any media in previous periods still * to be played. - * @param queue The queue of buffered {@link MediaChunk}s. Must not be modified. + * @param queue The queue of buffered {@link MediaChunk MediaChunks}. Must not be modified. * @return The number of chunks to retain in the queue. */ int evaluateQueueSize(long playbackPositionUs, List queue); + /** + * Returns whether an ongoing load of a chunk should be canceled. + * + *

May be called by sources that load media in discrete {@link MediaChunk MediaChunks} and + * support canceling the ongoing chunk load. The ongoing chunk load is either the last {@link + * MediaChunk} in the queue or another type of {@link Chunk}, for example, if the source loads + * initialization or encryption data. + * + *

To avoid excessive re-buffering, implementations should normally return {@code false}. An + * example where {@code true} might be returned is if a load of a high quality chunk gets stuck + * and canceling this load in favor of a lower quality alternative may avoid a rebuffer. + * + *

The source will call {@link #evaluateQueueSize(long, List)} after the cancelation finishes + * to allow discarding of chunks, and {@link #updateSelectedTrack(long, long, long, List, + * MediaChunkIterator[])} before loading a new chunk to allow switching to another quality. + * + *

This method will only be called when the selection is enabled. + * + * @param playbackPositionUs The current playback position in microseconds. If playback of the + * period to which this track selection belongs has not yet started, the value will be the + * starting position in the period minus the duration of any media in previous periods still + * to be played. + * @param loadingChunk The currently loading {@link Chunk} that will be canceled if this method + * returns {@code true}. + * @param queue The queue of buffered {@link MediaChunk MediaChunks}, including the {@code + * loadingChunk} if it's a {@link MediaChunk}. Must not be modified. + * @return Whether the ongoing load of {@code loadingChunk} should be canceled. + */ + default boolean shouldCancelChunkLoad( + long playbackPositionUs, Chunk loadingChunk, List queue) { + return false; + } + /** * Attempts to blacklist the track at the specified index in the selection, making it ineligible * for selection by calls to {@link #updateSelectedTrack(long, long, long, List, - * MediaChunkIterator[])} for the specified period of time. Blacklisting will fail if all other - * tracks are currently blacklisted. If blacklisting the currently selected track, note that it - * will remain selected until the next call to {@link #updateSelectedTrack(long, long, long, List, - * MediaChunkIterator[])}. + * MediaChunkIterator[])} for the specified period of time. * - *

This method may only be called when the selection is enabled. + *

Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the + * currently selected track, note that it will remain selected until the next call to {@link + * #updateSelectedTrack(long, long, long, List, MediaChunkIterator[])}. + * + *

This method will only be called when the selection is enabled. * * @param index The index of the track in the selection. * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 21d1bc4d6b..8f88160ec3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -458,6 +458,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * could be replaced with chunks of a significantly higher quality (e.g. because the available * bandwidth has substantially increased). * + *

Will only be called if no {@link MediaChunk} in the queue is currently loading. + * * @param playbackPositionUs The current playback position, in microseconds. * @param queue The queue of buffered {@link MediaChunk MediaChunks}. * @return The preferred queue size. @@ -469,6 +471,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return trackSelection.evaluateQueueSize(playbackPositionUs, queue); } + /** + * Returns whether an ongoing load of a chunk should be canceled. + * + * @param playbackPositionUs The current playback position, in microseconds. + * @param loadingChunk The currently loading {@link Chunk}. + * @param queue The queue of buffered {@link MediaChunk MediaChunks}. + * @return Whether the ongoing load of {@code loadingChunk} should be canceled. + */ + public boolean shouldCancelLoad( + long playbackPositionUs, Chunk loadingChunk, List queue) { + if (fatalError != null) { + return false; + } + return trackSelection.shouldCancelChunkLoad(playbackPositionUs, loadingChunk, queue); + } + // Private methods. /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 28aba78558..979b24f939 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -133,6 +133,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final ArrayList hlsSampleStreams; private final Map overridingDrmInitData; + @Nullable private Chunk loadingChunk; private HlsSampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; private Set sampleQueueMappingDoneByType; @@ -674,6 +675,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; if (isMediaChunk(loadable)) { initMediaChunkLoad((HlsMediaChunk) loadable); } + loadingChunk = loadable; long elapsedRealtimeMs = loader.startLoading( loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); @@ -700,14 +702,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return; } - int currentQueueSize = mediaChunks.size(); - int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); - if (currentQueueSize <= preferredQueueSize) { + if (loader.isLoading()) { + Assertions.checkNotNull(loadingChunk); + if (chunkSource.shouldCancelLoad(positionUs, loadingChunk, readOnlyMediaChunks)) { + loader.cancelLoading(); + } return; } - if (loader.isLoading()) { - loader.cancelLoading(); - } else { + + int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); + if (preferredQueueSize < mediaChunks.size()) { discardUpstream(preferredQueueSize); } } @@ -716,6 +720,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { + loadingChunk = null; chunkSource.onChunkLoadCompleted(loadable); LoadEventInfo loadEventInfo = new LoadEventInfo( @@ -746,6 +751,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public void onLoadCanceled( Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { + loadingChunk = null; LoadEventInfo loadEventInfo = new LoadEventInfo( loadable.loadTaskId, @@ -841,6 +847,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; error, wasCanceled); if (wasCanceled) { + loadingChunk = null; loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); } @@ -885,7 +892,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; if (newQueueSize == C.LENGTH_UNSET) { return; } - + long endTimeUs = getLastMediaChunk().endTimeUs; HlsMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(newQueueSize); if (mediaChunks.isEmpty()) {