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