Clear the PreloadMediaSource when deprioritized by the preload manager

PiperOrigin-RevId: 633917110
This commit is contained in:
tianyifeng 2024-05-15 05:37:54 -07:00 committed by Copybara-Service
parent a5c94245f8
commit e4f0ff8177
3 changed files with 176 additions and 9 deletions

View File

@ -265,6 +265,14 @@ public abstract class BasePreloadManager<T> {
*/
protected abstract void preloadSourceInternal(MediaSource mediaSource, long startPositionsUs);
/**
* Clears the preloaded data of the given {@link MediaSource}, while not releasing the instance of
* it.
*
* @param mediaSource The media source to clear.
*/
protected abstract void clearSourceInternal(MediaSource mediaSource);
/**
* Releases the given {@link MediaSource}.
*
@ -292,6 +300,8 @@ public abstract class BasePreloadManager<T> {
if (targetPreloadStatusOfCurrentPreloadingSource != null) {
preloadSourceInternal(preloadingHolder.mediaSource, preloadingHolder.startPositionUs);
return true;
} else {
clearSourceInternal(preloadingHolder.mediaSource);
}
}
return false;

View File

@ -169,15 +169,19 @@ public final class DefaultPreloadManager extends BasePreloadManager<Integer> {
@Override
protected void preloadSourceInternal(MediaSource mediaSource, long startPositionsUs) {
checkArgument(mediaSource instanceof PreloadMediaSource);
PreloadMediaSource preloadMediaSource = (PreloadMediaSource) mediaSource;
preloadMediaSource.preload(startPositionsUs);
((PreloadMediaSource) mediaSource).preload(startPositionsUs);
}
@Override
protected void clearSourceInternal(MediaSource mediaSource) {
checkArgument(mediaSource instanceof PreloadMediaSource);
((PreloadMediaSource) mediaSource).clear();
}
@Override
protected void releaseSourceInternal(MediaSource mediaSource) {
checkArgument(mediaSource instanceof PreloadMediaSource);
PreloadMediaSource preloadMediaSource = (PreloadMediaSource) mediaSource;
preloadMediaSource.releasePreloadMediaSource();
((PreloadMediaSource) mediaSource).releasePreloadMediaSource();
}
@Override
@ -202,28 +206,38 @@ public final class DefaultPreloadManager extends BasePreloadManager<Integer> {
private final class SourcePreloadControl implements PreloadMediaSource.PreloadControl {
@Override
public boolean onTimelineRefreshed(PreloadMediaSource mediaSource) {
// The PreloadMediaSource may have more data preloaded than the target preload status if it
// has been preloaded before, thus we set `clearExceededDataFromTargetPreloadStatus` to
// `true` to clear the exceeded data.
return continueOrCompletePreloading(
mediaSource,
/* continueLoadingPredicate= */ status ->
status.getStage() > Status.STAGE_TIMELINE_REFRESHED);
status.getStage() > Status.STAGE_TIMELINE_REFRESHED,
/* clearExceededDataFromTargetPreloadStatus= */ true);
}
@Override
public boolean onPrepared(PreloadMediaSource mediaSource) {
// Set `clearExceededDataFromTargetPreloadStatus` to `false` as clearing the exceeded data
// from the status STAGE_SOURCE_PREPARED is not supported.
return continueOrCompletePreloading(
mediaSource,
/* continueLoadingPredicate= */ status ->
status.getStage() > Status.STAGE_SOURCE_PREPARED);
status.getStage() > Status.STAGE_SOURCE_PREPARED,
/* clearExceededDataFromTargetPreloadStatus= */ false);
}
@Override
public boolean onContinueLoadingRequested(
PreloadMediaSource mediaSource, long bufferedPositionUs) {
// Set `clearExceededDataFromTargetPreloadStatus` to `false` as clearing the exceeded data
// from the status STAGE_LOADED_TO_POSITION_MS is not supported.
return continueOrCompletePreloading(
mediaSource,
/* continueLoadingPredicate= */ status ->
status.getStage() == Status.STAGE_LOADED_TO_POSITION_MS
&& status.getValue() > Util.usToMs(bufferedPositionUs));
&& status.getValue() > Util.usToMs(bufferedPositionUs),
/* clearExceededDataFromTargetPreloadStatus= */ false);
}
@Override
@ -237,7 +251,9 @@ public final class DefaultPreloadManager extends BasePreloadManager<Integer> {
}
private boolean continueOrCompletePreloading(
MediaSource mediaSource, Predicate<Status> continueLoadingPredicate) {
PreloadMediaSource mediaSource,
Predicate<Status> continueLoadingPredicate,
boolean clearExceededDataFromTargetPreloadStatus) {
@Nullable
TargetPreloadStatusControl.PreloadStatus targetPreloadStatus =
getTargetPreloadStatus(mediaSource);
@ -246,8 +262,11 @@ public final class DefaultPreloadManager extends BasePreloadManager<Integer> {
if (continueLoadingPredicate.apply(checkNotNull(status))) {
return true;
}
onPreloadCompleted(mediaSource);
if (clearExceededDataFromTargetPreloadStatus) {
clearSourceInternal(mediaSource);
}
}
onPreloadCompleted(mediaSource);
return false;
}
}

View File

@ -16,7 +16,9 @@
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_SOURCE_PREPARED;
import static androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED;
import static androidx.media3.test.utils.FakeMediaSourceFactory.DEFAULT_WINDOW_UID;
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.Math.abs;
@ -29,19 +31,26 @@ import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.media3.common.AdPlaybackState;
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.datasource.TransferListener;
import androidx.media3.exoplayer.DefaultRendererCapabilitiesList;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.RendererCapabilitiesList;
import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSourceEventListener;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.trackselection.TrackSelector;
import androidx.media3.exoplayer.upstream.Allocator;
@ -49,12 +58,15 @@ import androidx.media3.exoplayer.upstream.BandwidthMeter;
import androidx.media3.exoplayer.upstream.DefaultAllocator;
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
import androidx.media3.test.utils.FakeAudioRenderer;
import androidx.media3.test.utils.FakeMediaPeriod;
import androidx.media3.test.utils.FakeMediaSource;
import androidx.media3.test.utils.FakeMediaSourceFactory;
import androidx.media3.test.utils.FakeRenderer;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeVideoRenderer;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@ -66,6 +78,7 @@ import org.mockito.Mock;
/** Unit test for {@link DefaultPreloadManager}. */
@RunWith(AndroidJUnit4.class)
public class DefaultPreloadManagerTest {
@Mock private TargetPreloadStatusControl<Integer> mockTargetPreloadStatusControl;
private TrackSelector trackSelector;
private Allocator allocator;
@ -432,6 +445,131 @@ public class DefaultPreloadManagerTest {
assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1, 2);
}
@Test
public void invalidate_clearsDeprioritizedSources() throws Exception {
final AtomicInteger currentPlayingIndex = new AtomicInteger();
ArrayList<Integer> targetPreloadStatusControlCallStates = new ArrayList<>();
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =
rankingData -> {
targetPreloadStatusControlCallStates.add(rankingData);
if (abs(rankingData - currentPlayingIndex.get()) <= 2) {
return new DefaultPreloadManager.Status(STAGE_SOURCE_PREPARED);
} else if (abs(rankingData - currentPlayingIndex.get()) == 3) {
return new DefaultPreloadManager.Status(STAGE_TIMELINE_REFRESHED);
}
return null;
};
MediaSource.Factory mockMediaSourceFactory = mock(MediaSource.Factory.class);
ArrayList<String> releasedPreloadingPeriodMediaIds = new ArrayList<>();
when(mockMediaSourceFactory.createMediaSource(any()))
.thenAnswer(
invocation -> {
MediaItem mediaItem = invocation.getArgument(0);
FakeTimeline.TimelineWindowDefinition timelineWindowDefinition =
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ DEFAULT_WINDOW_UID,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 2 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(123456789),
ImmutableList.of(AdPlaybackState.NONE),
mediaItem);
return new FakeMediaSource(new FakeTimeline(timelineWindowDefinition)) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
return new FakeMediaPeriod(
trackGroupArray,
allocator,
FakeTimeline.TimelineWindowDefinition
.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
mediaSourceEventDispatcher) {
@Override
public void release() {
releasedPreloadingPeriodMediaIds.add(mediaItem.mediaId);
}
};
}
};
});
DefaultPreloadManager preloadManager =
new DefaultPreloadManager(
targetPreloadStatusControl,
mockMediaSourceFactory,
trackSelector,
bandwidthMeter,
rendererCapabilitiesListFactory,
allocator,
Util.getCurrentOrMainLooper());
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem0 =
mediaItemBuilder
.setMediaId("mediaId0")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
MediaItem mediaItem1 =
mediaItemBuilder
.setMediaId("mediaId1")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
MediaItem mediaItem2 =
mediaItemBuilder
.setMediaId("mediaId2")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
MediaItem mediaItem3 =
mediaItemBuilder
.setMediaId("mediaId3")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
MediaItem mediaItem4 =
mediaItemBuilder
.setMediaId("mediaId4")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.build();
preloadManager.add(mediaItem0, /* rankingData= */ 0);
preloadManager.add(mediaItem1, /* rankingData= */ 1);
preloadManager.add(mediaItem2, /* rankingData= */ 2);
preloadManager.add(mediaItem3, /* rankingData= */ 3);
preloadManager.add(mediaItem4, /* rankingData= */ 4);
currentPlayingIndex.set(C.INDEX_UNSET);
preloadManager.invalidate();
runMainLooperUntil(() -> targetPreloadStatusControlCallStates.size() == 5);
assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1, 2, 3, 4).inOrder();
assertThat(releasedPreloadingPeriodMediaIds).isEmpty();
targetPreloadStatusControlCallStates.clear();
PreloadMediaSource preloadMediaSource4 =
(PreloadMediaSource) preloadManager.getMediaSource(mediaItem4);
// Simulate that preloadMediaSource4 is using by the player.
preloadMediaSource4.prepareSource(
(source, timeline) -> {}, bandwidthMeter.getTransferListener(), PlayerId.UNSET);
currentPlayingIndex.set(4);
preloadManager.setCurrentPlayingIndex(4);
preloadManager.invalidate();
runMainLooperUntil(() -> releasedPreloadingPeriodMediaIds.size() == 2);
assertThat(targetPreloadStatusControlCallStates).containsExactly(4, 3, 2, 1, 0).inOrder();
// The sources for mediaItem4, mediaItem3 and mediaItem2 either got used by the player or
// preload more after the second invalidate() call because their priorities increased. Thus the
// sources got cleared are the ones for mediaItem1 and mediaItem0 due to their decreased
// priorities.
assertThat(releasedPreloadingPeriodMediaIds).containsExactly("mediaId1", "mediaId0");
}
@Test
public void removeByMediaItems_correspondingHeldSourceRemovedAndReleased() {
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =