Commit 9282710f by olly Committed by Oliver Woodman

Decouple next chunk evaluation and queue trimming.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=118925372
parent 1581b915
...@@ -16,20 +16,9 @@ ...@@ -16,20 +16,9 @@
package com.google.android.exoplayer.chunk; package com.google.android.exoplayer.chunk;
/** /**
* Holds a chunk operation, which consists of a either: * Holds a chunk or an indication that the end of the stream has been reached.
* <ul>
* <li>The number of {@link MediaChunk}s that should be retained on the queue ({@link #queueSize})
* together with the next {@link Chunk} to load ({@link #chunk}). {@link #chunk} may be null if the
* next chunk cannot be provided yet.</li>
* <li>A flag indicating that the end of the stream has been reached ({@link #endOfStream}).</li>
* </ul>
*/ */
public final class ChunkOperationHolder { public final class ChunkHolder {
/**
* The number of {@link MediaChunk}s to retain in a queue.
*/
public int queueSize;
/** /**
* The chunk. * The chunk.
...@@ -45,7 +34,6 @@ public final class ChunkOperationHolder { ...@@ -45,7 +34,6 @@ public final class ChunkOperationHolder {
* Clears the holder. * Clears the holder.
*/ */
public void clear() { public void clear() {
queueSize = 0;
chunk = null; chunk = null;
endOfStream = false; endOfStream = false;
} }
......
...@@ -55,7 +55,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -55,7 +55,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
private final Loader loader; private final Loader loader;
private final LoadControl loadControl; private final LoadControl loadControl;
private final ChunkSource chunkSource; private final ChunkSource chunkSource;
private final ChunkOperationHolder currentLoadableHolder; private final ChunkHolder nextChunkHolder;
private final LinkedList<BaseMediaChunk> mediaChunks; private final LinkedList<BaseMediaChunk> mediaChunks;
private final List<BaseMediaChunk> readOnlyMediaChunks; private final List<BaseMediaChunk> readOnlyMediaChunks;
private final DefaultTrackOutput sampleQueue; private final DefaultTrackOutput sampleQueue;
...@@ -66,7 +66,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -66,7 +66,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
private long downstreamPositionUs; private long downstreamPositionUs;
private long lastSeekPositionUs; private long lastSeekPositionUs;
private long pendingResetPositionUs; private long pendingResetPositionUs;
private long lastPerformedBufferOperation; private long lastPreferredQueueSizeEvaluationTimeMs;
private boolean pendingReset; private boolean pendingReset;
private boolean loadControlRegistered; private boolean loadControlRegistered;
...@@ -76,6 +76,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -76,6 +76,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
private boolean trackEnabled; private boolean trackEnabled;
private long currentLoadStartTimeMs; private long currentLoadStartTimeMs;
private Chunk currentLoadable;
private Format downstreamFormat; private Format downstreamFormat;
private Format downstreamSampleFormat; private Format downstreamSampleFormat;
...@@ -124,7 +125,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -124,7 +125,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
this.bufferSizeContribution = bufferSizeContribution; this.bufferSizeContribution = bufferSizeContribution;
loader = new Loader("Loader:ChunkSampleSource", minLoadableRetryCount); loader = new Loader("Loader:ChunkSampleSource", minLoadableRetryCount);
eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId); eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId);
currentLoadableHolder = new ChunkOperationHolder(); nextChunkHolder = new ChunkHolder();
mediaChunks = new LinkedList<>(); mediaChunks = new LinkedList<>();
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); sampleQueue = new DefaultTrackOutput(loadControl.getAllocator());
...@@ -267,9 +268,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -267,9 +268,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override @Override
public void maybeThrowError() throws IOException { public void maybeThrowError() throws IOException {
loader.maybeThrowError(); loader.maybeThrowError();
if (currentLoadableHolder.chunk == null) { chunkSource.maybeThrowError();
chunkSource.maybeThrowError();
}
} }
@Override @Override
...@@ -336,7 +335,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -336,7 +335,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
public void onLoadCompleted(Loadable loadable) { public void onLoadCompleted(Loadable loadable) {
long now = SystemClock.elapsedRealtime(); long now = SystemClock.elapsedRealtime();
long loadDurationMs = now - currentLoadStartTimeMs; long loadDurationMs = now - currentLoadStartTimeMs;
Chunk currentLoadable = currentLoadableHolder.chunk;
chunkSource.onChunkLoadCompleted(currentLoadable); chunkSource.onChunkLoadCompleted(currentLoadable);
if (isMediaChunk(currentLoadable)) { if (isMediaChunk(currentLoadable)) {
BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable; BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable;
...@@ -353,7 +351,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -353,7 +351,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override @Override
public void onLoadCanceled(Loadable loadable) { public void onLoadCanceled(Loadable loadable) {
Chunk currentLoadable = currentLoadableHolder.chunk;
eventDispatcher.loadCanceled(currentLoadable.bytesLoaded()); eventDispatcher.loadCanceled(currentLoadable.bytesLoaded());
if (trackEnabled) { if (trackEnabled) {
restartFrom(pendingResetPositionUs); restartFrom(pendingResetPositionUs);
...@@ -365,7 +362,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -365,7 +362,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override @Override
public int onLoadError(Loadable loadable, IOException e) { public int onLoadError(Loadable loadable, IOException e) {
Chunk currentLoadable = currentLoadableHolder.chunk;
long bytesLoaded = currentLoadable.bytesLoaded(); long bytesLoaded = currentLoadable.bytesLoaded();
boolean isMediaChunk = isMediaChunk(currentLoadable); boolean isMediaChunk = isMediaChunk(currentLoadable);
boolean cancelable = !isMediaChunk || bytesLoaded == 0 || mediaChunks.size() > 1; boolean cancelable = !isMediaChunk || bytesLoaded == 0 || mediaChunks.size() > 1;
...@@ -420,7 +416,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -420,7 +416,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
} }
private void clearCurrentLoadable() { private void clearCurrentLoadable() {
currentLoadableHolder.chunk = null; currentLoadable = null;
} }
private void maybeStartLoading() { private void maybeStartLoading() {
...@@ -429,44 +425,38 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -429,44 +425,38 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
} }
long now = SystemClock.elapsedRealtime(); long now = SystemClock.elapsedRealtime();
long nextLoadPositionUs = getNextLoadPositionUs(); if (now - lastPreferredQueueSizeEvaluationTimeMs > 5000) {
int queueSize = chunkSource.getPreferredQueueSize(downstreamPositionUs, readOnlyMediaChunks);
// Never discard the first chunk.
discardUpstreamMediaChunks(Math.max(1, queueSize));
lastPreferredQueueSizeEvaluationTimeMs = now;
}
// Evaluate the operation if (a) we don't have the next chunk yet and we're not finished, or (b) long nextLoadPositionUs = getNextLoadPositionUs();
// if the last evaluation was over 2000ms ago. boolean isNext = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, false);
if ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1) if (!isNext) {
|| (now - lastPerformedBufferOperation > 2000)) { return;
// Perform the evaluation.
currentLoadableHolder.endOfStream = false;
currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
long playbackPositionUs = pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs
: downstreamPositionUs;
chunkSource.getChunkOperation(readOnlyMediaChunks, playbackPositionUs, currentLoadableHolder);
loadingFinished = currentLoadableHolder.endOfStream;
boolean chunksDiscarded = discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
lastPerformedBufferOperation = now;
// Update the next load position as appropriate.
if (currentLoadableHolder.chunk == null) {
// Set loadPosition to -1 to indicate that we don't have anything to load.
nextLoadPositionUs = -1;
} else if (chunksDiscarded) {
// Chunks were discarded, so we need to re-evaluate the load position.
nextLoadPositionUs = getNextLoadPositionUs();
}
} }
boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, false); chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(),
if (!nextLoader) { pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs : downstreamPositionUs,
// We're not allowed to start loading yet. nextChunkHolder);
boolean endOfStream = nextChunkHolder.endOfStream;
Chunk nextLoadable = nextChunkHolder.chunk;
nextChunkHolder.clear();
if (endOfStream) {
loadingFinished = true;
loadControl.update(this, downstreamPositionUs, -1, false);
return; return;
} }
Chunk currentLoadable = currentLoadableHolder.chunk; if (nextLoadable == null) {
if (currentLoadable == null) {
// We're allowed to start loading, but have nothing to load.
return; return;
} }
currentLoadStartTimeMs = SystemClock.elapsedRealtime(); currentLoadStartTimeMs = now;
currentLoadable = nextLoadable;
if (isMediaChunk(currentLoadable)) { if (isMediaChunk(currentLoadable)) {
BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable; BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable;
mediaChunk.init(sampleQueue); mediaChunk.init(sampleQueue);
...@@ -514,6 +504,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -514,6 +504,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
while (mediaChunks.size() > queueLength) { while (mediaChunks.size() > queueLength) {
removed = mediaChunks.removeLast(); removed = mediaChunks.removeLast();
startTimeUs = removed.startTimeUs; startTimeUs = removed.startTimeUs;
loadingFinished = false;
} }
sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex()); sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex());
eventDispatcher.upstreamDiscarded(startTimeUs, endTimeUs); eventDispatcher.upstreamDiscarded(startTimeUs, endTimeUs);
......
...@@ -90,25 +90,32 @@ public interface ChunkSource { ...@@ -90,25 +90,32 @@ public interface ChunkSource {
void continueBuffering(long playbackPositionUs); void continueBuffering(long playbackPositionUs);
/** /**
* Updates the provided {@link ChunkOperationHolder} to contain the next operation that should * Evaluates whether {@link MediaChunk}s should be removed from the back of the queue.
* be performed by the calling {@link ChunkSampleSource}.
* <p> * <p>
* This method should only be called when the source is enabled. * Removing {@link MediaChunk}s 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.
* @param queue The queue of buffered {@link MediaChunk}s.
* @return The preferred queue size.
*/
int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue);
/**
* Gets the next chunk to load.
* <p>
* If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has
* been reached then {@link ChunkHolder#endOfStream} is set. If a chunk is not available but the
* end of the stream has not been reached, the {@link ChunkHolder} is not modified.
* *
* @param queue A representation of the currently buffered {@link MediaChunk}s. * @param previous The most recently loaded media chunk.
* @param playbackPositionUs The current playback position. If the queue is empty then this * @param playbackPositionUs The current playback position. If {@code previous} is null then this
* parameter is the position from which playback is expected to start (or restart) and hence * parameter is the position from which playback is expected to start (or restart) and hence
* should be interpreted as a seek position. * should be interpreted as a seek position.
* @param out A holder for the next operation, whose {@link ChunkOperationHolder#endOfStream} is * @param out A holder to populate.
* initially set to false, whose {@link ChunkOperationHolder#queueSize} is initially equal to
* the length of the queue, and whose {@link ChunkOperationHolder#chunk} is initially equal to
* null or a {@link Chunk} previously supplied by the {@link ChunkSource} that the caller has
* not yet finished loading. In the latter case the chunk can either be replaced or left
* unchanged. Note that leaving the chunk unchanged is both preferred and more efficient than
* replacing it with a new but identical chunk.
*/ */
void getChunkOperation(List<? extends MediaChunk> queue, long playbackPositionUs, void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out);
ChunkOperationHolder out);
/** /**
* Invoked when the {@link ChunkSampleSource} has finished loading a chunk obtained from this * Invoked when the {@link ChunkSampleSource} has finished loading a chunk obtained from this
......
...@@ -42,26 +42,30 @@ public interface FormatEvaluator { ...@@ -42,26 +42,30 @@ public interface FormatEvaluator {
* Update the supplied evaluation. * Update the supplied evaluation.
* <p> * <p>
* When invoked, {@code evaluation} must contain the currently selected format (null for an * When invoked, {@code evaluation} must contain the currently selected format (null for an
* initial evaluation), the most recent trigger (@link Chunk#TRIGGER_INITIAL} for an initial * initial evaluation) and the most recent trigger {@link Chunk#TRIGGER_INITIAL} for an initial
* evaluation) and the size of {@code queue}. The invocation will update the format and trigger, * evaluation).
* and may also reduce {@link Evaluation#queueSize} to indicate that chunks should be discarded
* from the end of the queue to allow re-buffering in a different format. The evaluation will
* always retain the first chunk in the queue, if there is one.
* *
* @param queue A read only representation of currently buffered chunks. Must not be empty unless * @param bufferedDurationUs The duration of media currently buffered in microseconds.
* the evaluation is at the start of playback or immediately follows a seek. All but the first
* chunk may be discarded. A caller may pass a singleton list containing only the most
* recently buffered chunk in the case that it does not support discarding of chunks.
* @param playbackPositionUs The current playback position in microseconds.
* @param switchingOverlapUs If switching format requires downloading overlapping media then this
* is the duration of the required overlap in microseconds. 0 otherwise.
* @param blacklistFlags An array whose length is equal to the number of available formats. A * @param blacklistFlags An array whose length is equal to the number of available formats. A
* {@code true} element indicates that a format is currently blacklisted and should not be * {@code true} element indicates that a format is currently blacklisted and should not be
* selected by the evaluation. At least one element must be {@code false}. * selected by the evaluation. At least one element must be {@code false}.
* @param evaluation The evaluation to be updated. * @param evaluation The evaluation to be updated.
*/ */
void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags,
long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation); Evaluation evaluation);
/**
* Evaluates whether to discard {@link MediaChunk}s from the queue.
*
* @param playbackPositionUs The current playback position in microseconds.
* @param queue The queue of buffered {@link MediaChunk}s.
* @param blacklistFlags An array whose length is equal to the number of available formats. A
* {@code true} element indicates that a format is currently blacklisted and should not be
* selected by the evaluation. At least one element must be {@code false}.
* @return The preferred queue size.
*/
int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue,
boolean[] blacklistFlags);
/** /**
* A format evaluation. * A format evaluation.
...@@ -78,11 +82,6 @@ public interface FormatEvaluator { ...@@ -78,11 +82,6 @@ public interface FormatEvaluator {
*/ */
public int trigger; public int trigger;
/**
* The desired size of the queue.
*/
public int queueSize;
public Evaluation() { public Evaluation() {
trigger = Chunk.TRIGGER_INITIAL; trigger = Chunk.TRIGGER_INITIAL;
} }
...@@ -128,8 +127,8 @@ public interface FormatEvaluator { ...@@ -128,8 +127,8 @@ public interface FormatEvaluator {
} }
@Override @Override
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, public void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags,
long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation) { Evaluation evaluation) {
// Count the number of non-blacklisted formats. // Count the number of non-blacklisted formats.
int nonBlacklistedFormatCount = 0; int nonBlacklistedFormatCount = 0;
for (int i = 0; i < blacklistFlags.length; i++) { for (int i = 0; i < blacklistFlags.length; i++) {
...@@ -156,6 +155,12 @@ public interface FormatEvaluator { ...@@ -156,6 +155,12 @@ public interface FormatEvaluator {
evaluation.format = newFormat; evaluation.format = newFormat;
} }
@Override
public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue,
boolean[] blacklistFlags) {
return queue.size();
}
} }
/** /**
...@@ -235,43 +240,18 @@ public interface FormatEvaluator { ...@@ -235,43 +240,18 @@ public interface FormatEvaluator {
} }
@Override @Override
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, public void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags,
long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation) { Evaluation evaluation) {
long bufferedDurationUs = queue.isEmpty() ? 0
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
if (switchingOverlapUs > 0) {
bufferedDurationUs = Math.max(0, bufferedDurationUs - switchingOverlapUs);
}
Format current = evaluation.format; Format current = evaluation.format;
Format ideal = determineIdealFormat(formats, blacklistFlags, Format ideal = determineIdealFormat(formats, blacklistFlags,
bandwidthMeter.getBitrateEstimate()); bandwidthMeter.getBitrateEstimate());
boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate; boolean isHigher = current != null && ideal.bitrate > current.bitrate;
boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate; boolean isLower = current != null && ideal.bitrate < current.bitrate;
if (isHigher) { if (isHigher && bufferedDurationUs < minDurationForQualityIncreaseUs) {
if (bufferedDurationUs < minDurationForQualityIncreaseUs) { // The ideal format is a higher quality, but we have insufficient buffer to safely switch
// The ideal format is a higher quality, but we have insufficient buffer to // up. Defer switching up for now.
// safely switch up. Defer switching up for now. ideal = current;
ideal = current; } else if (isLower && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
} else if (bufferedDurationUs >= minDurationToRetainAfterDiscardUs) {
// We're switching from an SD stream to a stream of higher resolution. Consider
// discarding already buffered media chunks. Specifically, discard media chunks starting
// from the first one that is of lower bandwidth, lower resolution and that is not HD.
for (int i = 1; i < queue.size(); i++) {
MediaChunk thisChunk = queue.get(i);
long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
&& thisChunk.format.bitrate < ideal.bitrate
&& thisChunk.format.height < ideal.height
&& thisChunk.format.height < 720
&& thisChunk.format.width < 1280) {
// Discard chunks from this one onwards.
evaluation.queueSize = i;
break;
}
}
}
} else if (isLower && current != null
&& bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
// The ideal format is a lower quality, but we have sufficient buffer to defer switching // The ideal format is a lower quality, but we have sufficient buffer to defer switching
// down for now. // down for now.
ideal = current; ideal = current;
...@@ -282,6 +262,40 @@ public interface FormatEvaluator { ...@@ -282,6 +262,40 @@ public interface FormatEvaluator {
evaluation.format = ideal; evaluation.format = ideal;
} }
@Override
public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue,
boolean[] blacklistFlags) {
if (queue.isEmpty()) {
return 0;
}
int queueSize = queue.size();
long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs;
if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) {
return queueSize;
}
Format current = queue.get(queueSize - 1).format;
Format ideal = determineIdealFormat(formats, blacklistFlags,
bandwidthMeter.getBitrateEstimate());
if (ideal.bitrate <= current.bitrate) {
return queueSize;
}
// Discard from the first SD chunk beyond minDurationToRetainAfterDiscardUs whose resolution
// and bitrate are both lower than the ideal format.
for (int i = 0; i < queueSize; i++) {
MediaChunk thisChunk = queue.get(i);
long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
&& thisChunk.format.bitrate < ideal.bitrate
&& thisChunk.format.height < ideal.height
&& thisChunk.format.height < 720
&& thisChunk.format.width < 1280) {
// Discard chunks from this one onwards.
return i;
}
}
return queueSize;
}
/** /**
* Compute the ideal format ignoring buffer health. * Compute the ideal format ignoring buffer health.
*/ */
......
...@@ -25,7 +25,7 @@ import com.google.android.exoplayer.TimeRange.StaticTimeRange; ...@@ -25,7 +25,7 @@ import com.google.android.exoplayer.TimeRange.StaticTimeRange;
import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroup;
import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkExtractorWrapper; import com.google.android.exoplayer.chunk.ChunkExtractorWrapper;
import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.ChunkHolder;
import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.ContainerMediaChunk; import com.google.android.exoplayer.chunk.ContainerMediaChunk;
import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator;
...@@ -297,17 +297,24 @@ public class DashChunkSource implements ChunkSource { ...@@ -297,17 +297,24 @@ public class DashChunkSource implements ChunkSource {
} }
@Override @Override
public final void getChunkOperation(List<? extends MediaChunk> queue, long playbackPositionUs, public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
ChunkOperationHolder out) { if (fatalError != null || enabledFormats.length < 2) {
return queue.size();
}
return adaptiveFormatEvaluator.evaluateQueueSize(playbackPositionUs, queue,
adaptiveFormatBlacklistFlags);
}
@Override
public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) {
if (fatalError != null) { if (fatalError != null) {
out.chunk = null;
return; return;
} }
evaluation.queueSize = queue.size();
if (evaluation.format == null || !lastChunkWasInitialization) { if (evaluation.format == null || !lastChunkWasInitialization) {
if (enabledFormats.length > 1) { if (enabledFormats.length > 1) {
adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, 0, adaptiveFormatBlacklistFlags, long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0;
adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags,
evaluation); evaluation);
} else { } else {
evaluation.format = enabledFormats[0]; evaluation.format = enabledFormats[0];
...@@ -316,26 +323,15 @@ public class DashChunkSource implements ChunkSource { ...@@ -316,26 +323,15 @@ public class DashChunkSource implements ChunkSource {
} }
Format selectedFormat = evaluation.format; Format selectedFormat = evaluation.format;
out.queueSize = evaluation.queueSize;
if (selectedFormat == null) { if (selectedFormat == null) {
out.chunk = null;
return;
} else if (out.queueSize == queue.size() && out.chunk != null
&& out.chunk.format == selectedFormat) {
// We already have a chunk, and the evaluation hasn't changed either the format or the size
// of the queue. Leave unchanged.
return; return;
} }
// In all cases where we return before instantiating a new chunk, we want out.chunk to be null.
out.chunk = null;
boolean startingNewPeriod; boolean startingNewPeriod;
PeriodHolder periodHolder; PeriodHolder periodHolder;
availableRange.getCurrentBoundsUs(availableRangeValues); availableRange.getCurrentBoundsUs(availableRangeValues);
if (queue.isEmpty()) { if (previous == null) {
if (live) { if (live) {
if (startAtLiveEdge) { if (startAtLiveEdge) {
// We want live streams to start at the live edge instead of the beginning of the // We want live streams to start at the live edge instead of the beginning of the
...@@ -358,7 +354,6 @@ public class DashChunkSource implements ChunkSource { ...@@ -358,7 +354,6 @@ public class DashChunkSource implements ChunkSource {
startAtLiveEdge = false; startAtLiveEdge = false;
} }
MediaChunk previous = queue.get(out.queueSize - 1);
long nextSegmentStartTimeUs = previous.endTimeUs; long nextSegmentStartTimeUs = previous.endTimeUs;
if (live && nextSegmentStartTimeUs < availableRangeValues[0]) { if (live && nextSegmentStartTimeUs < availableRangeValues[0]) {
// This is before the first chunk in the current manifest. // This is before the first chunk in the current manifest.
...@@ -433,9 +428,9 @@ public class DashChunkSource implements ChunkSource { ...@@ -433,9 +428,9 @@ public class DashChunkSource implements ChunkSource {
return; return;
} }
int segmentNum = queue.isEmpty() ? representationHolder.getSegmentNum(playbackPositionUs) int segmentNum = previous == null ? representationHolder.getSegmentNum(playbackPositionUs)
: startingNewPeriod ? representationHolder.getFirstAvailableSegmentNum() : startingNewPeriod ? representationHolder.getFirstAvailableSegmentNum()
: queue.get(out.queueSize - 1).getNextChunkIndex(); : previous.getNextChunkIndex();
Chunk nextMediaChunk = newMediaChunk(periodHolder, representationHolder, dataSource, Chunk nextMediaChunk = newMediaChunk(periodHolder, representationHolder, dataSource,
selectedFormat, sampleFormat, segmentNum, evaluation.trigger); selectedFormat, sampleFormat, segmentNum, evaluation.trigger);
lastChunkWasInitialization = false; lastChunkWasInitialization = false;
......
...@@ -19,7 +19,7 @@ import com.google.android.exoplayer.BehindLiveWindowException; ...@@ -19,7 +19,7 @@ import com.google.android.exoplayer.BehindLiveWindowException;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.Format; import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.ChunkHolder;
import com.google.android.exoplayer.chunk.DataChunk; import com.google.android.exoplayer.chunk.DataChunk;
import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
...@@ -317,21 +317,22 @@ public class HlsChunkSource { ...@@ -317,21 +317,22 @@ public class HlsChunkSource {
} }
/** /**
* Updates the provided {@link ChunkOperationHolder} to contain the next operation that should * Gets the next chunk to load.
* be performed by the calling {@link HlsSampleSource}. * <p>
* If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has
* been reached then {@link ChunkHolder#endOfStream} is set. If a chunk is not available but the
* end of the stream has not been reached, the {@link ChunkHolder} is not modified.
* *
* @param previousTsChunk The previously loaded chunk that the next chunk should follow. * @param previous The most recently loaded media chunk.
* @param playbackPositionUs The current playback position. If previousTsChunk is null then this * @param playbackPositionUs The current playback position. If {@code previous} is null then this
* parameter is the position from which playback is expected to start (or restart) and hence * parameter is the position from which playback is expected to start (or restart) and hence
* should be interpreted as a seek position. * should be interpreted as a seek position.
* @param out The holder to populate with the result. {@link ChunkOperationHolder#queueSize} is * @param out A holder to populate.
* unused.
*/ */
public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs, public void getNextChunk(TsChunk previous, long playbackPositionUs, ChunkHolder out) {
ChunkOperationHolder out) { int variantIndex = getNextVariantIndex(previous, playbackPositionUs);
int variantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs); boolean switchingVariant = previous != null
boolean switchingVariant = previousTsChunk != null && variants[variantIndex].format != previous.format;
&& variants[variantIndex].format != previousTsChunk.format;
HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex]; HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex];
if (mediaPlaylist == null) { if (mediaPlaylist == null) {
...@@ -342,11 +343,10 @@ public class HlsChunkSource { ...@@ -342,11 +343,10 @@ public class HlsChunkSource {
int chunkMediaSequence = 0; int chunkMediaSequence = 0;
if (live) { if (live) {
if (previousTsChunk == null) { if (previous == null) {
chunkMediaSequence = getLiveStartChunkMediaSequence(variantIndex); chunkMediaSequence = getLiveStartChunkMediaSequence(variantIndex);
} else { } else {
chunkMediaSequence = switchingVariant ? previousTsChunk.chunkIndex chunkMediaSequence = switchingVariant ? previous.chunkIndex : previous.chunkIndex + 1;
: previousTsChunk.chunkIndex + 1;
if (chunkMediaSequence < mediaPlaylist.mediaSequence) { if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
fatalError = new BehindLiveWindowException(); fatalError = new BehindLiveWindowException();
return; return;
...@@ -354,12 +354,11 @@ public class HlsChunkSource { ...@@ -354,12 +354,11 @@ public class HlsChunkSource {
} }
} else { } else {
// Not live. // Not live.
if (previousTsChunk == null) { if (previous == null) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs,
true, true) + mediaPlaylist.mediaSequence; true, true) + mediaPlaylist.mediaSequence;
} else { } else {
chunkMediaSequence = switchingVariant ? previousTsChunk.chunkIndex chunkMediaSequence = switchingVariant ? previous.chunkIndex : previous.chunkIndex + 1;
: previousTsChunk.chunkIndex + 1;
} }
} }
...@@ -398,12 +397,12 @@ public class HlsChunkSource { ...@@ -398,12 +397,12 @@ public class HlsChunkSource {
// Compute start and end times, and the sequence number of the next chunk. // Compute start and end times, and the sequence number of the next chunk.
long startTimeUs; long startTimeUs;
if (live) { if (live) {
if (previousTsChunk == null) { if (previous == null) {
startTimeUs = 0; startTimeUs = 0;
} else if (switchingVariant) { } else if (switchingVariant) {
startTimeUs = previousTsChunk.startTimeUs; startTimeUs = previous.startTimeUs;
} else { } else {
startTimeUs = previousTsChunk.endTimeUs; startTimeUs = previous.endTimeUs;
} }
} else /* Not live */ { } else /* Not live */ {
startTimeUs = segment.startTimeUs; startTimeUs = segment.startTimeUs;
...@@ -439,9 +438,9 @@ public class HlsChunkSource { ...@@ -439,9 +438,9 @@ public class HlsChunkSource {
Extractor extractor = new WebvttExtractor(format.language, timestampAdjuster); Extractor extractor = new WebvttExtractor(format.language, timestampAdjuster);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariant); switchingVariant);
} else if (previousTsChunk == null } else if (previous == null
|| previousTsChunk.discontinuitySequenceNumber != segment.discontinuitySequenceNumber || previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber
|| format != previousTsChunk.format) { || format != previous.format) {
// MPEG-2 TS segments, but we need a new extractor. // MPEG-2 TS segments, but we need a new extractor.
PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(true, PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(true,
segment.discontinuitySequenceNumber, startTimeUs); segment.discontinuitySequenceNumber, startTimeUs);
...@@ -467,7 +466,7 @@ public class HlsChunkSource { ...@@ -467,7 +466,7 @@ public class HlsChunkSource {
switchingVariant); switchingVariant);
} else { } else {
// MPEG-2 TS segments, and we need to continue using the same extractor. // MPEG-2 TS segments, and we need to continue using the same extractor.
extractorWrapper = previousTsChunk.extractorWrapper; extractorWrapper = previous.extractorWrapper;
} }
out.chunk = new TsChunk(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, out.chunk = new TsChunk(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs,
...@@ -596,20 +595,19 @@ public class HlsChunkSource { ...@@ -596,20 +595,19 @@ public class HlsChunkSource {
return false; return false;
} }
private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) { private int getNextVariantIndex(TsChunk previous, long playbackPositionUs) {
clearStaleBlacklistedVariants(); clearStaleBlacklistedVariants();
long switchingOverlapUs; long bufferedDurationUs;
List<TsChunk> queue; if (previous != null) {
if (previousTsChunk != null) { // Use start time of the previous chunk rather than its end time because switching format will
switchingOverlapUs = previousTsChunk.endTimeUs - previousTsChunk.startTimeUs; // require downloading overlapping segments.
queue = Collections.singletonList(previousTsChunk); bufferedDurationUs = Math.max(0, previous.startTimeUs - playbackPositionUs);
} else { } else {
switchingOverlapUs = 0; bufferedDurationUs = 0;
queue = Collections.<TsChunk>emptyList();
} }
if (enabledVariants.length > 1) { if (enabledVariants.length > 1) {
adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, switchingOverlapUs, adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, enabledVariantBlacklistFlags,
enabledVariantBlacklistFlags, evaluation); evaluation);
} else { } else {
evaluation.format = enabledVariants[0].format; evaluation.format = enabledVariants[0].format;
evaluation.trigger = Chunk.TRIGGER_MANUAL; evaluation.trigger = Chunk.TRIGGER_MANUAL;
......
...@@ -26,7 +26,7 @@ import com.google.android.exoplayer.TrackGroupArray; ...@@ -26,7 +26,7 @@ import com.google.android.exoplayer.TrackGroupArray;
import com.google.android.exoplayer.TrackSelection; import com.google.android.exoplayer.TrackSelection;
import com.google.android.exoplayer.TrackStream; import com.google.android.exoplayer.TrackStream;
import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.ChunkHolder;
import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener; import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener;
import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener.EventDispatcher; import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener.EventDispatcher;
import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader;
...@@ -63,7 +63,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -63,7 +63,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
private final HlsChunkSource chunkSource; private final HlsChunkSource chunkSource;
private final LinkedList<HlsExtractorWrapper> extractors; private final LinkedList<HlsExtractorWrapper> extractors;
private final int bufferSizeContribution; private final int bufferSizeContribution;
private final ChunkOperationHolder chunkOperationHolder; private final ChunkHolder nextChunkHolder;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final LoadControl loadControl; private final LoadControl loadControl;
...@@ -116,7 +116,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -116,7 +116,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
loader = new Loader("Loader:HLS", minLoadableRetryCount); loader = new Loader("Loader:HLS", minLoadableRetryCount);
eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId); eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId);
extractors = new LinkedList<>(); extractors = new LinkedList<>();
chunkOperationHolder = new ChunkOperationHolder(); nextChunkHolder = new ChunkHolder();
} }
// SampleSource implementation. // SampleSource implementation.
...@@ -301,9 +301,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -301,9 +301,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
/* package */ void maybeThrowError() throws IOException { /* package */ void maybeThrowError() throws IOException {
loader.maybeThrowError(); loader.maybeThrowError();
if (currentLoadable == null) { chunkSource.maybeThrowError();
chunkSource.maybeThrowError();
}
} }
/* package */ long readReset(int group) { /* package */ long readReset(int group) {
...@@ -641,12 +639,12 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -641,12 +639,12 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
return; return;
} }
chunkSource.getChunkOperation(previousTsLoadable, chunkSource.getNextChunk(previousTsLoadable,
pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs : downstreamPositionUs, pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs : downstreamPositionUs,
chunkOperationHolder); nextChunkHolder);
boolean endOfStream = chunkOperationHolder.endOfStream; boolean endOfStream = nextChunkHolder.endOfStream;
Chunk nextLoadable = chunkOperationHolder.chunk; Chunk nextLoadable = nextChunkHolder.chunk;
chunkOperationHolder.clear(); nextChunkHolder.clear();
if (endOfStream) { if (endOfStream) {
loadingFinished = true; loadingFinished = true;
......
...@@ -22,7 +22,7 @@ import com.google.android.exoplayer.Format.DecreasingBandwidthComparator; ...@@ -22,7 +22,7 @@ import com.google.android.exoplayer.Format.DecreasingBandwidthComparator;
import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroup;
import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkExtractorWrapper; import com.google.android.exoplayer.chunk.ChunkExtractorWrapper;
import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.ChunkHolder;
import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.ContainerMediaChunk; import com.google.android.exoplayer.chunk.ContainerMediaChunk;
import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator;
...@@ -208,16 +208,23 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -208,16 +208,23 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
@Override @Override
public final void getChunkOperation(List<? extends MediaChunk> queue, long playbackPositionUs, public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
ChunkOperationHolder out) { if (fatalError != null || enabledFormats.length < 2) {
return queue.size();
}
return adaptiveFormatEvaluator.evaluateQueueSize(playbackPositionUs, queue,
adaptiveFormatBlacklistFlags);
}
@Override
public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) {
if (fatalError != null) { if (fatalError != null) {
out.chunk = null;
return; return;
} }
evaluation.queueSize = queue.size();
if (enabledFormats.length > 1) { if (enabledFormats.length > 1) {
adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, 0, adaptiveFormatBlacklistFlags, long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0;
adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags,
evaluation); evaluation);
} else { } else {
evaluation.format = enabledFormats[0]; evaluation.format = enabledFormats[0];
...@@ -225,21 +232,10 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -225,21 +232,10 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
Format selectedFormat = evaluation.format; Format selectedFormat = evaluation.format;
out.queueSize = evaluation.queueSize;
if (selectedFormat == null) { if (selectedFormat == null) {
out.chunk = null;
return;
} else if (out.queueSize == queue.size() && out.chunk != null
&& out.chunk.format == selectedFormat) {
// We already have a chunk, and the evaluation hasn't changed either the format or the size
// of the queue. Leave unchanged.
return; return;
} }
// In all cases where we return before instantiating a new chunk, we want out.chunk to be null.
out.chunk = null;
StreamElement streamElement = currentManifest.streamElements[elementIndex]; StreamElement streamElement = currentManifest.streamElements[elementIndex];
if (streamElement.chunkCount == 0) { if (streamElement.chunkCount == 0) {
if (currentManifest.isLive) { if (currentManifest.isLive) {
...@@ -251,13 +247,12 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -251,13 +247,12 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
int chunkIndex; int chunkIndex;
if (queue.isEmpty()) { if (previous == null) {
if (live) { if (live) {
playbackPositionUs = getLiveSeekPosition(currentManifest, liveEdgeLatencyUs); playbackPositionUs = getLiveSeekPosition(currentManifest, liveEdgeLatencyUs);
} }
chunkIndex = streamElement.getChunkIndex(playbackPositionUs); chunkIndex = streamElement.getChunkIndex(playbackPositionUs);
} else { } else {
MediaChunk previous = queue.get(out.queueSize - 1);
chunkIndex = previous.chunkIndex + 1 - currentManifestChunkOffset; chunkIndex = previous.chunkIndex + 1 - currentManifestChunkOffset;
} }
......
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