From d625af67db3205447f91ceba946aa759dbf3c228 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 31 Jul 2020 11:12:35 +0100 Subject: [PATCH] Add load cancelation support to DASH and SS Issue: #7244 added this feature to HLS. This change is the exact copy in ChunkSampleStream to add the same support to the other adaptive formats. Note that ChunkSampleStream doesn't support slicing, so we can't cancel a read-from chunk, and we need to prevent reading into an already canceled chunk load so that the chunk can be automatically discarded after the cancelation. Issue: #2848 PiperOrigin-RevId: 324179972 --- RELEASENOTES.md | 2 + .../source/chunk/ChunkSampleStream.java | 101 +++++++++++++++--- .../exoplayer2/source/chunk/ChunkSource.java | 13 ++- .../source/dash/DefaultDashChunkSource.java | 9 ++ .../source/hls/HlsSampleStreamWrapper.java | 2 + .../smoothstreaming/DefaultSsChunkSource.java | 9 ++ .../exoplayer2/testutil/FakeChunkSource.java | 6 ++ 7 files changed, 125 insertions(+), 17 deletions(-) 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,