/* * 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 androidx.media3.exoplayer.upstream; import static java.lang.Math.min; import static java.lang.annotation.ElementType.TYPE_USE; 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 androidx.media3.common.C; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; import androidx.media3.common.util.TraceUtil; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; /** Manages the background loading of {@link Loadable}s. */ @UnstableApi 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. * *
Loadable implementations should ensure that a currently executing {@link #load()} call * will exit reasonably quickly after this method is called. The {@link #load()} call may exit * either by returning or by throwing an {@link IOException}. * *
If there is a currently executing {@link #load()} call, then the thread on which that call * is being made will be interrupted immediately after the call to this method. Hence * implementations do not need to (and should not attempt to) interrupt the loading thread * themselves. * *
Although the loading thread will be interrupted, Loadable implementations should not use
* the interrupted status of the loading thread in {@link #load()} to determine whether the load
* has been canceled. This approach is not robust [Internal ref: b/79223737]. Instead,
* implementations should use their own flag to signal cancelation (for example, using {@link
* AtomicBoolean}).
*/
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 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.
*
* 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.
*
* 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();
}
private static final String THREAD_NAME_PREFIX = "ExoPlayer:Loader:";
/** Types of action that can be taken in response to a load error. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@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 threadNameSuffix A name suffix for the loader's thread. This should be the name of the
* component using the loader.
*/
public Loader(String threadNameSuffix) {
this.downloadExecutorService =
Util.newSingleThreadExecutor(THREAD_NAME_PREFIX + threadNameSuffix);
}
/**
* 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}.
*
* The calling thread must be a {@link Looper} thread, which is the thread on which the {@link
* Callback} will be called.
*
* @param