diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java new file mode 100644 index 0000000000..ecf6e5744b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 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 com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; +import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; +import com.google.android.exoplayer2.upstream.Loader.Loadable; +import java.io.IOException; + +/** + * Defines how errors encountered by {@link Loader Loaders} are handled. + * + *
Loader clients may blacklist a resource when a load error occurs. Blacklisting works around + * load errors by loading an alternative resource. Clients do not try blacklisting when a resource + * does not have an alternative. When a resource does have valid alternatives, {@link + * #getBlacklistDurationMsFor(T, long, IOException, int)} defines whether the resource should be + * blacklisted. Blacklisting will succeed if any of the alternatives is not in the black list. + * + *
When blacklisting does not take place, {@link #getRetryDelayMsFor(T, long, IOException, int)}
+ * defines whether the load is retried. Loader clients define when to propagate retry attempt
+ * errors. Errors that are not retried are propagated.
+ *
+ * @param {@link Loader} clients may ignore the retry delay returned by this method in order to wait
+ * for a specific event before retrying. However, the load is retried if and only if this method
+ * does not return {@link C#TIME_UNSET}.
+ *
+ * @param loadable The loadable whose load failed.
+ * @param loadDurationMs The duration in milliseconds of the load up to the point at which the
+ * error occurred, including any previous attempts.
+ * @param exception The load error.
+ * @param errorCount The number of errors this load has encountered, including this one.
+ * @return The number of milliseconds to wait before attempting the load again, or {@link
+ * C#TIME_UNSET} if the error is fatal and should not be retried.
+ */
+ long getRetryDelayMsFor(T loadable, long loadDurationMs, IOException exception, int errorCount);
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java
index b4b378c152..ce26f06848 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java
@@ -75,27 +75,29 @@ public final class Loader implements LoaderErrorThrower {
/**
* Called when a load has completed.
- *
- * Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting and
- * this callback being called.
+ *
+ * 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 of the load.
+ * @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.
+ *
+ * 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 of the load up to the point at which it 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.
*/
@@ -109,7 +111,8 @@ public final class Loader implements LoaderErrorThrower {
*
* @param loadable The loadable whose load has encountered an error.
* @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the error occurred.
- * @param loadDurationMs The duration of the load up to the point at which 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
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicyTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicyTest.java
new file mode 100644
index 0000000000..295ebfb518
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicyTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2018 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 static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
+import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
+import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Collections;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Unit tests for {@link LoadErrorHandlingPolicy#DEFAULT}. */
+@RunWith(RobolectricTestRunner.class)
+public final class DefaultLoadErrorHandlingPolicyTest {
+
+ private static final Loadable DUMMY_LOADABLE =
+ new Loadable() {
+ @Override
+ public void cancelLoad() {
+ // Do nothing.
+ }
+
+ @Override
+ public void load() throws IOException, InterruptedException {
+ // Do nothing.
+ }
+ };
+
+ @Test
+ public void getBlacklistDurationMsFor_blacklist404() throws Exception {
+ InvalidResponseCodeException exception =
+ new InvalidResponseCodeException(404, Collections.emptyMap(), new DataSpec(Uri.EMPTY));
+ assertThat(getDefaultPolicyBlacklistOutputFor(exception))
+ .isEqualTo(ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS);
+ }
+
+ @Test
+ public void getBlacklistDurationMsFor_blacklist410() throws Exception {
+ InvalidResponseCodeException exception =
+ new InvalidResponseCodeException(410, Collections.emptyMap(), new DataSpec(Uri.EMPTY));
+ assertThat(getDefaultPolicyBlacklistOutputFor(exception))
+ .isEqualTo(ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS);
+ }
+
+ @Test
+ public void getBlacklistDurationMsFor_dontBlacklistUnexpectedHttpCodes() throws Exception {
+ InvalidResponseCodeException exception =
+ new InvalidResponseCodeException(500, Collections.emptyMap(), new DataSpec(Uri.EMPTY));
+ assertThat(getDefaultPolicyBlacklistOutputFor(exception)).isEqualTo(C.TIME_UNSET);
+ }
+
+ @Test
+ public void getBlacklistDurationMsFor_dontBlacklistUnexpectedExceptions() throws Exception {
+ FileNotFoundException exception = new FileNotFoundException();
+ assertThat(getDefaultPolicyBlacklistOutputFor(exception)).isEqualTo(C.TIME_UNSET);
+ }
+
+ @Test
+ public void getRetryDelayMsFor_dontRetryParserException() throws Exception {
+ assertThat(getDefaultPolicyRetryDelayOutputFor(new ParserException(), 1))
+ .isEqualTo(C.TIME_UNSET);
+ }
+
+ @Test
+ public void getRetryDelayMsFor_successiveRetryDelays() throws Exception {
+ assertThat(getDefaultPolicyRetryDelayOutputFor(new FileNotFoundException(), 3)).isEqualTo(2000);
+ assertThat(getDefaultPolicyRetryDelayOutputFor(new FileNotFoundException(), 5)).isEqualTo(4000);
+ assertThat(getDefaultPolicyRetryDelayOutputFor(new FileNotFoundException(), 9)).isEqualTo(5000);
+ }
+
+ private static long getDefaultPolicyBlacklistOutputFor(IOException exception) {
+ return LoadErrorHandlingPolicy.DEFAULT.getBlacklistDurationMsFor(
+ DUMMY_LOADABLE, /* loadDurationMs= */ 1000, exception, /* errorCount= */ 1);
+ }
+
+ private static long getDefaultPolicyRetryDelayOutputFor(IOException exception, int errorCount) {
+ return LoadErrorHandlingPolicy.DEFAULT.getRetryDelayMsFor(
+ DUMMY_LOADABLE, /* loadDurationMs= */ 1000, exception, errorCount);
+ }
+}