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 The type of the object being loaded. + */ +public interface LoadErrorHandlingPolicy { + + /** Default implementation of {@link LoadErrorHandlingPolicy}. */ + LoadErrorHandlingPolicy DEFAULT = + new LoadErrorHandlingPolicy() { + + /** + * Blacklists resources whose load error was an {@link InvalidResponseCodeException} with + * response code HTTP 404 or 410. The duration of the blacklisting is {@link + * ChunkedTrackBlacklistUtil#DEFAULT_TRACK_BLACKLIST_MS}. + */ + @Override + public long getBlacklistDurationMsFor( + Loadable loadable, long loadDurationMs, IOException exception, int errorCount) { + if (exception instanceof InvalidResponseCodeException) { + int responseCode = ((InvalidResponseCodeException) exception).responseCode; + return responseCode == 404 // HTTP 404 Not Found. + || responseCode == 410 // HTTP 410 Gone. + ? ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS + : C.TIME_UNSET; + } + return C.TIME_UNSET; + } + + /** + * Retries for any exception that is not a subclass of {@link ParserException}. The retry + * delay is calculated as {@code Math.min((errorCount - 1) * 1000, 5000)}. + */ + @Override + public long getRetryDelayMsFor( + Loadable loadable, long loadDurationMs, IOException exception, int errorCount) { + return exception instanceof ParserException + ? C.TIME_UNSET + : Math.min((errorCount - 1) * 1000, 5000); + } + }; + + /** Returns {@link #DEFAULT}. */ + static LoadErrorHandlingPolicy getDefault() { + @SuppressWarnings("unchecked") // Safe contravariant cast. + LoadErrorHandlingPolicy policy = (LoadErrorHandlingPolicy) DEFAULT; + return policy; + } + + /** + * Returns the number of milliseconds for which a resource associated to a provided load error + * should be blacklisted, or {@link C#TIME_UNSET} if the resource should not be blacklisted. + * + * @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 blacklist duration in milliseconds, or {@link C#TIME_UNSET} if the resource should + * not be blacklisted. + */ + long getBlacklistDurationMsFor( + T loadable, long loadDurationMs, IOException exception, int errorCount); + + /** + * Returns 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. + * + *

{@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); + } +}