blob: 4ff58b108cbaddeaea22c54ffcfb3c23d7e58269 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream;
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.ExecutorService;
/**
* Manages the background loading of {@link Loadable}s.
*/
public final class Loader implements LoaderErrorThrower {
/**
* Thrown when an unexpected exception or error is encountered during loading.
*/
public static final class UnexpectedLoaderException extends IOException {
public UnexpectedLoaderException(Throwable cause) {
super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
}
}
/**
* An object that can be loaded using a {@link Loader}.
*/
public interface Loadable {
/**
* Cancels the load.
*/
void cancelLoad();
/**
* Performs the load, returning on completion or cancellation.
*
* @throws IOException If the input could not be loaded.
*/
void load() throws IOException;
}
/**
* A callback to be notified of {@link Loader} events.
*/
public interface Callback<T extends Loadable> {
/**
* Called when a load has completed.
*
* <p>Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting
* and this callback being called.
*
* @param loadable The loadable whose load has completed.
* @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load ended.
* @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}
* was called.
*/
void onLoadCompleted(T loadable, long elapsedRealtimeMs, long loadDurationMs);
/**
* Called when a load has been canceled.
*
* <p>Note: If the {@link Loader} has not been released then there is guaranteed to be a memory
* barrier between {@link Loadable#load()} exiting and this callback being called. If the {@link
* Loader} has been released then this callback may be called before {@link Loadable#load()}
* exits.
*
* @param loadable The loadable whose load has been canceled.
* @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load was canceled.
* @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}
* was called up to the point at which it was canceled.
* @param released True if the load was canceled because the {@link Loader} was released. False
* otherwise.
*/
void onLoadCanceled(T loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released);
/**
* Called when a load encounters an error.
*
* <p>Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting
* and this callback being called.
*
* @param loadable The loadable whose load has encountered an error.
* @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the error occurred.
* @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}
* was called up to the point at which the error occurred.
* @param error The load error.
* @param errorCount The number of errors this load has encountered, including this one.
* @return The desired error handling action. One of {@link Loader#RETRY}, {@link
* Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY}, {@link
* Loader#DONT_RETRY_FATAL} or a retry action created by {@link #createRetryAction}.
*/
LoadErrorAction onLoadError(
T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error, int errorCount);
}
/**
* A callback to be notified when a {@link Loader} has finished being released.
*/
public interface ReleaseCallback {
/**
* Called when the {@link Loader} has finished being released.
*/
void onLoaderReleased();
}
/** Types of action that can be taken in response to a load error. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
ACTION_TYPE_RETRY,
ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT,
ACTION_TYPE_DONT_RETRY,
ACTION_TYPE_DONT_RETRY_FATAL
})
private @interface RetryActionType {}
private static final int ACTION_TYPE_RETRY = 0;
private static final int ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT = 1;
private static final int ACTION_TYPE_DONT_RETRY = 2;
private static final int ACTION_TYPE_DONT_RETRY_FATAL = 3;
/** Retries the load using the default delay. */
public static final LoadErrorAction RETRY =
createRetryAction(/* resetErrorCount= */ false, C.TIME_UNSET);
/** Retries the load using the default delay and resets the error count. */
public static final LoadErrorAction RETRY_RESET_ERROR_COUNT =
createRetryAction(/* resetErrorCount= */ true, C.TIME_UNSET);
/** Discards the failed {@link Loadable} and ignores any errors that have occurred. */
public static final LoadErrorAction DONT_RETRY =
new LoadErrorAction(ACTION_TYPE_DONT_RETRY, C.TIME_UNSET);
/**
* Discards the failed {@link Loadable}. The next call to {@link #maybeThrowError()} will throw
* the last load error.
*/
public static final LoadErrorAction DONT_RETRY_FATAL =
new LoadErrorAction(ACTION_TYPE_DONT_RETRY_FATAL, C.TIME_UNSET);
/**
* Action that can be taken in response to {@link Callback#onLoadError(Loadable, long, long,
* IOException, int)}.
*/
public static final class LoadErrorAction {
private final @RetryActionType int type;
private final long retryDelayMillis;
private LoadErrorAction(@RetryActionType int type, long retryDelayMillis) {
this.type = type;
this.retryDelayMillis = retryDelayMillis;
}
/** Returns whether this is a retry action. */
public boolean isRetry() {
return type == ACTION_TYPE_RETRY || type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT;
}
}
private final ExecutorService downloadExecutorService;
@Nullable private LoadTask<? extends Loadable> currentTask;
@Nullable private IOException fatalError;
/**
* @param threadName A name for the loader's thread.
*/
public Loader(String threadName) {
this.downloadExecutorService = Util.newSingleThreadExecutor(threadName);
}
/**
* Creates a {@link LoadErrorAction} for retrying with the given parameters.
*
* @param resetErrorCount Whether the previous error count should be set to zero.
* @param retryDelayMillis The number of milliseconds to wait before retrying.
* @return A {@link LoadErrorAction} for retrying with the given parameters.
*/
public static LoadErrorAction createRetryAction(boolean resetErrorCount, long retryDelayMillis) {
return new LoadErrorAction(
resetErrorCount ? ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT : ACTION_TYPE_RETRY,
retryDelayMillis);
}
/**
* Whether the last call to {@link #startLoading} resulted in a fatal error. Calling {@link
* #maybeThrowError()} will throw the fatal error.
*/
public boolean hasFatalError() {
return fatalError != null;
}
/** Clears any stored fatal error. */
public void clearFatalError() {
fatalError = null;
}
/**
* Starts loading a {@link Loadable}.
*
* <p>The calling thread must be a {@link Looper} thread, which is the thread on which the {@link
* Callback} will be called.
*
* @param <T> The type of the loadable.
* @param loadable The {@link Loadable} to load.
* @param callback A callback to be called when the load ends.
* @param defaultMinRetryCount The minimum number of times the load must be retried before {@link
* #maybeThrowError()} will propagate an error.
* @throws IllegalStateException If the calling thread does not have an associated {@link Looper}.
* @return {@link SystemClock#elapsedRealtime} when the load started.
*/
public <T extends Loadable> long startLoading(
T loadable, Callback<T> callback, int defaultMinRetryCount) {
Looper looper = Assertions.checkStateNotNull(Looper.myLooper());
fatalError = null;
long startTimeMs = SystemClock.elapsedRealtime();
new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0);
return startTimeMs;
}
/** Returns whether the loader is currently loading. */
public boolean isLoading() {
return currentTask != null;
}
/**
* Cancels the current load.
*
* @throws IllegalStateException If the loader is not currently loading.
*/
public void cancelLoading() {
Assertions.checkStateNotNull(currentTask).cancel(false);
}
/** Releases the loader. This method should be called when the loader is no longer required. */
public void release() {
release(null);
}
/**
* Releases the loader. This method should be called when the loader is no longer required.
*
* @param callback An optional callback to be called on the loading thread once the loader has
* been released.
*/
public void release(@Nullable ReleaseCallback callback) {
if (currentTask != null) {
currentTask.cancel(true);
}
if (callback != null) {
downloadExecutorService.execute(new ReleaseTask(callback));
}
downloadExecutorService.shutdown();
}
// LoaderErrorThrower implementation.
@Override
public void maybeThrowError() throws IOException {
maybeThrowError(Integer.MIN_VALUE);
}
@Override
public void maybeThrowError(int minRetryCount) throws IOException {
if (fatalError != null) {
throw fatalError;
} else if (currentTask != null) {
currentTask.maybeThrowError(minRetryCount == Integer.MIN_VALUE
? currentTask.defaultMinRetryCount : minRetryCount);
}
}
// Internal classes.
@SuppressLint("HandlerLeak")
private final class LoadTask<T extends Loadable> extends Handler implements Runnable {
private static final String TAG = "LoadTask";
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;
public final int defaultMinRetryCount;
private final T loadable;
private final long startTimeMs;
@Nullable private Loader.Callback<T> callback;
@Nullable private IOException currentError;
private int errorCount;
@Nullable private volatile Thread executorThread;
private volatile boolean canceled;
private volatile boolean released;
public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback,
int defaultMinRetryCount, long startTimeMs) {
super(looper);
this.loadable = loadable;
this.callback = callback;
this.defaultMinRetryCount = defaultMinRetryCount;
this.startTimeMs = startTimeMs;
}
public void maybeThrowError(int minRetryCount) throws IOException {
if (currentError != null && errorCount > minRetryCount) {
throw currentError;
}
}
public void start(long delayMillis) {
Assertions.checkState(currentTask == null);
currentTask = this;
if (delayMillis > 0) {
sendEmptyMessageDelayed(MSG_START, delayMillis);
} else {
execute();
}
}
public void cancel(boolean released) {
this.released = released;
currentError = null;
if (hasMessages(MSG_START)) {
removeMessages(MSG_START);
if (!released) {
sendEmptyMessage(MSG_CANCEL);
}
} else {
canceled = true;
loadable.cancelLoad();
@Nullable Thread executorThread = this.executorThread;
if (executorThread != null) {
executorThread.interrupt();
}
}
if (released) {
finish();
long nowMs = SystemClock.elapsedRealtime();
Assertions.checkNotNull(callback)
.onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true);
// If loading, this task will be referenced from a GC root (the loading thread) until
// cancellation completes. The time taken for cancellation to complete depends on the
// implementation of the Loadable that the task is loading. We null the callback reference
// here so that it doesn't prevent garbage collection whilst cancellation is ongoing.
callback = null;
}
}
@Override
public void run() {
try {
executorThread = Thread.currentThread();
if (!canceled) {
TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName());
try {
loadable.load();
} finally {
TraceUtil.endSection();
}
}
if (!released) {
sendEmptyMessage(MSG_END_OF_SOURCE);
}
} catch (IOException e) {
if (!released) {
obtainMessage(MSG_IO_EXCEPTION, e).sendToTarget();
}
} catch (Exception e) {
// This should never happen, but handle it anyway.
Log.e(TAG, "Unexpected exception loading stream", e);
if (!released) {
obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();
}
} catch (OutOfMemoryError e) {
// This can occur if a stream is malformed in a way that causes an extractor to think it
// needs to allocate a large amount of memory. We don't want the process to die in this
// case, but we do want the playback to fail.
Log.e(TAG, "OutOfMemory error loading stream", e);
if (!released) {
obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();
}
} catch (Error e) {
// We'd hope that the platform would kill the process if an Error is thrown here, but the
// executor may catch the error (b/20616433). Throw it here, but also pass and throw it from
// the handler thread so that the process dies even if the executor behaves in this way.
Log.e(TAG, "Unexpected error loading stream", e);
if (!released) {
obtainMessage(MSG_FATAL_ERROR, e).sendToTarget();
}
throw e;
}
}
@Override
public void handleMessage(Message msg) {
if (released) {
return;
}
if (msg.what == MSG_START) {
execute();
return;
}
if (msg.what == MSG_FATAL_ERROR) {
throw (Error) msg.obj;
}
finish();
long nowMs = SystemClock.elapsedRealtime();
long durationMs = nowMs - startTimeMs;
Loader.Callback<T> callback = Assertions.checkNotNull(this.callback);
if (canceled) {
callback.onLoadCanceled(loadable, nowMs, durationMs, false);
return;
}
switch (msg.what) {
case MSG_CANCEL:
callback.onLoadCanceled(loadable, nowMs, durationMs, false);
break;
case MSG_END_OF_SOURCE:
try {
callback.onLoadCompleted(loadable, nowMs, durationMs);
} catch (RuntimeException e) {
// This should never happen, but handle it anyway.
Log.e(TAG, "Unexpected exception handling load completed", e);
fatalError = new UnexpectedLoaderException(e);
}
break;
case MSG_IO_EXCEPTION:
currentError = (IOException) msg.obj;
errorCount++;
LoadErrorAction action =
callback.onLoadError(loadable, nowMs, durationMs, currentError, errorCount);
if (action.type == ACTION_TYPE_DONT_RETRY_FATAL) {
fatalError = currentError;
} else if (action.type != ACTION_TYPE_DONT_RETRY) {
if (action.type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT) {
errorCount = 1;
}
start(
action.retryDelayMillis != C.TIME_UNSET
? action.retryDelayMillis
: getRetryDelayMillis());
}
break;
default:
// Never happens.
break;
}
}
private void execute() {
currentError = null;
downloadExecutorService.execute(Assertions.checkNotNull(currentTask));
}
private void finish() {
currentTask = null;
}
private long getRetryDelayMillis() {
return Math.min((errorCount - 1) * 1000, 5000);
}
}
private static final class ReleaseTask implements Runnable {
private final ReleaseCallback callback;
public ReleaseTask(ReleaseCallback callback) {
this.callback = callback;
}
@Override
public void run() {
callback.onLoaderReleased();
}
}
}