• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.upstream;
17 
18 import android.annotation.SuppressLint;
19 import android.os.Handler;
20 import android.os.Looper;
21 import android.os.Message;
22 import android.os.SystemClock;
23 import androidx.annotation.IntDef;
24 import androidx.annotation.Nullable;
25 import com.google.android.exoplayer2.C;
26 import com.google.android.exoplayer2.util.Assertions;
27 import com.google.android.exoplayer2.util.Log;
28 import com.google.android.exoplayer2.util.TraceUtil;
29 import com.google.android.exoplayer2.util.Util;
30 import java.io.IOException;
31 import java.lang.annotation.Documented;
32 import java.lang.annotation.Retention;
33 import java.lang.annotation.RetentionPolicy;
34 import java.util.concurrent.ExecutorService;
35 
36 /**
37  * Manages the background loading of {@link Loadable}s.
38  */
39 public final class Loader implements LoaderErrorThrower {
40 
41   /**
42    * Thrown when an unexpected exception or error is encountered during loading.
43    */
44   public static final class UnexpectedLoaderException extends IOException {
45 
UnexpectedLoaderException(Throwable cause)46     public UnexpectedLoaderException(Throwable cause) {
47       super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
48     }
49 
50   }
51 
52   /**
53    * An object that can be loaded using a {@link Loader}.
54    */
55   public interface Loadable {
56 
57     /**
58      * Cancels the load.
59      */
cancelLoad()60     void cancelLoad();
61 
62     /**
63      * Performs the load, returning on completion or cancellation.
64      *
65      * @throws IOException If the input could not be loaded.
66      */
load()67     void load() throws IOException;
68   }
69 
70   /**
71    * A callback to be notified of {@link Loader} events.
72    */
73   public interface Callback<T extends Loadable> {
74 
75     /**
76      * Called when a load has completed.
77      *
78      * <p>Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting
79      * and this callback being called.
80      *
81      * @param loadable The loadable whose load has completed.
82      * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load ended.
83      * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}
84      *     was called.
85      */
onLoadCompleted(T loadable, long elapsedRealtimeMs, long loadDurationMs)86     void onLoadCompleted(T loadable, long elapsedRealtimeMs, long loadDurationMs);
87 
88     /**
89      * Called when a load has been canceled.
90      *
91      * <p>Note: If the {@link Loader} has not been released then there is guaranteed to be a memory
92      * barrier between {@link Loadable#load()} exiting and this callback being called. If the {@link
93      * Loader} has been released then this callback may be called before {@link Loadable#load()}
94      * exits.
95      *
96      * @param loadable The loadable whose load has been canceled.
97      * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load was canceled.
98      * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}
99      *     was called up to the point at which it was canceled.
100      * @param released True if the load was canceled because the {@link Loader} was released. False
101      *     otherwise.
102      */
onLoadCanceled(T loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released)103     void onLoadCanceled(T loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released);
104 
105     /**
106      * Called when a load encounters an error.
107      *
108      * <p>Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting
109      * and this callback being called.
110      *
111      * @param loadable The loadable whose load has encountered an error.
112      * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the error occurred.
113      * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}
114      *     was called up to the point at which the error occurred.
115      * @param error The load error.
116      * @param errorCount The number of errors this load has encountered, including this one.
117      * @return The desired error handling action. One of {@link Loader#RETRY}, {@link
118      *     Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY}, {@link
119      *     Loader#DONT_RETRY_FATAL} or a retry action created by {@link #createRetryAction}.
120      */
onLoadError( T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error, int errorCount)121     LoadErrorAction onLoadError(
122         T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error, int errorCount);
123   }
124 
125   /**
126    * A callback to be notified when a {@link Loader} has finished being released.
127    */
128   public interface ReleaseCallback {
129 
130     /**
131      * Called when the {@link Loader} has finished being released.
132      */
onLoaderReleased()133     void onLoaderReleased();
134 
135   }
136 
137   /** Types of action that can be taken in response to a load error. */
138   @Documented
139   @Retention(RetentionPolicy.SOURCE)
140   @IntDef({
141     ACTION_TYPE_RETRY,
142     ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT,
143     ACTION_TYPE_DONT_RETRY,
144     ACTION_TYPE_DONT_RETRY_FATAL
145   })
146   private @interface RetryActionType {}
147 
148   private static final int ACTION_TYPE_RETRY = 0;
149   private static final int ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT = 1;
150   private static final int ACTION_TYPE_DONT_RETRY = 2;
151   private static final int ACTION_TYPE_DONT_RETRY_FATAL = 3;
152 
153   /** Retries the load using the default delay. */
154   public static final LoadErrorAction RETRY =
155       createRetryAction(/* resetErrorCount= */ false, C.TIME_UNSET);
156   /** Retries the load using the default delay and resets the error count. */
157   public static final LoadErrorAction RETRY_RESET_ERROR_COUNT =
158       createRetryAction(/* resetErrorCount= */ true, C.TIME_UNSET);
159   /** Discards the failed {@link Loadable} and ignores any errors that have occurred. */
160   public static final LoadErrorAction DONT_RETRY =
161       new LoadErrorAction(ACTION_TYPE_DONT_RETRY, C.TIME_UNSET);
162   /**
163    * Discards the failed {@link Loadable}. The next call to {@link #maybeThrowError()} will throw
164    * the last load error.
165    */
166   public static final LoadErrorAction DONT_RETRY_FATAL =
167       new LoadErrorAction(ACTION_TYPE_DONT_RETRY_FATAL, C.TIME_UNSET);
168 
169   /**
170    * Action that can be taken in response to {@link Callback#onLoadError(Loadable, long, long,
171    * IOException, int)}.
172    */
173   public static final class LoadErrorAction {
174 
175     private final @RetryActionType int type;
176     private final long retryDelayMillis;
177 
LoadErrorAction(@etryActionType int type, long retryDelayMillis)178     private LoadErrorAction(@RetryActionType int type, long retryDelayMillis) {
179       this.type = type;
180       this.retryDelayMillis = retryDelayMillis;
181     }
182 
183     /** Returns whether this is a retry action. */
isRetry()184     public boolean isRetry() {
185       return type == ACTION_TYPE_RETRY || type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT;
186     }
187   }
188 
189   private final ExecutorService downloadExecutorService;
190 
191   @Nullable private LoadTask<? extends Loadable> currentTask;
192   @Nullable private IOException fatalError;
193 
194   /**
195    * @param threadName A name for the loader's thread.
196    */
Loader(String threadName)197   public Loader(String threadName) {
198     this.downloadExecutorService = Util.newSingleThreadExecutor(threadName);
199   }
200 
201   /**
202    * Creates a {@link LoadErrorAction} for retrying with the given parameters.
203    *
204    * @param resetErrorCount Whether the previous error count should be set to zero.
205    * @param retryDelayMillis The number of milliseconds to wait before retrying.
206    * @return A {@link LoadErrorAction} for retrying with the given parameters.
207    */
createRetryAction(boolean resetErrorCount, long retryDelayMillis)208   public static LoadErrorAction createRetryAction(boolean resetErrorCount, long retryDelayMillis) {
209     return new LoadErrorAction(
210         resetErrorCount ? ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT : ACTION_TYPE_RETRY,
211         retryDelayMillis);
212   }
213 
214   /**
215    * Whether the last call to {@link #startLoading} resulted in a fatal error. Calling {@link
216    * #maybeThrowError()} will throw the fatal error.
217    */
hasFatalError()218   public boolean hasFatalError() {
219     return fatalError != null;
220   }
221 
222   /** Clears any stored fatal error. */
clearFatalError()223   public void clearFatalError() {
224     fatalError = null;
225   }
226 
227   /**
228    * Starts loading a {@link Loadable}.
229    *
230    * <p>The calling thread must be a {@link Looper} thread, which is the thread on which the {@link
231    * Callback} will be called.
232    *
233    * @param <T> The type of the loadable.
234    * @param loadable The {@link Loadable} to load.
235    * @param callback A callback to be called when the load ends.
236    * @param defaultMinRetryCount The minimum number of times the load must be retried before {@link
237    *     #maybeThrowError()} will propagate an error.
238    * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}.
239    * @return {@link SystemClock#elapsedRealtime} when the load started.
240    */
startLoading( T loadable, Callback<T> callback, int defaultMinRetryCount)241   public <T extends Loadable> long startLoading(
242       T loadable, Callback<T> callback, int defaultMinRetryCount) {
243     Looper looper = Assertions.checkStateNotNull(Looper.myLooper());
244     fatalError = null;
245     long startTimeMs = SystemClock.elapsedRealtime();
246     new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0);
247     return startTimeMs;
248   }
249 
250   /** Returns whether the loader is currently loading. */
isLoading()251   public boolean isLoading() {
252     return currentTask != null;
253   }
254 
255   /**
256    * Cancels the current load.
257    *
258    * @throws IllegalStateException If the loader is not currently loading.
259    */
cancelLoading()260   public void cancelLoading() {
261     Assertions.checkStateNotNull(currentTask).cancel(false);
262   }
263 
264   /** Releases the loader. This method should be called when the loader is no longer required. */
release()265   public void release() {
266     release(null);
267   }
268 
269   /**
270    * Releases the loader. This method should be called when the loader is no longer required.
271    *
272    * @param callback An optional callback to be called on the loading thread once the loader has
273    *     been released.
274    */
release(@ullable ReleaseCallback callback)275   public void release(@Nullable ReleaseCallback callback) {
276     if (currentTask != null) {
277       currentTask.cancel(true);
278     }
279     if (callback != null) {
280       downloadExecutorService.execute(new ReleaseTask(callback));
281     }
282     downloadExecutorService.shutdown();
283   }
284 
285   // LoaderErrorThrower implementation.
286 
287   @Override
maybeThrowError()288   public void maybeThrowError() throws IOException {
289     maybeThrowError(Integer.MIN_VALUE);
290   }
291 
292   @Override
maybeThrowError(int minRetryCount)293   public void maybeThrowError(int minRetryCount) throws IOException {
294     if (fatalError != null) {
295       throw fatalError;
296     } else if (currentTask != null) {
297       currentTask.maybeThrowError(minRetryCount == Integer.MIN_VALUE
298           ? currentTask.defaultMinRetryCount : minRetryCount);
299     }
300   }
301 
302   // Internal classes.
303 
304   @SuppressLint("HandlerLeak")
305   private final class LoadTask<T extends Loadable> extends Handler implements Runnable {
306 
307     private static final String TAG = "LoadTask";
308 
309     private static final int MSG_START = 0;
310     private static final int MSG_CANCEL = 1;
311     private static final int MSG_END_OF_SOURCE = 2;
312     private static final int MSG_IO_EXCEPTION = 3;
313     private static final int MSG_FATAL_ERROR = 4;
314 
315     public final int defaultMinRetryCount;
316 
317     private final T loadable;
318     private final long startTimeMs;
319 
320     @Nullable private Loader.Callback<T> callback;
321     @Nullable private IOException currentError;
322     private int errorCount;
323 
324     @Nullable private volatile Thread executorThread;
325     private volatile boolean canceled;
326     private volatile boolean released;
327 
LoadTask(Looper looper, T loadable, Loader.Callback<T> callback, int defaultMinRetryCount, long startTimeMs)328     public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback,
329         int defaultMinRetryCount, long startTimeMs) {
330       super(looper);
331       this.loadable = loadable;
332       this.callback = callback;
333       this.defaultMinRetryCount = defaultMinRetryCount;
334       this.startTimeMs = startTimeMs;
335     }
336 
maybeThrowError(int minRetryCount)337     public void maybeThrowError(int minRetryCount) throws IOException {
338       if (currentError != null && errorCount > minRetryCount) {
339         throw currentError;
340       }
341     }
342 
start(long delayMillis)343     public void start(long delayMillis) {
344       Assertions.checkState(currentTask == null);
345       currentTask = this;
346       if (delayMillis > 0) {
347         sendEmptyMessageDelayed(MSG_START, delayMillis);
348       } else {
349         execute();
350       }
351     }
352 
cancel(boolean released)353     public void cancel(boolean released) {
354       this.released = released;
355       currentError = null;
356       if (hasMessages(MSG_START)) {
357         removeMessages(MSG_START);
358         if (!released) {
359           sendEmptyMessage(MSG_CANCEL);
360         }
361       } else {
362         canceled = true;
363         loadable.cancelLoad();
364         @Nullable Thread executorThread = this.executorThread;
365         if (executorThread != null) {
366           executorThread.interrupt();
367         }
368       }
369       if (released) {
370         finish();
371         long nowMs = SystemClock.elapsedRealtime();
372         Assertions.checkNotNull(callback)
373             .onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true);
374         // If loading, this task will be referenced from a GC root (the loading thread) until
375         // cancellation completes. The time taken for cancellation to complete depends on the
376         // implementation of the Loadable that the task is loading. We null the callback reference
377         // here so that it doesn't prevent garbage collection whilst cancellation is ongoing.
378         callback = null;
379       }
380     }
381 
382     @Override
run()383     public void run() {
384       try {
385         executorThread = Thread.currentThread();
386         if (!canceled) {
387           TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName());
388           try {
389             loadable.load();
390           } finally {
391             TraceUtil.endSection();
392           }
393         }
394         if (!released) {
395           sendEmptyMessage(MSG_END_OF_SOURCE);
396         }
397       } catch (IOException e) {
398         if (!released) {
399           obtainMessage(MSG_IO_EXCEPTION, e).sendToTarget();
400         }
401       } catch (Exception e) {
402         // This should never happen, but handle it anyway.
403         Log.e(TAG, "Unexpected exception loading stream", e);
404         if (!released) {
405           obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();
406         }
407       } catch (OutOfMemoryError e) {
408         // This can occur if a stream is malformed in a way that causes an extractor to think it
409         // needs to allocate a large amount of memory. We don't want the process to die in this
410         // case, but we do want the playback to fail.
411         Log.e(TAG, "OutOfMemory error loading stream", e);
412         if (!released) {
413           obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();
414         }
415       } catch (Error e) {
416         // We'd hope that the platform would kill the process if an Error is thrown here, but the
417         // executor may catch the error (b/20616433). Throw it here, but also pass and throw it from
418         // the handler thread so that the process dies even if the executor behaves in this way.
419         Log.e(TAG, "Unexpected error loading stream", e);
420         if (!released) {
421           obtainMessage(MSG_FATAL_ERROR, e).sendToTarget();
422         }
423         throw e;
424       }
425     }
426 
427     @Override
handleMessage(Message msg)428     public void handleMessage(Message msg) {
429       if (released) {
430         return;
431       }
432       if (msg.what == MSG_START) {
433         execute();
434         return;
435       }
436       if (msg.what == MSG_FATAL_ERROR) {
437         throw (Error) msg.obj;
438       }
439       finish();
440       long nowMs = SystemClock.elapsedRealtime();
441       long durationMs = nowMs - startTimeMs;
442       Loader.Callback<T> callback = Assertions.checkNotNull(this.callback);
443       if (canceled) {
444         callback.onLoadCanceled(loadable, nowMs, durationMs, false);
445         return;
446       }
447       switch (msg.what) {
448         case MSG_CANCEL:
449           callback.onLoadCanceled(loadable, nowMs, durationMs, false);
450           break;
451         case MSG_END_OF_SOURCE:
452           try {
453             callback.onLoadCompleted(loadable, nowMs, durationMs);
454           } catch (RuntimeException e) {
455             // This should never happen, but handle it anyway.
456             Log.e(TAG, "Unexpected exception handling load completed", e);
457             fatalError = new UnexpectedLoaderException(e);
458           }
459           break;
460         case MSG_IO_EXCEPTION:
461           currentError = (IOException) msg.obj;
462           errorCount++;
463           LoadErrorAction action =
464               callback.onLoadError(loadable, nowMs, durationMs, currentError, errorCount);
465           if (action.type == ACTION_TYPE_DONT_RETRY_FATAL) {
466             fatalError = currentError;
467           } else if (action.type != ACTION_TYPE_DONT_RETRY) {
468             if (action.type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT) {
469               errorCount = 1;
470             }
471             start(
472                 action.retryDelayMillis != C.TIME_UNSET
473                     ? action.retryDelayMillis
474                     : getRetryDelayMillis());
475           }
476           break;
477         default:
478           // Never happens.
479           break;
480       }
481     }
482 
execute()483     private void execute() {
484       currentError = null;
485       downloadExecutorService.execute(Assertions.checkNotNull(currentTask));
486     }
487 
finish()488     private void finish() {
489       currentTask = null;
490     }
491 
getRetryDelayMillis()492     private long getRetryDelayMillis() {
493       return Math.min((errorCount - 1) * 1000, 5000);
494     }
495 
496   }
497 
498   private static final class ReleaseTask implements Runnable {
499 
500     private final ReleaseCallback callback;
501 
ReleaseTask(ReleaseCallback callback)502     public ReleaseTask(ReleaseCallback callback) {
503       this.callback = callback;
504     }
505 
506     @Override
run()507     public void run() {
508       callback.onLoaderReleased();
509     }
510 
511   }
512 
513 }
514