diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java index 343d4f0bbe..a85d589762 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java @@ -63,7 +63,11 @@ public final class CompositeSequenceableLoader implements SequenceableLoader { break; } for (SequenceableLoader loader : loaders) { - if (loader.getNextLoadPositionUs() == nextLoadPositionUs) { + long loaderNextLoadPositionUs = loader.getNextLoadPositionUs(); + boolean isLoaderBehind = + loaderNextLoadPositionUs != C.TIME_END_OF_SOURCE + && loaderNextLoadPositionUs <= positionUs; + if (loaderNextLoadPositionUs == nextLoadPositionUs || isLoaderBehind) { madeProgressThisIteration |= loader.continueLoading(positionUs); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java new file mode 100644 index 0000000000..e3ac104754 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2017 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.source; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link CompositeSequenceableLoader}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class CompositeSequenceableLoaderTest { + + /** + * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns minimum buffered + * position among all sub-loaders. + */ + @Test + public void testGetBufferedPositionUsReturnsMinimumLoaderBufferedPosition() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(1000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns minimum buffered + * position that is not {@link C#TIME_END_OF_SOURCE} among all sub-loaders. + */ + @Test + public void testGetBufferedPositionUsReturnsMinimumNonEndOfSourceLoaderBufferedPosition() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader3 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ C.TIME_END_OF_SOURCE, + /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2, loader3}); + assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(1000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns + * {@link C#TIME_END_OF_SOURCE} when all sub-loaders have buffered till end-of-source. + */ + @Test + public void testGetBufferedPositionUsReturnsEndOfSourceWhenAllLoaderBufferedTillEndOfSource() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ C.TIME_END_OF_SOURCE, + /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ C.TIME_END_OF_SOURCE, + /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns minimum next + * load position among all sub-loaders. + */ + @Test + public void testGetNextLoadPositionUsReturnMinimumLoaderNextLoadPositionUs() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2001); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2000); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(2000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns minimum next + * load position that is not {@link C#TIME_END_OF_SOURCE} among all sub-loaders. + */ + @Test + public void testGetNextLoadPositionUsReturnMinimumNonEndOfSourceLoaderNextLoadPositionUs() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + FakeSequenceableLoader loader3 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2, loader3}); + assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(2000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns + * {@link C#TIME_END_OF_SOURCE} when all sub-loaders have next load position at end-of-source. + */ + @Test + public void testGetNextLoadPositionUsReturnsEndOfSourceWhenAllLoaderLoadingLastChunk() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} only allows the loader + * with minimum next load position to continue loading if next load positions are not behind + * current playback position. + */ + @Test + public void testContinueLoadingOnlyAllowFurthestBehindLoaderToLoadIfNotBehindPlaybackPosition() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + compositeSequenceableLoader.continueLoading(100); + + assertThat(loader1.numInvocations).isEqualTo(1); + assertThat(loader2.numInvocations).isEqualTo(0); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} allows all loaders + * with next load position behind current playback position to continue loading. + */ + @Test + public void testContinueLoadingReturnAllowAllLoadersBehindPlaybackPositionToLoad() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + FakeSequenceableLoader loader3 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1002, /* nextLoadPositionUs */ 2002); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2, loader3}); + compositeSequenceableLoader.continueLoading(3000); + + assertThat(loader1.numInvocations).isEqualTo(1); + assertThat(loader2.numInvocations).isEqualTo(1); + assertThat(loader3.numInvocations).isEqualTo(1); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} does not allow loader + * with next load position at end-of-source to continue loading. + */ + @Test + public void testContinueLoadingOnlyNotAllowEndOfSourceLoaderToLoad() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + compositeSequenceableLoader.continueLoading(3000); + + assertThat(loader1.numInvocations).isEqualTo(0); + assertThat(loader2.numInvocations).isEqualTo(0); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} returns true if the loader + * with minimum next load position can make progress if next load positions are not behind + * current playback position. + */ + @Test + public void testContinueLoadingReturnTrueIfFurthestBehindLoaderCanMakeProgress() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + loader1.setNextChunkDurationUs(1000); + + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + + assertThat(compositeSequenceableLoader.continueLoading(100)).isTrue(); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} returns true if any loader + * that are behind current playback position can make progress, even if it is not the one with + * minimum next load position. + */ + @Test + public void testContinueLoadingReturnTrueIfLoaderBehindPlaybackPositionCanMakeProgress() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + // loader2 is not the furthest behind, but it can make progress if allowed. + loader2.setNextChunkDurationUs(1000); + + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + + assertThat(compositeSequenceableLoader.continueLoading(3000)).isTrue(); + } + + private static class FakeSequenceableLoader implements SequenceableLoader { + + private long bufferedPositionUs; + private long nextLoadPositionUs; + private int numInvocations; + private int nextChunkDurationUs; + + private FakeSequenceableLoader(long bufferedPositionUs, long nextLoadPositionUs) { + this.bufferedPositionUs = bufferedPositionUs; + this.nextLoadPositionUs = nextLoadPositionUs; + } + + @Override + public long getBufferedPositionUs() { + return bufferedPositionUs; + } + + @Override + public long getNextLoadPositionUs() { + return nextLoadPositionUs; + } + + @Override + public boolean continueLoading(long positionUs) { + numInvocations++; + boolean loaded = nextChunkDurationUs != 0; + // The current chunk has been loaded, advance to next chunk. + bufferedPositionUs = nextLoadPositionUs; + nextLoadPositionUs += nextChunkDurationUs; + nextChunkDurationUs = 0; + return loaded; + } + + private void setNextChunkDurationUs(int nextChunkDurationUs) { + this.nextChunkDurationUs = nextChunkDurationUs; + } + + } + +}