Decouple next chunk evaluation and queue trimming.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=118925372
This commit is contained in:
olly 2016-04-04 03:51:11 -07:00 committed by Oliver Woodman
parent 1581b915d3
commit 9282710f04
8 changed files with 196 additions and 210 deletions

View file

@ -16,20 +16,9 @@
package com.google.android.exoplayer.chunk;
/**
* Holds a chunk operation, which consists of a either:
* <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>
* Holds a chunk or an indication that the end of the stream has been reached.
*/
public final class ChunkOperationHolder {
/**
* The number of {@link MediaChunk}s to retain in a queue.
*/
public int queueSize;
public final class ChunkHolder {
/**
* The chunk.
@ -45,7 +34,6 @@ public final class ChunkOperationHolder {
* Clears the holder.
*/
public void clear() {
queueSize = 0;
chunk = null;
endOfStream = false;
}

View file

@ -55,7 +55,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
private final Loader loader;
private final LoadControl loadControl;
private final ChunkSource chunkSource;
private final ChunkOperationHolder currentLoadableHolder;
private final ChunkHolder nextChunkHolder;
private final LinkedList<BaseMediaChunk> mediaChunks;
private final List<BaseMediaChunk> readOnlyMediaChunks;
private final DefaultTrackOutput sampleQueue;
@ -66,7 +66,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
private long downstreamPositionUs;
private long lastSeekPositionUs;
private long pendingResetPositionUs;
private long lastPerformedBufferOperation;
private long lastPreferredQueueSizeEvaluationTimeMs;
private boolean pendingReset;
private boolean loadControlRegistered;
@ -76,6 +76,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
private boolean trackEnabled;
private long currentLoadStartTimeMs;
private Chunk currentLoadable;
private Format downstreamFormat;
private Format downstreamSampleFormat;
@ -124,7 +125,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
this.bufferSizeContribution = bufferSizeContribution;
loader = new Loader("Loader:ChunkSampleSource", minLoadableRetryCount);
eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId);
currentLoadableHolder = new ChunkOperationHolder();
nextChunkHolder = new ChunkHolder();
mediaChunks = new LinkedList<>();
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
sampleQueue = new DefaultTrackOutput(loadControl.getAllocator());
@ -267,9 +268,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override
public void maybeThrowError() throws IOException {
loader.maybeThrowError();
if (currentLoadableHolder.chunk == null) {
chunkSource.maybeThrowError();
}
chunkSource.maybeThrowError();
}
@Override
@ -336,7 +335,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
public void onLoadCompleted(Loadable loadable) {
long now = SystemClock.elapsedRealtime();
long loadDurationMs = now - currentLoadStartTimeMs;
Chunk currentLoadable = currentLoadableHolder.chunk;
chunkSource.onChunkLoadCompleted(currentLoadable);
if (isMediaChunk(currentLoadable)) {
BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable;
@ -353,7 +351,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override
public void onLoadCanceled(Loadable loadable) {
Chunk currentLoadable = currentLoadableHolder.chunk;
eventDispatcher.loadCanceled(currentLoadable.bytesLoaded());
if (trackEnabled) {
restartFrom(pendingResetPositionUs);
@ -365,7 +362,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override
public int onLoadError(Loadable loadable, IOException e) {
Chunk currentLoadable = currentLoadableHolder.chunk;
long bytesLoaded = currentLoadable.bytesLoaded();
boolean isMediaChunk = isMediaChunk(currentLoadable);
boolean cancelable = !isMediaChunk || bytesLoaded == 0 || mediaChunks.size() > 1;
@ -420,7 +416,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
}
private void clearCurrentLoadable() {
currentLoadableHolder.chunk = null;
currentLoadable = null;
}
private void maybeStartLoading() {
@ -429,44 +425,38 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
}
long now = SystemClock.elapsedRealtime();
if (now - lastPreferredQueueSizeEvaluationTimeMs > 5000) {
int queueSize = chunkSource.getPreferredQueueSize(downstreamPositionUs, readOnlyMediaChunks);
// Never discard the first chunk.
discardUpstreamMediaChunks(Math.max(1, queueSize));
lastPreferredQueueSizeEvaluationTimeMs = now;
}
long nextLoadPositionUs = getNextLoadPositionUs();
// Evaluate the operation if (a) we don't have the next chunk yet and we're not finished, or (b)
// if the last evaluation was over 2000ms ago.
if ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1)
|| (now - lastPerformedBufferOperation > 2000)) {
// 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);
if (!nextLoader) {
// We're not allowed to start loading yet.
boolean isNext = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, false);
if (!isNext) {
return;
}
Chunk currentLoadable = currentLoadableHolder.chunk;
if (currentLoadable == null) {
// We're allowed to start loading, but have nothing to load.
chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(),
pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs : downstreamPositionUs,
nextChunkHolder);
boolean endOfStream = nextChunkHolder.endOfStream;
Chunk nextLoadable = nextChunkHolder.chunk;
nextChunkHolder.clear();
if (endOfStream) {
loadingFinished = true;
loadControl.update(this, downstreamPositionUs, -1, false);
return;
}
currentLoadStartTimeMs = SystemClock.elapsedRealtime();
if (nextLoadable == null) {
return;
}
currentLoadStartTimeMs = now;
currentLoadable = nextLoadable;
if (isMediaChunk(currentLoadable)) {
BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable;
mediaChunk.init(sampleQueue);
@ -514,6 +504,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
while (mediaChunks.size() > queueLength) {
removed = mediaChunks.removeLast();
startTimeUs = removed.startTimeUs;
loadingFinished = false;
}
sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex());
eventDispatcher.upstreamDiscarded(startTimeUs, endTimeUs);

View file

@ -90,25 +90,32 @@ public interface ChunkSource {
void continueBuffering(long playbackPositionUs);
/**
* Updates the provided {@link ChunkOperationHolder} to contain the next operation that should
* be performed by the calling {@link ChunkSampleSource}.
* Evaluates whether {@link MediaChunk}s should be removed from the back of the queue.
* <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 queue A representation of the currently buffered {@link MediaChunk}s.
* @param playbackPositionUs The current playback position. If the queue is empty then this
* @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 previous The most recently loaded media chunk.
* @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
* should be interpreted as a seek position.
* @param out A holder for the next operation, whose {@link ChunkOperationHolder#endOfStream} is
* 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.
* @param out A holder to populate.
*/
void getChunkOperation(List<? extends MediaChunk> queue, long playbackPositionUs,
ChunkOperationHolder out);
void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out);
/**
* Invoked when the {@link ChunkSampleSource} has finished loading a chunk obtained from this

View file

@ -42,26 +42,30 @@ public interface FormatEvaluator {
* Update the supplied evaluation.
* <p>
* 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
* evaluation) and the size of {@code queue}. The invocation will update the format and trigger,
* 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.
* initial evaluation) and the most recent trigger {@link Chunk#TRIGGER_INITIAL} for an initial
* evaluation).
*
* @param queue A read only representation of currently buffered chunks. Must not be empty unless
* 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 bufferedDurationUs The duration of media currently buffered in microseconds.
* @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}.
* @param evaluation The evaluation to be updated.
*/
void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation);
void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags,
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.
@ -78,11 +82,6 @@ public interface FormatEvaluator {
*/
public int trigger;
/**
* The desired size of the queue.
*/
public int queueSize;
public Evaluation() {
trigger = Chunk.TRIGGER_INITIAL;
}
@ -128,8 +127,8 @@ public interface FormatEvaluator {
}
@Override
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation) {
public void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags,
Evaluation evaluation) {
// Count the number of non-blacklisted formats.
int nonBlacklistedFormatCount = 0;
for (int i = 0; i < blacklistFlags.length; i++) {
@ -156,6 +155,12 @@ public interface FormatEvaluator {
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 {
}
@Override
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation) {
long bufferedDurationUs = queue.isEmpty() ? 0
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
if (switchingOverlapUs > 0) {
bufferedDurationUs = Math.max(0, bufferedDurationUs - switchingOverlapUs);
}
public void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags,
Evaluation evaluation) {
Format current = evaluation.format;
Format ideal = determineIdealFormat(formats, blacklistFlags,
bandwidthMeter.getBitrateEstimate());
boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate;
boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate;
if (isHigher) {
if (bufferedDurationUs < minDurationForQualityIncreaseUs) {
// The ideal format is a higher quality, but we have insufficient buffer to
// safely switch up. Defer switching up for now.
ideal = current;
} 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) {
boolean isHigher = current != null && ideal.bitrate > current.bitrate;
boolean isLower = current != null && ideal.bitrate < current.bitrate;
if (isHigher && bufferedDurationUs < minDurationForQualityIncreaseUs) {
// The ideal format is a higher quality, but we have insufficient buffer to safely switch
// up. Defer switching up for now.
ideal = current;
} else if (isLower && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
// The ideal format is a lower quality, but we have sufficient buffer to defer switching
// down for now.
ideal = current;
@ -282,6 +262,40 @@ public interface FormatEvaluator {
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.
*/

View file

@ -25,7 +25,7 @@ import com.google.android.exoplayer.TimeRange.StaticTimeRange;
import com.google.android.exoplayer.TrackGroup;
import com.google.android.exoplayer.chunk.Chunk;
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.ContainerMediaChunk;
import com.google.android.exoplayer.chunk.FormatEvaluator;
@ -297,17 +297,24 @@ public class DashChunkSource implements ChunkSource {
}
@Override
public final void getChunkOperation(List<? extends MediaChunk> queue, long playbackPositionUs,
ChunkOperationHolder out) {
public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
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) {
out.chunk = null;
return;
}
evaluation.queueSize = queue.size();
if (evaluation.format == null || !lastChunkWasInitialization) {
if (enabledFormats.length > 1) {
adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, 0, adaptiveFormatBlacklistFlags,
long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0;
adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags,
evaluation);
} else {
evaluation.format = enabledFormats[0];
@ -316,26 +323,15 @@ public class DashChunkSource implements ChunkSource {
}
Format selectedFormat = evaluation.format;
out.queueSize = evaluation.queueSize;
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;
}
// In all cases where we return before instantiating a new chunk, we want out.chunk to be null.
out.chunk = null;
boolean startingNewPeriod;
PeriodHolder periodHolder;
availableRange.getCurrentBoundsUs(availableRangeValues);
if (queue.isEmpty()) {
if (previous == null) {
if (live) {
if (startAtLiveEdge) {
// 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 {
startAtLiveEdge = false;
}
MediaChunk previous = queue.get(out.queueSize - 1);
long nextSegmentStartTimeUs = previous.endTimeUs;
if (live && nextSegmentStartTimeUs < availableRangeValues[0]) {
// This is before the first chunk in the current manifest.
@ -433,9 +428,9 @@ public class DashChunkSource implements ChunkSource {
return;
}
int segmentNum = queue.isEmpty() ? representationHolder.getSegmentNum(playbackPositionUs)
int segmentNum = previous == null ? representationHolder.getSegmentNum(playbackPositionUs)
: startingNewPeriod ? representationHolder.getFirstAvailableSegmentNum()
: queue.get(out.queueSize - 1).getNextChunkIndex();
: previous.getNextChunkIndex();
Chunk nextMediaChunk = newMediaChunk(periodHolder, representationHolder, dataSource,
selectedFormat, sampleFormat, segmentNum, evaluation.trigger);
lastChunkWasInitialization = false;

View file

@ -19,7 +19,7 @@ import com.google.android.exoplayer.BehindLiveWindowException;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.Format;
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.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
@ -317,21 +317,22 @@ public class HlsChunkSource {
}
/**
* Updates the provided {@link ChunkOperationHolder} to contain the next operation that should
* be performed by the calling {@link HlsSampleSource}.
* 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 previousTsChunk The previously loaded chunk that the next chunk should follow.
* @param playbackPositionUs The current playback position. If previousTsChunk is null then this
* @param previous The most recently loaded media chunk.
* @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
* should be interpreted as a seek position.
* @param out The holder to populate with the result. {@link ChunkOperationHolder#queueSize} is
* unused.
* @param out A holder to populate.
*/
public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs,
ChunkOperationHolder out) {
int variantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs);
boolean switchingVariant = previousTsChunk != null
&& variants[variantIndex].format != previousTsChunk.format;
public void getNextChunk(TsChunk previous, long playbackPositionUs, ChunkHolder out) {
int variantIndex = getNextVariantIndex(previous, playbackPositionUs);
boolean switchingVariant = previous != null
&& variants[variantIndex].format != previous.format;
HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex];
if (mediaPlaylist == null) {
@ -342,11 +343,10 @@ public class HlsChunkSource {
int chunkMediaSequence = 0;
if (live) {
if (previousTsChunk == null) {
if (previous == null) {
chunkMediaSequence = getLiveStartChunkMediaSequence(variantIndex);
} else {
chunkMediaSequence = switchingVariant ? previousTsChunk.chunkIndex
: previousTsChunk.chunkIndex + 1;
chunkMediaSequence = switchingVariant ? previous.chunkIndex : previous.chunkIndex + 1;
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
fatalError = new BehindLiveWindowException();
return;
@ -354,12 +354,11 @@ public class HlsChunkSource {
}
} else {
// Not live.
if (previousTsChunk == null) {
if (previous == null) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs,
true, true) + mediaPlaylist.mediaSequence;
} else {
chunkMediaSequence = switchingVariant ? previousTsChunk.chunkIndex
: previousTsChunk.chunkIndex + 1;
chunkMediaSequence = switchingVariant ? previous.chunkIndex : previous.chunkIndex + 1;
}
}
@ -398,12 +397,12 @@ public class HlsChunkSource {
// Compute start and end times, and the sequence number of the next chunk.
long startTimeUs;
if (live) {
if (previousTsChunk == null) {
if (previous == null) {
startTimeUs = 0;
} else if (switchingVariant) {
startTimeUs = previousTsChunk.startTimeUs;
startTimeUs = previous.startTimeUs;
} else {
startTimeUs = previousTsChunk.endTimeUs;
startTimeUs = previous.endTimeUs;
}
} else /* Not live */ {
startTimeUs = segment.startTimeUs;
@ -439,9 +438,9 @@ public class HlsChunkSource {
Extractor extractor = new WebvttExtractor(format.language, timestampAdjuster);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariant);
} else if (previousTsChunk == null
|| previousTsChunk.discontinuitySequenceNumber != segment.discontinuitySequenceNumber
|| format != previousTsChunk.format) {
} else if (previous == null
|| previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber
|| format != previous.format) {
// MPEG-2 TS segments, but we need a new extractor.
PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(true,
segment.discontinuitySequenceNumber, startTimeUs);
@ -467,7 +466,7 @@ public class HlsChunkSource {
switchingVariant);
} else {
// 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,
@ -596,20 +595,19 @@ public class HlsChunkSource {
return false;
}
private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) {
private int getNextVariantIndex(TsChunk previous, long playbackPositionUs) {
clearStaleBlacklistedVariants();
long switchingOverlapUs;
List<TsChunk> queue;
if (previousTsChunk != null) {
switchingOverlapUs = previousTsChunk.endTimeUs - previousTsChunk.startTimeUs;
queue = Collections.singletonList(previousTsChunk);
long bufferedDurationUs;
if (previous != null) {
// Use start time of the previous chunk rather than its end time because switching format will
// require downloading overlapping segments.
bufferedDurationUs = Math.max(0, previous.startTimeUs - playbackPositionUs);
} else {
switchingOverlapUs = 0;
queue = Collections.<TsChunk>emptyList();
bufferedDurationUs = 0;
}
if (enabledVariants.length > 1) {
adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, switchingOverlapUs,
enabledVariantBlacklistFlags, evaluation);
adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, enabledVariantBlacklistFlags,
evaluation);
} else {
evaluation.format = enabledVariants[0].format;
evaluation.trigger = Chunk.TRIGGER_MANUAL;

View file

@ -26,7 +26,7 @@ import com.google.android.exoplayer.TrackGroupArray;
import com.google.android.exoplayer.TrackSelection;
import com.google.android.exoplayer.TrackStream;
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.EventDispatcher;
import com.google.android.exoplayer.upstream.Loader;
@ -63,7 +63,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
private final HlsChunkSource chunkSource;
private final LinkedList<HlsExtractorWrapper> extractors;
private final int bufferSizeContribution;
private final ChunkOperationHolder chunkOperationHolder;
private final ChunkHolder nextChunkHolder;
private final EventDispatcher eventDispatcher;
private final LoadControl loadControl;
@ -116,7 +116,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
loader = new Loader("Loader:HLS", minLoadableRetryCount);
eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId);
extractors = new LinkedList<>();
chunkOperationHolder = new ChunkOperationHolder();
nextChunkHolder = new ChunkHolder();
}
// SampleSource implementation.
@ -301,9 +301,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
/* package */ void maybeThrowError() throws IOException {
loader.maybeThrowError();
if (currentLoadable == null) {
chunkSource.maybeThrowError();
}
chunkSource.maybeThrowError();
}
/* package */ long readReset(int group) {
@ -641,12 +639,12 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
return;
}
chunkSource.getChunkOperation(previousTsLoadable,
chunkSource.getNextChunk(previousTsLoadable,
pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs : downstreamPositionUs,
chunkOperationHolder);
boolean endOfStream = chunkOperationHolder.endOfStream;
Chunk nextLoadable = chunkOperationHolder.chunk;
chunkOperationHolder.clear();
nextChunkHolder);
boolean endOfStream = nextChunkHolder.endOfStream;
Chunk nextLoadable = nextChunkHolder.chunk;
nextChunkHolder.clear();
if (endOfStream) {
loadingFinished = true;

View file

@ -22,7 +22,7 @@ import com.google.android.exoplayer.Format.DecreasingBandwidthComparator;
import com.google.android.exoplayer.TrackGroup;
import com.google.android.exoplayer.chunk.Chunk;
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.ContainerMediaChunk;
import com.google.android.exoplayer.chunk.FormatEvaluator;
@ -208,16 +208,23 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
@Override
public final void getChunkOperation(List<? extends MediaChunk> queue, long playbackPositionUs,
ChunkOperationHolder out) {
public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
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) {
out.chunk = null;
return;
}
evaluation.queueSize = queue.size();
if (enabledFormats.length > 1) {
adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, 0, adaptiveFormatBlacklistFlags,
long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0;
adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags,
evaluation);
} else {
evaluation.format = enabledFormats[0];
@ -225,21 +232,10 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
Format selectedFormat = evaluation.format;
out.queueSize = evaluation.queueSize;
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;
}
// 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];
if (streamElement.chunkCount == 0) {
if (currentManifest.isLive) {
@ -251,13 +247,12 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
int chunkIndex;
if (queue.isEmpty()) {
if (previous == null) {
if (live) {
playbackPositionUs = getLiveSeekPosition(currentManifest, liveEdgeLatencyUs);
}
chunkIndex = streamElement.getChunkIndex(playbackPositionUs);
} else {
MediaChunk previous = queue.get(out.queueSize - 1);
chunkIndex = previous.chunkIndex + 1 - currentManifestChunkOffset;
}