From 759431048d7c990c2c3ef8a28e73c8e3e843b680 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:27:20 +0100 Subject: [PATCH] Treat "no chunk to load yet" in the same way as finished. The key change here is that nextLoadPositionUs is set to -1 if we're not loading but don't have a next chunk ready to load. This ensures that "missing chunks" in one stream don't prevent chunks in another stream from loading. This occurs in SmoothStreaming with TTML subtitles, where the chunks are sparse. --- .../android/exoplayer/DefaultLoadControl.java | 12 +-- .../google/android/exoplayer/LoadControl.java | 5 +- .../exoplayer/chunk/ChunkSampleSource.java | 73 ++++++++++++------- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java b/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java index 91bcac53ce..9131c4816c 100644 --- a/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java @@ -166,9 +166,9 @@ public class DefaultLoadControl implements LoadControl { // Update the loader state. int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs); LoaderState loaderState = loaderStates.get(loader); - boolean loaderStateChanged = loaderState.bufferState != loaderBufferState || - loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading || - loaderState.failed != failed; + boolean loaderStateChanged = loaderState.bufferState != loaderBufferState + || loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading + || loaderState.failed != failed; if (loaderStateChanged) { loaderState.bufferState = loaderBufferState; loaderState.nextLoadPositionUs = nextLoadPositionUs; @@ -214,17 +214,17 @@ public class DefaultLoadControl implements LoadControl { private void updateControlState() { boolean loading = false; boolean failed = false; - boolean finished = true; + boolean haveNextLoadPosition = false; int highestState = bufferPoolState; for (int i = 0; i < loaders.size(); i++) { LoaderState loaderState = loaderStates.get(loaders.get(i)); loading |= loaderState.loading; failed |= loaderState.failed; - finished &= loaderState.nextLoadPositionUs == -1; + haveNextLoadPosition |= loaderState.nextLoadPositionUs != -1; highestState = Math.max(highestState, loaderState.bufferState); } - fillingBuffers = !loaders.isEmpty() && !finished && !failed + fillingBuffers = !loaders.isEmpty() && !failed && (loading || haveNextLoadPosition) && (highestState == BELOW_LOW_WATERMARK || (highestState == BETWEEN_WATERMARKS && fillingBuffers)); if (fillingBuffers && !streamingPrioritySet) { diff --git a/library/src/main/java/com/google/android/exoplayer/LoadControl.java b/library/src/main/java/com/google/android/exoplayer/LoadControl.java index edc6ff023f..df6130017f 100644 --- a/library/src/main/java/com/google/android/exoplayer/LoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer/LoadControl.java @@ -65,9 +65,10 @@ public interface LoadControl { * * @param loader The loader invoking the update. * @param playbackPositionUs The loader's playback position. - * @param nextLoadPositionUs The loader's next load position, or -1 if finished. + * @param nextLoadPositionUs The loader's next load position. -1 if finished, failed, or if the + * next load position is not yet known. * @param loading Whether the loader is currently loading data. - * @param failed Whether the loader has failed, meaning it does not wish to load more data. + * @param failed Whether the loader has failed. * @return True if the loader is allowed to start its next load. False otherwise. */ boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs, diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index a436077d5f..f7d556f3c8 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -499,23 +499,40 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } private void updateLoadControl() { - long loadPositionUs; - if (isPendingReset()) { - loadPositionUs = pendingResetPositionUs; - } else { - MediaChunk lastMediaChunk = mediaChunks.getLast(); - loadPositionUs = lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs; - } - - boolean isBackedOff = currentLoadableException != null && !currentLoadableExceptionFatal; - boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs, - isBackedOff || loader.isLoading(), currentLoadableExceptionFatal); - if (currentLoadableExceptionFatal) { + // We've failed, but we still need to update the control with our current state. + loadControl.update(this, downstreamPositionUs, -1, false, true); return; } long now = SystemClock.elapsedRealtime(); + long nextLoadPositionUs = getNextLoadPositionUs(); + boolean isBackedOff = currentLoadableException != null; + boolean loadingOrBackedOff = loader.isLoading() || isBackedOff; + + // If we're not loading or backed off, 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 (!loadingOrBackedOff && ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1) + || (now - lastPerformedBufferOperation > 2000))) { + // Perform the evaluation. + lastPerformedBufferOperation = now; + currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); + chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, + downstreamPositionUs, currentLoadableHolder); + boolean chunksDiscarded = discardUpstreamMediaChunks(currentLoadableHolder.queueSize); + // 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(); + } + } + + // Update the control with our current state, and determine whether we're the next loader. + boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, + loadingOrBackedOff, false); if (isBackedOff) { long elapsedMillis = now - currentLoadableExceptionTimestamp; @@ -525,17 +542,21 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { return; } - if (!loader.isLoading()) { - if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) { - lastPerformedBufferOperation = now; - currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, - downstreamPositionUs, currentLoadableHolder); - discardUpstreamMediaChunks(currentLoadableHolder.queueSize); - } - if (nextLoader) { - maybeStartLoading(); - } + if (!loader.isLoading() && nextLoader) { + maybeStartLoading(); + } + } + + /** + * Gets the next load time, assuming that the next load starts where the previous chunk ended (or + * from the pending reset time, if there is one). + */ + private long getNextLoadPositionUs() { + if (isPendingReset()) { + return pendingResetPositionUs; + } else { + MediaChunk lastMediaChunk = mediaChunks.getLast(); + return lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs; } } @@ -652,10 +673,11 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { * Discard upstream media chunks until the queue length is equal to the length specified. * * @param queueLength The desired length of the queue. + * @return True if chunks were discarded. False otherwise. */ - private void discardUpstreamMediaChunks(int queueLength) { + private boolean discardUpstreamMediaChunks(int queueLength) { if (mediaChunks.size() <= queueLength) { - return; + return false; } long totalBytes = 0; long startTimeUs = 0; @@ -667,6 +689,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { removed.release(); } notifyUpstreamDiscarded(startTimeUs, endTimeUs, totalBytes); + return true; } private boolean isMediaChunk(Chunk chunk) {