public final class

DownloadHelper

extends java.lang.Object

 java.lang.Object

↳androidx.media3.exoplayer.offline.DownloadHelper

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.0.0-alpha03'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer
  • version: 1.0.0-alpha03

Artifact androidx.media3:media3-exoplayer:1.0.0-alpha03 it located at Google repository (https://maven.google.com/)

Overview

A helper for initializing and removing downloads.

The helper extracts track information from the media, selects tracks for downloading, and creates download requests based on the selected tracks.

A typical usage of DownloadHelper follows these steps:

  1. Build the helper using one of the forMediaItem methods.
  2. Prepare the helper using DownloadHelper.prepare(DownloadHelper.Callback) and wait for the callback.
  3. Optional: Inspect the selected tracks using DownloadHelper.getMappedTrackInfo(int) and DownloadHelper.getTrackSelections(int, int), and make adjustments using DownloadHelper.clearTrackSelections(int), DownloadHelper and DownloadHelper.
  4. Create a download request for the selected track using DownloadHelper.getDownloadRequest(byte[]).
  5. Release the helper using DownloadHelper.release().

Summary

Fields
public static final DefaultTrackSelector.ParametersDEFAULT_TRACK_SELECTOR_PARAMETERS

public static final DefaultTrackSelector.ParametersDEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT

Default track selection parameters for downloading, but without any constraints.

public static final DefaultTrackSelector.ParametersDEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT

Constructors
publicDownloadHelper(MediaItem mediaItem, MediaSource mediaSource, DefaultTrackSelector.Parameters trackSelectorParameters, RendererCapabilities rendererCapabilities[])

Creates download helper.

Methods
public voidaddAudioLanguagesToSelection(java.lang.String languages[])

Convenience method to add selections of tracks for all specified audio languages.

public voidaddTextLanguagesToSelection(boolean selectUndeterminedTextLanguage, java.lang.String languages[])

Convenience method to add selections of tracks for all specified text languages.

public voidaddTrackSelection(int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters)

Adds a selection of tracks to be downloaded.

public voidaddTrackSelectionForSingleRenderer(int periodIndex, int rendererIndex, DefaultTrackSelector.Parameters trackSelectorParameters, java.util.List<DefaultTrackSelector.SelectionOverride> overrides)

Convenience method to add a selection of tracks to be downloaded for a single renderer.

public voidclearTrackSelections(int periodIndex)

Clears the selection of tracks for a period.

public static MediaSourcecreateMediaSource(DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory)

Equivalent to createMediaSource(downloadRequest, dataSourceFactory, null).

public static MediaSourcecreateMediaSource(DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory, DrmSessionManager drmSessionManager)

Utility method to create a MediaSource that only exposes the tracks defined in downloadRequest.

public static DownloadHelperforDash(Context context, Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory)

public static DownloadHelperforDash(Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, DrmSessionManager drmSessionManager, DefaultTrackSelector.Parameters trackSelectorParameters)

public static DownloadHelperforHls(Context context, Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory)

public static DownloadHelperforHls(Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, DrmSessionManager drmSessionManager, DefaultTrackSelector.Parameters trackSelectorParameters)

public static DownloadHelperforMediaItem(Context context, MediaItem mediaItem)

Creates a DownloadHelper for the given progressive media item.

public static DownloadHelperforMediaItem(Context context, MediaItem mediaItem, RenderersFactory renderersFactory, DataSource.Factory dataSourceFactory)

Creates a DownloadHelper for the given media item.

public static DownloadHelperforMediaItem(MediaItem mediaItem, DefaultTrackSelector.Parameters trackSelectorParameters, RenderersFactory renderersFactory, DataSource.Factory dataSourceFactory)

Creates a DownloadHelper for the given media item.

public static DownloadHelperforMediaItem(MediaItem mediaItem, DefaultTrackSelector.Parameters trackSelectorParameters, RenderersFactory renderersFactory, DataSource.Factory dataSourceFactory, DrmSessionManager drmSessionManager)

Creates a DownloadHelper for the given media item.

public static DownloadHelperforProgressive(Context context, Uri uri)

public static DownloadHelperforProgressive(Context context, Uri uri, java.lang.String cacheKey)

public static DownloadHelperforSmoothStreaming(Context context, Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory)

public static DownloadHelperforSmoothStreaming(Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory)

public static DownloadHelperforSmoothStreaming(Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, DrmSessionManager drmSessionManager, DefaultTrackSelector.Parameters trackSelectorParameters)

public static DefaultTrackSelector.ParametersgetDefaultTrackSelectorParameters(Context context)

Returns the default parameters used for track selection for downloading.

public DownloadRequestgetDownloadRequest(byte[] data[])

Builds a DownloadRequest for downloading the selected tracks.

public DownloadRequestgetDownloadRequest(java.lang.String id, byte[] data[])

Builds a DownloadRequest for downloading the selected tracks.

public java.lang.ObjectgetManifest()

Returns the manifest, or null if no manifest is loaded.

public MappingTrackSelector.MappedTrackInfogetMappedTrackInfo(int periodIndex)

Returns the mapped track info for the given period.

public intgetPeriodCount()

Returns the number of periods for which media is available.

public static RendererCapabilitiesgetRendererCapabilities(RenderersFactory renderersFactory)

Extracts renderer capabilities for the renderers created by the provided renderers factory.

public TrackGroupArraygetTrackGroups(int periodIndex)

Returns the track groups for the given period.

public java.util.List<ExoTrackSelection>getTrackSelections(int periodIndex, int rendererIndex)

Returns all track selections for a period and renderer.

public voidprepare(DownloadHelper.Callback callback)

Initializes the helper for starting a download.

public voidrelease()

Releases the helper and all resources it is holding.

public voidreplaceTrackSelections(int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters)

Replaces a selection of tracks to be downloaded.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT

Default track selection parameters for downloading, but without any constraints.

If possible, use DownloadHelper.getDefaultTrackSelectorParameters(Context) instead.

See also: DefaultTrackSelector.Parameters.DEFAULT_WITHOUT_CONTEXT

public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT

Deprecated: This instance does not have constraints. Use DownloadHelper.getDefaultTrackSelectorParameters(Context) instead.

public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS

Deprecated: This instance does not have constraints. Use DownloadHelper.getDefaultTrackSelectorParameters(Context) instead.

Constructors

public DownloadHelper(MediaItem mediaItem, MediaSource mediaSource, DefaultTrackSelector.Parameters trackSelectorParameters, RendererCapabilities rendererCapabilities[])

Creates download helper.

Parameters:

mediaItem: The media item.
mediaSource: A MediaSource for which tracks are selected, or null if no track selection needs to be made.
trackSelectorParameters: for selecting tracks for downloading.
rendererCapabilities: The RendererCapabilities of the renderers for which tracks are selected.

Methods

public static DefaultTrackSelector.Parameters getDefaultTrackSelectorParameters(Context context)

Returns the default parameters used for track selection for downloading.

public static RendererCapabilities getRendererCapabilities(RenderersFactory renderersFactory)

Extracts renderer capabilities for the renderers created by the provided renderers factory.

Parameters:

renderersFactory: A RenderersFactory.

Returns:

The RendererCapabilities for each renderer created by the renderersFactory.

public static DownloadHelper forProgressive(Context context, Uri uri)

Deprecated: Use DownloadHelper.forMediaItem(Context, MediaItem)

public static DownloadHelper forProgressive(Context context, Uri uri, java.lang.String cacheKey)

Deprecated: Use DownloadHelper.forMediaItem(Context, MediaItem)

public static DownloadHelper forDash(Context context, Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory)

Deprecated: Use DownloadHelper instead.

public static DownloadHelper forDash(Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, DrmSessionManager drmSessionManager, DefaultTrackSelector.Parameters trackSelectorParameters)

Deprecated: Use DownloadHelper instead.

public static DownloadHelper forHls(Context context, Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory)

Deprecated: Use DownloadHelper instead.

public static DownloadHelper forHls(Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, DrmSessionManager drmSessionManager, DefaultTrackSelector.Parameters trackSelectorParameters)

Deprecated: Use DownloadHelper instead.

public static DownloadHelper forSmoothStreaming(Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory)

Deprecated: Use DownloadHelper instead.

public static DownloadHelper forSmoothStreaming(Context context, Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory)

Deprecated: Use DownloadHelper instead.

public static DownloadHelper forSmoothStreaming(Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, DrmSessionManager drmSessionManager, DefaultTrackSelector.Parameters trackSelectorParameters)

Deprecated: Use DownloadHelper instead.

public static DownloadHelper forMediaItem(Context context, MediaItem mediaItem)

Creates a DownloadHelper for the given progressive media item.

Parameters:

context: The context.
mediaItem: A MediaItem.

Returns:

A DownloadHelper for progressive streams.

public static DownloadHelper forMediaItem(Context context, MediaItem mediaItem, RenderersFactory renderersFactory, DataSource.Factory dataSourceFactory)

Creates a DownloadHelper for the given media item.

Parameters:

context: The context.
mediaItem: A MediaItem.
renderersFactory: A RenderersFactory creating the renderers for which tracks are selected.
dataSourceFactory: A used to load the manifest for adaptive streams. This argument is required for adaptive streams and ignored for progressive streams.

Returns:

A DownloadHelper.

public static DownloadHelper forMediaItem(MediaItem mediaItem, DefaultTrackSelector.Parameters trackSelectorParameters, RenderersFactory renderersFactory, DataSource.Factory dataSourceFactory)

Creates a DownloadHelper for the given media item.

Parameters:

mediaItem: A MediaItem.
renderersFactory: A RenderersFactory creating the renderers for which tracks are selected.
trackSelectorParameters: for selecting tracks for downloading.
dataSourceFactory: A used to load the manifest for adaptive streams. This argument is required for adaptive streams and ignored for progressive streams.

Returns:

A DownloadHelper.

public static DownloadHelper forMediaItem(MediaItem mediaItem, DefaultTrackSelector.Parameters trackSelectorParameters, RenderersFactory renderersFactory, DataSource.Factory dataSourceFactory, DrmSessionManager drmSessionManager)

Creates a DownloadHelper for the given media item.

Parameters:

mediaItem: A MediaItem.
renderersFactory: A RenderersFactory creating the renderers for which tracks are selected.
trackSelectorParameters: for selecting tracks for downloading.
dataSourceFactory: A used to load the manifest for adaptive streams. This argument is required for adaptive streams and ignored for progressive streams.
drmSessionManager: An optional DrmSessionManager. Used to help determine which tracks can be selected.

Returns:

A DownloadHelper.

public static MediaSource createMediaSource(DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory)

Equivalent to createMediaSource(downloadRequest, dataSourceFactory, null).

public static MediaSource createMediaSource(DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory, DrmSessionManager drmSessionManager)

Utility method to create a MediaSource that only exposes the tracks defined in downloadRequest.

Parameters:

downloadRequest: A DownloadRequest.
dataSourceFactory: A factory for DataSources to read the media.
drmSessionManager: An optional DrmSessionManager to be passed to the MediaSource.

Returns:

A MediaSource that only exposes the tracks defined in downloadRequest.

public void prepare(DownloadHelper.Callback callback)

Initializes the helper for starting a download.

Parameters:

callback: A callback to be notified when preparation completes or fails.

public void release()

Releases the helper and all resources it is holding.

public java.lang.Object getManifest()

Returns the manifest, or null if no manifest is loaded. Must not be called until after preparation completes.

public int getPeriodCount()

Returns the number of periods for which media is available. Must not be called until after preparation completes.

public TrackGroupArray getTrackGroups(int periodIndex)

Returns the track groups for the given period. Must not be called until after preparation completes.

Use DownloadHelper.getMappedTrackInfo(int) to get the track groups mapped to renderers.

Parameters:

periodIndex: The period index.

Returns:

The track groups for the period. May be TrackGroupArray.EMPTY for single stream content.

public MappingTrackSelector.MappedTrackInfo getMappedTrackInfo(int periodIndex)

Returns the mapped track info for the given period. Must not be called until after preparation completes.

Parameters:

periodIndex: The period index.

Returns:

The MappingTrackSelector.MappedTrackInfo for the period.

public java.util.List<ExoTrackSelection> getTrackSelections(int periodIndex, int rendererIndex)

Returns all track selections for a period and renderer. Must not be called until after preparation completes.

Parameters:

periodIndex: The period index.
rendererIndex: The renderer index.

Returns:

A list of selected track selections.

public void clearTrackSelections(int periodIndex)

Clears the selection of tracks for a period. Must not be called until after preparation completes.

Parameters:

periodIndex: The period index for which track selections are cleared.

public void replaceTrackSelections(int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters)

Replaces a selection of tracks to be downloaded. Must not be called until after preparation completes.

Parameters:

periodIndex: The period index for which the track selection is replaced.
trackSelectorParameters: The to obtain the new selection of tracks.

public void addTrackSelection(int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters)

Adds a selection of tracks to be downloaded. Must not be called until after preparation completes.

Parameters:

periodIndex: The period index this track selection is added for.
trackSelectorParameters: The to obtain the new selection of tracks.

public void addAudioLanguagesToSelection(java.lang.String languages[])

Convenience method to add selections of tracks for all specified audio languages. If an audio track in one of the specified languages is not available, the default fallback audio track is used instead. Must not be called until after preparation completes.

Parameters:

languages: A list of audio languages for which tracks should be added to the download selection, as IETF BCP 47 conformant tags.

public void addTextLanguagesToSelection(boolean selectUndeterminedTextLanguage, java.lang.String languages[])

Convenience method to add selections of tracks for all specified text languages. Must not be called until after preparation completes.

Parameters:

selectUndeterminedTextLanguage: Whether a text track with undetermined language should be selected for downloading if no track with one of the specified languages is available.
languages: A list of text languages for which tracks should be added to the download selection, as IETF BCP 47 conformant tags.

public void addTrackSelectionForSingleRenderer(int periodIndex, int rendererIndex, DefaultTrackSelector.Parameters trackSelectorParameters, java.util.List<DefaultTrackSelector.SelectionOverride> overrides)

Convenience method to add a selection of tracks to be downloaded for a single renderer. Must not be called until after preparation completes.

Parameters:

periodIndex: The period index the track selection is added for.
rendererIndex: The renderer index the track selection is added for.
trackSelectorParameters: The to obtain the new selection of tracks.
overrides: A list of SelectionOverrides to apply to the trackSelectorParameters. If empty, trackSelectorParameters are used as they are.

public DownloadRequest getDownloadRequest(byte[] data[])

Builds a DownloadRequest for downloading the selected tracks. Must not be called until after preparation completes. The uri of the DownloadRequest will be used as content id.

Parameters:

data: Application provided data to store in DownloadRequest.data.

Returns:

The built DownloadRequest.

public DownloadRequest getDownloadRequest(java.lang.String id, byte[] data[])

Builds a DownloadRequest for downloading the selected tracks. Must not be called until after preparation completes.

Parameters:

id: The unique content id.
data: Application provided data to store in DownloadRequest.data.

Returns:

The built DownloadRequest.

Source

/*
 * Copyright (C) 2018 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 androidx.media3.exoplayer.offline;

import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;

import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.util.SparseIntArray;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.StreamKey;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackGroupArray;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.RendererCapabilities;
import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
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.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller;
import androidx.media3.exoplayer.source.chunk.MediaChunk;
import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
import androidx.media3.exoplayer.trackselection.BaseTrackSelection;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.Parameters;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo;
import androidx.media3.exoplayer.trackselection.TrackSelectorResult;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import androidx.media3.exoplayer.upstream.DefaultAllocator;
import androidx.media3.exoplayer.video.VideoRendererEventListener;
import androidx.media3.extractor.ExtractorsFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;

/**
 * A helper for initializing and removing downloads.
 *
 * <p>The helper extracts track information from the media, selects tracks for downloading, and
 * creates {@link DownloadRequest download requests} based on the selected tracks.
 *
 * <p>A typical usage of DownloadHelper follows these steps:
 *
 * <ol>
 *   <li>Build the helper using one of the {@code forMediaItem} methods.
 *   <li>Prepare the helper using {@link #prepare(Callback)} and wait for the callback.
 *   <li>Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link
 *       #getTrackSelections(int, int)}, and make adjustments using {@link
 *       #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link
 *       #addTrackSelection(int, Parameters)}.
 *   <li>Create a download request for the selected track using {@link #getDownloadRequest(byte[])}.
 *   <li>Release the helper using {@link #release()}.
 * </ol>
 */
@UnstableApi
public final class DownloadHelper {

  /**
   * Default track selection parameters for downloading, but without any {@link Context}
   * constraints.
   *
   * <p>If possible, use {@link #getDefaultTrackSelectorParameters(Context)} instead.
   *
   * @see Parameters#DEFAULT_WITHOUT_CONTEXT
   */
  public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT =
      Parameters.DEFAULT_WITHOUT_CONTEXT.buildUpon().setForceHighestSupportedBitrate(true).build();

  /**
   * @deprecated This instance does not have {@link Context} constraints. Use {@link
   *     #getDefaultTrackSelectorParameters(Context)} instead.
   */
  @Deprecated
  public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT =
      DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT;

  /**
   * @deprecated This instance does not have {@link Context} constraints. Use {@link
   *     #getDefaultTrackSelectorParameters(Context)} instead.
   */
  @Deprecated
  public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS =
      DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT;

  /** Returns the default parameters used for track selection for downloading. */
  public static DefaultTrackSelector.Parameters getDefaultTrackSelectorParameters(Context context) {
    return Parameters.getDefaults(context)
        .buildUpon()
        .setForceHighestSupportedBitrate(true)
        .build();
  }

  /** A callback to be notified when the {@link DownloadHelper} is prepared. */
  public interface Callback {

    /**
     * Called when preparation completes.
     *
     * @param helper The reporting {@link DownloadHelper}.
     */
    void onPrepared(DownloadHelper helper);

    /**
     * Called when preparation fails.
     *
     * @param helper The reporting {@link DownloadHelper}.
     * @param e The error.
     */
    void onPrepareError(DownloadHelper helper, IOException e);
  }

  /** Thrown at an attempt to download live content. */
  public static class LiveContentUnsupportedException extends IOException {}

  /**
   * Extracts renderer capabilities for the renderers created by the provided renderers factory.
   *
   * @param renderersFactory A {@link RenderersFactory}.
   * @return The {@link RendererCapabilities} for each renderer created by the {@code
   *     renderersFactory}.
   */
  public static RendererCapabilities[] getRendererCapabilities(RenderersFactory renderersFactory) {
    Renderer[] renderers =
        renderersFactory.createRenderers(
            Util.createHandlerForCurrentOrMainLooper(),
            new VideoRendererEventListener() {},
            new AudioRendererEventListener() {},
            (cues) -> {},
            (metadata) -> {});
    RendererCapabilities[] capabilities = new RendererCapabilities[renderers.length];
    for (int i = 0; i < renderers.length; i++) {
      capabilities[i] = renderers[i].getCapabilities();
    }
    return capabilities;
  }

  /** @deprecated Use {@link #forMediaItem(Context, MediaItem)} */
  @Deprecated
  public static DownloadHelper forProgressive(Context context, Uri uri) {
    return forMediaItem(context, new MediaItem.Builder().setUri(uri).build());
  }

  /** @deprecated Use {@link #forMediaItem(Context, MediaItem)} */
  @Deprecated
  public static DownloadHelper forProgressive(Context context, Uri uri, @Nullable String cacheKey) {
    return forMediaItem(
        context, new MediaItem.Builder().setUri(uri).setCustomCacheKey(cacheKey).build());
  }

  /**
   * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory,
   *     DataSource.Factory)} instead.
   */
  @SuppressWarnings("deprecation")
  @Deprecated
  public static DownloadHelper forDash(
      Context context,
      Uri uri,
      DataSource.Factory dataSourceFactory,
      RenderersFactory renderersFactory) {
    return forDash(
        uri,
        dataSourceFactory,
        renderersFactory,
        /* drmSessionManager= */ null,
        getDefaultTrackSelectorParameters(context));
  }

  /**
   * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory,
   *     DataSource.Factory, DrmSessionManager)} instead.
   */
  @Deprecated
  public static DownloadHelper forDash(
      Uri uri,
      DataSource.Factory dataSourceFactory,
      RenderersFactory renderersFactory,
      @Nullable DrmSessionManager drmSessionManager,
      DefaultTrackSelector.Parameters trackSelectorParameters) {
    return forMediaItem(
        new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_MPD).build(),
        trackSelectorParameters,
        renderersFactory,
        dataSourceFactory,
        drmSessionManager);
  }

  /**
   * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory,
   *     DataSource.Factory)} instead.
   */
  @SuppressWarnings("deprecation")
  @Deprecated
  public static DownloadHelper forHls(
      Context context,
      Uri uri,
      DataSource.Factory dataSourceFactory,
      RenderersFactory renderersFactory) {
    return forHls(
        uri,
        dataSourceFactory,
        renderersFactory,
        /* drmSessionManager= */ null,
        getDefaultTrackSelectorParameters(context));
  }

  /**
   * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory,
   *     DataSource.Factory, DrmSessionManager)} instead.
   */
  @Deprecated
  public static DownloadHelper forHls(
      Uri uri,
      DataSource.Factory dataSourceFactory,
      RenderersFactory renderersFactory,
      @Nullable DrmSessionManager drmSessionManager,
      DefaultTrackSelector.Parameters trackSelectorParameters) {
    return forMediaItem(
        new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_M3U8).build(),
        trackSelectorParameters,
        renderersFactory,
        dataSourceFactory,
        drmSessionManager);
  }

  /**
   * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory,
   *     DataSource.Factory)} instead.
   */
  @SuppressWarnings("deprecation")
  @Deprecated
  public static DownloadHelper forSmoothStreaming(
      Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {
    return forSmoothStreaming(
        uri,
        dataSourceFactory,
        renderersFactory,
        /* drmSessionManager= */ null,
        DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT);
  }

  /**
   * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory,
   *     DataSource.Factory)} instead.
   */
  @SuppressWarnings("deprecation")
  @Deprecated
  public static DownloadHelper forSmoothStreaming(
      Context context,
      Uri uri,
      DataSource.Factory dataSourceFactory,
      RenderersFactory renderersFactory) {
    return forSmoothStreaming(
        uri,
        dataSourceFactory,
        renderersFactory,
        /* drmSessionManager= */ null,
        getDefaultTrackSelectorParameters(context));
  }

  /**
   * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory,
   *     DataSource.Factory, DrmSessionManager)} instead.
   */
  @Deprecated
  public static DownloadHelper forSmoothStreaming(
      Uri uri,
      DataSource.Factory dataSourceFactory,
      RenderersFactory renderersFactory,
      @Nullable DrmSessionManager drmSessionManager,
      DefaultTrackSelector.Parameters trackSelectorParameters) {
    return forMediaItem(
        new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_SS).build(),
        trackSelectorParameters,
        renderersFactory,
        dataSourceFactory,
        drmSessionManager);
  }

  /**
   * Creates a {@link DownloadHelper} for the given progressive media item.
   *
   * @param context The context.
   * @param mediaItem A {@link MediaItem}.
   * @return A {@link DownloadHelper} for progressive streams.
   * @throws IllegalStateException If the media item is of type DASH, HLS or SmoothStreaming.
   */
  public static DownloadHelper forMediaItem(Context context, MediaItem mediaItem) {
    Assertions.checkArgument(isProgressive(checkNotNull(mediaItem.localConfiguration)));
    return forMediaItem(
        mediaItem,
        getDefaultTrackSelectorParameters(context),
        /* renderersFactory= */ null,
        /* dataSourceFactory= */ null,
        /* drmSessionManager= */ null);
  }

  /**
   * Creates a {@link DownloadHelper} for the given media item.
   *
   * @param context The context.
   * @param mediaItem A {@link MediaItem}.
   * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
   *     selected.
   * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive
   *     streams. This argument is required for adaptive streams and ignored for progressive
   *     streams.
   * @return A {@link DownloadHelper}.
   * @throws IllegalStateException If the corresponding module is missing for DASH, HLS or
   *     SmoothStreaming media items.
   * @throws IllegalArgumentException If the {@code dataSourceFactory} is null for adaptive streams.
   */
  public static DownloadHelper forMediaItem(
      Context context,
      MediaItem mediaItem,
      @Nullable RenderersFactory renderersFactory,
      @Nullable DataSource.Factory dataSourceFactory) {
    return forMediaItem(
        mediaItem,
        getDefaultTrackSelectorParameters(context),
        renderersFactory,
        dataSourceFactory,
        /* drmSessionManager= */ null);
  }

  /**
   * Creates a {@link DownloadHelper} for the given media item.
   *
   * @param mediaItem A {@link MediaItem}.
   * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
   *     selected.
   * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
   *     downloading.
   * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive
   *     streams. This argument is required for adaptive streams and ignored for progressive
   *     streams.
   * @return A {@link DownloadHelper}.
   * @throws IllegalStateException If the corresponding module is missing for DASH, HLS or
   *     SmoothStreaming media items.
   * @throws IllegalArgumentException If the {@code dataSourceFactory} is null for adaptive streams.
   */
  public static DownloadHelper forMediaItem(
      MediaItem mediaItem,
      DefaultTrackSelector.Parameters trackSelectorParameters,
      @Nullable RenderersFactory renderersFactory,
      @Nullable DataSource.Factory dataSourceFactory) {
    return forMediaItem(
        mediaItem,
        trackSelectorParameters,
        renderersFactory,
        dataSourceFactory,
        /* drmSessionManager= */ null);
  }

  /**
   * Creates a {@link DownloadHelper} for the given media item.
   *
   * @param mediaItem A {@link MediaItem}.
   * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
   *     selected.
   * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
   *     downloading.
   * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive
   *     streams. This argument is required for adaptive streams and ignored for progressive
   *     streams.
   * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which
   *     tracks can be selected.
   * @return A {@link DownloadHelper}.
   * @throws IllegalStateException If the corresponding module is missing for DASH, HLS or
   *     SmoothStreaming media items.
   * @throws IllegalArgumentException If the {@code dataSourceFactory} is null for adaptive streams.
   */
  public static DownloadHelper forMediaItem(
      MediaItem mediaItem,
      DefaultTrackSelector.Parameters trackSelectorParameters,
      @Nullable RenderersFactory renderersFactory,
      @Nullable DataSource.Factory dataSourceFactory,
      @Nullable DrmSessionManager drmSessionManager) {
    boolean isProgressive = isProgressive(checkNotNull(mediaItem.localConfiguration));
    Assertions.checkArgument(isProgressive || dataSourceFactory != null);
    return new DownloadHelper(
        mediaItem,
        isProgressive
            ? null
            : createMediaSourceInternal(
                mediaItem, castNonNull(dataSourceFactory), drmSessionManager),
        trackSelectorParameters,
        renderersFactory != null
            ? getRendererCapabilities(renderersFactory)
            : new RendererCapabilities[0]);
  }

  /**
   * Equivalent to {@link #createMediaSource(DownloadRequest, DataSource.Factory, DrmSessionManager)
   * createMediaSource(downloadRequest, dataSourceFactory, null)}.
   */
  public static MediaSource createMediaSource(
      DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) {
    return createMediaSource(downloadRequest, dataSourceFactory, /* drmSessionManager= */ null);
  }

  /**
   * Utility method to create a {@link MediaSource} that only exposes the tracks defined in {@code
   * downloadRequest}.
   *
   * @param downloadRequest A {@link DownloadRequest}.
   * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
   * @param drmSessionManager An optional {@link DrmSessionManager} to be passed to the {@link
   *     MediaSource}.
   * @return A {@link MediaSource} that only exposes the tracks defined in {@code downloadRequest}.
   */
  public static MediaSource createMediaSource(
      DownloadRequest downloadRequest,
      DataSource.Factory dataSourceFactory,
      @Nullable DrmSessionManager drmSessionManager) {
    return createMediaSourceInternal(
        downloadRequest.toMediaItem(), dataSourceFactory, drmSessionManager);
  }

  private final MediaItem.LocalConfiguration localConfiguration;
  @Nullable private final MediaSource mediaSource;
  private final DefaultTrackSelector trackSelector;
  private final RendererCapabilities[] rendererCapabilities;
  private final SparseIntArray scratchSet;
  private final Handler callbackHandler;
  private final Timeline.Window window;

  private boolean isPreparedWithMedia;
  private @MonotonicNonNull Callback callback;
  private @MonotonicNonNull MediaPreparer mediaPreparer;
  private TrackGroupArray @MonotonicNonNull [] trackGroupArrays;
  private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos;
  private List<ExoTrackSelection> @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer;
  private List<ExoTrackSelection> @MonotonicNonNull [][]
      immutableTrackSelectionsByPeriodAndRenderer;

  /**
   * Creates download helper.
   *
   * @param mediaItem The media item.
   * @param mediaSource A {@link MediaSource} for which tracks are selected, or null if no track
   *     selection needs to be made.
   * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
   *     downloading.
   * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks
   *     are selected.
   */
  public DownloadHelper(
      MediaItem mediaItem,
      @Nullable MediaSource mediaSource,
      DefaultTrackSelector.Parameters trackSelectorParameters,
      RendererCapabilities[] rendererCapabilities) {
    this.localConfiguration = checkNotNull(mediaItem.localConfiguration);
    this.mediaSource = mediaSource;
    this.trackSelector =
        new DefaultTrackSelector(trackSelectorParameters, new DownloadTrackSelection.Factory());
    this.rendererCapabilities = rendererCapabilities;
    this.scratchSet = new SparseIntArray();
    trackSelector.init(/* listener= */ () -> {}, new FakeBandwidthMeter());
    callbackHandler = Util.createHandlerForCurrentOrMainLooper();
    window = new Timeline.Window();
  }

  /**
   * Initializes the helper for starting a download.
   *
   * @param callback A callback to be notified when preparation completes or fails.
   * @throws IllegalStateException If the download helper has already been prepared.
   */
  public void prepare(Callback callback) {
    Assertions.checkState(this.callback == null);
    this.callback = callback;
    if (mediaSource != null) {
      mediaPreparer = new MediaPreparer(mediaSource, /* downloadHelper= */ this);
    } else {
      callbackHandler.post(() -> callback.onPrepared(this));
    }
  }

  /** Releases the helper and all resources it is holding. */
  public void release() {
    if (mediaPreparer != null) {
      mediaPreparer.release();
    }
  }

  /**
   * Returns the manifest, or null if no manifest is loaded. Must not be called until after
   * preparation completes.
   */
  @Nullable
  public Object getManifest() {
    if (mediaSource == null) {
      return null;
    }
    assertPreparedWithMedia();
    return mediaPreparer.timeline.getWindowCount() > 0
        ? mediaPreparer.timeline.getWindow(/* windowIndex= */ 0, window).manifest
        : null;
  }

  /**
   * Returns the number of periods for which media is available. Must not be called until after
   * preparation completes.
   */
  public int getPeriodCount() {
    if (mediaSource == null) {
      return 0;
    }
    assertPreparedWithMedia();
    return trackGroupArrays.length;
  }

  /**
   * Returns the track groups for the given period. Must not be called until after preparation
   * completes.
   *
   * <p>Use {@link #getMappedTrackInfo(int)} to get the track groups mapped to renderers.
   *
   * @param periodIndex The period index.
   * @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream
   *     content.
   */
  public TrackGroupArray getTrackGroups(int periodIndex) {
    assertPreparedWithMedia();
    return trackGroupArrays[periodIndex];
  }

  /**
   * Returns the mapped track info for the given period. Must not be called until after preparation
   * completes.
   *
   * @param periodIndex The period index.
   * @return The {@link MappedTrackInfo} for the period.
   */
  public MappedTrackInfo getMappedTrackInfo(int periodIndex) {
    assertPreparedWithMedia();
    return mappedTrackInfos[periodIndex];
  }

  /**
   * Returns all {@link ExoTrackSelection track selections} for a period and renderer. Must not be
   * called until after preparation completes.
   *
   * @param periodIndex The period index.
   * @param rendererIndex The renderer index.
   * @return A list of selected {@link ExoTrackSelection track selections}.
   */
  public List<ExoTrackSelection> getTrackSelections(int periodIndex, int rendererIndex) {
    assertPreparedWithMedia();
    return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
  }

  /**
   * Clears the selection of tracks for a period. Must not be called until after preparation
   * completes.
   *
   * @param periodIndex The period index for which track selections are cleared.
   */
  public void clearTrackSelections(int periodIndex) {
    assertPreparedWithMedia();
    for (int i = 0; i < rendererCapabilities.length; i++) {
      trackSelectionsByPeriodAndRenderer[periodIndex][i].clear();
    }
  }

  /**
   * Replaces a selection of tracks to be downloaded. Must not be called until after preparation
   * completes.
   *
   * @param periodIndex The period index for which the track selection is replaced.
   * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
   *     selection of tracks.
   */
  public void replaceTrackSelections(
      int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
    clearTrackSelections(periodIndex);
    addTrackSelection(periodIndex, trackSelectorParameters);
  }

  /**
   * Adds a selection of tracks to be downloaded. Must not be called until after preparation
   * completes.
   *
   * @param periodIndex The period index this track selection is added for.
   * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
   *     selection of tracks.
   */
  public void addTrackSelection(
      int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
    assertPreparedWithMedia();
    trackSelector.setParameters(trackSelectorParameters);
    runTrackSelection(periodIndex);
  }

  /**
   * Convenience method to add selections of tracks for all specified audio languages. If an audio
   * track in one of the specified languages is not available, the default fallback audio track is
   * used instead. Must not be called until after preparation completes.
   *
   * @param languages A list of audio languages for which tracks should be added to the download
   *     selection, as IETF BCP 47 conformant tags.
   */
  public void addAudioLanguagesToSelection(String... languages) {
    assertPreparedWithMedia();
    for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) {
      DefaultTrackSelector.ParametersBuilder parametersBuilder =
          DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon();
      MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex];
      int rendererCount = mappedTrackInfo.getRendererCount();
      for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
        if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_AUDIO) {
          parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true);
        }
      }
      for (String language : languages) {
        parametersBuilder.setPreferredAudioLanguage(language);
        addTrackSelection(periodIndex, parametersBuilder.build());
      }
    }
  }

  /**
   * Convenience method to add selections of tracks for all specified text languages. Must not be
   * called until after preparation completes.
   *
   * @param selectUndeterminedTextLanguage Whether a text track with undetermined language should be
   *     selected for downloading if no track with one of the specified {@code languages} is
   *     available.
   * @param languages A list of text languages for which tracks should be added to the download
   *     selection, as IETF BCP 47 conformant tags.
   */
  public void addTextLanguagesToSelection(
      boolean selectUndeterminedTextLanguage, String... languages) {
    assertPreparedWithMedia();
    for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) {
      DefaultTrackSelector.ParametersBuilder parametersBuilder =
          DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon();
      MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex];
      int rendererCount = mappedTrackInfo.getRendererCount();
      for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
        if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_TEXT) {
          parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true);
        }
      }
      parametersBuilder.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage);
      for (String language : languages) {
        parametersBuilder.setPreferredTextLanguage(language);
        addTrackSelection(periodIndex, parametersBuilder.build());
      }
    }
  }

  /**
   * Convenience method to add a selection of tracks to be downloaded for a single renderer. Must
   * not be called until after preparation completes.
   *
   * @param periodIndex The period index the track selection is added for.
   * @param rendererIndex The renderer index the track selection is added for.
   * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
   *     selection of tracks.
   * @param overrides A list of {@link SelectionOverride SelectionOverrides} to apply to the {@code
   *     trackSelectorParameters}. If empty, {@code trackSelectorParameters} are used as they are.
   */
  public void addTrackSelectionForSingleRenderer(
      int periodIndex,
      int rendererIndex,
      DefaultTrackSelector.Parameters trackSelectorParameters,
      List<SelectionOverride> overrides) {
    assertPreparedWithMedia();
    DefaultTrackSelector.ParametersBuilder builder = trackSelectorParameters.buildUpon();
    for (int i = 0; i < mappedTrackInfos[periodIndex].getRendererCount(); i++) {
      builder.setRendererDisabled(/* rendererIndex= */ i, /* disabled= */ i != rendererIndex);
    }
    if (overrides.isEmpty()) {
      addTrackSelection(periodIndex, builder.build());
    } else {
      TrackGroupArray trackGroupArray = mappedTrackInfos[periodIndex].getTrackGroups(rendererIndex);
      for (int i = 0; i < overrides.size(); i++) {
        builder.setSelectionOverride(rendererIndex, trackGroupArray, overrides.get(i));
        addTrackSelection(periodIndex, builder.build());
      }
    }
  }

  /**
   * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until
   * after preparation completes. The uri of the {@link DownloadRequest} will be used as content id.
   *
   * @param data Application provided data to store in {@link DownloadRequest#data}.
   * @return The built {@link DownloadRequest}.
   */
  public DownloadRequest getDownloadRequest(@Nullable byte[] data) {
    return getDownloadRequest(localConfiguration.uri.toString(), data);
  }

  /**
   * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until
   * after preparation completes.
   *
   * @param id The unique content id.
   * @param data Application provided data to store in {@link DownloadRequest#data}.
   * @return The built {@link DownloadRequest}.
   */
  public DownloadRequest getDownloadRequest(String id, @Nullable byte[] data) {
    DownloadRequest.Builder requestBuilder =
        new DownloadRequest.Builder(id, localConfiguration.uri)
            .setMimeType(localConfiguration.mimeType)
            .setKeySetId(
                localConfiguration.drmConfiguration != null
                    ? localConfiguration.drmConfiguration.getKeySetId()
                    : null)
            .setCustomCacheKey(localConfiguration.customCacheKey)
            .setData(data);
    if (mediaSource == null) {
      return requestBuilder.build();
    }
    assertPreparedWithMedia();
    List<StreamKey> streamKeys = new ArrayList<>();
    List<ExoTrackSelection> allSelections = new ArrayList<>();
    int periodCount = trackSelectionsByPeriodAndRenderer.length;
    for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
      allSelections.clear();
      int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length;
      for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
        allSelections.addAll(trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]);
      }
      streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections));
    }
    return requestBuilder.setStreamKeys(streamKeys).build();
  }

  // Initialization of array of Lists.
  @SuppressWarnings("unchecked")
  private void onMediaPrepared() {
    checkNotNull(mediaPreparer);
    checkNotNull(mediaPreparer.mediaPeriods);
    checkNotNull(mediaPreparer.timeline);
    int periodCount = mediaPreparer.mediaPeriods.length;
    int rendererCount = rendererCapabilities.length;
    trackSelectionsByPeriodAndRenderer =
        (List<ExoTrackSelection>[][]) new List<?>[periodCount][rendererCount];
    immutableTrackSelectionsByPeriodAndRenderer =
        (List<ExoTrackSelection>[][]) new List<?>[periodCount][rendererCount];
    for (int i = 0; i < periodCount; i++) {
      for (int j = 0; j < rendererCount; j++) {
        trackSelectionsByPeriodAndRenderer[i][j] = new ArrayList<>();
        immutableTrackSelectionsByPeriodAndRenderer[i][j] =
            Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]);
      }
    }
    trackGroupArrays = new TrackGroupArray[periodCount];
    mappedTrackInfos = new MappedTrackInfo[periodCount];
    for (int i = 0; i < periodCount; i++) {
      trackGroupArrays[i] = mediaPreparer.mediaPeriods[i].getTrackGroups();
      TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i);
      trackSelector.onSelectionActivated(trackSelectorResult.info);
      mappedTrackInfos[i] = checkNotNull(trackSelector.getCurrentMappedTrackInfo());
    }
    setPreparedWithMedia();
    checkNotNull(callbackHandler).post(() -> checkNotNull(callback).onPrepared(this));
  }

  private void onMediaPreparationFailed(IOException error) {
    checkNotNull(callbackHandler).post(() -> checkNotNull(callback).onPrepareError(this, error));
  }

  @RequiresNonNull({
    "trackGroupArrays",
    "mappedTrackInfos",
    "trackSelectionsByPeriodAndRenderer",
    "immutableTrackSelectionsByPeriodAndRenderer",
    "mediaPreparer",
    "mediaPreparer.timeline",
    "mediaPreparer.mediaPeriods"
  })
  private void setPreparedWithMedia() {
    isPreparedWithMedia = true;
  }

  @EnsuresNonNull({
    "trackGroupArrays",
    "mappedTrackInfos",
    "trackSelectionsByPeriodAndRenderer",
    "immutableTrackSelectionsByPeriodAndRenderer",
    "mediaPreparer",
    "mediaPreparer.timeline",
    "mediaPreparer.mediaPeriods"
  })
  @SuppressWarnings("nullness:contracts.postcondition")
  private void assertPreparedWithMedia() {
    Assertions.checkState(isPreparedWithMedia);
  }

  /**
   * Runs the track selection for a given period index with the current parameters. The selected
   * tracks will be added to {@link #trackSelectionsByPeriodAndRenderer}.
   */
  @RequiresNonNull({
    "trackGroupArrays",
    "trackSelectionsByPeriodAndRenderer",
    "mediaPreparer",
    "mediaPreparer.timeline"
  })
  private TrackSelectorResult runTrackSelection(int periodIndex) {
    try {
      TrackSelectorResult trackSelectorResult =
          trackSelector.selectTracks(
              rendererCapabilities,
              trackGroupArrays[periodIndex],
              new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)),
              mediaPreparer.timeline);
      for (int i = 0; i < trackSelectorResult.length; i++) {
        @Nullable ExoTrackSelection newSelection = trackSelectorResult.selections[i];
        if (newSelection == null) {
          continue;
        }
        List<ExoTrackSelection> existingSelectionList =
            trackSelectionsByPeriodAndRenderer[periodIndex][i];
        boolean mergedWithExistingSelection = false;
        for (int j = 0; j < existingSelectionList.size(); j++) {
          ExoTrackSelection existingSelection = existingSelectionList.get(j);
          if (existingSelection.getTrackGroup().equals(newSelection.getTrackGroup())) {
            // Merge with existing selection.
            scratchSet.clear();
            for (int k = 0; k < existingSelection.length(); k++) {
              scratchSet.put(existingSelection.getIndexInTrackGroup(k), 0);
            }
            for (int k = 0; k < newSelection.length(); k++) {
              scratchSet.put(newSelection.getIndexInTrackGroup(k), 0);
            }
            int[] mergedTracks = new int[scratchSet.size()];
            for (int k = 0; k < scratchSet.size(); k++) {
              mergedTracks[k] = scratchSet.keyAt(k);
            }
            existingSelectionList.set(
                j, new DownloadTrackSelection(existingSelection.getTrackGroup(), mergedTracks));
            mergedWithExistingSelection = true;
            break;
          }
        }
        if (!mergedWithExistingSelection) {
          existingSelectionList.add(newSelection);
        }
      }
      return trackSelectorResult;
    } catch (ExoPlaybackException e) {
      // DefaultTrackSelector does not throw exceptions during track selection.
      throw new UnsupportedOperationException(e);
    }
  }

  private static MediaSource createMediaSourceInternal(
      MediaItem mediaItem,
      DataSource.Factory dataSourceFactory,
      @Nullable DrmSessionManager drmSessionManager) {
    return new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY)
        .setDrmSessionManagerProvider(
            drmSessionManager != null ? unusedMediaItem -> drmSessionManager : null)
        .createMediaSource(mediaItem);
  }

  private static boolean isProgressive(MediaItem.LocalConfiguration localConfiguration) {
    return Util.inferContentTypeForUriAndMimeType(
            localConfiguration.uri, localConfiguration.mimeType)
        == C.TYPE_OTHER;
  }

  private static final class MediaPreparer
      implements MediaSourceCaller, MediaPeriod.Callback, Handler.Callback {

    private static final int MESSAGE_PREPARE_SOURCE = 0;
    private static final int MESSAGE_CHECK_FOR_FAILURE = 1;
    private static final int MESSAGE_CONTINUE_LOADING = 2;
    private static final int MESSAGE_RELEASE = 3;

    private static final int DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED = 0;
    private static final int DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED = 1;

    private final MediaSource mediaSource;
    private final DownloadHelper downloadHelper;
    private final Allocator allocator;
    private final ArrayList<MediaPeriod> pendingMediaPeriods;
    private final Handler downloadHelperHandler;
    private final HandlerThread mediaSourceThread;
    private final Handler mediaSourceHandler;

    public @MonotonicNonNull Timeline timeline;
    public MediaPeriod @MonotonicNonNull [] mediaPeriods;

    private boolean released;

    public MediaPreparer(MediaSource mediaSource, DownloadHelper downloadHelper) {
      this.mediaSource = mediaSource;
      this.downloadHelper = downloadHelper;
      allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
      pendingMediaPeriods = new ArrayList<>();
      @SuppressWarnings("nullness:methodref.receiver.bound")
      Handler downloadThreadHandler =
          Util.createHandlerForCurrentOrMainLooper(this::handleDownloadHelperCallbackMessage);
      this.downloadHelperHandler = downloadThreadHandler;
      mediaSourceThread = new HandlerThread("ExoPlayer:DownloadHelper");
      mediaSourceThread.start();
      mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this);
      mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE);
    }

    public void release() {
      if (released) {
        return;
      }
      released = true;
      mediaSourceHandler.sendEmptyMessage(MESSAGE_RELEASE);
    }

    // Handler.Callback

    @Override
    public boolean handleMessage(Message msg) {
      switch (msg.what) {
        case MESSAGE_PREPARE_SOURCE:
          mediaSource.prepareSource(
              /* caller= */ this, /* mediaTransferListener= */ null, PlayerId.UNSET);
          mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE);
          return true;
        case MESSAGE_CHECK_FOR_FAILURE:
          try {
            if (mediaPeriods == null) {
              mediaSource.maybeThrowSourceInfoRefreshError();
            } else {
              for (int i = 0; i < pendingMediaPeriods.size(); i++) {
                pendingMediaPeriods.get(i).maybeThrowPrepareError();
              }
            }
            mediaSourceHandler.sendEmptyMessageDelayed(
                MESSAGE_CHECK_FOR_FAILURE, /* delayMillis= */ 100);
          } catch (IOException e) {
            downloadHelperHandler
                .obtainMessage(DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED, /* obj= */ e)
                .sendToTarget();
          }
          return true;
        case MESSAGE_CONTINUE_LOADING:
          MediaPeriod mediaPeriod = (MediaPeriod) msg.obj;
          if (pendingMediaPeriods.contains(mediaPeriod)) {
            mediaPeriod.continueLoading(/* positionUs= */ 0);
          }
          return true;
        case MESSAGE_RELEASE:
          if (mediaPeriods != null) {
            for (MediaPeriod period : mediaPeriods) {
              mediaSource.releasePeriod(period);
            }
          }
          mediaSource.releaseSource(this);
          mediaSourceHandler.removeCallbacksAndMessages(null);
          mediaSourceThread.quit();
          return true;
        default:
          return false;
      }
    }

    // MediaSource.MediaSourceCaller implementation.

    @Override
    public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
      if (this.timeline != null) {
        // Ignore dynamic updates.
        return;
      }
      if (timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).isLive()) {
        downloadHelperHandler
            .obtainMessage(
                DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED,
                /* obj= */ new LiveContentUnsupportedException())
            .sendToTarget();
        return;
      }
      this.timeline = timeline;
      mediaPeriods = new MediaPeriod[timeline.getPeriodCount()];
      for (int i = 0; i < mediaPeriods.length; i++) {
        MediaPeriod mediaPeriod =
            mediaSource.createPeriod(
                new MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ i)),
                allocator,
                /* startPositionUs= */ 0);
        mediaPeriods[i] = mediaPeriod;
        pendingMediaPeriods.add(mediaPeriod);
      }
      for (MediaPeriod mediaPeriod : mediaPeriods) {
        mediaPeriod.prepare(/* callback= */ this, /* positionUs= */ 0);
      }
    }

    // MediaPeriod.Callback implementation.

    @Override
    public void onPrepared(MediaPeriod mediaPeriod) {
      pendingMediaPeriods.remove(mediaPeriod);
      if (pendingMediaPeriods.isEmpty()) {
        mediaSourceHandler.removeMessages(MESSAGE_CHECK_FOR_FAILURE);
        downloadHelperHandler.sendEmptyMessage(DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED);
      }
    }

    @Override
    public void onContinueLoadingRequested(MediaPeriod mediaPeriod) {
      if (pendingMediaPeriods.contains(mediaPeriod)) {
        mediaSourceHandler.obtainMessage(MESSAGE_CONTINUE_LOADING, mediaPeriod).sendToTarget();
      }
    }

    private boolean handleDownloadHelperCallbackMessage(Message msg) {
      if (released) {
        // Stale message.
        return false;
      }
      switch (msg.what) {
        case DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED:
          downloadHelper.onMediaPrepared();
          return true;
        case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED:
          release();
          downloadHelper.onMediaPreparationFailed((IOException) castNonNull(msg.obj));
          return true;
        default:
          return false;
      }
    }
  }

  private static final class DownloadTrackSelection extends BaseTrackSelection {

    private static final class Factory implements ExoTrackSelection.Factory {

      @Override
      public @NullableType ExoTrackSelection[] createTrackSelections(
          @NullableType Definition[] definitions,
          BandwidthMeter bandwidthMeter,
          MediaPeriodId mediaPeriodId,
          Timeline timeline) {
        @NullableType ExoTrackSelection[] selections = new ExoTrackSelection[definitions.length];
        for (int i = 0; i < definitions.length; i++) {
          selections[i] =
              definitions[i] == null
                  ? null
                  : new DownloadTrackSelection(definitions[i].group, definitions[i].tracks);
        }
        return selections;
      }
    }

    public DownloadTrackSelection(TrackGroup trackGroup, int[] tracks) {
      super(trackGroup, tracks);
    }

    @Override
    public int getSelectedIndex() {
      return 0;
    }

    @Override
    public @C.SelectionReason int getSelectionReason() {
      return C.SELECTION_REASON_UNKNOWN;
    }

    @Override
    @Nullable
    public Object getSelectionData() {
      return null;
    }

    @Override
    public void updateSelectedTrack(
        long playbackPositionUs,
        long bufferedDurationUs,
        long availableDurationUs,
        List<? extends MediaChunk> queue,
        MediaChunkIterator[] mediaChunkIterators) {
      // Do nothing.
    }
  }

  private static final class FakeBandwidthMeter implements BandwidthMeter {

    @Override
    public long getBitrateEstimate() {
      return 0;
    }

    @Override
    @Nullable
    public TransferListener getTransferListener() {
      return null;
    }

    @Override
    public void addEventListener(Handler eventHandler, EventListener eventListener) {
      // Do nothing.
    }

    @Override
    public void removeEventListener(EventListener eventListener) {
      // Do nothing.
    }
  }
}