Commit a4d17282 by olly Committed by Oliver Woodman

Have loader implement retry logic.

This was made possible by the simplification of how DASH/SS
chunk replacement works. It is also a step towards eliminating
continueBuffering(), since continueBuffering() calls are no
longer relied upon to resume a backed off load.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=118774865
parent d83b89cf
...@@ -23,7 +23,6 @@ import com.google.android.exoplayer.util.Assertions; ...@@ -23,7 +23,6 @@ import com.google.android.exoplayer.util.Assertions;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
...@@ -78,9 +77,6 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load ...@@ -78,9 +77,6 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
private long pendingResetPositionUs; private long pendingResetPositionUs;
private boolean loadingFinished; private boolean loadingFinished;
private Loader loader; private Loader loader;
private IOException currentLoadableException;
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
private int streamState; private int streamState;
private byte[] sampleData; private byte[] sampleData;
...@@ -112,9 +108,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load ...@@ -112,9 +108,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
@Override @Override
public void maybeThrowError() throws IOException { public void maybeThrowError() throws IOException {
if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { loader.maybeThrowError();
throw currentLoadableException;
}
} }
@Override @Override
...@@ -122,7 +116,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load ...@@ -122,7 +116,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
if (prepared) { if (prepared) {
return true; return true;
} }
loader = new Loader("Loader:" + format.sampleMimeType); loader = new Loader("Loader:" + format.sampleMimeType, minLoadableRetryCount);
prepared = true; prepared = true;
return true; return true;
} }
...@@ -146,6 +140,9 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load ...@@ -146,6 +140,9 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
// Unselect old tracks. // Unselect old tracks.
if (!oldStreams.isEmpty()) { if (!oldStreams.isEmpty()) {
streamState = STREAM_STATE_END_OF_STREAM; streamState = STREAM_STATE_END_OF_STREAM;
if (loader.isLoading()) {
loader.cancelLoading();
}
} }
// Select new tracks. // Select new tracks.
TrackStream[] newStreams = new TrackStream[newSelections.size()]; TrackStream[] newStreams = new TrackStream[newSelections.size()];
...@@ -153,7 +150,6 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load ...@@ -153,7 +150,6 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
newStreams[0] = this; newStreams[0] = this;
streamState = STREAM_STATE_SEND_FORMAT; streamState = STREAM_STATE_SEND_FORMAT;
pendingResetPositionUs = NO_RESET; pendingResetPositionUs = NO_RESET;
clearCurrentLoadableException();
maybeStartLoading(); maybeStartLoading();
} }
return newStreams; return newStreams;
...@@ -161,7 +157,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load ...@@ -161,7 +157,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
@Override @Override
public void continueBuffering(long positionUs) { public void continueBuffering(long positionUs) {
maybeStartLoading(); // Do nothing.
} }
@Override @Override
...@@ -223,51 +219,22 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load ...@@ -223,51 +219,22 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
} }
} }
// Private methods.
private void maybeStartLoading() {
if (loadingFinished || streamState == STREAM_STATE_END_OF_STREAM || loader.isLoading()) {
return;
}
if (currentLoadableException != null) {
long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp;
if (elapsedMillis < getRetryDelayMillis(currentLoadableExceptionCount)) {
return;
}
currentLoadableException = null;
}
loader.startLoading(this, this);
}
private void clearCurrentLoadableException() {
currentLoadableException = null;
currentLoadableExceptionCount = 0;
}
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
// Loader.Callback implementation. // Loader.Callback implementation.
@Override @Override
public void onLoadCompleted(Loadable loadable) { public void onLoadCompleted(Loadable loadable) {
loadingFinished = true; loadingFinished = true;
clearCurrentLoadableException();
} }
@Override @Override
public void onLoadCanceled(Loadable loadable) { public void onLoadCanceled(Loadable loadable) {
// Never happens. maybeStartLoading();
} }
@Override @Override
public void onLoadError(Loadable loadable, IOException e) { public int onLoadError(Loadable loadable, IOException e) {
currentLoadableException = e;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
notifyLoadError(e); notifyLoadError(e);
maybeStartLoading(); return Loader.RETRY;
} }
// Loadable implementation. // Loadable implementation.
...@@ -303,6 +270,15 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load ...@@ -303,6 +270,15 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
} }
} }
// Private methods.
private void maybeStartLoading() {
if (loadingFinished || streamState == STREAM_STATE_END_OF_STREAM || loader.isLoading()) {
return;
}
loader.startLoading(this, this);
}
private void notifyLoadError(final IOException e) { private void notifyLoadError(final IOException e) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
......
...@@ -81,9 +81,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -81,9 +81,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
private Loader loader; private Loader loader;
private boolean loadingFinished; private boolean loadingFinished;
private boolean trackEnabled; private boolean trackEnabled;
private IOException currentLoadableException;
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
private long currentLoadStartTimeMs; private long currentLoadStartTimeMs;
private Format downstreamFormat; private Format downstreamFormat;
...@@ -154,7 +151,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -154,7 +151,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
durationUs = chunkSource.getDurationUs(); durationUs = chunkSource.getDurationUs();
TrackGroup tracks = chunkSource.getTracks(); TrackGroup tracks = chunkSource.getTracks();
if (tracks != null) { if (tracks != null) {
loader = new Loader("Loader:" + tracks.getFormat(0).containerMimeType); loader = new Loader("Loader:" + tracks.getFormat(0).containerMimeType, minLoadableRetryCount);
trackGroups = new TrackGroupArray(tracks); trackGroups = new TrackGroupArray(tracks);
} else { } else {
trackGroups = new TrackGroupArray(); trackGroups = new TrackGroupArray();
...@@ -315,9 +312,8 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -315,9 +312,8 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override @Override
public void maybeThrowError() throws IOException { public void maybeThrowError() throws IOException {
if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { loader.maybeThrowError();
throw currentLoadableException; if (currentLoadableHolder.chunk == null) {
} else if (currentLoadableHolder.chunk == null) {
chunkSource.maybeThrowError(); chunkSource.maybeThrowError();
} }
} }
...@@ -345,6 +341,8 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -345,6 +341,8 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
} }
} }
// Loadable.Callback implementation.
@Override @Override
public void onLoadCompleted(Loadable loadable) { public void onLoadCompleted(Loadable loadable) {
long now = SystemClock.elapsedRealtime(); long now = SystemClock.elapsedRealtime();
...@@ -373,13 +371,12 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -373,13 +371,12 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
} else { } else {
sampleQueue.clear(); sampleQueue.clear();
mediaChunks.clear(); mediaChunks.clear();
clearCurrentLoadable();
loadControl.trimAllocator(); loadControl.trimAllocator();
} }
} }
@Override @Override
public void onLoadError(Loadable loadable, IOException e) { public int onLoadError(Loadable loadable, IOException e) {
Chunk currentLoadable = currentLoadableHolder.chunk; Chunk currentLoadable = currentLoadableHolder.chunk;
long bytesLoaded = currentLoadable.bytesLoaded(); long bytesLoaded = currentLoadable.bytesLoaded();
boolean isMediaChunk = isMediaChunk(currentLoadable); boolean isMediaChunk = isMediaChunk(currentLoadable);
...@@ -396,13 +393,12 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -396,13 +393,12 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
clearCurrentLoadable(); clearCurrentLoadable();
notifyLoadError(e); notifyLoadError(e);
notifyLoadCanceled(bytesLoaded); notifyLoadCanceled(bytesLoaded);
maybeStartLoading();
return Loader.DONT_RETRY;
} else { } else {
currentLoadableException = e;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
notifyLoadError(e); notifyLoadError(e);
return Loader.RETRY;
} }
maybeStartLoading();
} }
/** /**
...@@ -431,24 +427,20 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -431,24 +427,20 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
private void clearCurrentLoadable() { private void clearCurrentLoadable() {
currentLoadableHolder.chunk = null; currentLoadableHolder.chunk = null;
clearCurrentLoadableException();
}
private void clearCurrentLoadableException() {
currentLoadableException = null;
currentLoadableExceptionCount = 0;
} }
private void maybeStartLoading() { private void maybeStartLoading() {
if (loader.isLoading()) {
return;
}
long now = SystemClock.elapsedRealtime(); long now = SystemClock.elapsedRealtime();
long nextLoadPositionUs = getNextLoadPositionUs(); 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 // Evaluate the operation if (a) we don't have the next chunk yet and we're not finished, or (b)
// chunk yet and we're not finished, or (b) if the last evaluation was over 2000ms ago. // if the last evaluation was over 2000ms ago.
if (!loadingOrBackedOff && ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1) if ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1)
|| (now - lastPerformedBufferOperation > 2000))) { || (now - lastPerformedBufferOperation > 2000)) {
// Perform the evaluation. // Perform the evaluation.
currentLoadableHolder.endOfStream = false; currentLoadableHolder.endOfStream = false;
currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
...@@ -468,41 +460,35 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -468,41 +460,35 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
} }
} }
// Update the control with our current state, and determine whether we're the next loader. boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, false);
boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, if (!nextLoader) {
loadingOrBackedOff); // We're not allowed to start loading yet.
return;
}
if (isBackedOff) { Chunk currentLoadable = currentLoadableHolder.chunk;
long elapsedMillis = now - currentLoadableExceptionTimestamp; if (currentLoadable == null) {
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { // We're allowed to start loading, but have nothing to load.
currentLoadableException = null;
loader.startLoading(currentLoadableHolder.chunk, this);
}
return; return;
} }
if (!loader.isLoading() && nextLoader) { currentLoadStartTimeMs = SystemClock.elapsedRealtime();
Chunk currentLoadable = currentLoadableHolder.chunk; if (isMediaChunk(currentLoadable)) {
if (currentLoadable == null) { BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable;
// Nothing to load. mediaChunk.init(sampleQueue);
return; mediaChunks.add(mediaChunk);
} if (isPendingReset()) {
currentLoadStartTimeMs = SystemClock.elapsedRealtime(); pendingResetPositionUs = NO_RESET_PENDING;
if (isMediaChunk(currentLoadable)) {
BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable;
mediaChunk.init(sampleQueue);
mediaChunks.add(mediaChunk);
if (isPendingReset()) {
pendingResetPositionUs = NO_RESET_PENDING;
}
notifyLoadStarted(mediaChunk.dataSpec.length, mediaChunk.type, mediaChunk.trigger,
mediaChunk.format, mediaChunk.startTimeUs, mediaChunk.endTimeUs);
} else {
notifyLoadStarted(currentLoadable.dataSpec.length, currentLoadable.type,
currentLoadable.trigger, currentLoadable.format, -1, -1);
} }
loader.startLoading(currentLoadable, this); notifyLoadStarted(mediaChunk.dataSpec.length, mediaChunk.type, mediaChunk.trigger,
mediaChunk.format, mediaChunk.startTimeUs, mediaChunk.endTimeUs);
} else {
notifyLoadStarted(currentLoadable.dataSpec.length, currentLoadable.type,
currentLoadable.trigger, currentLoadable.format, -1, -1);
} }
loader.startLoading(currentLoadable, this);
// Update the load control again to indicate that we're now loading.
loadControl.update(this, downstreamPositionUs, getNextLoadPositionUs(), true);
} }
/** /**
...@@ -549,10 +535,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call ...@@ -549,10 +535,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
return pendingResetPositionUs != NO_RESET_PENDING; return pendingResetPositionUs != NO_RESET_PENDING;
} }
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
protected final long usToMs(long timeUs) { protected final long usToMs(long timeUs) {
return timeUs / 1000; return timeUs / 1000;
} }
......
...@@ -123,7 +123,7 @@ public final class UtcTimingElementResolver implements Loader.Callback { ...@@ -123,7 +123,7 @@ public final class UtcTimingElementResolver implements Loader.Callback {
} }
private void resolveHttp(UriLoadable.Parser<Long> parser) { private void resolveHttp(UriLoadable.Parser<Long> parser) {
singleUseLoader = new Loader("utctiming"); singleUseLoader = new Loader("utctiming", 0);
singleUseLoadable = new UriLoadable<>(Uri.parse(timingElement.value), dataSource, parser); singleUseLoadable = new UriLoadable<>(Uri.parse(timingElement.value), dataSource, parser);
singleUseLoader.startLoading(singleUseLoadable, this); singleUseLoader.startLoading(singleUseLoadable, this);
} }
...@@ -141,9 +141,10 @@ public final class UtcTimingElementResolver implements Loader.Callback { ...@@ -141,9 +141,10 @@ public final class UtcTimingElementResolver implements Loader.Callback {
} }
@Override @Override
public void onLoadError(Loadable loadable, IOException exception) { public int onLoadError(Loadable loadable, IOException exception) {
releaseLoader(); releaseLoader();
callback.onTimestampError(timingElement, exception); callback.onTimestampError(timingElement, exception);
return Loader.DONT_RETRY;
} }
private void releaseLoader() { private void releaseLoader() {
......
...@@ -100,9 +100,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -100,9 +100,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
private TsChunk previousTsLoadable; private TsChunk previousTsLoadable;
private Loader loader; private Loader loader;
private IOException currentLoadableException;
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
private long currentLoadStartTimeMs; private long currentLoadStartTimeMs;
public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl,
...@@ -163,7 +160,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -163,7 +160,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
} }
// We're not prepared and we haven't loaded what we need. // We're not prepared and we haven't loaded what we need.
if (loader == null) { if (loader == null) {
loader = new Loader("Loader:HLS"); loader = new Loader("Loader:HLS", minLoadableRetryCount);
loadControl.register(this, bufferSizeContribution); loadControl.register(this, bufferSizeContribution);
loadControlRegistered = true; loadControlRegistered = true;
} }
...@@ -333,9 +330,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -333,9 +330,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
} }
/* package */ void maybeThrowError() throws IOException { /* package */ void maybeThrowError() throws IOException {
if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { loader.maybeThrowError();
throw currentLoadableException; if (currentLoadable == null) {
} else if (currentLoadable == null) {
chunkSource.maybeThrowError(); chunkSource.maybeThrowError();
} }
} }
...@@ -416,7 +412,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -416,7 +412,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
} }
@Override @Override
public void onLoadError(Loadable loadable, IOException e) { public int onLoadError(Loadable loadable, IOException e) {
long bytesLoaded = currentLoadable.bytesLoaded(); long bytesLoaded = currentLoadable.bytesLoaded();
boolean cancelable = !isTsChunk(currentLoadable) || bytesLoaded == 0; boolean cancelable = !isTsChunk(currentLoadable) || bytesLoaded == 0;
if (chunkSource.onChunkLoadError(currentLoadable, cancelable, e)) { if (chunkSource.onChunkLoadError(currentLoadable, cancelable, e)) {
...@@ -426,13 +422,12 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -426,13 +422,12 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
clearCurrentLoadable(); clearCurrentLoadable();
notifyLoadError(e); notifyLoadError(e);
notifyLoadCanceled(bytesLoaded); notifyLoadCanceled(bytesLoaded);
maybeStartLoading();
return Loader.DONT_RETRY;
} else { } else {
currentLoadableException = e;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
notifyLoadError(e); notifyLoadError(e);
return Loader.RETRY;
} }
maybeStartLoading();
} }
// Internal stuff. // Internal stuff.
...@@ -642,30 +637,16 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -642,30 +637,16 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
private void clearCurrentLoadable() { private void clearCurrentLoadable() {
currentTsLoadable = null; currentTsLoadable = null;
currentLoadable = null; currentLoadable = null;
currentLoadableException = null;
currentLoadableExceptionCount = 0;
} }
private void maybeStartLoading() { private void maybeStartLoading() {
long now = SystemClock.elapsedRealtime(); if (loader.isLoading()) {
long nextLoadPositionUs = getNextLoadPositionUs();
boolean isBackedOff = currentLoadableException != null;
boolean loadingOrBackedOff = loader.isLoading() || isBackedOff;
// Update the control with our current state, and determine whether we're the next loader.
boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs,
loadingOrBackedOff);
if (isBackedOff) {
long elapsedMillis = now - currentLoadableExceptionTimestamp;
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
currentLoadableException = null;
loader.startLoading(currentLoadable, this);
}
return; return;
} }
if (loader.isLoading() || !nextLoader || (prepared && enabledTrackCount == 0)) { long nextLoadPositionUs = getNextLoadPositionUs();
boolean isNext = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, false);
if (!isNext || (prepared && enabledTrackCount == 0)) {
return; return;
} }
...@@ -681,11 +662,12 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -681,11 +662,12 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
loadControl.update(this, downstreamPositionUs, -1, false); loadControl.update(this, downstreamPositionUs, -1, false);
return; return;
} }
if (nextLoadable == null) { if (nextLoadable == null) {
return; return;
} }
currentLoadStartTimeMs = now; currentLoadStartTimeMs = SystemClock.elapsedRealtime();
currentLoadable = nextLoadable; currentLoadable = nextLoadable;
if (isTsChunk(currentLoadable)) { if (isTsChunk(currentLoadable)) {
TsChunk tsChunk = (TsChunk) currentLoadable; TsChunk tsChunk = (TsChunk) currentLoadable;
...@@ -705,6 +687,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -705,6 +687,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
currentLoadable.trigger, currentLoadable.format, -1, -1); currentLoadable.trigger, currentLoadable.format, -1, -1);
} }
loader.startLoading(currentLoadable, this); loader.startLoading(currentLoadable, this);
// Update the load control again to indicate that we're now loading.
loadControl.update(this, downstreamPositionUs, getNextLoadPositionUs(), true);
} }
/** /**
...@@ -728,10 +712,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -728,10 +712,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
return pendingResetPositionUs != NO_RESET_PENDING; return pendingResetPositionUs != NO_RESET_PENDING;
} }
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
/* package */ long usToMs(long timeUs) { /* package */ long usToMs(long timeUs) {
return timeUs / 1000; return timeUs / 1000;
} }
......
...@@ -77,82 +77,100 @@ public final class Loader { ...@@ -77,82 +77,100 @@ public final class Loader {
public interface Callback { public interface Callback {
/** /**
* Invoked when loading has been canceled. * Invoked when a load has been canceled.
* *
* @param loadable The loadable whose load has been canceled. * @param loadable The loadable whose load has been canceled.
*/ */
void onLoadCanceled(Loadable loadable); void onLoadCanceled(Loadable loadable);
/** /**
* Invoked when the data source has been fully loaded. * Invoked when a load has completed.
* *
* @param loadable The loadable whose load has completed. * @param loadable The loadable whose load has completed.
*/ */
void onLoadCompleted(Loadable loadable); void onLoadCompleted(Loadable loadable);
/** /**
* Invoked when the data source is stopped due to an error. * Invoked when a load encounters an error.
* *
* @param loadable The loadable whose load has failed. * @param loadable The loadable whose load has encountered an error.
* @param exception The error.
* @return The desired retry action. One of {@link Loader#DONT_RETRY}, {@link Loader#RETRY} and
* {@link Loader#RETRY_RESET_ERROR_COUNT}.
*/ */
void onLoadError(Loadable loadable, IOException exception); int onLoadError(Loadable loadable, IOException exception);
} }
private static final int MSG_END_OF_SOURCE = 0; public static final int DONT_RETRY = 0;
private static final int MSG_IO_EXCEPTION = 1; public static final int RETRY = 1;
private static final int MSG_FATAL_ERROR = 2; public static final int RETRY_RESET_ERROR_COUNT = 2;
private static final int MSG_START = 0;
private static final int MSG_CANCEL = 1;
private static final int MSG_END_OF_SOURCE = 2;
private static final int MSG_IO_EXCEPTION = 3;
private static final int MSG_FATAL_ERROR = 4;
private final ExecutorService downloadExecutorService; private final ExecutorService downloadExecutorService;
private int minRetryCount;
private LoadTask currentTask; private LoadTask currentTask;
private boolean loading;
/** /**
* @param threadName A name for the loader's thread. * @param threadName A name for the loader's thread.
* @param minRetryCount The minimum retry count.
*/ */
public Loader(String threadName) { public Loader(String threadName, int minRetryCount) {
this.downloadExecutorService = Util.newSingleThreadExecutor(threadName); this.downloadExecutorService = Util.newSingleThreadExecutor(threadName);
this.minRetryCount = minRetryCount;
} }
/** /**
* Invokes {@link #startLoading(Looper, Loadable, Callback)}, using the {@link Looper} * Start loading a {@link Loadable}.
* associated with the calling thread. * <p>
* The calling thread must be a {@link Looper} thread, which is the thread on which the
* {@link Callback} will be invoked.
* *
* @param loadable The {@link Loadable} to load. * @param loadable The {@link Loadable} to load.
* @param callback A callback to invoke when the load ends. * @param callback A callback to invoke when the load ends.
* @throws IllegalStateException If the calling thread does not have an associated {@link Looper}. * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}.
*/ */
public void startLoading(Loadable loadable, Callback callback) { public void startLoading(Loadable loadable, Callback callback) {
Looper myLooper = Looper.myLooper(); Looper looper = Looper.myLooper();
Assertions.checkState(myLooper != null); Assertions.checkState(looper != null);
startLoading(myLooper, loadable, callback); new LoadTask(looper, loadable, callback).start(0);
} }
/** /**
* Start loading a {@link Loadable}. * Whether the {@link Loader} is currently loading a {@link Loadable}.
* <p>
* A {@link Loader} instance can only load one {@link Loadable} at a time, and so this method
* must not be called when another load is in progress.
* *
* @param looper The looper of the thread on which the callback should be invoked. * @return Whether the {@link Loader} is currently loading a {@link Loadable}.
* @param loadable The {@link Loadable} to load.
* @param callback A callback to invoke when the load ends.
*/ */
public void startLoading(Looper looper, Loadable loadable, Callback callback) { public boolean isLoading() {
Assertions.checkState(!loading); return currentTask != null;
loading = true;
currentTask = new LoadTask(looper, loadable, callback);
downloadExecutorService.submit(currentTask);
} }
/** /**
* Whether the {@link Loader} is currently loading a {@link Loadable}. * Sets the minimum retry count.
* *
* @return Whether the {@link Loader} is currently loading a {@link Loadable}. * @param minRetryCount The minimum retry count.
*/ */
public boolean isLoading() { public void setMinRetryCount(int minRetryCount) {
return loading; this.minRetryCount = minRetryCount;
}
/**
* If the current {@link Loadable} has incurred a number of errors greater than the minimum
* number of retries and if the load is currently backed off, then the most recent error is
* thrown. Else does nothing.
*
* @throws IOException The most recent error encountered by the current {@link Loadable}.
*/
public void maybeThrowError() throws IOException {
if (currentTask != null) {
currentTask.maybeThrowError(minRetryCount);
}
} }
/** /**
...@@ -161,8 +179,7 @@ public final class Loader { ...@@ -161,8 +179,7 @@ public final class Loader {
* This method should only be called when a load is in progress. * This method should only be called when a load is in progress.
*/ */
public void cancelLoading() { public void cancelLoading() {
Assertions.checkState(loading); currentTask.cancel();
currentTask.quit();
} }
/** /**
...@@ -171,7 +188,7 @@ public final class Loader { ...@@ -171,7 +188,7 @@ public final class Loader {
* This method should be called when the {@link Loader} is no longer required. * This method should be called when the {@link Loader} is no longer required.
*/ */
public void release() { public void release() {
if (loading) { if (currentTask != null) {
cancelLoading(); cancelLoading();
} }
downloadExecutorService.shutdown(); downloadExecutorService.shutdown();
...@@ -185,6 +202,9 @@ public final class Loader { ...@@ -185,6 +202,9 @@ public final class Loader {
private final Loadable loadable; private final Loadable loadable;
private final Loader.Callback callback; private final Loader.Callback callback;
private IOException currentError;
private int errorCount;
private volatile Thread executorThread; private volatile Thread executorThread;
public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback) { public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback) {
...@@ -193,10 +213,32 @@ public final class Loader { ...@@ -193,10 +213,32 @@ public final class Loader {
this.callback = callback; this.callback = callback;
} }
public void quit() { public void maybeThrowError(int minRetryCount) throws IOException {
loadable.cancelLoad(); if (currentError != null && errorCount > minRetryCount) {
if (executorThread != null) { throw currentError;
executorThread.interrupt(); }
}
public void start(long delayMillis) {
Assertions.checkState(currentTask == null);
currentTask = this;
if (delayMillis > 0) {
sendEmptyMessageDelayed(MSG_START, delayMillis);
} else {
submitToExecutor();
}
}
public void cancel() {
currentError = null;
if (hasMessages(MSG_START)) {
removeMessages(MSG_START);
sendEmptyMessage(MSG_CANCEL);
} else {
loadable.cancelLoad();
if (executorThread != null) {
executorThread.interrupt();
}
} }
} }
...@@ -235,29 +277,49 @@ public final class Loader { ...@@ -235,29 +277,49 @@ public final class Loader {
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
if (msg.what == MSG_START) {
submitToExecutor();
return;
}
if (msg.what == MSG_FATAL_ERROR) { if (msg.what == MSG_FATAL_ERROR) {
throw (Error) msg.obj; throw (Error) msg.obj;
} }
onFinished(); finish();
if (loadable.isLoadCanceled()) { if (loadable.isLoadCanceled()) {
callback.onLoadCanceled(loadable); callback.onLoadCanceled(loadable);
return; return;
} }
switch (msg.what) { switch (msg.what) {
case MSG_CANCEL:
callback.onLoadCanceled(loadable);
break;
case MSG_END_OF_SOURCE: case MSG_END_OF_SOURCE:
callback.onLoadCompleted(loadable); callback.onLoadCompleted(loadable);
break; break;
case MSG_IO_EXCEPTION: case MSG_IO_EXCEPTION:
callback.onLoadError(loadable, (IOException) msg.obj); currentError = (IOException) msg.obj;
int retryAction = callback.onLoadError(loadable, currentError);
if (retryAction != DONT_RETRY) {
errorCount = retryAction == RETRY_RESET_ERROR_COUNT ? 1 : errorCount + 1;
start(getRetryDelayMillis());
}
break; break;
} }
} }
private void onFinished() { private void submitToExecutor() {
loading = false; currentError = null;
downloadExecutorService.submit(currentTask);
}
private void finish() {
currentTask = null; currentTask = null;
} }
private long getRetryDelayMillis() {
return Math.min((errorCount - 1) * 1000, 5000);
}
} }
} }
...@@ -81,10 +81,6 @@ public class ManifestFetcher<T> implements Loader.Callback { ...@@ -81,10 +81,6 @@ public class ManifestFetcher<T> implements Loader.Callback {
private UriLoadable<T> currentLoadable; private UriLoadable<T> currentLoadable;
private long currentLoadStartTimestamp; private long currentLoadStartTimestamp;
private int loadExceptionCount;
private long loadExceptionTimestamp;
private ManifestIOException loadException;
private volatile T manifest; private volatile T manifest;
private volatile long manifestLoadStartTimestamp; private volatile long manifestLoadStartTimestamp;
private volatile long manifestLoadCompleteTimestamp; private volatile long manifestLoadCompleteTimestamp;
...@@ -162,21 +158,20 @@ public class ManifestFetcher<T> implements Loader.Callback { ...@@ -162,21 +158,20 @@ public class ManifestFetcher<T> implements Loader.Callback {
* manifest. * manifest.
*/ */
public void maybeThrowError() throws ManifestIOException { public void maybeThrowError() throws ManifestIOException {
// Don't throw an exception until at least 1 retry attempt has been made. if (loader != null) {
if (loadException == null || loadExceptionCount <= 1) { try {
return; loader.maybeThrowError();
} catch (IOException e) {
throw new ManifestIOException(e);
}
} }
throw loadException;
} }
/** /**
* Enables refresh functionality. * Enables refresh functionality.
*/ */
public void enable() { public void enable() {
if (enabledCount++ == 0) { enabledCount++;
loadExceptionCount = 0;
loadException = null;
}
} }
/** /**
...@@ -195,20 +190,16 @@ public class ManifestFetcher<T> implements Loader.Callback { ...@@ -195,20 +190,16 @@ public class ManifestFetcher<T> implements Loader.Callback {
* Should be invoked repeatedly by callers who require an updated manifest. * Should be invoked repeatedly by callers who require an updated manifest.
*/ */
public void requestRefresh() { public void requestRefresh() {
if (loadException != null && SystemClock.elapsedRealtime()
< (loadExceptionTimestamp + getRetryDelayMillis(loadExceptionCount))) {
// The previous load failed, and it's too soon to try again.
return;
}
if (loader == null) { if (loader == null) {
loader = new Loader("manifestLoader"); loader = new Loader("manifestLoader", 1);
} }
if (!loader.isLoading()) { if (loader.isLoading()) {
currentLoadable = new UriLoadable<>(manifestUri, dataSource, parser); return;
currentLoadStartTimestamp = SystemClock.elapsedRealtime();
loader.startLoading(currentLoadable, this);
notifyManifestRefreshStarted();
} }
currentLoadable = new UriLoadable<>(manifestUri, dataSource, parser);
currentLoadStartTimestamp = SystemClock.elapsedRealtime();
loader.startLoading(currentLoadable, this);
notifyManifestRefreshStarted();
} }
@Override @Override
...@@ -221,8 +212,6 @@ public class ManifestFetcher<T> implements Loader.Callback { ...@@ -221,8 +212,6 @@ public class ManifestFetcher<T> implements Loader.Callback {
manifest = currentLoadable.getResult(); manifest = currentLoadable.getResult();
manifestLoadStartTimestamp = currentLoadStartTimestamp; manifestLoadStartTimestamp = currentLoadStartTimestamp;
manifestLoadCompleteTimestamp = SystemClock.elapsedRealtime(); manifestLoadCompleteTimestamp = SystemClock.elapsedRealtime();
loadExceptionCount = 0;
loadException = null;
if (manifest instanceof RedirectingManifest) { if (manifest instanceof RedirectingManifest) {
RedirectingManifest redirectingManifest = (RedirectingManifest) manifest; RedirectingManifest redirectingManifest = (RedirectingManifest) manifest;
...@@ -241,21 +230,14 @@ public class ManifestFetcher<T> implements Loader.Callback { ...@@ -241,21 +230,14 @@ public class ManifestFetcher<T> implements Loader.Callback {
} }
@Override @Override
public void onLoadError(Loadable loadable, IOException exception) { public int onLoadError(Loadable loadable, IOException exception) {
if (currentLoadable != loadable) { if (currentLoadable != loadable) {
// Stale event. // Stale event.
return; return Loader.DONT_RETRY;
} }
loadExceptionCount++; notifyManifestError(new ManifestIOException(exception));
loadExceptionTimestamp = SystemClock.elapsedRealtime(); return Loader.RETRY;
loadException = new ManifestIOException(exception);
notifyManifestError(loadException);
}
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
} }
private void notifyManifestRefreshStarted() { private void notifyManifestRefreshStarted() {
......
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