mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Merge pull request #7244 from tvarga-dss:cancel-hls-chunk-download-and-discard-upstream
PiperOrigin-RevId: 312679454
This commit is contained in:
commit
80eb5d4235
5 changed files with 133 additions and 17 deletions
|
|
@ -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
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
/** 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;
|
||||||
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;
|
||||||
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;
|
||||||
@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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
|
|
||||||
@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;
|
||||||
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;
|
||||||
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;
|
||||||
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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue