Complete preloading when the period has loaded to the end of the source

When the period has loaded to the end of the source, the `period.getBufferedPositionUs` will be set to `C.TIME_END_OF_SOURCE`, which is a negative value. Thus, the original `continueLoadingPredicate` will never turn to `false`, as the `bufferedPositionUs` is definitely less than the target preload position that is expected to be positive.

In this change, we added `PreloadMediaSource.PreloadControl.onLoadedToTheEndOfSource(PreloadMediaSource)` to indicate that the source has loaded to the end. This allows the `DefaultPreloadManager` and the custom `PreloadMediaSource.PreloadControl` implementations to preload the next source or take other actions.

This bug was not revealed by the the `DefaultPreloadManagerTest` because the related tests were all using the `FakeMediaSource` and only setting the preload target to `STAGE_TIMELINE_REFRESHED`. Thus, the tests for testing the `invalidate()` behaviors were modified to use the real progressive media whenever possible, unless we have to use `FakeMediaSource` to squeeze a chance to do more operations between the preloading of sources to test some special scenarios.

PiperOrigin-RevId: 631776442
This commit is contained in:
tianyifeng 2024-05-08 06:14:19 -07:00 committed by Copybara-Service
parent 9ece3932e8
commit 1a5f57e9eb
5 changed files with 192 additions and 68 deletions

View file

@ -18,6 +18,12 @@
* Fix issue with updating the last rebuffer time which resulted in
incorrect `bs` (buffer starvation) key in CMCD
([#1124](https://github.com/androidx/media/issues/1124)).
* Add
`PreloadMediaSource.PreloadControl.onLoadedToTheEndOfSource(PreloadMediaSource)`
to indicate that the source has loaded to the end. This allows the
`DefaultPreloadManager` and the custom
`PreloadMediaSource.PreloadControl` implementations to preload the next
source or take other actions.
* Transformer:
* Work around a decoder bug where the number of audio channels was capped
at stereo when handling PCM input.

View file

@ -203,13 +203,17 @@ public final class DefaultPreloadManager extends BasePreloadManager<Integer> {
@Override
public boolean onTimelineRefreshed(PreloadMediaSource mediaSource) {
return continueOrCompletePreloading(
mediaSource, status -> status.getStage() > Status.STAGE_TIMELINE_REFRESHED);
mediaSource,
/* continueLoadingPredicate= */ status ->
status.getStage() > Status.STAGE_TIMELINE_REFRESHED);
}
@Override
public boolean onPrepared(PreloadMediaSource mediaSource) {
return continueOrCompletePreloading(
mediaSource, status -> status.getStage() > Status.STAGE_SOURCE_PREPARED);
mediaSource,
/* continueLoadingPredicate= */ status ->
status.getStage() > Status.STAGE_SOURCE_PREPARED);
}
@Override
@ -217,9 +221,9 @@ public final class DefaultPreloadManager extends BasePreloadManager<Integer> {
PreloadMediaSource mediaSource, long bufferedPositionUs) {
return continueOrCompletePreloading(
mediaSource,
status ->
(status.getStage() == Status.STAGE_LOADED_TO_POSITION_MS
&& status.getValue() > Util.usToMs(bufferedPositionUs)));
/* continueLoadingPredicate= */ status ->
status.getStage() == Status.STAGE_LOADED_TO_POSITION_MS
&& status.getValue() > Util.usToMs(bufferedPositionUs));
}
@Override
@ -227,6 +231,11 @@ public final class DefaultPreloadManager extends BasePreloadManager<Integer> {
onPreloadCompleted(mediaSource);
}
@Override
public void onLoadedToTheEndOfSource(PreloadMediaSource mediaSource) {
onPreloadCompleted(mediaSource);
}
private boolean continueOrCompletePreloading(
MediaSource mediaSource, Predicate<Status> continueLoadingPredicate) {
@Nullable

View file

@ -81,9 +81,12 @@ public final class PreloadMediaSource extends WrappingMediaSource {
/**
* Called from {@link PreloadMediaSource} when it requests to continue loading.
*
* <p>If fully loaded, then {@link #onLoadedToTheEndOfSource(PreloadMediaSource)} will be called
* instead.
*
* @param mediaSource The {@link PreloadMediaSource} that requests to continue loading.
* @param bufferedPositionUs An estimate of the absolute position in microseconds up to which
* data is buffered, or {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
* data is buffered.
*/
boolean onContinueLoadingRequested(PreloadMediaSource mediaSource, long bufferedPositionUs);
@ -93,6 +96,15 @@ public final class PreloadMediaSource extends WrappingMediaSource {
* @param mediaSource The {@link PreloadMediaSource} that the player starts using.
*/
void onUsedByPlayer(PreloadMediaSource mediaSource);
/**
* Called from {@link PreloadMediaSource} when it has loaded to the end of source.
*
* <p>The default implementation is a no-op.
*
* @param mediaSource The {@link PreloadMediaSource} that has loaded to the end of source.
*/
default void onLoadedToTheEndOfSource(PreloadMediaSource mediaSource) {}
}
/** Factory for {@link PreloadMediaSource}. */
@ -402,7 +414,9 @@ public final class PreloadMediaSource extends WrappingMediaSource {
return;
}
PreloadMediaPeriod preloadMediaPeriod = (PreloadMediaPeriod) mediaPeriod;
if (!prepared
if (prepared && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE) {
preloadControl.onLoadedToTheEndOfSource(PreloadMediaSource.this);
} else if (!prepared
|| preloadControl.onContinueLoadingRequested(
PreloadMediaSource.this, preloadMediaPeriod.getBufferedPositionUs())) {
preloadMediaPeriod.continueLoading(

View file

@ -15,19 +15,25 @@
*/
package androidx.media3.exoplayer.source.preload;
import static androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status.STAGE_LOADED_TO_POSITION_MS;
import static androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED;
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.Math.abs;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.SystemClock;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.exoplayer.DefaultRendererCapabilitiesList;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.RendererCapabilitiesList;
@ -35,6 +41,7 @@ import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.trackselection.TrackSelector;
import androidx.media3.exoplayer.upstream.Allocator;
@ -50,6 +57,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -161,20 +169,26 @@ public class DefaultPreloadManagerTest {
}
@Test
public void
invalidate_withoutSettingCurrentPlayingIndex_sourcesPreloadedToTargetStatusesInOrder() {
public void invalidate_withoutSettingCurrentPlayingIndex_sourcesPreloadedToTargetStatusesInOrder()
throws Exception {
ArrayList<Integer> targetPreloadStatusControlCallStates = new ArrayList<>();
AtomicInteger currentPlayingItemIndex = new AtomicInteger();
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =
rankingData -> {
targetPreloadStatusControlCallStates.add(rankingData);
return new DefaultPreloadManager.Status(
DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
if (abs(rankingData - currentPlayingItemIndex.get()) == 1) {
return new DefaultPreloadManager.Status(STAGE_LOADED_TO_POSITION_MS, 100L);
} else {
return new DefaultPreloadManager.Status(STAGE_TIMELINE_REFRESHED);
}
};
FakeMediaSourceFactory fakeMediaSourceFactory = new FakeMediaSourceFactory();
ProgressiveMediaSource.Factory mediaSourceFactory =
new ProgressiveMediaSource.Factory(
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()));
DefaultPreloadManager preloadManager =
new DefaultPreloadManager(
targetPreloadStatusControl,
fakeMediaSourceFactory,
mediaSourceFactory,
trackSelector,
bandwidthMeter,
rendererCapabilitiesListFactory,
@ -182,44 +196,51 @@ public class DefaultPreloadManagerTest {
Util.getCurrentOrMainLooper());
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem0 =
mediaItemBuilder.setMediaId("mediaId0").setUri("http://exoplayer.dev/video0").build();
mediaItemBuilder
.setMediaId("mediaId0")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
MediaItem mediaItem1 =
mediaItemBuilder.setMediaId("mediaId1").setUri("http://exoplayer.dev/video1").build();
mediaItemBuilder
.setMediaId("mediaId1")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
MediaItem mediaItem2 =
mediaItemBuilder.setMediaId("mediaId2").setUri("http://exoplayer.dev/video2").build();
mediaItemBuilder
.setMediaId("mediaId2")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
preloadManager.add(mediaItem0, /* rankingData= */ 0);
FakeMediaSource wrappedMediaSource0 = fakeMediaSourceFactory.getLastCreatedSource();
wrappedMediaSource0.setAllowPreparation(false);
preloadManager.add(mediaItem1, /* rankingData= */ 1);
FakeMediaSource wrappedMediaSource1 = fakeMediaSourceFactory.getLastCreatedSource();
wrappedMediaSource1.setAllowPreparation(false);
preloadManager.add(mediaItem2, /* rankingData= */ 2);
FakeMediaSource wrappedMediaSource2 = fakeMediaSourceFactory.getLastCreatedSource();
wrappedMediaSource2.setAllowPreparation(false);
preloadManager.invalidate();
shadowOf(Looper.getMainLooper()).idle();
runMainLooperUntil(() -> targetPreloadStatusControlCallStates.size() == 3);
assertThat(targetPreloadStatusControlCallStates).containsExactly(0);
wrappedMediaSource0.setAllowPreparation(true);
shadowOf(Looper.getMainLooper()).idle();
assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1).inOrder();
assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1, 2).inOrder();
}
@Test
public void invalidate_withSettingCurrentPlayingIndex_sourcesPreloadedToTargetStatusesInOrder() {
public void invalidate_withSettingCurrentPlayingIndex_sourcesPreloadedToTargetStatusesInOrder()
throws Exception {
ArrayList<Integer> targetPreloadStatusControlCallStates = new ArrayList<>();
AtomicInteger currentPlayingItemIndex = new AtomicInteger();
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =
rankingData -> {
targetPreloadStatusControlCallStates.add(rankingData);
return new DefaultPreloadManager.Status(
DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
if (abs(rankingData - currentPlayingItemIndex.get()) == 1) {
return new DefaultPreloadManager.Status(STAGE_LOADED_TO_POSITION_MS, 100L);
} else {
return new DefaultPreloadManager.Status(STAGE_TIMELINE_REFRESHED);
}
};
FakeMediaSourceFactory fakeMediaSourceFactory = new FakeMediaSourceFactory();
ProgressiveMediaSource.Factory mediaSourceFactory =
new ProgressiveMediaSource.Factory(
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()));
DefaultPreloadManager preloadManager =
new DefaultPreloadManager(
targetPreloadStatusControl,
fakeMediaSourceFactory,
mediaSourceFactory,
trackSelector,
bandwidthMeter,
rendererCapabilitiesListFactory,
@ -227,32 +248,33 @@ public class DefaultPreloadManagerTest {
Util.getCurrentOrMainLooper());
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem0 =
mediaItemBuilder.setMediaId("mediaId0").setUri("http://exoplayer.dev/video0").build();
mediaItemBuilder
.setMediaId("mediaId0")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
MediaItem mediaItem1 =
mediaItemBuilder.setMediaId("mediaId1").setUri("http://exoplayer.dev/video1").build();
mediaItemBuilder
.setMediaId("mediaId1")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
MediaItem mediaItem2 =
mediaItemBuilder.setMediaId("mediaId2").setUri("http://exoplayer.dev/video2").build();
mediaItemBuilder
.setMediaId("mediaId2")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
preloadManager.add(mediaItem0, /* rankingData= */ 0);
FakeMediaSource wrappedMediaSource0 = fakeMediaSourceFactory.getLastCreatedSource();
wrappedMediaSource0.setAllowPreparation(false);
preloadManager.add(mediaItem1, /* rankingData= */ 1);
FakeMediaSource wrappedMediaSource1 = fakeMediaSourceFactory.getLastCreatedSource();
wrappedMediaSource1.setAllowPreparation(false);
preloadManager.add(mediaItem2, /* rankingData= */ 2);
FakeMediaSource wrappedMediaSource2 = fakeMediaSourceFactory.getLastCreatedSource();
wrappedMediaSource2.setAllowPreparation(false);
PreloadMediaSource preloadMediaSource2 =
(PreloadMediaSource) preloadManager.getMediaSource(mediaItem2);
preloadMediaSource2.prepareSource(
(source, timeline) -> {}, bandwidthMeter.getTransferListener(), PlayerId.UNSET);
preloadManager.setCurrentPlayingIndex(2);
currentPlayingItemIndex.set(2);
preloadManager.invalidate();
shadowOf(Looper.getMainLooper()).idle();
runMainLooperUntil(() -> targetPreloadStatusControlCallStates.size() == 3);
assertThat(targetPreloadStatusControlCallStates).containsExactly(2, 1);
wrappedMediaSource1.setAllowPreparation(true);
shadowOf(Looper.getMainLooper()).idle();
assertThat(targetPreloadStatusControlCallStates).containsExactly(2, 1, 0).inOrder();
}
@ -262,8 +284,7 @@ public class DefaultPreloadManagerTest {
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =
rankingData -> {
targetPreloadStatusControlCallStates.add(rankingData);
return new DefaultPreloadManager.Status(
DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
return new DefaultPreloadManager.Status(STAGE_TIMELINE_REFRESHED);
};
FakeMediaSourceFactory fakeMediaSourceFactory = new FakeMediaSourceFactory();
DefaultPreloadManager preloadManager =
@ -306,8 +327,7 @@ public class DefaultPreloadManagerTest {
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =
rankingData -> {
targetPreloadStatusControlCallStates.add(rankingData);
return new DefaultPreloadManager.Status(
DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
return new DefaultPreloadManager.Status(STAGE_TIMELINE_REFRESHED);
};
FakeMediaSourceFactory fakeMediaSourceFactory = new FakeMediaSourceFactory();
DefaultPreloadManager preloadManager =
@ -374,11 +394,13 @@ public class DefaultPreloadManagerTest {
targetPreloadStatusControlCallStates.add(rankingData);
return null;
};
FakeMediaSourceFactory fakeMediaSourceFactory = new FakeMediaSourceFactory();
ProgressiveMediaSource.Factory mediaSourceFactory =
new ProgressiveMediaSource.Factory(
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()));
DefaultPreloadManager preloadManager =
new DefaultPreloadManager(
targetPreloadStatusControl,
fakeMediaSourceFactory,
mediaSourceFactory,
trackSelector,
bandwidthMeter,
rendererCapabilitiesListFactory,
@ -386,20 +408,23 @@ public class DefaultPreloadManagerTest {
Util.getCurrentOrMainLooper());
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem0 =
mediaItemBuilder.setMediaId("mediaId0").setUri("http://exoplayer.dev/video0").build();
mediaItemBuilder
.setMediaId("mediaId0")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
MediaItem mediaItem1 =
mediaItemBuilder.setMediaId("mediaId1").setUri("http://exoplayer.dev/video1").build();
mediaItemBuilder
.setMediaId("mediaId1")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
MediaItem mediaItem2 =
mediaItemBuilder.setMediaId("mediaId2").setUri("http://exoplayer.dev/video2").build();
mediaItemBuilder
.setMediaId("mediaId2")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
preloadManager.add(mediaItem0, /* rankingData= */ 0);
FakeMediaSource wrappedMediaSource0 = fakeMediaSourceFactory.getLastCreatedSource();
wrappedMediaSource0.setAllowPreparation(false);
preloadManager.add(mediaItem1, /* rankingData= */ 1);
FakeMediaSource wrappedMediaSource1 = fakeMediaSourceFactory.getLastCreatedSource();
wrappedMediaSource1.setAllowPreparation(false);
preloadManager.add(mediaItem2, /* rankingData= */ 2);
FakeMediaSource wrappedMediaSource2 = fakeMediaSourceFactory.getLastCreatedSource();
wrappedMediaSource2.setAllowPreparation(false);
preloadManager.invalidate();
shadowOf(Looper.getMainLooper()).idle();
@ -410,8 +435,7 @@ public class DefaultPreloadManagerTest {
@Test
public void removeByMediaItems_correspondingHeldSourceRemovedAndReleased() {
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =
rankingData ->
new DefaultPreloadManager.Status(DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
rankingData -> new DefaultPreloadManager.Status(STAGE_TIMELINE_REFRESHED);
MediaSource.Factory mockMediaSourceFactory = mock(MediaSource.Factory.class);
DefaultPreloadManager preloadManager =
new DefaultPreloadManager(
@ -462,8 +486,7 @@ public class DefaultPreloadManagerTest {
@Test
public void removeByMediaSources_heldSourceRemovedAndReleased() {
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =
rankingData ->
new DefaultPreloadManager.Status(DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
rankingData -> new DefaultPreloadManager.Status(STAGE_TIMELINE_REFRESHED);
MediaSource.Factory mockMediaSourceFactory = mock(MediaSource.Factory.class);
DefaultPreloadManager preloadManager =
new DefaultPreloadManager(
@ -521,8 +544,7 @@ public class DefaultPreloadManagerTest {
@Test
public void reset_returnZeroCount_sourcesButNotRendererCapabilitiesListReleased() {
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =
rankingData ->
new DefaultPreloadManager.Status(DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
rankingData -> new DefaultPreloadManager.Status(STAGE_TIMELINE_REFRESHED);
MediaSource.Factory mockMediaSourceFactory = mock(MediaSource.Factory.class);
List<FakeRenderer> underlyingRenderers = new ArrayList<>();
RenderersFactory renderersFactory =
@ -593,8 +615,7 @@ public class DefaultPreloadManagerTest {
@Test
public void release_returnZeroCount_sourcesAndRendererCapabilitiesListReleased() {
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =
rankingData ->
new DefaultPreloadManager.Status(DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
rankingData -> new DefaultPreloadManager.Status(STAGE_TIMELINE_REFRESHED);
MediaSource.Factory mockMediaSourceFactory = mock(MediaSource.Factory.class);
List<FakeRenderer> underlyingRenderers = new ArrayList<>();
RenderersFactory renderersFactory =

View file

@ -362,6 +362,80 @@ public final class PreloadMediaSourceTest {
assertThat(onUsedByPlayerCalled.get()).isTrue();
}
@Test
public void preload_loadToTheEndOfSource() throws Exception {
AtomicBoolean onTimelineRefreshedCalled = new AtomicBoolean();
AtomicBoolean onPreparedCalled = new AtomicBoolean();
AtomicBoolean onContinueLoadingRequestedCalled = new AtomicBoolean();
AtomicBoolean onLoadedToTheEndOfSourceCalled = new AtomicBoolean();
AtomicBoolean onUsedByPlayerCalled = new AtomicBoolean();
PreloadMediaSource.PreloadControl preloadControl =
new PreloadMediaSource.PreloadControl() {
@Override
public boolean onTimelineRefreshed(PreloadMediaSource mediaSource) {
onTimelineRefreshedCalled.set(true);
return true;
}
@Override
public boolean onPrepared(PreloadMediaSource mediaSource) {
onPreparedCalled.set(true);
return true;
}
@Override
public boolean onContinueLoadingRequested(
PreloadMediaSource mediaSource, long bufferedPositionUs) {
// In fact, this method is not necessarily to be called if the
// LOADING_CHECK_INTERVAL_BYTES set for the ProgressiveMediaSource.Factory is large
// enough to have the media load to the end in one round. However, since we explicitly
// set with a small value below, we will still expect this method to be called for at
// least once.
onContinueLoadingRequestedCalled.set(true);
return true;
}
@Override
public void onUsedByPlayer(PreloadMediaSource mediaSource) {
onUsedByPlayerCalled.set(true);
}
@Override
public void onLoadedToTheEndOfSource(PreloadMediaSource mediaSource) {
onLoadedToTheEndOfSourceCalled.set(true);
}
};
ProgressiveMediaSource.Factory mediaSourceFactory =
new ProgressiveMediaSource.Factory(
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()));
mediaSourceFactory.setContinueLoadingCheckIntervalBytes(LOADING_CHECK_INTERVAL_BYTES);
TrackSelector trackSelector =
new DefaultTrackSelector(ApplicationProvider.getApplicationContext());
trackSelector.init(() -> {}, bandwidthMeter);
PreloadMediaSource.Factory preloadMediaSourceFactory =
new PreloadMediaSource.Factory(
mediaSourceFactory,
preloadControl,
trackSelector,
bandwidthMeter,
getRendererCapabilities(renderersFactory),
allocator,
Util.getCurrentOrMainLooper());
PreloadMediaSource preloadMediaSource =
preloadMediaSourceFactory.createMediaSource(
new MediaItem.Builder()
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build());
preloadMediaSource.preload(/* startPositionUs= */ 0L);
runMainLooperUntil(onLoadedToTheEndOfSourceCalled::get);
assertThat(onTimelineRefreshedCalled.get()).isTrue();
assertThat(onPreparedCalled.get()).isTrue();
assertThat(onContinueLoadingRequestedCalled.get()).isTrue();
assertThat(onUsedByPlayerCalled.get()).isFalse();
}
@Test
public void
prepareSource_beforeSourceInfoRefreshedForPreloading_onlyInvokeExternalCallerOnSourceInfoRefreshed() {