DefaultPreloadManager.java

/*
 * Copyright 2023 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
 *
 *      https://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 androidx.media3.exoplayer.source.preload;

import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.Math.abs;
import static java.lang.annotation.ElementType.TYPE_USE;

import android.os.Looper;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.RendererCapabilitiesList;
import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.SampleQueue;
import androidx.media3.exoplayer.trackselection.TrackSelector;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import com.google.common.base.Predicate;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Comparator;

/**
 * A preload manager that preloads with the {@link PreloadMediaSource} to load the media data into
 * the {@link SampleQueue}.
 */
@UnstableApi
public final class DefaultPreloadManager extends BasePreloadManager<Integer> {

  /**
   * An implementation of {@link TargetPreloadStatusControl.PreloadStatus} that describes the
   * preload status of the {@link PreloadMediaSource}.
   */
  public static class Status implements TargetPreloadStatusControl.PreloadStatus {

    /**
     * Stages that for the preload status. One of {@link #STAGE_TIMELINE_REFRESHED}, {@link
     * #STAGE_SOURCE_PREPARED} or {@link #STAGE_LOADED_TO_POSITION_MS}.
     */
    @Documented
    @Retention(RetentionPolicy.SOURCE)
    @Target(TYPE_USE)
    @IntDef(
        value = {
          STAGE_TIMELINE_REFRESHED,
          STAGE_SOURCE_PREPARED,
          STAGE_LOADED_TO_POSITION_MS,
        })
    public @interface Stage {}

    /** The {@link PreloadMediaSource} has its {@link Timeline} refreshed. */
    public static final int STAGE_TIMELINE_REFRESHED = 0;

    /** The {@link PreloadMediaSource} is prepared. */
    public static final int STAGE_SOURCE_PREPARED = 1;

    /** The {@link PreloadMediaSource} is loaded to a specific position in microseconds. */
    public static final int STAGE_LOADED_TO_POSITION_MS = 2;

    private final @Stage int stage;
    private final long value;

    public Status(@Stage int stage, long value) {
      this.stage = stage;
      this.value = value;
    }

    public Status(@Stage int stage) {
      this(stage, C.TIME_UNSET);
    }

    @Override
    public @Stage int getStage() {
      return stage;
    }

    @Override
    public long getValue() {
      return value;
    }
  }

  private final RendererCapabilitiesList rendererCapabilitiesList;
  private final PreloadMediaSource.Factory preloadMediaSourceFactory;

  /**
   * Constructs a new instance.
   *
   * @param targetPreloadStatusControl The {@link TargetPreloadStatusControl}.
   * @param mediaSourceFactory The {@link MediaSource.Factory}.
   * @param trackSelector The {@link TrackSelector}. The instance passed should be {@link
   *     TrackSelector#init(TrackSelector.InvalidationListener, BandwidthMeter) initialized}.
   * @param bandwidthMeter The {@link BandwidthMeter}. It should be the same bandwidth meter of the
   *     {@link ExoPlayer} that will play the managed {@link PreloadMediaSource}.
   * @param rendererCapabilitiesListFactory The {@link RendererCapabilitiesList.Factory}. To make
   *     preloading work properly, it must create a {@link RendererCapabilitiesList} holding an
   *     {@linkplain RendererCapabilitiesList#getRendererCapabilities() array of renderer
   *     capabilities} that matches the {@linkplain ExoPlayer#getRendererCount() count} and the
   *     {@linkplain ExoPlayer#getRendererType(int) renderer types} of the array of {@linkplain
   *     Renderer renderers} created by the {@link RenderersFactory} used by the {@link ExoPlayer}
   *     that will play the managed {@link PreloadMediaSource}.
   * @param allocator The {@link Allocator}. It should be the same allocator of the {@link
   *     ExoPlayer} that will play the managed {@link PreloadMediaSource}.
   * @param preloadLooper The {@link Looper} that will be used for preloading. It should be the same
   *     playback looper of the {@link ExoPlayer} that will play the manager {@link
   *     PreloadMediaSource}.
   */
  public DefaultPreloadManager(
      TargetPreloadStatusControl<Integer> targetPreloadStatusControl,
      MediaSource.Factory mediaSourceFactory,
      TrackSelector trackSelector,
      BandwidthMeter bandwidthMeter,
      RendererCapabilitiesList.Factory rendererCapabilitiesListFactory,
      Allocator allocator,
      Looper preloadLooper) {
    super(new RankingDataComparator(), targetPreloadStatusControl, mediaSourceFactory);
    this.rendererCapabilitiesList =
        rendererCapabilitiesListFactory.createRendererCapabilitiesList();
    preloadMediaSourceFactory =
        new PreloadMediaSource.Factory(
            mediaSourceFactory,
            new SourcePreloadControl(),
            trackSelector,
            bandwidthMeter,
            rendererCapabilitiesList.getRendererCapabilities(),
            allocator,
            preloadLooper);
  }

  /**
   * Sets the index of the current playing media.
   *
   * @param currentPlayingIndex The index of current playing media.
   */
  public void setCurrentPlayingIndex(int currentPlayingIndex) {
    RankingDataComparator rankingDataComparator =
        (RankingDataComparator) this.rankingDataComparator;
    rankingDataComparator.currentPlayingIndex = currentPlayingIndex;
  }

  @Override
  public MediaSource createMediaSourceForPreloading(MediaSource mediaSource) {
    return preloadMediaSourceFactory.createMediaSource(mediaSource);
  }

  @Override
  protected void preloadSourceInternal(MediaSource mediaSource, long startPositionsUs) {
    checkArgument(mediaSource instanceof PreloadMediaSource);
    PreloadMediaSource preloadMediaSource = (PreloadMediaSource) mediaSource;
    preloadMediaSource.preload(startPositionsUs);
  }

  @Override
  protected void releaseSourceInternal(MediaSource mediaSource) {
    checkArgument(mediaSource instanceof PreloadMediaSource);
    PreloadMediaSource preloadMediaSource = (PreloadMediaSource) mediaSource;
    preloadMediaSource.releasePreloadMediaSource();
  }

  @Override
  protected void releaseInternal() {
    rendererCapabilitiesList.release();
  }

  private static final class RankingDataComparator implements Comparator<Integer> {

    public int currentPlayingIndex;

    public RankingDataComparator() {
      this.currentPlayingIndex = C.INDEX_UNSET;
    }

    @Override
    public int compare(Integer o1, Integer o2) {
      return Integer.compare(abs(o1 - currentPlayingIndex), abs(o2 - currentPlayingIndex));
    }
  }

  private final class SourcePreloadControl implements PreloadMediaSource.PreloadControl {
    @Override
    public boolean onTimelineRefreshed(PreloadMediaSource mediaSource) {
      return continueOrCompletePreloading(
          mediaSource, status -> status.getStage() > Status.STAGE_TIMELINE_REFRESHED);
    }

    @Override
    public boolean onPrepared(PreloadMediaSource mediaSource) {
      return continueOrCompletePreloading(
          mediaSource, status -> status.getStage() > Status.STAGE_SOURCE_PREPARED);
    }

    @Override
    public boolean onContinueLoadingRequested(
        PreloadMediaSource mediaSource, long bufferedPositionUs) {
      return continueOrCompletePreloading(
          mediaSource,
          status ->
              (status.getStage() == Status.STAGE_LOADED_TO_POSITION_MS
                  && status.getValue() > Util.usToMs(bufferedPositionUs)));
    }

    @Override
    public void onUsedByPlayer(PreloadMediaSource mediaSource) {
      onPreloadCompleted(mediaSource);
    }

    private boolean continueOrCompletePreloading(
        MediaSource mediaSource, Predicate<Status> continueLoadingPredicate) {
      @Nullable
      TargetPreloadStatusControl.PreloadStatus targetPreloadStatus =
          getTargetPreloadStatus(mediaSource);
      if (targetPreloadStatus != null) {
        Status status = (Status) targetPreloadStatus;
        if (continueLoadingPredicate.apply(checkNotNull(status))) {
          return true;
        }
        onPreloadCompleted(mediaSource);
      }
      return false;
    }
  }
}