diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 8089175287..b6cd400f22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -516,13 +516,9 @@ public class ChunkSampleStream LoadErrorInfo loadErrorInfo = new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount); - long exclusionDurationMs = - cancelable - ? loadErrorHandlingPolicy.getExclusionDurationMsFor( - LoadErrorHandlingPolicy.FALLBACK_TYPE_TRACK, loadErrorInfo) - : C.TIME_UNSET; @Nullable LoadErrorAction loadErrorAction = null; - if (chunkSource.onChunkLoadError(loadable, cancelable, error, exclusionDurationMs)) { + if (chunkSource.onChunkLoadError( + loadable, cancelable, loadErrorInfo, loadErrorHandlingPolicy)) { if (cancelable) { loadErrorAction = Loader.DONT_RETRY; if (isMediaChunk) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java index 81ce2d63de..b2a46a0cd4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.source.chunk; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import java.io.IOException; import java.util.List; @@ -104,15 +104,19 @@ public interface ChunkSource { * * @param chunk The chunk whose load encountered the error. * @param cancelable Whether the load can be canceled. - * @param e The error. - * @param exclusionDurationMs The duration for which the associated track may be excluded, or - * {@link C#TIME_UNSET} if the track may not be excluded. + * @param loadErrorInfo The load error info. + * @param loadErrorHandlingPolicy The load error handling policy to customize the behaviour of + * handling the load error. * @return Whether the load should be canceled so that a replacement chunk can be loaded instead. * Must be {@code false} if {@code cancelable} is {@code false}. If {@code true}, {@link * #getNextChunk(long, long, List, ChunkHolder)} will be called to obtain the replacement * chunk. */ - boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e, long exclusionDurationMs); + boolean onChunkLoadError( + Chunk chunk, + boolean cancelable, + LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo, + LoadErrorHandlingPolicy loadErrorHandlingPolicy); /** Releases any held resources. */ void release(); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 19dc560e7d..8b4268940b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -46,6 +46,7 @@ import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Util; @@ -455,7 +456,10 @@ public class DefaultDashChunkSource implements DashChunkSource { @Override public boolean onChunkLoadError( - Chunk chunk, boolean cancelable, Exception e, long exclusionDurationMs) { + Chunk chunk, + boolean cancelable, + LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo, + LoadErrorHandlingPolicy loadErrorHandlingPolicy) { if (!cancelable) { return false; } @@ -465,8 +469,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // Workaround for missing segment at the end of the period if (!manifest.dynamic && chunk instanceof MediaChunk - && e instanceof InvalidResponseCodeException - && ((InvalidResponseCodeException) e).responseCode == 404) { + && loadErrorInfo.exception instanceof InvalidResponseCodeException + && ((InvalidResponseCodeException) loadErrorInfo.exception).responseCode == 404) { RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(chunk.trackFormat)]; long segmentCount = representationHolder.getSegmentCount(); @@ -478,6 +482,9 @@ public class DefaultDashChunkSource implements DashChunkSource { } } } + long exclusionDurationMs = + loadErrorHandlingPolicy.getExclusionDurationMsFor( + LoadErrorHandlingPolicy.FALLBACK_TYPE_TRACK, loadErrorInfo); return exclusionDurationMs != C.TIME_UNSET && trackSelection.blacklist(trackSelection.indexOf(chunk.trackFormat), exclusionDurationMs); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSourceTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSourceTest.java index fa5ee71e84..d9d7ea2630 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSourceTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSourceTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; @@ -23,6 +24,8 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.BundledChunkExtractor; import com.google.android.exoplayer2.source.chunk.ChunkHolder; @@ -30,10 +33,18 @@ import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.util.Assertions; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.internal.DoNotInstrument; @@ -132,4 +143,129 @@ public class DefaultDashChunkSourceTest { assertThat(output.chunk.dataSpec.flags & DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED) .isEqualTo(0); } + + @Test + public void onChunkLoadError_trackExclusionEnabled_requestReplacementChunkAsLongAsAvailable() + throws Exception { + DefaultLoadErrorHandlingPolicy loadErrorHandlingPolicy = + new DefaultLoadErrorHandlingPolicy() { + @Override + public long getExclusionDurationMsFor(int fallbackType, LoadErrorInfo loadErrorInfo) { + // Try to exclude tracks only. + return fallbackType == FALLBACK_TYPE_LOCATION + ? C.TIME_UNSET + : super.getExclusionDurationMsFor(fallbackType, loadErrorInfo); + } + }; + int numberOfTracks = 2; + DashChunkSource chunkSource = createDashChunkSource(numberOfTracks); + ChunkHolder output = new ChunkHolder(); + for (int i = 0; i < numberOfTracks; i++) { + chunkSource.getNextChunk( + /* playbackPositionUs= */ 0, + /* loadPositionUs= */ 0, + /* queue= */ ImmutableList.of(), + output); + + boolean alternativeTrackAvailable = + chunkSource.onChunkLoadError( + checkNotNull(output.chunk), + /* cancelable= */ true, + createFakeLoadErrorInfo( + output.chunk.dataSpec, /* httpResponseCode= */ 404, /* errorCount= */ 1), + loadErrorHandlingPolicy); + + // Expect true except for the last track remaining. + assertThat(alternativeTrackAvailable).isEqualTo(i != numberOfTracks - 1); + } + } + + @Test + public void onChunkLoadError_trackExclusionDisabled_neverRequestReplacementChunk() + throws Exception { + DefaultLoadErrorHandlingPolicy loadErrorHandlingPolicy = + new DefaultLoadErrorHandlingPolicy() { + @Override + public long getExclusionDurationMsFor(int fallbackType, LoadErrorInfo loadErrorInfo) { + // Never exclude, neither tracks nor locations. + return C.TIME_UNSET; + } + }; + DashChunkSource chunkSource = createDashChunkSource(/* numberOfTracks= */ 2); + ChunkHolder output = new ChunkHolder(); + chunkSource.getNextChunk( + /* playbackPositionUs= */ 0, + /* loadPositionUs= */ 0, + /* queue= */ ImmutableList.of(), + output); + + boolean alternativeTrackAvailable = + chunkSource.onChunkLoadError( + checkNotNull(output.chunk), + /* cancelable= */ true, + createFakeLoadErrorInfo( + output.chunk.dataSpec, /* httpResponseCode= */ 404, /* errorCount= */ 1), + loadErrorHandlingPolicy); + + assertThat(alternativeTrackAvailable).isFalse(); + } + + private DashChunkSource createDashChunkSource(int numberOfTracks) throws IOException { + Assertions.checkArgument(numberOfTracks < 6); + DashManifest manifest = + new DashManifestParser() + .parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_VOD)); + int[] adaptationSetIndices = new int[] {0}; + int[] selectedTracks = new int[numberOfTracks]; + Format[] formats = new Format[numberOfTracks]; + for (int i = 0; i < numberOfTracks; i++) { + selectedTracks[i] = i; + formats[i] = + manifest + .getPeriod(0) + .adaptationSets + .get(adaptationSetIndices[0]) + .representations + .get(i) + .format; + } + AdaptiveTrackSelection adaptiveTrackSelection = + new AdaptiveTrackSelection( + new TrackGroup(formats), + selectedTracks, + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build()); + return new DefaultDashChunkSource( + BundledChunkExtractor.FACTORY, + new LoaderErrorThrower.Dummy(), + manifest, + /* periodIndex= */ 0, + /* adaptationSetIndices= */ adaptationSetIndices, + adaptiveTrackSelection, + C.TRACK_TYPE_VIDEO, + new FakeDataSource(), + /* elapsedRealtimeOffsetMs= */ 0, + /* maxSegmentsPerLoad= */ 1, + /* enableEventMessageTrack= */ false, + /* closedCaptionFormats */ ImmutableList.of(), + /* playerTrackEmsgHandler= */ null); + } + + private LoadErrorHandlingPolicy.LoadErrorInfo createFakeLoadErrorInfo( + DataSpec dataSpec, int httpResponseCode, int errorCount) { + LoadEventInfo loadEventInfo = + new LoadEventInfo(/* loadTaskId= */ 0, dataSpec, SystemClock.elapsedRealtime()); + MediaLoadData mediaLoadData = new MediaLoadData(C.DATA_TYPE_MEDIA); + HttpDataSource.InvalidResponseCodeException invalidResponseCodeException = + new HttpDataSource.InvalidResponseCodeException( + httpResponseCode, + /* responseMessage= */ null, + ImmutableMap.of(), + dataSpec, + new byte[0]); + return new LoadErrorHandlingPolicy.LoadErrorInfo( + loadEventInfo, mediaLoadData, invalidResponseCodeException, errorCount); + } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index c5857c5f0f..1183ef668c 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest. import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; @@ -282,7 +283,13 @@ public class DefaultSsChunkSource implements SsChunkSource { @Override public boolean onChunkLoadError( - Chunk chunk, boolean cancelable, Exception e, long exclusionDurationMs) { + Chunk chunk, + boolean cancelable, + LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo, + LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + long exclusionDurationMs = + loadErrorHandlingPolicy.getExclusionDurationMsFor( + LoadErrorHandlingPolicy.FALLBACK_TYPE_TRACK, loadErrorInfo); return cancelable && exclusionDurationMs != C.TIME_UNSET && trackSelection.blacklist(trackSelection.indexOf(chunk.trackFormat), exclusionDurationMs); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index f6300bc45f..67c026f62f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; @@ -156,7 +157,10 @@ public class FakeChunkSource implements ChunkSource { @Override public boolean onChunkLoadError( - Chunk chunk, boolean cancelable, Exception e, long exclusionDurationMs) { + Chunk chunk, + boolean cancelable, + LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo, + LoadErrorHandlingPolicy loadErrorHandlingPolicy) { return false; }