Commit 80eb5d42 by tonihei

Merge pull request #7244 from tvarga-dss:cancel-hls-chunk-download-and-discard-upstream

PiperOrigin-RevId: 312679454
parents d487170e e4cb7405
...@@ -168,6 +168,9 @@ ...@@ -168,6 +168,9 @@
* Enable support for embedded CEA-708. * Enable support for embedded CEA-708.
* Fix assertion failure in `SampleQueue` when playing DASH streams with * Fix assertion failure in `SampleQueue` when playing DASH streams with
EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)). EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)).
* HLS:
* Add support for upstream discard including cancelation of ongoing load
([#6322](https://github.com/google/ExoPlayer/issues/6322)).
* MP3: * MP3:
* Add `IndexSeeker` for accurate seeks in VBR streams * Add `IndexSeeker` for accurate seeks in VBR streams
([#6787](https://github.com/google/ExoPlayer/issues/6787)). This seeker ([#6787](https://github.com/google/ExoPlayer/issues/6787)). This seeker
......
...@@ -628,7 +628,7 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S ...@@ -628,7 +628,7 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
return; return;
} }
int newQueueSize = currentQueueSize; int newQueueSize = Integer.MAX_VALUE;
for (int i = preferredQueueSize; i < currentQueueSize; i++) { for (int i = preferredQueueSize; i < currentQueueSize; i++) {
if (!haveReadFromMediaChunk(i)) { if (!haveReadFromMediaChunk(i)) {
newQueueSize = i; newQueueSize = i;
......
...@@ -451,6 +451,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -451,6 +451,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return chunkIterators; return chunkIterators;
} }
/**
* Evaluates whether {@link MediaChunk MediaChunks} should be removed from the back of the queue.
*
* <p>Removing {@link MediaChunk MediaChunks} 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).
*
* @param playbackPositionUs The current playback position, in microseconds.
* @param queue The queue of buffered {@link MediaChunk MediaChunks}.
* @return The preferred queue size.
*/
public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
if (fatalError != null || trackSelection.length() < 2) {
return queue.size();
}
return trackSelection.evaluateQueueSize(playbackPositionUs, queue);
}
// Private methods. // Private methods.
/** /**
......
...@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.extractor.PositionHolder; ...@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.id3.PrivFrame; 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.chunk.MediaChunk;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
...@@ -36,6 +37,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -36,6 +37,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.UriUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
...@@ -131,11 +133,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -131,11 +133,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
Id3Decoder id3Decoder; Id3Decoder id3Decoder;
ParsableByteArray scratchId3Data; ParsableByteArray scratchId3Data;
boolean shouldSpliceIn; boolean shouldSpliceIn;
ImmutableMap<SampleQueue, Integer> sampleQueueDiscardFromIndices = ImmutableMap.of();
if (previousChunk != null) { if (previousChunk != null) {
id3Decoder = previousChunk.id3Decoder; id3Decoder = previousChunk.id3Decoder;
scratchId3Data = previousChunk.scratchId3Data; scratchId3Data = previousChunk.scratchId3Data;
shouldSpliceIn = shouldSpliceIn =
!playlistUrl.equals(previousChunk.playlistUrl) || !previousChunk.loadCompleted; !playlistUrl.equals(previousChunk.playlistUrl) || !previousChunk.loadCompleted;
if (shouldSpliceIn) {
sampleQueueDiscardFromIndices = previousChunk.sampleQueueDiscardFromIndices;
}
previousExtractor = previousExtractor =
previousChunk.isExtractorReusable previousChunk.isExtractorReusable
&& previousChunk.discontinuitySequenceNumber == discontinuitySequenceNumber && previousChunk.discontinuitySequenceNumber == discontinuitySequenceNumber
...@@ -172,7 +178,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -172,7 +178,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
previousExtractor, previousExtractor,
id3Decoder, id3Decoder,
scratchId3Data, scratchId3Data,
shouldSpliceIn); shouldSpliceIn,
sampleQueueDiscardFromIndices);
} }
public static final String PRIV_TIMESTAMP_FRAME_OWNER = public static final String PRIV_TIMESTAMP_FRAME_OWNER =
...@@ -194,9 +201,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -194,9 +201,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** The url of the playlist from which this chunk was obtained. */ /** The url of the playlist from which this chunk was obtained. */
public final Uri playlistUrl; public final Uri playlistUrl;
/** Whether the samples parsed from this chunk should be spliced into already queued samples. */
public final boolean shouldSpliceIn;
@Nullable private final DataSource initDataSource; @Nullable private final DataSource initDataSource;
@Nullable private final DataSpec initDataSpec; @Nullable private final DataSpec initDataSpec;
@Nullable private final Extractor previousExtractor; @Nullable private final Extractor previousExtractor;
...@@ -211,6 +215,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -211,6 +215,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final ParsableByteArray scratchId3Data; private final ParsableByteArray scratchId3Data;
private final boolean mediaSegmentEncrypted; private final boolean mediaSegmentEncrypted;
private final boolean initSegmentEncrypted; private final boolean initSegmentEncrypted;
private final boolean shouldSpliceIn;
private @MonotonicNonNull Extractor extractor; private @MonotonicNonNull Extractor extractor;
private boolean isExtractorReusable; private boolean isExtractorReusable;
...@@ -221,6 +226,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -221,6 +226,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private boolean initDataLoadRequired; private boolean initDataLoadRequired;
private volatile boolean loadCanceled; private volatile boolean loadCanceled;
private boolean loadCompleted; private boolean loadCompleted;
private ImmutableMap<SampleQueue, Integer> sampleQueueDiscardFromIndices;
private HlsMediaChunk( private HlsMediaChunk(
HlsExtractorFactory extractorFactory, HlsExtractorFactory extractorFactory,
...@@ -246,7 +252,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -246,7 +252,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable Extractor previousExtractor, @Nullable Extractor previousExtractor,
Id3Decoder id3Decoder, Id3Decoder id3Decoder,
ParsableByteArray scratchId3Data, ParsableByteArray scratchId3Data,
boolean shouldSpliceIn) { boolean shouldSpliceIn,
ImmutableMap<SampleQueue, Integer> sampleQueueDiscardFromIndices) {
super( super(
mediaDataSource, mediaDataSource,
dataSpec, dataSpec,
...@@ -273,17 +280,43 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -273,17 +280,43 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
this.id3Decoder = id3Decoder; this.id3Decoder = id3Decoder;
this.scratchId3Data = scratchId3Data; this.scratchId3Data = scratchId3Data;
this.shouldSpliceIn = shouldSpliceIn; this.shouldSpliceIn = shouldSpliceIn;
this.sampleQueueDiscardFromIndices = sampleQueueDiscardFromIndices;
uid = uidSource.getAndIncrement(); uid = uidSource.getAndIncrement();
} }
/** /**
* Initializes the chunk for loading, setting the {@link HlsSampleStreamWrapper} that will receive * Initializes the chunk for loading.
* samples as they are loaded.
* *
* @param output The output that will receive the loaded samples. * @param output The {@link HlsSampleStreamWrapper} that will receive the loaded samples.
* @param sampleQueues The {@link SampleQueue sampleQueues} with already loaded samples.
*/ */
public void init(HlsSampleStreamWrapper output) { public void init(HlsSampleStreamWrapper output, SampleQueue[] sampleQueues) {
this.output = output; this.output = output;
if (shouldSpliceIn) {
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.splice();
}
// sampleQueueDiscardFromIndices already set to values of previous chunk in constructor.
} else {
ImmutableMap.Builder<SampleQueue, Integer> mapBuilder = ImmutableMap.builder();
for (SampleQueue sampleQueue : sampleQueues) {
mapBuilder.put(sampleQueue, sampleQueue.getWriteIndex());
}
sampleQueueDiscardFromIndices = mapBuilder.build();
}
}
/**
* Returns the absolute index from which samples need to be discarded in the given {@link
* SampleQueue} when this media chunk is discarded.
*
* @param sampleQueue The {@link SampleQueue}.
* @return The absolute index from which samples need to be discarded.
*/
int getSampleQueueDiscardFromIndex(SampleQueue sampleQueue) {
// If the sample queue was created by this chunk or a later chunk, return 0 to discard the whole
// stream from the beginning.
return sampleQueueDiscardFromIndices.getOrDefault(sampleQueue, /* defaultValue= */ 0);
} }
@Override @Override
......
...@@ -146,6 +146,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -146,6 +146,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private @MonotonicNonNull Format upstreamTrackFormat; private @MonotonicNonNull Format upstreamTrackFormat;
@Nullable private Format downstreamTrackFormat; @Nullable private Format downstreamTrackFormat;
private boolean released; private boolean released;
private int pendingDiscardUpstreamQueueSize;
// Tracks are complicated in HLS. See documentation of buildTracksFromSampleStreams for details. // Tracks are complicated in HLS. See documentation of buildTracksFromSampleStreams for details.
// Indexed by track (as exposed by this source). // Indexed by track (as exposed by this source).
...@@ -229,6 +230,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -229,6 +230,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
handler = Util.createHandler(); handler = Util.createHandler();
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
pendingResetPositionUs = positionUs; pendingResetPositionUs = positionUs;
pendingDiscardUpstreamQueueSize = C.LENGTH_UNSET;
} }
public void continuePreparing() { public void continuePreparing() {
...@@ -696,7 +698,21 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -696,7 +698,21 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Override @Override
public void reevaluateBuffer(long positionUs) { public void reevaluateBuffer(long positionUs) {
// Do nothing. if (loader.hasFatalError() || isPendingReset()) {
return;
}
int currentQueueSize = mediaChunks.size();
int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);
if (currentQueueSize <= preferredQueueSize) {
return;
}
if (loader.isLoading()) {
pendingDiscardUpstreamQueueSize = preferredQueueSize;
loader.cancelLoading();
} else {
discardUpstream(preferredQueueSize);
}
} }
// Loader.Callback implementation. // Loader.Callback implementation.
...@@ -753,7 +769,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -753,7 +769,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
loadable.startTimeUs, loadable.startTimeUs,
loadable.endTimeUs); loadable.endTimeUs);
if (!released) { if (!released) {
resetSampleQueues(); if (pendingDiscardUpstreamQueueSize != C.LENGTH_UNSET) {
discardUpstream(pendingDiscardUpstreamQueueSize);
pendingDiscardUpstreamQueueSize = C.LENGTH_UNSET;
} else {
resetSampleQueues();
}
if (enabledTrackGroupCount > 0) { if (enabledTrackGroupCount > 0) {
callback.onContinueLoadingRequested(this); callback.onContinueLoadingRequested(this);
} }
...@@ -851,16 +872,36 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -851,16 +872,36 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
upstreamTrackFormat = chunk.trackFormat; upstreamTrackFormat = chunk.trackFormat;
pendingResetPositionUs = C.TIME_UNSET; pendingResetPositionUs = C.TIME_UNSET;
mediaChunks.add(chunk); mediaChunks.add(chunk);
chunk.init(/* output= */ this, sampleQueues);
chunk.init(this);
for (HlsSampleQueue sampleQueue : sampleQueues) { for (HlsSampleQueue sampleQueue : sampleQueues) {
sampleQueue.setSourceChunk(chunk); sampleQueue.setSourceChunk(chunk);
} }
if (chunk.shouldSpliceIn) { }
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.splice(); private void discardUpstream(int preferredQueueSize) {
Assertions.checkState(!loader.isLoading());
int currentQueueSize = mediaChunks.size();
int newQueueSize = Integer.MAX_VALUE;
for (int i = preferredQueueSize; i < currentQueueSize; i++) {
if (!haveReadFromMediaChunkDiscardRange(i)) {
newQueueSize = i;
break;
} }
} }
if (newQueueSize >= currentQueueSize) {
return;
}
long endTimeUs = getLastMediaChunk().endTimeUs;
HlsMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(newQueueSize);
if (mediaChunks.isEmpty()) {
pendingResetPositionUs = lastSeekPositionUs;
}
loadingFinished = false;
eventDispatcher.upstreamDiscarded(
primarySampleQueueType, firstRemovedChunk.startTimeUs, endTimeUs);
} }
// ExtractorOutput implementation. Called by the loading thread. // ExtractorOutput implementation. Called by the loading thread.
...@@ -1061,6 +1102,27 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -1061,6 +1102,27 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return true; return true;
} }
private boolean haveReadFromMediaChunkDiscardRange(int mediaChunkIndex) {
HlsMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex);
for (SampleQueue sampleQueue : sampleQueues) {
int discardFromIndex = mediaChunk.getSampleQueueDiscardFromIndex(sampleQueue);
if (sampleQueue.getReadIndex() > discardFromIndex) {
return true;
}
}
return false;
}
private HlsMediaChunk discardUpstreamMediaChunksFromIndex(int chunkIndex) {
HlsMediaChunk firstRemovedChunk = mediaChunks.get(chunkIndex);
Util.removeRange(mediaChunks, /* fromIndex= */ chunkIndex, /* toIndex= */ mediaChunks.size());
for (SampleQueue sampleQueue : sampleQueues) {
int discardFromIndex = firstRemovedChunk.getSampleQueueDiscardFromIndex(sampleQueue);
sampleQueue.discardUpstreamSamples(discardFromIndex);
}
return firstRemovedChunk;
}
private void resetSampleQueues() { private void resetSampleQueues() {
for (SampleQueue sampleQueue : sampleQueues) { for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.reset(pendingResetUpstreamFormats); sampleQueue.reset(pendingResetUpstreamFormats);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment