From 57d516016189e68ef80063a61e08fcd306f8b1b8 Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Fri, 16 Aug 2019 15:56:32 -0700 Subject: [PATCH] add support SequenceableLoader.reevaluateBuffer() for HLS DASH implements this feature, extend the feature for HLS as well. First change just drops video samples. For demuxed audio the audio samples will continue to play out to match the dropped video, so need to keep indexes in all the sample queues related to a chunk and discard them all. --- .../exoplayer2/source/hls/HlsChunkSource.java | 7 ++ .../exoplayer2/source/hls/HlsMediaChunk.java | 17 +++++ .../source/hls/HlsSampleStreamWrapper.java | 74 ++++++++++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) 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 1a77715e71..a7caf2d7aa 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 @@ -451,6 +451,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return chunkIterators; } + public int getPreferredQueueSize(long playbackPositionUs, List queue) { + if (fatalError != null || trackSelection.length() < 2) { + return queue.size(); + } + return trackSelection.evaluateQueueSize(playbackPositionUs, queue); + } + // Private methods. /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 8845228900..562d820c49 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.PrivFrame; +import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.upstream.DataSource; @@ -222,6 +223,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private volatile boolean loadCanceled; private boolean loadCompleted; + /** + * Index of first sample written to the SampleQueue for the primary track from + * this segment. + */ + private int firstSampleIndex = C.INDEX_UNSET; + private HlsMediaChunk( HlsExtractorFactory extractorFactory, DataSource mediaDataSource, @@ -291,6 +298,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return loadCompleted; } + /** + * Return the index of the first sample from the primary sample stream for this media chunk + * + * @return sample index {@link SampleQueue#getWriteIndex()} + */ + public int getFirstPrimarySampleIndex() { + return firstSampleIndex; + } + // Loadable implementation @Override @@ -308,6 +324,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; initDataLoadRequired = false; output.init(uid, shouldSpliceIn, /* reusingExtractor= */ true); } + firstSampleIndex = output.getPrimaryTrackWritePosition(); maybeLoadInitData(); if (!loadCanceled) { if (!hasGapTag) { 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 d99bb817c1..4f4fe9e196 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 @@ -696,9 +696,65 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public void reevaluateBuffer(long positionUs) { - // Do nothing. + if (loader.isLoading() || isPendingReset()) { + return; + } + + int currentQueueSize = mediaChunks.size(); + int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); + if (currentQueueSize > preferredQueueSize) { + Log.d(TAG, "reevaluateBuffer() - position: " + positionUs + " preferredQueueSize: " + + preferredQueueSize + " current size: "+ currentQueueSize); + + long firstRemovedStartTimeUs = discardMediaChunks(preferredQueueSize); + long endTimeUs = getLastMediaChunk().endTimeUs; + eventDispatcher.upstreamDiscarded(primarySampleQueueType, firstRemovedStartTimeUs, endTimeUs); + } } + + /** + * Discards HlsMediaChunks, after currently playing chunk {@see #haveReadFromMediaChunk}, that have + * not yet started to play to allow (hopefully) higher quality chunks to replace them + * + * @param preferredQueueSize - desired media chunk queue size (always < mediaChunks.size()) + * @return endTimeUs of first chunk removed + */ + private long discardMediaChunks(int preferredQueueSize) { + Log.d(TAG, "discardChunksToIndex() - preferredQueueSize " + preferredQueueSize + + " currentSize " + mediaChunks.size() + + " write: "+sampleQueues[primarySampleQueueIndex].getWriteIndex() + + " read: "+sampleQueues[primarySampleQueueIndex].getReadIndex() + ); + for (int i=0; i 0) { + firstRemovedChunkIndex++; + preferredQueueSize--; + } + + HlsMediaChunk firstRemovedChunk = mediaChunks.get(firstRemovedChunkIndex); + Util.removeRange(mediaChunks, firstRemovedChunkIndex, mediaChunks.size() - 1); + Log.d(TAG, "discardChunksToIndex() - discard from: " + firstRemovedChunk.getFirstPrimarySampleIndex()); + + sampleQueues[primarySampleQueueIndex].discardUpstreamSamples(firstRemovedChunk.getFirstPrimarySampleIndex()); + + return firstRemovedChunk.endTimeUs; + } + + + /** Returns whether samples have been read from primary sample queue of the indicated chunk */ + private boolean haveReadFromMediaChunk(int mediaChunkIndex) { + HlsMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex); + return sampleQueues[primarySampleQueueIndex].getReadIndex() > mediaChunk.getFirstPrimarySampleIndex(); + } // Loader.Callback implementation. @Override @@ -959,6 +1015,22 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } } + /** + * Get the SampleQueue write position index. Used to associate group of + * samples with a MediaChunk. + * + * @return write position {@link SampleQueue#getWriteIndex()}, or 0 (safe bet) if sample queues not created + */ + int getPrimaryTrackWritePosition() { + int indexValue = 0; + + if (primaryTrackGroupIndex != C.INDEX_UNSET && prepared) { + int sampleQueueIndex = trackGroupToSampleQueueIndex[primaryTrackGroupIndex]; + indexValue = sampleQueues[sampleQueueIndex].getWriteIndex(); + } + return indexValue; + } + // Internal methods. private void updateSampleStreams(@NullableType SampleStream[] streams) {