Commit 88dea59c by hoangtc Committed by Oliver Woodman

Add ability for media period to discard buffered media at the back of the queue

In some occasions, we may want to discard a part of the buffered media to
improve playback quality. This CL adds this functionality by allowing the
loading media period to re-evaluate its buffer periodically (every 2s) and discard
chunks as it needs.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=177958910
parent 6606d73b
Showing with 213 additions and 87 deletions
......@@ -2,6 +2,8 @@
### dev-v2 (not yet released) ###
* Add ability for `SequenceableLoader` to reevaluate its buffer and discard
buffered media so that it can be re-buffered in a different quality.
* Replace `DefaultTrackSelector.Parameters` copy methods with a builder.
* Allow more flexible loading strategy when playing media containing multiple
sub-streams, by allowing injection of custom `CompositeSequenceableLoader`
......
......@@ -1283,6 +1283,7 @@ import java.io.IOException;
// Update the loading period if required.
maybeUpdateLoadingPeriod();
if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {
setIsLoading(false);
} else if (loadingPeriodHolder != null && !isLoading) {
......@@ -1386,6 +1387,7 @@ import java.io.IOException;
if (loadingPeriodHolder == null) {
info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo);
} else {
loadingPeriodHolder.reevaluateBuffer(rendererPositionUs);
if (loadingPeriodHolder.info.isFinal || !loadingPeriodHolder.isFullyBuffered()
|| loadingPeriodHolder.info.durationUs == C.TIME_UNSET) {
return;
......@@ -1440,6 +1442,7 @@ import java.io.IOException;
// Stale event.
return;
}
loadingPeriodHolder.reevaluateBuffer(rendererPositionUs);
maybeContinueLoading();
}
......@@ -1628,13 +1631,18 @@ import java.io.IOException;
info = info.copyWithStartPositionUs(newStartPositionUs);
}
public void reevaluateBuffer(long rendererPositionUs) {
if (prepared) {
mediaPeriod.reevaluateBuffer(toPeriodTime(rendererPositionUs));
}
}
public boolean shouldContinueLoading(long rendererPositionUs, float playbackSpeed) {
long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs();
if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
return false;
} else {
long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs);
long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs;
long bufferedDurationUs = nextLoadPositionUs - toPeriodTime(rendererPositionUs);
return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed);
}
}
......@@ -1694,7 +1702,6 @@ import java.io.IOException;
Assertions.checkState(trackSelections.get(i) == null);
}
}
// The track selection has changed.
loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections);
return positionUs;
......
......@@ -124,6 +124,11 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
@Override
public void reevaluateBuffer(long positionUs) {
mediaPeriod.reevaluateBuffer(positionUs + startUs);
}
@Override
public long readDiscontinuity() {
if (isPendingInitialDiscontinuity()) {
long initialDiscontinuityUs = pendingInitialDiscontinuityPositionUs;
......
......@@ -53,6 +53,13 @@ public class CompositeSequenceableLoader implements SequenceableLoader {
}
@Override
public final void reevaluateBuffer(long positionUs) {
for (SequenceableLoader loader : loaders) {
loader.reevaluateBuffer(positionUs);
}
}
@Override
public boolean continueLoading(long positionUs) {
boolean madeProgress = false;
boolean madeProgressThisIteration;
......
......@@ -120,6 +120,11 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
@Override
public void reevaluateBuffer(long positionUs) {
mediaPeriod.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
}
......
......@@ -289,6 +289,11 @@ import java.util.Arrays;
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override
public boolean continueLoading(long playbackPositionUs) {
if (loadingFinished || (prepared && enabledTrackCount == 0)) {
return false;
......
......@@ -35,27 +35,25 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Called when preparation completes.
* <p>
* Called on the playback thread. After invoking this method, the {@link MediaPeriod} can expect
* for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], long)} to be
* called with the initial track selection.
*
* <p>Called on the playback thread. After invoking this method, the {@link MediaPeriod} can
* expect for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[],
* long)} to be called with the initial track selection.
*
* @param mediaPeriod The prepared {@link MediaPeriod}.
*/
void onPrepared(MediaPeriod mediaPeriod);
}
/**
* Prepares this media period asynchronously.
* <p>
* {@code callback.onPrepared} is called when preparation completes. If preparation fails,
*
* <p>{@code callback.onPrepared} is called when preparation completes. If preparation fails,
* {@link #maybeThrowPrepareError()} will throw an {@link IOException}.
* <p>
* If preparation succeeds and results in a source timeline change (e.g. the period duration
* becoming known),
* {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} will be
* called before {@code callback.onPrepared}.
*
* <p>If preparation succeeds and results in a source timeline change (e.g. the period duration
* becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline,
* Object)} will be called before {@code callback.onPrepared}.
*
* @param callback Callback to receive updates from this period, including being notified when
* preparation completes.
......@@ -66,8 +64,8 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Throws an error that's preventing the period from becoming prepared. Does nothing if no such
* error exists.
* <p>
* This method should only be called before the period has completed preparation.
*
* <p>This method should only be called before the period has completed preparation.
*
* @throws IOException The underlying error.
*/
......@@ -75,8 +73,8 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Returns the {@link TrackGroup}s exposed by the period.
* <p>
* This method should only be called after the period has been prepared.
*
* <p>This method should only be called after the period has been prepared.
*
* @return The {@link TrackGroup}s.
*/
......@@ -84,16 +82,16 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Performs a track selection.
* <p>
* The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
*
* <p>The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
* indicating whether the existing {@code SampleStream} can be retained for each selection, and
* the existing {@code stream}s themselves. The call will update {@code streams} to reflect the
* provided selections, clearing, setting and replacing entries as required. If an existing sample
* stream is retained but with the requirement that the consuming renderer be reset, then the
* corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set
* if a new sample stream is created.
* <p>
* This method should only be called after the period has been prepared.
*
* <p>This method should only be called after the period has been prepared.
*
* @param selections The renderer track selections.
* @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained
......@@ -104,16 +102,20 @@ public interface MediaPeriod extends SequenceableLoader {
* @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that
* have been retained but with the requirement that the consuming renderer be reset.
* @param positionUs The current playback position in microseconds. If playback of this period has
* not yet started, the value will be the starting position.
* not yet started, the value will be the starting position.
* @return The actual position at which the tracks were enabled, in microseconds.
*/
long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs);
long selectTracks(
TrackSelection[] selections,
boolean[] mayRetainStreamFlags,
SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs);
/**
* Discards buffered media up to the specified position.
* <p>
* This method should only be called after the period has been prepared.
*
* <p>This method should only be called after the period has been prepared.
*
* @param positionUs The position in microseconds.
* @param toKeyframe If true then for each track discards samples up to the keyframe before or at
......@@ -123,11 +125,11 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Attempts to read a discontinuity.
* <p>
* After this method has returned a value other than {@link C#TIME_UNSET}, all
* {@link SampleStream}s provided by the period are guaranteed to start from a key frame.
* <p>
* This method should only be called after the period has been prepared.
*
* <p>After this method has returned a value other than {@link C#TIME_UNSET}, all {@link
* SampleStream}s provided by the period are guaranteed to start from a key frame.
*
* <p>This method should only be called after the period has been prepared.
*
* @return If a discontinuity was read then the playback position in microseconds after the
* discontinuity. Else {@link C#TIME_UNSET}.
......@@ -136,11 +138,11 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Attempts to seek to the specified position in microseconds.
* <p>
* After this method has been called, all {@link SampleStream}s provided by the period are
*
* <p>After this method has been called, all {@link SampleStream}s provided by the period are
* guaranteed to start from a key frame.
* <p>
* This method should only be called when at least one track is selected.
*
* <p>This method should only be called when at least one track is selected.
*
* @param positionUs The seek position in microseconds.
* @return The actual position to which the period was seeked, in microseconds.
......@@ -151,8 +153,8 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Returns an estimate of the position up to which data is buffered for the enabled tracks.
* <p>
* This method should only be called when at least one track is selected.
*
* <p>This method should only be called when at least one track is selected.
*
* @return An estimate of the absolute position in microseconds up to which data is buffered, or
* {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
......@@ -162,19 +164,19 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.
* <p>
* This method should only be called after the period has been prepared. It may be called when no
* tracks are selected.
*
* <p>This method should only be called after the period has been prepared. It may be called when
* no tracks are selected.
*/
@Override
long getNextLoadPositionUs();
/**
* Attempts to continue loading.
* <p>
* This method may be called both during and after the period has been prepared.
* <p>
* A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
*
* <p>This method may be called both during and after the period has been prepared.
*
* <p>A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
* {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be
* called when the period is permitted to continue loading data. A period may do this both during
* and after preparation.
......@@ -182,10 +184,24 @@ public interface MediaPeriod extends SequenceableLoader {
* @param positionUs The current playback position in microseconds. If playback of this period has
* not yet started, the value will be the starting position in this period minus the duration
* of any media in previous periods still to be played.
* @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return
* a different value than prior to the call. False otherwise.
* @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return a
* different value than prior to the call. False otherwise.
*/
@Override
boolean continueLoading(long positionUs);
/**
* Re-evaluates the buffer given the playback position.
*
* <p>This method should only be called after the period has been prepared.
*
* <p>A period may choose to discard buffered media so that it can be re-buffered in a different
* quality.
*
* @param positionUs The current playback position in microseconds. If playback of this period has
* not yet started, the value will be the starting position in this period minus the duration
* of any media in previous periods still to be played.
*/
@Override
void reevaluateBuffer(long positionUs);
}
......@@ -140,6 +140,11 @@ import java.util.IdentityHashMap;
}
@Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs);
}
......
......@@ -60,4 +60,15 @@ public interface SequenceableLoader {
*/
boolean continueLoading(long positionUs);
/**
* Re-evaluates the buffer given the playback position.
*
* <p>Re-evaluation may discard buffered media so that it can be re-buffered in a different
* quality.
*
* @param positionUs The current playback position in microseconds. If playback of this period has
* not yet started, the value will be the starting position in this period minus the duration
* of any media in previous periods still to be played.
*/
void reevaluateBuffer(long positionUs);
}
......@@ -121,6 +121,11 @@ import java.util.Arrays;
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override
public boolean continueLoading(long positionUs) {
if (loadingFinished || loader.isLoading()) {
return false;
......
......@@ -319,7 +319,9 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
IOException error) {
long bytesLoaded = loadable.bytesLoaded();
boolean isMediaChunk = isMediaChunk(loadable);
boolean cancelable = bytesLoaded == 0 || !isMediaChunk || !haveReadFromLastMediaChunk();
int lastChunkIndex = mediaChunks.size() - 1;
boolean cancelable =
bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex);
boolean canceled = false;
if (chunkSource.onChunkLoadError(loadable, cancelable, error)) {
if (!cancelable) {
......@@ -327,12 +329,8 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
} else {
canceled = true;
if (isMediaChunk) {
BaseMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1);
BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex);
Assertions.checkState(removed == loadable);
primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0));
for (int i = 0; i < embeddedSampleQueues.length; i++) {
embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1));
}
if (mediaChunks.isEmpty()) {
pendingResetPositionUs = lastSeekPositionUs;
}
......@@ -405,35 +403,29 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
}
}
// Internal methods
// TODO[REFACTOR]: Call maybeDiscardUpstream for DASH and SmoothStreaming.
/**
* Discards media chunks from the back of the buffer if conditions have changed such that it's
* preferable to re-buffer the media at a different quality.
*
* @param positionUs The current playback position in microseconds.
*/
@SuppressWarnings("unused")
private void maybeDiscardUpstream(long positionUs) {
@Override
public void reevaluateBuffer(long positionUs) {
if (loader.isLoading() || isPendingReset()) {
return;
}
int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);
discardUpstreamMediaChunks(Math.max(1, queueSize));
discardUpstreamMediaChunks(queueSize);
}
// Internal methods
private boolean isMediaChunk(Chunk chunk) {
return chunk instanceof BaseMediaChunk;
}
/**
* Returns whether samples have been read from {@code mediaChunks.getLast()}.
*/
private boolean haveReadFromLastMediaChunk() {
BaseMediaChunk lastChunk = getLastMediaChunk();
if (primarySampleQueue.getReadIndex() > lastChunk.getFirstSampleIndex(0)) {
/** Returns whether samples have been read from media chunk at given index. */
private boolean haveReadFromMediaChunk(int mediaChunkIndex) {
BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex);
if (primarySampleQueue.getReadIndex() > mediaChunk.getFirstSampleIndex(0)) {
return true;
}
for (int i = 0; i < embeddedSampleQueues.length; i++) {
if (embeddedSampleQueues[i].getReadIndex() > lastChunk.getFirstSampleIndex(i + 1)) {
if (embeddedSampleQueues[i].getReadIndex() > mediaChunk.getFirstSampleIndex(i + 1)) {
return true;
}
}
......@@ -492,27 +484,51 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
}
/**
* Discard upstream media chunks until the queue length is equal to the length specified.
* Discard upstream media chunks until the queue length is equal to the length specified, but
* avoid discarding any chunk whose samples have been read by either primary sample stream or
* embedded sample streams.
*
* @param queueLength The desired length of the queue.
* @return Whether chunks were discarded.
* @param desiredQueueSize The desired length of the queue. The final queue size after discarding
* maybe larger than this if there are chunks after the specified position that have been read
* by either primary sample stream or embedded sample streams.
*/
private boolean discardUpstreamMediaChunks(int queueLength) {
if (mediaChunks.size() <= queueLength) {
return false;
private void discardUpstreamMediaChunks(int desiredQueueSize) {
if (mediaChunks.size() <= desiredQueueSize) {
return;
}
int firstIndexToRemove = desiredQueueSize;
for (int i = firstIndexToRemove; i < mediaChunks.size(); i++) {
if (!haveReadFromMediaChunk(i)) {
firstIndexToRemove = i;
break;
}
}
if (firstIndexToRemove == mediaChunks.size()) {
return;
}
long endTimeUs = getLastMediaChunk().endTimeUs;
BaseMediaChunk firstRemovedChunk = mediaChunks.get(queueLength);
long startTimeUs = firstRemovedChunk.startTimeUs;
Util.removeRange(mediaChunks, /* fromIndex= */ queueLength, /* toIndex= */ mediaChunks.size());
BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(firstIndexToRemove);
loadingFinished = false;
eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs);
}
/**
* Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample
* queues.
*
* @param chunkIndex The index of the first chunk to discard.
* @return The chunk at given index.
*/
private BaseMediaChunk discardUpstreamMediaChunksFromIndex(int chunkIndex) {
BaseMediaChunk firstRemovedChunk = mediaChunks.get(chunkIndex);
Util.removeRange(mediaChunks, /* fromIndex= */ chunkIndex, /* toIndex= */ mediaChunks.size());
primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0));
for (int i = 0; i < embeddedSampleQueues.length; i++) {
embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1));
}
loadingFinished = false;
eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs);
return true;
return firstRemovedChunk;
}
/**
......
......@@ -265,6 +265,11 @@ public final class CompositeSequenceableLoaderTest {
return loaded;
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
private void setNextChunkDurationUs(int nextChunkDurationUs) {
this.nextChunkDurationUs = nextChunkDurationUs;
}
......
......@@ -271,6 +271,11 @@ import java.util.Map;
}
@Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs);
}
......
......@@ -196,6 +196,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
}
@Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs);
}
......
......@@ -524,6 +524,11 @@ import java.util.Arrays;
return true;
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
// Loader.Callback implementation.
@Override
......
......@@ -150,6 +150,11 @@ import java.util.ArrayList;
}
@Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs);
}
......
......@@ -203,8 +203,20 @@ public class SsManifest {
long timescale, String name, int maxWidth, int maxHeight, int displayWidth,
int displayHeight, String language, Format[] formats, List<Long> chunkStartTimes,
long lastChunkDuration) {
this (baseUri, chunkTemplate, type, subType, timescale, name, maxWidth, maxHeight,
displayWidth, displayHeight, language, formats, chunkStartTimes,
this(
baseUri,
chunkTemplate,
type,
subType,
timescale,
name,
maxWidth,
maxHeight,
displayWidth,
displayHeight,
language,
formats,
chunkStartTimes,
Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale),
Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale));
}
......
......@@ -152,6 +152,11 @@ public class FakeMediaPeriod implements MediaPeriod {
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override
public long readDiscontinuity() {
Assert.assertTrue(prepared);
long positionDiscontinuityUs = this.discontinuityPositionUs;
......
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