diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cfd8d11d663..310d013d431 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -200,6 +200,8 @@ decoders. * DASH: * Enable support for embedded CEA-708. + * Add support for load cancelation when discarding upstream + ([#2848](https://github.com/google/ExoPlayer/issues/2848)). * HLS: * Add support for upstream discard including cancelation of ongoing load ([#6322](https://github.com/google/ExoPlayer/issues/6322)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index e1ce17b396c..1a451cb0c34 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static java.lang.Math.max; import static java.lang.Math.min; @@ -85,11 +86,13 @@ public interface ReleaseCallback { private final SampleQueue[] embeddedSampleQueues; private final BaseMediaChunkOutput chunkOutput; + @Nullable private Chunk loadingChunk; private @MonotonicNonNull Format primaryDownstreamTrackFormat; @Nullable private ReleaseCallback releaseCallback; private long pendingResetPositionUs; private long lastSeekPositionUs; private int nextNotifyPrimaryFormatMediaChunkIndex; + @Nullable private BaseMediaChunk canceledMediaChunk; /* package */ boolean loadingFinished; @@ -144,7 +147,7 @@ public ChunkSampleStream( primarySampleQueue = new SampleQueue( allocator, - /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + /* playbackLooper= */ checkNotNull(Looper.myLooper()), drmSessionManager, drmEventDispatcher); trackTypes[0] = primaryTrackType; @@ -154,7 +157,7 @@ public ChunkSampleStream( SampleQueue sampleQueue = new SampleQueue( allocator, - /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + /* playbackLooper= */ checkNotNull(Looper.myLooper()), DrmSessionManager.getDummyDrmSessionManager(), drmEventDispatcher); embeddedSampleQueues[i] = sampleQueue; @@ -315,10 +318,7 @@ public void seekToUs(long positionUs) { loader.cancelLoading(); } else { loader.clearFatalError(); - primarySampleQueue.reset(); - for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.reset(); - } + resetSampleQueues(); } } } @@ -386,6 +386,13 @@ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, if (isPendingReset()) { return C.RESULT_NOTHING_READ; } + if (canceledMediaChunk != null + && canceledMediaChunk.getFirstSampleIndex(/* trackIndex= */ 0) + <= primarySampleQueue.getReadIndex()) { + // Don't read into chunk that's going to be discarded. + // TODO: Support splicing to allow this. See [internal b/161130873]. + return C.RESULT_NOTHING_READ; + } maybeNotifyPrimaryTrackFormatChanged(); return primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished); @@ -397,6 +404,14 @@ public int skipData(long positionUs) { return 0; } int skipCount = primarySampleQueue.getSkipCount(positionUs, loadingFinished); + if (canceledMediaChunk != null) { + // Don't skip into chunk that's going to be discarded. + // TODO: Support splicing to allow this. See [internal b/161130873]. + int maxSkipCount = + canceledMediaChunk.getFirstSampleIndex(/* trackIndex= */ 0) + - primarySampleQueue.getReadIndex(); + skipCount = min(skipCount, maxSkipCount); + } primarySampleQueue.skip(skipCount); maybeNotifyPrimaryTrackFormatChanged(); return skipCount; @@ -406,6 +421,7 @@ public int skipData(long positionUs) { @Override public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { + loadingChunk = null; chunkSource.onChunkLoadCompleted(loadable); LoadEventInfo loadEventInfo = new LoadEventInfo( @@ -432,6 +448,8 @@ public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDur @Override public void onLoadCanceled( Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { + loadingChunk = null; + canceledMediaChunk = null; LoadEventInfo loadEventInfo = new LoadEventInfo( loadable.loadTaskId, @@ -452,9 +470,14 @@ public void onLoadCanceled( loadable.startTimeUs, loadable.endTimeUs); if (!released) { - primarySampleQueue.reset(); - for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.reset(); + if (isPendingReset()) { + resetSampleQueues(); + } else if (isMediaChunk(loadable)) { + // TODO: Support splicing to keep data from canceled chunk. See [internal b/161130873]. + discardUpstreamMediaChunksFromIndex(mediaChunks.size() - 1); + if (mediaChunks.isEmpty()) { + pendingResetPositionUs = lastSeekPositionUs; + } } callback.onContinueLoadingRequested(this); } @@ -535,6 +558,7 @@ public LoadErrorAction onLoadError( error, canceled); if (canceled) { + loadingChunk = null; loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); callback.onContinueLoadingRequested(this); } @@ -574,6 +598,7 @@ public boolean continueLoading(long positionUs) { return false; } + loadingChunk = loadable; if (isMediaChunk(loadable)) { BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable; if (pendingReset) { @@ -625,19 +650,41 @@ public long getNextLoadPositionUs() { @Override public void reevaluateBuffer(long positionUs) { - if (loader.isLoading() || loader.hasFatalError() || isPendingReset()) { + if (loader.hasFatalError() || isPendingReset()) { return; } - int currentQueueSize = mediaChunks.size(); - int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); - if (currentQueueSize <= preferredQueueSize) { + if (loader.isLoading()) { + Chunk loadingChunk = checkNotNull(this.loadingChunk); + if (isMediaChunk(loadingChunk) + && haveReadFromMediaChunk(/* mediaChunkIndex= */ mediaChunks.size() - 1)) { + // Can't cancel anymore because the renderers have read from this chunk. + return; + } + if (chunkSource.shouldCancelLoad(positionUs, loadingChunk, readOnlyMediaChunks)) { + loader.cancelLoading(); + if (isMediaChunk(loadingChunk)) { + canceledMediaChunk = (BaseMediaChunk) loadingChunk; + } + } return; } + int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); + if (preferredQueueSize < mediaChunks.size()) { + discardUpstream(preferredQueueSize); + } + } + + private void discardUpstream(int preferredQueueSize) { + Assertions.checkState(!loader.isLoading()); + + int currentQueueSize = mediaChunks.size(); int newQueueSize = C.LENGTH_UNSET; for (int i = preferredQueueSize; i < currentQueueSize; i++) { if (!haveReadFromMediaChunk(i)) { + // TODO: Sparse tracks (e.g. ESMG) may prevent discarding in almost all cases because it + // means that most chunks have been read from already. See [internal b/161126666]. newQueueSize = i; break; } @@ -656,12 +703,17 @@ public void reevaluateBuffer(long positionUs) { primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs); } - // Internal methods - private boolean isMediaChunk(Chunk chunk) { return chunk instanceof BaseMediaChunk; } + private void resetSampleQueues() { + primarySampleQueue.reset(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(); + } + } + /** Returns whether samples have been read from media chunk at given index. */ private boolean haveReadFromMediaChunk(int mediaChunkIndex) { BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex); @@ -788,9 +840,19 @@ public int skipData(long positionUs) { if (isPendingReset()) { return 0; } - maybeNotifyDownstreamFormat(); int skipCount = sampleQueue.getSkipCount(positionUs, loadingFinished); + if (canceledMediaChunk != null) { + // Don't skip into chunk that's going to be discarded. + // TODO: Support splicing to allow this. See [internal b/161130873]. + int maxSkipCount = + canceledMediaChunk.getFirstSampleIndex(/* trackIndex= */ 1 + index) + - sampleQueue.getReadIndex(); + skipCount = min(skipCount, maxSkipCount); + } sampleQueue.skip(skipCount); + if (skipCount > 0) { + maybeNotifyDownstreamFormat(); + } return skipCount; } @@ -805,6 +867,13 @@ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, if (isPendingReset()) { return C.RESULT_NOTHING_READ; } + if (canceledMediaChunk != null + && canceledMediaChunk.getFirstSampleIndex(/* trackIndex= */ 1 + index) + <= sampleQueue.getReadIndex()) { + // Don't read into chunk that's going to be discarded. + // TODO: Support splicing to allow this. See [internal b/161130873]. + return C.RESULT_NOTHING_READ; + } maybeNotifyDownstreamFormat(); return sampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished); } 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 32ac6fee7aa..52756b378f0 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 @@ -52,12 +52,23 @@ public interface ChunkSource { * *

Will only be called if no {@link MediaChunk} in the queue is currently loading. * - * @param playbackPositionUs The current playback position. + * @param playbackPositionUs The current playback position, in microseconds. * @param queue The queue of buffered {@link MediaChunk}s. * @return The preferred queue size. */ int getPreferredQueueSize(long playbackPositionUs, List 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. + */ + boolean shouldCancelLoad( + long playbackPositionUs, Chunk loadingChunk, List queue); + /** * Returns the next chunk to load. * diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 5e61cd1b34e..01e51c3f6ce 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -247,6 +247,15 @@ public int getPreferredQueueSize(long playbackPositionUs, List queue) { + if (fatalError != null) { + return false; + } + return trackSelection.shouldCancelChunkLoad(playbackPositionUs, loadingChunk, queue); + } + @Override public void getNextChunk( long playbackPositionUs, 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 bedc9cefb88..18f9ac06371 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 @@ -1133,6 +1133,8 @@ private boolean canDiscardUpstreamMediaChunksFromIndex(int mediaChunkIndex) { int discardFromIndex = mediaChunk.getFirstSampleIndex(/* sampleQueueIndex= */ i); if (sampleQueues[i].getReadIndex() > discardFromIndex) { // Discarding not possible because we already read from the chunk. + // TODO: Sparse tracks (e.g. ID3) may prevent discarding in almost all cases because it + // means that most chunks have been read from already. See [internal b/161126666]. return false; } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index 3760a5337de..868cea7fd0c 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -186,6 +186,15 @@ public int getPreferredQueueSize(long playbackPositionUs, List queue) { + if (fatalError != null) { + return false; + } + return trackSelection.shouldCancelChunkLoad(playbackPositionUs, loadingChunk, queue); + } + @Override public final void getNextChunk( long playbackPositionUs, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index c703cf0bc32..3e25a13d9cf 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -102,6 +102,12 @@ public int getPreferredQueueSize(long playbackPositionUs, List queue) { + return trackSelection.shouldCancelChunkLoad(playbackPositionUs, loadingChunk, queue); + } + @Override public void getNextChunk( long playbackPositionUs,